在用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;
}
事实上,作为初学者,这个功能的许多方面让我感到困惑:
此外,强制模块中的所有功能使用相同的调用约定是否是常见的做法?例如,采取上述功能的双链接列表模块中的所有函数是否都符合status byte = function(argument list)
?此外,验证所有值可能不正确的函数参数是否是常见的做法?例如,检查所有指针值以确保它们不是null?
发布于 2013-01-08 20:01:07
为什么DLLIST指针是以这种方式返回的,而不是作为引用指针传递和修改?如果这样做,则可以释放返回值,作为报告函数成功/失败的状态字节。
通过从外部传入的参数返回结果是一种非常不优雅和繁琐的做法,只有在没有其他方法时才应该使用。它要求调用方定义一个接收对象,它本身就已经很糟糕了。但最重要的是,由于需要额外的声明,所以不能在纯表达式中使用函数。
大多数情况下,如果您可以通过一个纯函数实现您的功能,那么它应该通过一个纯函数来实现。即函数参数为输入,函数返回值为输出。这正是你在这个案例中所看到的。
为什么没有测试第二个参数以确保指针不是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-08 19:47:43
我不得不说,这都是相当主观的。
1)为什么DLLIST指针以这种方式返回,而不是作为引用指针传递和修改?如果这样做,则可以释放返回值,作为报告函数成功/失败的状态字节。
这在一定程度上是品味问题。我更喜欢将指针传递给函数,以便调用者管理内存,尽管返回动态分配的指针也是有效的。在某些情况下,它更好、更简单,这取决于API架构。无论哪种方式,返回值都可以用于成功/失败检查,因为返回NULL
指针将指示失败。
2)为什么没有测试第二个参数以确保指针不是空的?将空指针传递给memcpy肯定是一件非常糟糕的事情?
这是许多C程序员的一种心态(至少我知道的那些人),这也反映了语言是如何构建的。在大多数C库中,每个函数都有一个契约,而不尊重它会调用未定义的行为。我很少费心检查传递给函数的NULL
参数。当我这样做的时候,这是因为NULL
指针改变了函数的行为,而不是因为我确保调用者尊重契约。有些人可能会说,它的性能更好--从性能和/或大小上看--因为它到处都节省了一些指令(尤其是在有限的硬件上,因为有C语言而出名),我这样做主要是为了不让我的代码到处都是空检查。你可以这样做,只要它有适当的文档。
此外,强制模块中的所有功能使用相同的调用约定是否是常见的做法?
我必须同意这一点。我不能代表别人说话,但我相信大家都认为这是个好做法。在同一个API中的函数之间切换调用约定可能会使程序员感到困惑,并让他们浪费时间反复检查文档。
此外,验证所有值可能不正确的函数参数是否是常见的做法?
这是很常见的,但相反的情况可能也很常见(参见上面的第2条)。
发布于 2013-01-08 20:05:10
回答问题1:返回值有三个常见的约定,您可以选择最适合您的应用程序:
你可以根据自己的口味混合和搭配这些风格。许多图书馆都有一个混合体。尽量不要让API的用户不得不做扭曲才能使用它!
例如,如果您将API更改为
int dlCreate(uint16 tag, void *object, uint32 size, DLLIST **list)
现在,用户有两种方法来判断文件是否被打开(查看退出状态,并查看已填充的列表指针),并且必须做大量额外的工作才能找出对错误正确的thiong操作。所以不要写
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:这是草率的编程,你是对的;一个好的程序员应该永远不要相信调用者“做正确的事情”。始终添加代码来验证参数。如果你在这里传递了一个糟糕的指针,你就会崩溃!
https://stackoverflow.com/questions/14222721
复制相似问题