【问题标题】:Does C++ zero-initialise unspecified values during aggregate initialization?C++ 在聚合初始化期间是否对未指定的值进行零初始化?
【发布时间】:2020-07-18 01:30:55
【问题描述】:

一个有趣的问题是这里一些其他问题的副作用,关于 C 和 C++ 处理方式(非静态存储持续时间)之间可能存在的差异:

int arr[7] = {0};

有人说,在 C++ 中,其他元素不能保证为零,但我不确定我是否同意。

现在 C11 状态,在6.7.9 Initialization /19

初始化应按初始化器列表顺序进行,为特定子对象提供的每个初始化器都将覆盖先前为同一子对象列出的任何初始化器; 所有未显式初始化的子对象都应隐式初始化,与具有静态存储持续时间的对象相同。

这意味着arr 的其他六个元素初始化为零(因为static int x; 会将x 初始化为零)。


我不确定 C++ 是否也是这种情况。在 C++20 标准中,9.3.1 Aggregates /3 声明:

当聚合被9.3.4 中指定的初始化列表初始化时,初始化列表中的元素将作为聚合元素的初始化。聚合的显式初始化元素确定如下:

(3.1) —(与指定的初始化列表和类无关的东西 - pax)。

(3.2) — 如果初始化器列表是初始化器列表,则聚合的显式初始化元素是聚合的第一个 n 元素,其中 n 是初始化器列表中的元素数。

然后/4 说明显式 初始化如何工作,/5 处理非显式情况:

对于非联合聚合,每个不是显式初始化元素的元素都被初始化如下:

(5.1) — 如果元素具有默认成员初始化程序 (10.3),则从该初始化程序初始化元素。

(5.2) — 否则,如果元素不是引用,则从空的初始化列表 (9.3.4) 复制初始化元素。

(5.3) — 否则,程序格式错误。

在我看来,(5.2) 涵盖了我们的特殊情况,因此我们必须转到 9.3.4 以查看使用空列表 ({}) 初始化的 int 会发生什么。这经历了很多案例,但我相信第一个匹配的是:

(3.11) — 否则,如果初始化列表没有元素,则对象被值初始化。

还有,来自9.3 Initializers /8

对 T 类型的对象进行值初始化意味着:

(8.1) — 如果 T 是(可能是 cv 限定的)类类型(第 10 条),没有默认构造函数 (10.3.4) 或用户提供或删除的默认构造函数,则该对象是默认的-初始化;

(8.2) — 如果 T 是一个(可能是 cv 限定的)类类型,没有用户提供或删除的默认构造函数,则对象为零初始化并检查默认初始化的语义约束,如果 T有一个重要的默认构造函数,对象是默认初始化的;

(8.3) — 如果 T 是一个数组类型,那么每个元素都是值初始化的;

(8.4) — 否则,对象被零初始化。

因此,8.4 似乎是控制子句,这意味着 C++还将将数组的非显式元素初始化为零。

我的推理正确吗? C++ 会在遇到int arr[7] = {0}; 时将所有元素设置为零吗?

【问题讨论】:

  • "有人说,在 C++ 中,不能保证其他元素为零"这是在哪里说的?
  • int arr[7] = {0}; 会将第一个元素显式初始化为零,然后编译器将添加代码将其余元素初始化为零。
  • 简短回答:
  • @NicolBolas:重读评论时,他们实际上表示int arr[n]={value} 并不能保证所有元素都是value。所以,它们对于value != 0 是正确的,但是由于我 使用{0} 作为初始化列表,我觉得这很奇怪。我只是想澄清一下,因为 ISO C++so 比 ISO C 更令人头疼:-)
  • 我的理解是“是的”——您对当前(草案?)标准的推理和跟踪似乎是合适的。尽管最近的标准使事情变得更加复杂,但在 1998 年的 C++ 标准中,答案也毫不含糊地“是”——如果后续标准会悄悄地改变这一点,我会感到非常惊讶,因为很多现有代码都会中断。

标签: c++ arrays initialization


【解决方案1】:

是的。 C++ 通常保持与 C 的向后兼容性,允许您包含和使用 C 代码。考虑一下您是否有一些遗留的 C 代码,它会尝试像您描述的那样初始化一个数组:

int arr[7] = {0};

如果 C++ 的工作方式有所不同,并且 C 程序(在 C 下有效)假定此数组初始化为零,则如果包含在使用 C++ 编译器编译的 C++ 项目中,则代码可能会失败。

为了确认,我在 x64 Windows 上使用 Cygwin g++ 编译了这个 C++ 程序:

int main() {
    int arr[7] = {0};
}

然后在 GDB 中反汇编函数main

push   %rbp
mov    %rsp,%rbp
sub    $0x40,%rsp
callq  0x1004010d0 <__main>
movq   $0x0,-0x20(%rbp)
movq   $0x0,-0x18(%rbp)
movq   $0x0,-0x10(%rbp)
movl   $0x0,-0x8(%rbp)
mov    $0x0,%eax
add    $0x40,%rsp
pop    %rbp
retq

如您所见,程序将 3 个 qword 和 1 个 dword 的零移入堆栈。那是 28 个字节,在我的系统上是 7 个整数的大小。

【讨论】:

  • 其实,这是有道理的。虽然它不保持 完美 向后兼容性(例如:int new = 7;,但我猜你已经被覆盖了,因为你使用了“一般”这个词),你想在两个 C 中使用的代码如果数组的初始化方式不同,C++ 肯定是没用的。
  • 查看具有“预期”行为的编译器并不能确认行为。 C++ 有太多的 UB、未指定的行为、实现定义的行为,无法从中得出结论。这只是一个提示。该提示可以通过乘以编译器和编译器选项(尤其是打开优化)来强制执行。
  • Jarod42 无论如何,我们仍然有来自标准本身的证据以及关于 C 向后兼容性的推理。
【解决方案2】:

一个非常晚的附加答案,以便在此处添加一个相当官方的参考:

cppreference (for C) with Array initialization 有一个明确的例子:

int a[3] = {0}; // valid C and C++ way to zero-out a block-scope array
int a[3] = {}; // invalid C but valid C++ way to zero-out a block-scope array

所以你的具体例子的事情是完全清楚的。

有人说,在 C++ 中,其他元素不是 保证为零,但我不确定我是否同意。

这完全是错误的。作为同一参考:

所有未显式初始化的数组元素都是 零初始化

C++ 标准必须明确标记这种缺失的向下兼容性,但它没有。但从 http://eel.is/c++draft/dcl.init#general-16.5 开始,对于具有聚合抽象的 C++:

剩余的元素用它们的默认成员初始化 初始化器(如果有),否则为值初始化。

整数为零,见Zero initialization

如果 T 是标量类型,则对象的初始值为整数 常量零显式转换为 T。

所以int a[3] = {0}; 实际上并不是编译器说“将所有元素写入零”的单一语句,而是一个两步顺序:“使用此值显式初始化第一个元素(恰好已经为零),根据它们的默认值初始化器/默认值初始化来初始化其余元素,即 int" 为零。

这里有一点混乱的原因可能是因为 C 的 C++ 标准草案使一些东西变得非常抽象。很多程序员可能会认为剩余元素的值初始化可能与保持变量/成员未初始化一样具有相同的质量,但事实并非如此。

另外请注意,静态持续时间的情况在这里是一个额外的主题......

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-29
    • 1970-01-01
    • 2015-04-24
    • 1970-01-01
    • 2021-11-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多