this answer评论部分的一个论点促使我提出这个问题。
在下面的代码中,bar
指向一个可变长度数组,因此sizeof
是在运行时而不是编译时确定的。
int foo = 100;
double (*bar)[foo];
该论点是关于当操作数是可变长度数组时是否使用sizeof
评估其操作数,当sizeof(*bar)
未初始化时使bar
未定义行为。
使用sizeof(*bar)
是不确定的行为,因为我正在取消引用未初始化的指针?当类型是可变长度数组时,sizeof
的操作数是否实际被评估,或者它只是确定它的类型(sizeof
通常如何工作)?
编辑:每个人似乎都在引用C11选秀中的this passage。有谁知道这是否是官方标准中的措辞?
是的,这会导致未定义的行为。
在N1570 6.5.3.4/2中我们有:
sizeof运算符产生其操作数的大小(以字节为单位),该操作数可以是表达式或类型的带括号的名称。大小由操作数的类型确定。结果是整数。如果操作数的类型是可变长度数组类型,则计算操作数;否则,不评估操作数,结果是整数常量。
现在我们有一个问题:*bar
的类型是可变长度数组类型吗?
由于bar
被声明为指向VLA的指针,因此取消引用它应该产生VLA。 (但我没有看到具体的文字说明是否这样做)。
注意:这里可以进行进一步的讨论,也许可以说*bar
的类型double[100]
不是VLA。
假设我们同意*bar
的类型实际上是VLA类型,那么在sizeof *bar
中,表达*bar
被评估。
bar
在这一点上是不确定的。现在看6.3.2.1/1:
如果左值在评估时未指定对象,则行为未定义
由于bar
没有指向一个对象(由于是不确定的),评估*bar
会导致未定义的行为。
另外两个答案已经引用了N1570 6.5.3.4p2:
sizeof
运算符产生其操作数的大小(以字节为单位),该操作数可以是表达式或类型的带括号的名称。大小由操作数的类型确定。结果是整数。如果操作数的类型是可变长度数组类型,则计算操作数;否则,不评估操作数,结果是整数常量。
根据标准中的那一段,是的,sizeof
的操作数被评估。
我要说这是标准中的缺陷;在运行时评估某些东西,但操作数不是。
让我们考虑一个更简单的例子:
int len = 100;
double vla[len];
printf("sizeof vla = %zu\n", sizeof vla);
根据标准,sizeof vla
评估表达式vla
。但是,这是什么意思?
在大多数情况下,评估数组表达式会产生初始元素的地址 - 但sizeof
运算符是一个明确的例外。我们可以假设评估vla
意味着访问其元素的值,这些元素具有未定义的行为,因为这些元素尚未初始化。但是没有其他上下文对数组表达式的求值访问其元素的值,在这种情况下绝对不需要这样做。 (更正:如果使用字符串文字初始化数组对象,则会评估元素的值。)
当执行vla
的声明时,编译器将创建一些匿名元数据来保存数组的长度(它必须,因为在定义len
并且分配后不会改变vla
的长度时为vla
分配新值)。确定sizeof vla
所需要做的就是将存储的值乘以sizeof (double)
(或者只是为了检索存储的值,如果它以字节为单位存储大小)。
sizeof
也可以应用于带括号的类型名称:
int len = 100;
printf("sizeof (double[len]) = %zu\n", sizeof (double[len]));
根据标准,sizeof
表达式评估类型。那是什么意思?显然,它必须评估len
的当前价值。另一个例子:
size_t func(void);
printf("sizeof (double[func()]) = %zu\n", sizeof (double[func()]));
这里的类型名称包括函数调用。评估sizeof
表达式必须调用该函数。
但是在所有这些情况下,没有实际需要评估数组对象的元素(如果有的话),并且没有必要这样做。
sizeof
适用于VLA以外的任何其他任何东西都可以在编译时进行评估。将sizeof
应用于VLA(对象或类型)时的差异在于必须在运行时评估某些内容。但是必须要评估的事情不是sizeof
的操作数;它只是确定操作数大小所需要的,而不是操作数本身。
该标准表示如果该操作数是可变长度数组类型,则评估sizeof
的操作数。这是标准的缺陷。
回到问题中的示例:
int foo = 100;
double (*bar)[foo] = NULL;
printf("sizeof *bar = %zu\n", sizeof *bar);
我已经为NULL
添加了一个初始化,以便更清楚地解释引用bar
有未定义的行为。
*bar
是int[foo]
类型,是VLA类型。原则上,*bar
被评估,由于bar
未初始化,因此会有不确定的行为。但同样,没有必要取消引用bar
。编译器在处理类型int[foo]
时会生成一些代码,包括将foo
(或foo * sizeof (int)
)的值保存在匿名变量中。评估sizeof *bar
所需要做的就是检索该匿名变量的值。如果标准被更新以一致地定义sizeof
的语义,那么很明显,评估sizeof *bar
是明确定义的并且产生100 * sizeof (double)
而不必解除引用bar
。
事实上,该标准似乎暗示行为未定义:
重新引用N1570 6.5.3.4/2:
sizeof运算符产生其操作数的大小(以字节为单位),该操作数可以是表达式或类型的带括号的名称。大小由操作数的类型确定。结果是整数。如果操作数的类型是可变长度数组类型,则计算操作数;否则,不评估操作数,结果是整数常量。
我认为标准中的措辞令人困惑:评估操作数并不意味着将评估*bar
。评估*bar
不会以任何方式帮助计算其大小。 sizeof(*bar)
确实需要在运行时计算,但为此生成的代码不需要取消引用bar
,它更有可能从保存bar
实例化时大小计算结果的隐藏变量中检索大小信息。