1

在 C 中编写函数时,哪些因素决定了函数返回结果的方式?我的问题来自阅读以下代码,该代码在双向链表中创建了一个节点,并且来自本书

typedef struct DLLIST
{
    uint16 tag;                 /* This node's object type tag */
    struct DLLIST *next;
    struct DLLIST *previous;
    void *object;
    uint32 size;                /* This node's object size */
} DLLIST;

DLLIST *dlCreate(uint16 tag, void *object, uint32 size)
{
    DLLIST *newNode;

    newNode = malloc(sizeof(*newNode));

    if (newNode != NULL)
    {
        newNode->previous = NULL;
        newNode->next = NULL;
        newNode->tag = tag;
        newNode->size = size;
        newNode->object = malloc(size);

        if (newNode->object != NULL)
        {
            memcpy(newNode->object, object, size);
        }
        else
        {
            free(newNode);
            newNode = NULL;
        }
    }

    return newNode;
}

事实上,作为初学者,我发现这个函数的许多方面令人困惑:

  1. 为什么 DLLIST 指针以这种方式返回,而不是作为引用指针传递并修改?如果你这样做了,你可以释放返回值以用作报告函数成功/失败的状态字节。
  2. 为什么不测试第二个参数以确保指针不为 NULL?将 NULL 指针传递给 memcpy 肯定是一件非常糟糕的事情吗?

此外,强制模块中的所有函数使用相同的调用约定是常见的做法吗?例如,从上面的函数中提取的双向链表模块中的所有函数都应该符合status byte = function(argument list)吗?此外,验证所有值可能不正确的函数参数是否普遍?例如,检查所有指针值以确保它们不为空?

4

3 回答 3

2

我不得不说,这都是相当主观的。

1)为什么DLLIST指针以这种方式返回,而不是作为引用指针传递并修改?如果你这样做了,你可以释放返回值以用作报告函数成功/失败的状态字节。

这在某种程度上是一个品味问题。我更喜欢将指针传递给函数,以便调用者管理内存,尽管返回动态分配的指针也是有效的。在某些情况下,它会更好更简单,具体取决于 API 架构。无论哪种方式,返回值都可用于成功/失败检查,因为返回NULL指针将指示失败。

2)为什么不测试第二个参数以确保指针不为NULL?肯定将 NULL 指针传递给 memcpy 是一件非常糟糕的事情吗?

这是很多 C 程序员(反正我知道的)的一种心态,这也反映了该语言是如何构建的。在大多数 C 库中,每个函数都有一个契约,不尊重它会调用未定义的行为。我很少费心检查NULL传递给函数的参数。当我这样做时,是因为NULL指针改变了函数的行为,而不是因为我确保调用者尊重合同。有些人可能会争辩说它在性能方面和/或大小方面更好,因为它在这里和那里节省了一些指令(尤其是在有限的硬件上确实如此,C 以 C 闻名),我这样做主要是为了不让我的代码与 null - 到处检查。只要有正确的文档记录,您都可以这样做。

此外,强制模块中的所有函数使用相同的调用约定是常见的做法吗?

我必须同意这一点。我不能代表别人说话,但我相信被普遍接受为良好做法。在同一 API 中的函数之间切换调用约定可能会使程序员感到困惑,并让他们浪费时间一遍又一遍地检查文档。

此外,验证所有值可能不正确的函数参数是否普遍?

这很常见,但相反的情况可能也很常见(请参阅上面的#2)。

于 2013-01-08T19:47:43.670 回答
1

为什么 DLLIST 指针以这种方式返回,而不是作为引用指针传递并修改?如果你这样做了,你可以释放返回值以用作报告函数成功/失败的状态字节。

通过从外部传入的参数返回结果是一种相当不优雅和繁琐的做法,只有在没有其他方法的情况下才应该使用它。它要求调用者定义一个接收者对象,这本身就已经很糟糕了。但除此之外,对附加声明的需要排除了在纯表达式中使用该函数。

大多数时候,如果你可以通过纯函数来实现你的功能,那么它应该通过纯函数来实现。即函数参数用于输入,函数返回值用于输出。这正是您在这种情况下看到的。

为什么不测试第二个参数以确保指针不为 NULL?将 NULL 指针传递给 memcpy 肯定是一件非常糟糕的事情吗?

好吧,没有争论,执行这样的测试总是一个好主意。但是,确切的测试方法可能会有所不同,具体取决于程序层次结构中函数的“级别”(从“低级别”到“高级”)。它应该是仅调试断言吗?它应该是一个永久断言(即受控中止,带有错误日志和“可能打电话给我”消息)吗?它应该是一个运行时测试,它意味着一些有意义的恢复策略吗?回答这个问题没有“一条真正的规则”。这是代码设计的问题。

是否有一个有意义的故障安全策略应该在测试失败的情况下执行?如果不是,那么在低级函数中进行运行时测试将毫无意义。它只会使代码杂乱无章,并浪费性能。如果这确实是一个“低级”功能,那么运行时测试在if这里并不合适。更合适的是仅调试断言assert(object != NULL && size > 0)

另请注意,使object != NULL测试特别适合的dlCreate细节是该object值被传递给库函数memcpy,已知在输入空指针的情况下会产生未定义的行为。如果memcpy使用某些专有my_copy_data功能而不是代码,那么断言有效性的适当位置object将是my_copy_data. dlCreate函数本身并不真正关心值的有效性object

于 2013-01-08T20:01:07.077 回答
1

回答问题 1:返回值有三种常见约定,您可以选择最适合您的应用程序的一种:

  1. 返回状态值(int)。通常负值表示错误,>=0 成功。例如,POSIX“open()”函数。如果您需要传递某个对象,则传递一个指向它的指针(对于“IN”参数,或指向它的指针的指针(对于“OUT”参数)
  2. 对于“面向对象”函数,特别是对于分配某些东西的函数,返回一个指向对象的指针(如您给出的双向链表示例)。返回 NULL 表示失败。以 POSIX“fopen()”为例。
  3. 绝对值函数。这些值并不表示成功或失败,而是某些计算的实际结果(想想“sin()”)。

您可以根据自己的口味混合搭配这些款式。许多图书馆都有混合。尽量不要让 API 的用户不得不扭曲才能使用它!

例如,使用上面的代码,如果您将 API 更改为

int dlCreate(uint16 tag, void *object, uint32 size, DLLIST **list)

现在用户有两种方法可以确定文件是否已打开(查看退出状态,并查看填充的列表指针),并且必须做很多额外的工作来确定什么是正确的为错误。所以不要写

DLLIST *list;
if ((list = dlCreate(tag, object, sz) == NULL) {
  // handle out of memeory error
  ...

用户不得不说

DLList *list = NULL // must initializein this case
if (dlCreate(tag, object, sz, &list) == ERROR || list == NULL)  {
  if (list != NULL) {
    free(list);
  }
  // and other error handling as above

也就是说,为库使用一致的样式通常是一种好习惯。

回答问题 2:这是草率的编程,你是对的;一个好的程序员永远不应该相信调用者“做正确的事”。始终添加代码来验证参数。如果你在这里传递了一个错误的指针,你会崩溃!

于 2013-01-08T20:05:10.403 回答