C++数组初始化

17

可能是重复的问题:
数组初始化,引用之前的元素是否安全?

我想知道在C/C++标准中这样的初始化是否安全:

int a = 5;
int tab[] = { a , tab[0] + 1 , tab[1] };

它在gcc 4.5和clang 2.9下成功编译并执行,但这种情况是否总是成立?


打印此表将输出5 6 6。它在全局范围内初始化。


通常对于C和C++都很有趣,但我想在C++中使用它:)


6
不是真的没有。 - Mat
1
为什么不直接写成 int tab[] = {a, a+1, a+1} 呢?我不明白它的意义。 - Andreas Grapentin
1
@Tomalak 虽然我知道并不存在 C/C++,但了解两个标准对此的看法仍然是很好的。 - Christian Rau
@Andreas,问题肯定不是如何最好地完成这个任务或者你可以用什么替代方法来完成它,而是这是否是UB。在我看来,这是一个非常有趣的问题,我自己也想知道答案。 - Christian Rau
@Andreas:重点是要知道在列表初始化器中稍后引用先前元素初始化器的规则。看起来很清晰。 - Lightness Races in Orbit
显示剩余3条评论
5个回答

7

C++03/C++11答案


不,它不会。

=的右侧,tab存在1,但是如果它具有自动存储期,则尚未初始化,因此您对tab[0]tab[1]的使用将使用未初始化的变量。

如果tab处于命名空间范围(因此具有静态存储期并已进行了零初始化),则这是“安全”的,但是您在那里使用tab[0]将不会给您5

很难为此提供标准参考资料,除了说在8.5“初始值设定项”中没有明确说明这一点,其他规则填补了其余部分。


1 [n3290: 3.3.2/1]: 名称的声明点位于其完整声明符(第8条)之后,其初始值设定项(如果有)之前[..]


使用g++和clang编译的结果是5 6 6,与我预期的一致。 - qba
2
@qba:请再次阅读我的回答。你无法保证这个结果。 - Lightness Races in Orbit
@Charles:你假设了一些我已经在整个回答中说明不存在的语义。 没有指定在评估初始值设定项中的第二个项目之前是否会写入第一个元素。 并且使用括号初始化符的数组初始化使用列表初始化。 - Lightness Races in Orbit
我理解你在评论中所说的是它是UB(我倾向于相信),因为标准没有在不同元素初始化之间建立序列点。另一方面,你的回答表明对于具有静态持续时间的对象来说是安全的,这与此相矛盾,也就是说,在静态持续时间的情况下仍存在相同的UB来源...不是吗? - David Rodríguez - dribeas
@David:嗯,好问题。我现在明白Charles的意思了。我不太确定。我想这可能是未定义行为... - Lightness Races in Orbit
显示剩余8条评论

5
int a =5;
int tab[] = { a , tab[0] + 1 , tab[1] };

如果这些变量在命名空间范围内声明,那么它们是可以的,因为在命名空间范围内,变量会进行零初始化(由于静态初始化 - 详见此处)。但是,如果它们在函数作用域中声明,那么第二行会引发未定义行为,因为局部变量没有进行静态初始化,也就是说,tab[0]tab[1]是未初始化的,而你使用它们来初始化数组。读取未初始化的变量将引发未定义行为。

那么对于 tab[0],你将得到 0 而不是 5? - Christian Rau
有趣的是你回答了这个评论,因为我看到了你的回答,然后想到这个评论的不完整性。 - Christian Rau

4

现在我已经运行了几个测试来解决你的问题。

所有编译都是使用上面的示例代码,并采用以下方案进行的:

$(GCC) -o a.out test.c -Wall -Wextra -pedantic -std=$(STD)

这导致了以下结果:
对于 GCC = gcc,标准 -std=c89; -std=iso9899:1990; -std=iso9899:199409; -std=gnu89 导致出现一个警告:initializer element is not computable at load time 并在运行时显示未定义的行为,这意味着数组的第二个和第三个值是随机垃圾。
标准 -std=c99; std=iso9899:1999; -std=gnu99 没有产生此警告,但在运行时也表现出未定义的行为。
对于 GCC = g++,标准 -std=c++98; -std=gnu++98; -std=c++0x 没有产生任何警告,并且代码按照预期工作,导致包含值 {5, 6, 6} 的数组。
然而,正如大多数人建议的那样,使用这种方法可能不明智,因为您的代码可能在其他编译器上或甚至同一编译器的其他版本上表现不同,这通常是不好的 : )
希望有所帮助。

你是如何检测到未定义行为的呢?请告诉我。 - Lightness Races in Orbit
顺便说一句,这不仅仅涉及“其他编译器,甚至是同一编译器的不同版本”,而且还包括使用相同编译器的不同运行;或者可能在你的情况下看起来工作正常,但实际上根本没有工作,反而在宇宙中留下了一个手指大小的洞? - Lightness Races in Orbit
@Tomalak 我使用printf()打印了数组的内容。在我看来,这是一种非常可靠的检测UD的方法。我只是陈述了实验结果,并没有声称完整性。如果你找到一个g++创建可执行文件的例子,其中上面的声明不会产生预期的结果,请告诉我 :) - 我猜想,g++检测到这种初始化并适当地处理它们。但是,正如我所说,我不会依赖它,并且肯定永远不会使用它。 - Andreas Grapentin
在许多情况下,从数学上讲,“检测”未定义行为是不可能的。当然,使用 printf 输出某些内容绝对不是一种未定义行为检测工具。 - Lightness Races in Orbit
当然,精确检测通常是不可能的。但是,我们在这里处理的情况相当简单,可以通过排除所有其他可能导致“出错”的来源来识别未定义行为,如果出现了问题。当出现问题时,它不是UB的概率微乎其微,因此可以通过至少运行每个测试两次来几乎完全消除它。 - Andreas Grapentin
感谢您的快速分析,尽管这并未说明代码是否符合标准。如果不同的编译器设置会产生不同的输出,我认为避免使用这种初始化是可行的。但是我认为在Haskell中定义无限数组(因为它的惰性求值)相当正常。+1 - Alexander Oh

4
在C99标准中,成员初始化顺序似乎是有保证的:
§6.7.8/17:每个花括号括起来的初始化列表都有一个相关联的当前对象。当没有指定时,当前对象的子对象按照当前对象类型的顺序进行初始化:数组元素按递增下标顺序初始化,结构体成员按声明顺序初始化,并且联合体的第一个命名成员。相比之下,指定会导致以下初始化程序开始初始化由描述符描述的子对象。然后初始化继续向前按顺序进行,从描述符所描述的下一个子对象开始。
但正如@Tomalak在评论中提到的那样,这并不能完全保证操作的顺序,因为编译器首先会评估所有参数,然后按照先前的顺序应用结果。也就是说,上述引用并未规定tab[0]的初始化顺序和用于初始化tab[1]的表达式tab[0]+1的求值顺序之间的顺序(它只规定了tab[0]tab[1]的初始化顺序)。
至于C++标准,无论是当前标准还是即将发布的C++0x标准的FDIS,似乎都没有特定条款定义初始化的顺序。唯一提到顺序的是:
§8.5.1/2 当初始化聚合体时,初始化器可以包含一个由逗号分隔的大括号括起来的初始化器列表,用于聚合体成员,按照递增下标或成员顺序编写。
但这只涉及初始化程序中条目的书写顺序,而不涉及实际评估的顺序。

C99引用并不保证操作数的评估发生在这个顺序初始化过程中。 - Lightness Races in Orbit

0

是的 - 它很可能会按照您的期望工作。
不是 - (你没有问,但)不要使用它,它没有逻辑性并且是一种不好的做法。


定义 "probably"。这里的正确答案是 "不",不要掩饰它。 - Lightness Races in Orbit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接