问题描述
案例 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 这些仍然是垃圾值吗?
解决方法
C++ 中的变量未初始化为默认值,因此无法确定该值。您可以阅读更多相关信息here。
,恐怕接受的答案没有触及问题的要点: 为什么
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"; }
函数 g
与 f
的不同之处在于它使用未初始化的变量 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,std::char_traits<char> >::operator<<(int)
call g()
movl %eax,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 中首先被调用。
现在您应该对什么是未定义行为有了更好的了解。