【C++深度解剖】空指针调用成员函数,为什么有时不崩?从零开始图解汇编
2026/6/10 19:55:20 网站建设 项目流程

一道经典面试题:

程序会崩溃吗?
答案:不一定。如果Print函数里没有访问成员变量,程序可能安然无恙;一旦访问了成员变量,必定崩溃。本文将为你彻底揭开背后的this指针机制,并带你逐步看懂汇编代码。

1. 两道题,猜猜结果

先看两段非常相似的代码,猜一猜它们会不会崩溃。

代码1(只打印一行文字):

代码2(打印文字 + 访问成员变量_a):

答案

  • 代码1:正常运行(大多数编译器下输出A::Print()然后正常结束)

  • 代码2:运行崩溃(段错误,Segmentation Fault)

为什么?我们从头开始解释。

2. 预备知识(轻松搞懂地址、指针、空指针、函数调用)

2.1 内存地址与指针

计算机的内存就像一个巨大的带编号的柜子,每个柜子(字节)都有一个唯一的编号,这个编号就是地址。

指针是一个变量,它里面存的是别人的地址。比如

你可以认为p这个小纸条上写着“a 的柜子编号是 0x1234”

2.2 空指针

空指针(nullptrNULL)就是值等于0的指针。它不指向任何有效的柜子
就像一张空白纸条,上面写着“0号柜子” —— 但 0 号柜子通常是系统保留的,不允许你访问。

2.3 函数调用与call指令

当我们写p->Print();时,表面上是在通过指针调用函数。实际上,编译器会把它转化为一个普通的函数调用,只是悄悄地把p的值作为第一个参数传递进去。这个隐藏的参数叫做this指针。

你可以想象成:Print(p);—— 把p当成参数传给函数。

但是函数调用本身,只需要知道函数的代码地址(好比函数的“门牌号”),然后把参数准备好,执行call指令跳过去。

关键点:调用Print函数并不需要去访问p指向的内存(即不需要从地址 0 读数据),只需要把p的值(0)作为参数传递。所以不会立刻崩溃

3. 成员函数的秘密 —— 隐藏的this指针

C++ 的成员函数(非静态)在底层编译后,会多一个参数:指向当前对象的指针,名字叫this

例如

实际为(隐含this指针,且其不能显示再参数列表中)

p->Print();时,编译器生成:A_Print(p);

所以this指针的值就是p的值(在空指针情况下是0)

3.1 成员变量是怎么访问的?

如果Print函数内部写了_a(成员变量),实际会被翻译成this->_a,也就是this指针加上_a在对象中的偏移量(通常是 0 或 4 字节等)。

相当于

如果this == nullptr,那么this->_a就等于访问地址0 + offset,这个地址是无效的,CPU 会触发“段错误”,程序崩溃。

4. 第一段代码为什么不崩溃?(图解 + 伪代码)

原代码(不访问成员变量):

4.1 成员函数在底层被“翻译”成普通函数

C++ 编译器会把成员函数变成类似这样的普通函数,多了一个隐藏参数:this指针。

4.2 调用过程的本质:函数地址 + 参数传递

要理解为什么空指针调用成员函数本身不崩溃,需要分清两个步骤:

  1. 调用函数
    编译器在编译时就已经知道Print函数的入口地址(即代码在内存中的位置)。
    执行p->Print();时,生成的是call 地址指令——这个指令只要求目标地址是合法的代码段地址,与p的值无关。
    无论pnullptr还是有效地址,函数调用都能成功发生。

  2. 传递this参数
    按照调用约定(x86-64 用rdi寄存器,x86 用栈),编译器生成的代码会把p(即0)复制到约定的位置。
    这只是一个数值拷贝操作,并不是“通过地址去访问内存”。
    就像你把数字0写在一张纸条上交给别人,纸条上的数字并不会因为它代表空地址而产生任何问题。

所以,p->Print();这一行在执行时:

  • 没有去读p所指向的内存(地址0处的内容)

  • 只是把0作为参数传给了函数

  • 然后跳转到Print的代码处执行

  • 结论:空指针调用成员函数,调用动作本身是合法的,不会触发内存访问异常。异常只会发生在函数内部真正使用this去访问成员变量的时候。

  • 5. 第二段代码为什么会崩溃?(访问成员变量)

    代码改为:

    5.1 成员变量的访问本质是“基址+偏移”

    编译器把_a的访问翻译成:*(this + 偏移量(_a))

  • 偏移量(_a)_a在对象中的位置距离对象起始地址的字节数。

  • 由于类中只有一个int _a,它的偏移量通常是0(即对象起始地址就是_a的地址)。

    5.2 空指针时发生了什么?

    this的值是0(空指针)。
    那么this + 0还是0
    然后代码试图从地址0读取 4 字节的数据(因为int占 4 字节)。

    地址 0 在操作系统中是受保护的,任何用户程序都不能读写该地址。CPU 检测到非法访问,立刻向程序发送“段错误”信号,程序崩溃。

  • 崩溃的直接原因:对空指针进行了解引用(即通过空指针去访问它“指向”的内存)。

  • 即使偏移量不是 0(比如_a偏移 4 字节),访问地址 4 通常也是非法的(除非有特殊硬件映射,但一般不会),所以同样会崩溃。

  • 6.this指针到底存在哪里?

    题目:this指针存在内存哪个区域?
    A、栈 B、堆 C、静态区 D、常量区 E、对象里面

    正确答案:A(栈)——更准确地说,this是函数的一个形参。

  • 在调用成员函数时,调用方把对象的地址作为实参传递。这个参数可以存放在寄存器中(如 x86-64 的rdi),也可以压入中。无论哪种方式,它都不在对象内部。

  • 对象内部只存储成员变量,绝不存储this指针

  • this的生命周期和函数调用有关,属于栈上(或寄存器)的临时数据。

  • 7. 其他情况:虚函数与静态成员函数

    7.1 虚函数 —— 一定崩溃

  • 原因:虚函数调用需要通过对象的虚表指针vptr)找到真正的函数地址。虚表指针位于对象的起始地址(偏移 0)。
  • p = nullptr时,代码试图从地址 0 读取虚表指针,立刻非法访问 → 崩溃。
    即使Print函数体为空,依然崩溃。
  • 7.2 静态成员函数 —— 永不崩溃


  • 原因:静态成员函数没有this指针。它本质上就是一个普通函数,只是作用域在类内。
  • 如p->StaticFunc()中的p根本不会被使用(编译器可能发出警告,但代码合法)。
  • 8.总结表格

    成员函数类型是否访问成员变量空指针调用结果根本原因
    非静态,非虚✅ 正常运行只传递this值,不解引用
    非静态,非虚❌ 崩溃通过this解引用访问成员
    虚函数(任意)❌ 崩溃需要先读取虚表指针(解引用)
    静态成员函数(无this✅ 正常运行没有this,不依赖对象

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询