以前存在一个误解,只要是对空指针访问就会引起程序崩溃,实际上却不是,如下代码:
#include <iostream>
class A
{
public:
void func()
{
std::cout << "call func" << std::endl;
int a = n; // 注释本行不会程序崩溃
}
private:
int n = 1;
};
int main()
{
A* pa = nullptr;
A* pb = nullptr;
A& ra = *pa;
ra.func();
pb->func();
return 0;
}
将ra引用绑定到对pa的取值上和调用func都可以正常执行,不会导致崩溃,这是因为真正导致崩溃的原因在于对空指针内部成员变量的访问,即对this
的访问,而非空指针本身,类方法是类的一部分,而非对象的一部分。我们可以利用vs的反汇编功能从汇编代码的角度理解这件事情:
A* pa = nullptr;
00DE25F5 mov dword ptr [pa],0
A* pb = nullptr;
00DE25FC mov dword ptr [pb],0
A& ra = *pa;
00DE2603 mov eax,dword ptr [pa]
00DE2606 mov dword ptr [ra],eax
ra.func();
00DE2609 mov ecx,dword ptr [ra]
00DE260C call A::func (0DE135Ch)
pb->func();
00DE2611 mov ecx,dword ptr [pb]
00DE2614 call A::func (0DE135Ch)
return 0;
00DE2619 xor eax,eax
}
可以看到对方法的调用跳转的位置是一样的,这证明了类方法是类的一部分而非对象的一部分,调用后走到0DE135Ch
00DE135C jmp A::func (0DE24D0h)
紧接着又跳到0DE24D0h
class A
{
public:
void func()
{
00DE24D0 push ebp
00DE24D1 mov ebp,esp
00DE24D3 sub esp,0D8h
00DE24D9 push ebx
00DE24DA push esi
00DE24DB push edi
00DE24DC push ecx
00DE24DD lea edi,[ebp-18h]
00DE24E0 mov ecx,6
00DE24E5 mov eax,0CCCCCCCCh
00DE24EA rep stos dword ptr es:[edi]
00DE24EC pop ecx
00DE24ED mov dword ptr [this],ecx // this 赋值
00DE24F0 mov ecx,offset _1A2CA9B9_源@cpp (0DEF029h)
00DE24F5 call @__CheckForDebuggerJustMyCode@4 (0DE1384h)
std::cout << "call func" << std::endl;
00DE24FA mov esi,esp
00DE24FC push offset std::endl<char,std::char_traits<char> > (0DE103Ch)
00DE2501 push offset string "call func" (0DE9B30h)
00DE2506 mov eax,dword ptr [__imp_std::cout (0DED0D4h)]
00DE250B push eax
00DE250C call std::operator<<<std::char_traits<char> > (0DE11A9h)
00DE2511 add esp,8
00DE2514 mov ecx,eax
00DE2516 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0DED0A0h)]
00DE251C cmp esi,esp
00DE251E call __RTC_CheckEsp (0DE128Fh)
int a = n;
00DE2523 mov eax,dword ptr [this]
00DE2526 mov ecx,dword ptr [eax]
00DE2528 mov dword ptr [a],ecx
}
00DE24ED指令把this指针赋值为了0,后面std调用部分并没有用到this指针,只是再次跳到了另一个函数执行输出字符串的功能,因此不会造成崩溃。如果写了int a = n;
,汇编对应的代码将会基于0地址取对象中的n的值,此时才会造成崩溃。
注意:在C++中对nullptr的访问是undefined behavior,虽然大多数时候实际不会造成崩溃,但我们不能依赖这种未定义的行为,因为它可能崩溃也可能不崩溃,跟编译器也有关系,还是应判断空指针,避免这种情况发生。
参考:
- https://stackoverflow.com/questions/49872721/why-calling-function-with-nullptr-does-not-crash-my-application
- https://stackoverflow.com/questions/5431420/why-doesnt-the-program-crash-when-i-call-a-member-function-through-a-null-point