为啥零长度 VLA 是 UB?
Posted
技术标签:
【中文标题】为啥零长度 VLA 是 UB?【英文标题】:Why Are Zero Length VLAs UB?为什么零长度 VLA 是 UB? 【发布时间】:2016-01-25 22:51:15 【问题描述】:2011 年标准明确规定...
6.7.6.2 数组声明符
如果大小是一个不是整数常量表达式的表达式:如果它出现在一个 在函数原型范围内声明,它被视为被替换为
*
;否则, 每次对其进行评估时,它的值都应大于零。每个实例的大小 可变长度数组类型在其生命周期内不会改变。如果大小表达式是sizeof
运算符的操作数的一部分,并且更改大小表达式的值不会影响运算符的结果,则未指定是否 计算大小表达式。
这是人为的,但下面的代码似乎是合理的。
size_t vla(const size_t x)
size_t a[x];
size_t y = 0;
for (size_t i = 0; i < x; i++)
a[x] = i;
for (size_t i = 0; i < x; i++)
y += a[i % 2];
return y;
Clang 似乎为它生成了合理的 x64 程序集(没有优化)。显然索引一个零长度的 VLA 没有意义,但是越界访问会调用未定义的行为。
为什么零长度数组未定义?
【问题讨论】:
C 也不允许零长度的非 VLA;不允许它们作为 VLA 是一致的。 GCC(因此也有 clang)具有允许零长度数组的扩展。你可以争论这是否好。 “显然索引零长度 VLA 没有意义,但越界访问会调用未定义的行为。” - 示例中都没有发生。 @KarolyHorvath 我的想法是索引零长度的东西已经被禁止了。与空列表或零长度向量类似,只要值没有被索引(语言已经禁止),零长度数组对我来说是有意义的。 @JonathanLeffler 有趣的是,std::array in C++ does special case 的长度为零。 @Jason,该语言不禁止索引零长度数组-语法允许!只有越界访问的结果是UB。这适用于所有数组,与类型或大小无关。 【参考方案1】:int i = 0;
int a[i], b[i];
是a == b
?它不应该——它们是不同的对象——但避免它是有问题的。如果您无条件地在a
和b
之间留下一个空白,那么您在i > 0
案例中浪费了空间。如果您检查是否i == 0
并且只留下一个空白,那么您在i > 0
案例中浪费时间。
多维数组会变得更糟:
int i = 0;
int a[2][i];
你可以在两个变量之间填充,但是你可以在哪里填充呢?如果不破坏sizeof (int[2][i]) == 2 * i * sizeof (int)
的不变量,就没有办法做到这一点。如果您不填充,那么a[0]
和a[1]
具有相同的地址,并且您正在破坏不同的重要不变量。
这是一个不值得定义的令人头疼的问题。
【讨论】:
"如果是肯定的,我会为 a 和 b 分配相同的地址" - 这是脑放屁还是一些错字?这没有任何意义...... @KarolyHorvath: "for positivei
" 附加到它前面的短语,而不是后面的短语。生成的代码(对于积极的i
来说是合理且节省空间的)将为a
和b
分配相同的地址为i == 0
。
刮掉整个文本并从零开始。它仍然是一个巨大的混乱。
@KarolyHorvath:对我来说似乎并不难解析,但我写了它。现在怎么样?
我喜欢我们的回答,但是“如果你在 a 和 b 之间无条件地留出一个间隙,那么你在 i > 0 的情况下是在浪费空间”,如果“无条件”意味着“至少一个元素”(如果你明白我的意思的话)。如果 i>0 则没有浪费。【参考方案2】:
虽然我们可以看到gcc supports zero length arrays an extension,但很明显它们很有用。从标准的角度来看,这似乎会产生一些问题,因为现在每个对象都应该有一个唯一的地址。我们可以从草案 C99 和 C11 标准第 6.5.9 节平等运算符中看到这一点:
两个指针比较相等当且仅当两者都是空指针,两者都是指向 相同的对象(包括指向对象的指针和开头的子对象)或函数, 两者都是指向同一数组对象的最后一个元素的指针,或者一个是指针 一个指向一个数组对象的末尾,另一个是指向另一个数组对象开头的指针 恰好紧跟在地址中的第一个数组对象之后的数组对象 空间.94)
因此,这需要一些特殊的外壳,并且可以使用其他方法提供大部分有用的功能,例如灵活数组。
它也可能需要在其他地方进行更改,如 M.M.在6.3.2.1
左值、数组和函数指示符中指出数组到指针的衰减:
[...]一个表达式 type ‘‘array of type’’ 被转换为类型为 ‘‘pointer to type’’ 的表达式,它指向 到数组对象的初始元素并且不是左值[...]
这似乎需要进行一些重要的更改才能获得最小的附加收益。
【讨论】:
"或者一个是指向一个数组对象末尾的指针,另一个是指向另一个数组对象的开头的指针,该数组对象恰好紧跟在第一个数组对象之后地址空间”意味着必须分配至少一个元素,但是,x 仍然可以是零,功能和实际,运行时现在将分配一个元素。如果我们期望 a 和 b 在内存中是连续的,并且想要计算 b 之后的 c 的地址为c==a+sizeof(a)+sizeof(b)
,那么 UB 将跟随,因为 a 和 b 现在不是零大小(除非 sizeof 可以处理)。
它和malloc(0)
有什么不同?
@Jason from 7.20.3
如果请求的空间大小为零,则行为由实现定义:返回空指针,或者行为好像大小是一些非零值,但返回的指针不得用于访问对象
谢谢。实际上,定义的实现对我来说似乎更直观。不过,我不是编译器作者或语言维护者。【参考方案3】:
看C标准:
C11- 6.7.6.2 数组声明符(p1):
[...] 如果表达式是一个常量表达式,它应该有一个大于零的值。 [...]
(p5):
如果大小是一个不是整数常量表达式的表达式:如果它出现在函数原型范围的声明中,则将其视为被
*
替换;否则,每次对其进行评估时,应具有大于零的值。 [...]
4。一致性:
如果违反了出现在约束或运行时约束之外的“应”或“不应”要求,则行为未定义。未定义的行为在本国际标准中以“未定义的行为”一词或省略任何明确的行为定义来表示。 这三者在侧重点上没有区别;它们都描述了“未定义的行为”。
因此,声明一个大小为零的数组会导致程序的未定义行为。
【讨论】:
"因此,C 不允许数组长度为零。" - 为什么?那里的解释有很大的跳跃。 @KarolyHorvath;我不是说通俗地说吗?现在我正在等待你解释清楚的答案。 我只是指出我不明白你的解释。我希望你习惯于无限繁忙循环或无限阻塞 API 调用,因为我没有解释。并不是说我需要对您的模糊解释发表评论。 我们是否可以使用pointerToObject = &vla[0]
之类的方式访问vla[0]
?
@Jongware;不,我们不能。以上是关于为啥零长度 VLA 是 UB?的主要内容,如果未能解决你的问题,请参考以下文章