请考虑以下代码:
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
void foo(const char *arg, ...) {
va_list args_list;
va_start(args_list, arg);
for (const char *str = arg;
str != NULL;
str = va_arg(args_list, const char *)) {
printf("%s\n", str);
}
va_end(args_list);
}
int main(int argc, char **argv) {
foo("Some", "arguments", "for", "foo", NULL);
foo("Some", "arguments", "for", "foo", 0);
return 0;
}
正如我们所看到的,foo()
使用变量参数列表来获取字符串列表然后将它们全部打印出来。假设最后一个参数是空指针,因此处理参数列表直到检测到NULL
。
函数foo()
以两种不同的方式从main()
调用,NULL
和0
作为最后一个参数。
我的问题是:第二次调用0
,因为最后一个参数是正确的吗?
我想,我们不应该使用foo()
致电0
。原因是在这种情况下,例如,编译器无法从上下文中猜测0
应该被视为空指针。所以它将它作为通常的整数处理。然后foo()
处理0
并将其投放到const char*
。当空指针具有与0
不同的内部表示时,魔术就开始了。我可以理解它导致检查失败str != NULL
(因为str
将等于0
已转换为const char*
,这与我们情况中的空指针不同)和错误程序行为。
我的想法是对的吗?任何好的解释都表示赞赏。
答案 0 :(得分:8)
通常,两个调用都不正确。
裸0
的通话肯定是不正确的,但不是因为您声明的原因。在编译对具有可变参数的函数foo()
的调用时,编译器无法知道foo()
期望的类型。
如果将0投射到const char *
,那就没问题;即使空指针具有与all-bits-zero不同的内部表示,该语言也保证在指针上下文中使用值0会产生空指针。 (这可能要求编译器为类型转换实际生成一些非平凡的代码,但如果是这样,则需要这样做。)
但它没有理由认为0本来是一个指针。相反,它将作为int
传递0。如果int
具有与指针不同的大小,或者由于任何其他原因,int
0具有与空指针不同的表示,或者如果此系统传递指针参数,则这可能导致问题与整数不同的方式。
所以这是未定义的行为:foo
使用va_arg
来获取实际作为const char *
类型传递的int
类型的参数。
使用NULL
怎么样?根据{{3}}及其中的引用,C标准允许将宏NULL
定义为简单0
或任何其他“值为0的整数常量表达式”。与流行的看法相反,它不一定是(void *)0
,尽管可能是NULL
。
因此传递裸0
是不安全的,因为您可能位于定义为 foo("Some", "arguments", "to", "foo", (const char *)0);
的平台上。然后,您的代码可能会因上述原因而失败。
为了安全和便携,您可以写下:
foo("Some", "arguments", "to", "foo", (const char *)NULL);
或
{{1}}
但你不能放弃演员。
答案 1 :(得分:7)
第二次调用不正确,因为您传递类型为int
的参数,而您使用const char*
获取类型为va_arg
的参数。这是未定义的行为。
第一次调用仅在NULL
声明为(void*)0
或类似时才正确。请注意,根据标准,NULL
仅需要是空指针常量。它不必定义为((void*)0)
,但通常就是这种情况。某些系统将NULL
定义为0
,在这种情况下,第一个调用是未定义的行为。 POSIX mandates表示“宏应扩展为整数常量表达式,值为0强制转换为 void * ”,因此在类似POSIX的系统上,您可以安全地假设{{1是} NULL
。
以下是ISO 9899:2011§6.5.2.2的相关标准引用:
6.5.2.2函数调用
(...)
6如果表示被调用函数的表达式具有不包含原型的类型,则对每个参数执行整数提升,并将类型为
((void*0)
的参数提升为float
。这些被称为默认参数促销。如果参数数量不等于参数数量,则行为未定义。如果使用包含原型的类型定义函数,并且原型以省略号(double
)结尾或者促销后的参数类型与参数类型不兼容,则行为未定义。 如果使用不包含原型的类型定义函数,并且促销后的参数类型与促销后的参数类型不兼容,则行为未定义,但以下情况除外:
- 一个提升类型是有符号整数类型,另一个提升类型是相应的无符号整数类型,并且该值可在两种类型中表示;
- 这两种类型都是指向字符类型的合格或非限定版本或
, ...
的指针。7如果表示被调用函数的表达式具有包含原型的类型,则将参数隐式转换为相应参数的类型,就像通过赋值一样,将每个参数的类型设置为不合格其声明类型的版本。函数原型声明符中的省略号表示法导致参数类型转换在最后声明的参数之后停止。默认参数 促销是在尾随参数上进行的。
8不会隐式执行其他转换;特别是,参数的数量和类型不与不包含函数原型声明符的函数定义中的参数进行比较。
¶8澄清了为void
参数传递时整数常量0
未转换为指针类型。