编写函数的两种方法

时间:2015-08-15 04:54:04

标签: c function pointers

我在C语言的上下文中提出这个问题,尽管它实际上适用于任何支持指针或传递引用功能的语言。

我来自Java背景,但已经编写了足够的低级代码(C和C ++)来观察这个有趣的现象。假设我们有一些对象X(这里没有使用“最严格的OOP意义上的”对象“),我们希望通过其他函数填充信息,似乎有两种方法可以这样做:

  1. 返回该对象类型的实例并指定它,例如如果X有T型,那么我们就有:
    T func(){...}

    X = func();

  2. 传入对象的指针/引用并在函数内部修改它,并返回void或其他一些值(例如,在C中,很多函数返回{{1对应于操作的成功/失败)。这方面的一个例子是:

    int

    int func(T* x){...x = 1;...}

  3. 我的问题是:在什么情况下使一种方法比另一种更好?它们是实现相同结果的等效方法吗?每个都有什么限制?

    谢谢!

8 个答案:

答案 0 :(得分:3)

有一个原因是你应该总是考虑使用第二种方法,而不是第一种方法。如果查看整个C标准库的返回值,您会注意到它们中几乎总是包含错误处理的元素。例如,在假设它们成功之前,您必须检查以下函数的返回值:

  • callocmallocrealloc
  • getchar
  • fopen
  • scanf和家人
  • strtok

还有其他非标准功能遵循此模式:

  • pthread_create
  • socketconnect
  • openreadwrite

一般来说,返回值表示成功读取/写入/转换的项目数量或者布局成功/失败的平均值,实际上你几乎总是需要这样的返回值,除非你要去exit(EXIT_FAILURE);任何错误(在这种情况下我宁愿不使用你的模块,因为它们没有机会在我自己的代码中清理)。

有些函数在标准C库中不使用此模式,因为它们不使用任何资源(例如分配或文件),因此不会出现任何错误。如果你的函数是一个基本的翻译函数(比如touppertolower和翻译单个字符值的朋友),那么你就不需要一个返回值来处理错误,因为没有错误。我认为你会发现这种情况非常罕见,但如果这是你的情况,那么一定要使用第一个选项!

总之,为了与世界其他地方保持一致,您应该始终高度考虑使用选项2,为类似用途保留返回值,并且因为您可能稍后决定需要返回值进行通信错误或处理的项目数。

答案 1 :(得分:1)

方法(1)按值传递对象,这需要复制对象。它在您传入时复制,并在返回时再次复制。方法(2)仅传递指针。当你传递一个原语时,(1)就好了,但是当你传递一个对象,一个结构或一个数组时,那只是浪费了空间和时间。

在Java和许多其他语言中,对象总是通过引用传递。在幕后,只复制指针。这意味着即使语法看起来像(1),它实际上也像(2)一样工作。

答案 2 :(得分:1)

在方法2中,我们将x称为输出参数。这实际上是在很多地方使用的非常常见的设计...想一些填充文本缓冲区的各种内置C函数,如snprintf

这样做的好处是节省空间,因为你不会将结构/数组/数据复制到堆栈中并返回全新的实例。

方法2的真正,非常方便的质量是你基本上可以有任意数量的“返回值”。您可以通过输出参数“返回”数据,但也可以从函数返回成功/错误指示符。

有效使用方法2的一个很好的例子是内置的C函数strtol。此函数将字符串转换为long(基本上,从字符串中解析数字)。其中一个参数是char **。调用该函数时,您在本地声明char * endptr,并传入&endptr

该函数将返回:

  • 转换后的值,如果成功,
  • 0如果失败,或
  • LONG_MINLONG_MAX如果超出范围

以及endptr设置为指向它找到的第一个非数字。

如果您的程序依赖于用户输入,这非常适合错误报告,因为您可以通过多种方式检查故障并为每个方法报告不同的错误。

如果在调用endptrnull不是strtol,那么您确切地知道用户输入的是非整数,并且您可以直接打印出该字符如果您愿意,转换失败。

正如Thom指出的那样,Java通过模拟传递引用行为使得实现方法2更简单,这只是在源代码中没有指针语法的情况下的指针。

回答你的问题:我认为C非常适合第二种方法。像realloc这样的函数可以在您需要时为您提供更多空间。但是,没有太多阻止您使用第一种方法。

也许你正试图实现某种不可变对象。第一种方法是那里的选择。但总的来说,我选择了第二个。

答案 3 :(得分:1)

我想我找到了你。

这些方法非常不同。 当你试图决定采取哪种方法时,你必须问自己的问题是:

哪个班级有责任?

如果您将对该对象的引用传递给decapul,则将该对象创建给调用者并创建此功能以提高可维护性,并且您将能够创建一个util类,其中所有函数都将是无状态的,他们正在让对象操纵输入并返回它。

另一种方法更有可能是API,您正在请求操作。

举个例子,你得到的是字节数组,你想把它转换成字符串,你可能会选择第一个approch。

如果你想在DB中做一些操作,你会选择第二个。

当你从第一个approch中有多个函数覆盖同一个区域时,你将它封装到一个util类中,同样的applay到第二个,你将它封装到一个API中。

答案 4 :(得分:1)

简短回答:如果您没有必要采取2的理由,请选择1

答案很长:在C ++及其派生语言的世界中,Java,C#,异常有很大帮助。在C世界中,你无能为力。以下是我从CUDA库中获取的示例API,这是一个我喜欢并考虑设计良好的库:

cudaError_t cudaMalloc (void **devPtr, size_t size);

将此API与malloc

进行比较
void *malloc(size_t size);

在旧的C接口中,有很多这样的例子:

int open(const char *pathname, int flags);
FILE *fopen(const char *path, const char *mode);

我认为世界末日,CUDA所提供的界面非常明显,并导致正确的结果。

还有其他一组接口,有效的返回值空间实际上与错误代码重叠,因此这些接口的设计者抓住了他们的头脑并且提出了不那么精彩的想法,比如说:

ssize_t read(int fd, void *buf, size_t count);

像阅读文件内容这样的日常功能受到ssize_t定义的限制。由于返回值也必须编码错误代码,因此必须提供负数。在32位系统中,ssize_t的最大值为2G,这非常限制了您可以从文件中读取的字节数。

如果您的错误指示符是在函数返回值内编码的,我打赌10/10程序员不会尝试检查它,尽管他们真的知道它们应该;他们只是没有,或者不记得,因为形式并不明显。

另一个原因是,人类是非常懒惰而且不善于处理。这些功能的文档将描述:

如果返回值为NULL,那么......等等。

如果返回值为0,那么......等等。

牦牛。

在第一种形式中,事情发生了变化。您如何判断价值是否已经退回?暂无NULL0。您必须使用SUCCESSFAILURE1FAILURE2或类似内容。该界面迫使用户编写更安全的代码,并使代码更加健壮。

使用这些宏或枚举,程序员更容易了解API的影响以及不同异常的原因。有了所有这些优点,实际上也没有额外的运行时开销。

答案 5 :(得分:1)

(假设我们正在讨论仅从函数中返回一个值。)

通常,当类型T相对较小时,使用第一种方法。标量类型绝对是可取的。它可以用于更大的类型。对于这些目的而言,“足够小”的内容取决于平台和预期的性能影响。 (后者是由复制返回的对象引起的。)

当对象相对较大时使用第二种方法,因为此方法不执行任何复制。对于不可复制的类型,如数组,你别无选择,只能使用第二种方法。

当然,当性能不成问题时,第一种方法可以很容易地用于返回大型对象。

一个有趣的问题是C编译器可用的优化机会。在C ++语言中,允许编译器执行返回值优化(RVO,NRVO),在第二种方法提供更好性能的情况下,有效地将第一种方法转换为第二种方法。为了促进这种优化,C ++语言放松了对所涉及对象施加的一些地址标识要求。 AFAIK,C不提供此类放松,从而防止(或至少阻碍)对RVO / NRVO的任何尝试。

答案 6 :(得分:1)

我会尽力解释:)
假设你必须将一枚巨型火箭加载到半开,

  • 方法1)  卡车司机将一辆卡车放在一个停车场上,然后继续找到一个妓女,你把它放在叉车或某种拖车上以便将它带到赛道上。
  • 方法2) 卡车司机忘记了妓女并将卡车向后靠近火箭,然后你需要将其推入。

那是两者之间的区别:)。在编程中归结为:

  • 方法1) 调用函数为被调用函数保留和地址返回其返回值,但调用函数如何获取该值无关紧要,是否必须保留另一个地址无关紧要,我需要返回的东西,这是你的得到它的工作:)。所谓的函数进行并保留其计算的地址,然后将值存储在地址中,然后将值返回给调用者。所以打电话去说哦谢谢你让我把它复制到我之前保留的地址。
  • 方法2) 来电者的功能说:“嘿,我会帮助你,我会告诉你我保留的地址,存储你在里面做的计算”,这样你不仅可以节省内存,还可以节省时间。

我认为第二个更好,这就是为什么:

所以,假设你的内部有1000个int的结构,方法1没有意义,它必须保留2 * 100 * 32位的内存,这是6400加上你必须将它复制到第一个位置把它复制到第二个。因此,如果每个副本需要1毫秒,则需要6.4秒来存储和复制变量。如果你有地址,你只需要存储一次。

答案 7 :(得分:1)

它们与我相同,但不在实施中。

#include <stdio.h>
#include <stdlib.h>

int func(int a,int b){
    return a+b;
}

int funn(int *x){
    *x=1;
    return 777;
}

int main(void){
    int sx,*dx;
    /* case static' */
    sx=func(4,6); /* looks legit */
    funn(&sx); /* looks wrong in this case */
    /* case dynamic' */
    dx=malloc(sizeof(int));
    if(dx){
        *dx=func(4,6); /* looks wrong in this case */
        sx=funn(dx); /* looks legit */
        free(dx);
    }
    return 0;
}

静态&#39;接近我做第一种方法更舒服。因为我不想搞乱动态部分(有合法的指针)。 但是在充满活力的情况下方法我将使用你的第二种方法。因为它是为它而制造的。 所以它们是等价但不相同的,第二种方法显然是针对指针的,因此对于动态部分。

到目前为止更清楚 - &gt;

int main(void){
    int sx,*dx;
    sx=func(4,6);
    dx=malloc(sizeof(int));
    if(dx){
        sx=funn(dx);
        free(dx);
    }
    return 0;
}

比 - &gt;

int main(void){
    int sx,*dx;
    funn(&sx);
    dx=malloc(sizeof(int));
    if(dx){
        *dx=func(4,6);
        free(dx);
    }
    return 0;
}