以前存在一个误解,只要是对空指针访问就会引起程序崩溃,实际上却不是,如下代码:

#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,虽然大多数时候实际不会造成崩溃,但我们不能依赖这种未定义的行为,因为它可能崩溃也可能不崩溃,跟编译器也有关系,还是应判断空指针,避免这种情况发生。

参考:

  1. https://stackoverflow.com/questions/49872721/why-calling-function-with-nullptr-does-not-crash-my-application
  2. https://stackoverflow.com/questions/5431420/why-doesnt-the-program-crash-when-i-call-a-member-function-through-a-null-point