int main()
{
int b = 10;
int* bPointer;
*bPointer = b;
cout << *bPointer << endl;
cout << bPointer;
}
第一个cout打印10,第二个打印0.指向 0 的指针如何存储一些值?
答案 0 :(得分:2)
正如其他评论和答案已经注意到的那样,使用未初始化的指针会产生未定义的行为 - 所以你真的不应该这样做。但是,你似乎明白这一点,但是你仍然很好奇为什么你会看到你正在观察的行为......
由于bPointer
未声明volatile
,编译器可以假定*bPointer
在此函数范围内无法更改。
基本上,编译器会看到这个赋值
*bPointer = b;
然后优化此行
cout << *bPointer << endl;
这样的事情
cout << 10 << endl;
它可以假设是等价的。
编译器甚至可以推断出程序此时结束,因此完全跳过赋值*bPointer
;然而,这似乎对我来说有点牵强。
我尝试的所有平台和编译器组合都产生了错误,所以我无法确认我的解释 - 但如果你真的在观察这种行为,那就是我对发生的事情的有根据的猜测。
在使用-O1
标志进行编译时,我能够使用g ++(GCC)4.8.3重现行为。这是相关的程序集 1 :
main:
.LFB975:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
; Next 3 lines print "10" (via cout)
movl $10, %esi
movl std::cout, %edi
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
; Next 2 lines print the `endl` (via cout)
movq %rax, %rdi
call std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
; Next 3 lines print "0" (via cout)
movl $0, %esi
movl std::cout, %edi
call std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<void const*>(void const*)
movl $0, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
正如您所看到的,编译器完全摆脱了两个变量(即,b
和bPointer
都没有堆栈上任何相应的内存位置),而只是假设常量值({{ 1}}和10
),然后打印出来。
同样,由于使用未初始化的指针会导致 undefined 行为,编译器可以随意做任何事情...我认为这种特殊行为有点奇怪,但它仍然“有效”因为你的指针是未初始化的。
1 如果您有兴趣,汇编就像这样生成:0
答案 1 :(得分:1)
这是未定义的行为(未初始化指针的使用)不做这样的事情它经常导致访问违规行为和许多其他坏事。
理论上,你可以写入虚拟内存地址的任何地方,并从中回复你的值,但现代的编译器和操作系统不允许这样做,因为容易出错的情况。
在这种情况下,许多编译器导致访问冲突限制或分段错误,您的编译器只是将其传递出去。这可能会在将来导致您的程序出现许多问题。
答案 2 :(得分:1)
正如许多其他人已经说过的那样,您的代码具有未定义的行为。它可以崩溃,它可以工作,它可以工作一段时间然后停止,一切皆有可能。
您可能知道,局部变量存储在堆栈中。 (或者在寄存器中,如果已经过优化,但这里不太相关。)无论如何,未初始化的本地POD(普通旧数据)变量不能保证包含0
或NULL
或false
或其他一些“默认”值。它将被设置为之前恰好位于相同堆栈地址的任何垃圾。
例如,当我在本地计算机上编译32位程序时,我得到如下输出:
10
004EAF18
后者显然是一些地址,其中可以写入4字节int
值,并且不会发生任何致命事件,可能是堆栈上或堆上的某个地方。但这只是运气,它可能指向一个只读段并在写入期间失败,或者程序可能会在以后崩溃,因为它可能会覆盖例如argv
列表,然后尝试使用它或其他任何内容。
在任何情况下,这就是大多数人都希望它不会崩溃的原因以及为什么它不会让我崩溃 - bPointer
设置为某个错误但可用的地址而不是NULL
,因为几乎每个操作系统和进程虚拟内存设置都保留了零地址。
但实际上,当我尝试使用相同的代码时,例如cpp.sh,输出是你看到的奇怪的结果:
10
0
然而,它并没有崩溃!有什么收获?
这是优化,正如DaoWen在答案中指出的那样。
如果你转到Compiler Explorer并使用默认值,即没有优化,输出就是这个汇编代码:
; b = 10
mov DWORD PTR [rbp-4], 10
; *bPointer = b
mov rax, QWORD PTR [rbp-16]
mov edx, DWORD PTR [rbp-4]
mov DWORD PTR [rax], edx
; cout << *bPointer
mov rax, QWORD PTR [rbp-16]
mov eax, DWORD PTR [rax]
mov esi, eax
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
; cout << endl
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
; cout << bPointer
mov rax, QWORD PTR [rbp-16]
mov rsi, rax
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)
但是如果你在右上角的“编译器选项”中添加基本优化-O1
,它就会被优化掉:
; cout << 10
mov esi, 10
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
; cout << endl
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
; cout << 0
mov esi, 0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<void const*>(void const*)
所以这就解释了。最后一个难题是由于某种原因,编译器(这里是GCC)在优化期间将未初始化的指针作为0
。
可能因为0
和那里的任何值一样好,所以它是未定义的行为。修复错误!
答案 3 :(得分:0)
在这里阅读了一些评论和答案后,有一件事让我感到震惊的是,有多少人似乎在假设在原始问题中打印的0实际上是指针所指向的地址。在显示的代码中:
cout << *bPointer << endl;
正在取消引用bPointer。因此,观察到的0不是地址本身,而是(据说,并且遵循先前关于优化和未定义行为的点)存储在由bPointer指向的地址的数字。我们应该问的问题是,为什么这个数字不是10,在数字10被明确写在那里之后:
*bPointer = b;
由于其他人已经提出的原因,只能假设无法写入有问题的UNKNOWN内存地址,或者编译器没有按照您的预期进行操作。