C++ 声明和初始化/变量未初始化为默认值

时间:2021-05-14 13:22:52

标签: c++11

案例 1->

int a;
std :: cout << a << endl; // prints 0

案例 2->

int a;
std :: cout << &a << " " << a << endl; // 0x7ffc057370f4 32764

每当我打印变量的地址时,它们都没有初始化为默认值,为什么会这样。 我认为 a 的值在 case 2 中是垃圾值,但每次运行代码时它都会显示 32764,5,6,7 这些仍然是垃圾值吗?

2 个答案:

答案 0 :(得分:1)

C++ 中的变量未初始化为默认值,因此无法确定该值。您可以阅读更多相关信息here

答案 1 :(得分:0)

恐怕接受的答案没有触及问题的要点: 为什么

int a;
std :: cout << a << endl; // prints 0

总是打印 0,就好像 a 被初始化为其默认值一样,而在

int a;
std :: cout << &a << " " << a << endl; // 0x7ffc057370f4 32764

编译器为 a 生成了一些垃圾值。

是的,在这两种情况下,我们都有一个未定义行为的示例,a 的任何值都是可能的,那么为什么在情况 1 中总是 0?

首先要记住,只要程序的含义保持不变,C/C++ 编译器就可以随意修改源代码。所以,如果你写

int a;
std :: cout << a << endl; // prints 0

编译器可以自由假设 a 不需要与任何实际 RAM 单元相关联。你不读它,也不写信给a。因此编译器可以自由地在其寄存器之一中为 a 分配内存。在这种情况下,a 没有地址,在功能上相当于“命名的、无地址的临时”这样奇怪的东西。但是,在案例 2 中,您要求编译器打印 a 的地址。在这种情况下,编译器无法忽略请求并为分配 a 的内存生成代码,即使 a 的值可能是垃圾。

下一个因素是优化。您可以在 Debug 编译模式下完全关闭它,也可以在 Release 模式下打开积极优化。因此,无论您将其编译为 Debug 还是 Release,您都可以预期您的简单代码的行为会有所不同。此外,由于它是未定义的行为,如果使用不同的编译器甚至同一编译器的不同版本进行编译,您的代码可能会以不同的方式运行。

我准备了一个更容易分析的程序版本:

#include <iostream>

int f()
{
    int a;
    return a;  // prints 0
}

int g()
{
    int a;
    return reinterpret_cast<long long int>(&a) + a;  // prints 0
}

int main() { std::cout << f() << " " << g() << "\n"; }

函数 gf 的不同之处在于它使用未初始化的变量 a 的地址。我在 Godbolt Compiler Explorer 中对其进行了测试:https://godbolt.org/z/os8b583ss 您可以在各种编译器和各种优化选项之间切换。请自己做实验。对于 Debug 和 gcc 或 clang,使用 -O0-g,对于 Release 使用 -O3

对于最新的(主干)gcc,我们有以下等价的汇编:

f():
        xorl    %eax, %eax
        ret
g():
        leaq    -4(%rsp), %rax
        addl    -4(%rsp), %eax
        ret
main:
        subq    $24, %rsp
        xorl    %esi, %esi
        movl    $_ZSt4cout, %edi
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        leaq    12(%rsp), %rsi
        movl    $_ZSt4cout, %edi
        addl    12(%rsp), %esi
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        xorl    %eax, %eax
        addq    $24, %rsp
        ret

请注意,f() 被简化为将 eax 寄存器设置为零(对于整数 a 的任何值,a xor a 等于 0)。 eax 是此函数返回其值的寄存器。因此在 Release 中为 0。嗯,实际上,不,编译器甚至更智能:它从不调用 f()!相反,它会将调用 operator<< 中使用的 esi 寄存器清零。同样,g 被读取 12(%rsp) 代替,一次作为值,一次作为地址。这会为 a 生成一个随机值,而为 &a 生成一个相当相似的值。 AFIK,它们有点随机,让黑客攻击我们的代码变得更加困难。

现在调试中的代码相同:

f():
        pushq   %rbp
        movq    %rsp, %rbp
        movl    -4(%rbp), %eax
        popq    %rbp
        ret
g():
        pushq   %rbp
        movq    %rsp, %rbp
        leaq    -4(%rbp), %rax
        movl    %eax, %edx
        movl    -4(%rbp), %eax
        addl    %edx, %eax
        popq    %rbp
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        call    f()
        movl    %eax, %esi
        movl    $_ZSt4cout, %edi
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        call    g()
        movl    %eax, %esi
        movl    $_ZSt4cout, %edi
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        movl    $0, %eax
        popq    %rbp
        ret

您现在可以清楚地看到,即使不知道 386 程序集(我也不知道),在调试模式 (-g) 下,编译器根本不执行任何优化。在 f() 中,它读取 a(低于帧指针寄存器值 -4(%rbp) 的 4 个字节)并将其移动到“结果寄存器”eax。在 g() 中,也是如此,但 a 作为值读取一次,作为地址读取一次。此外,f()g() 都在 main() 中被调用。在这种编译器模式下,程序会为 a 生成“随机”结果(请自行尝试!)。

为了让事情变得更有趣,这里是在 Release 中由 clang (trunk) 编译的 f()

f():                                  # @f()
        retq
g():                                  # @g()
        retq

你能看到吗?这些函数对 clang 来说是微不足道的,以至于它没有为它们生成任何代码。此外,它没有将对应于 a 的寄存器清零,因此,与 g++ 不同,clang 为 a 生成一个随机值(在 Release 和 Debug 中)。

您可以进一步进行实验,发现 clang 为 f 生成的内容取决于 f 还是 g 在 main 中首先被调用。

现在您应该对什么是未定义行为有了更好的了解。