编程语言研究书库

面向对象编程 — 对象是带隐藏参数的记录,方法是加了类名的函数

面向对象编程 — 对象是带隐藏参数的记录,方法是加了类名的函数

这一章讲什么: 前面用 C 讲了半本书,本章换 C++ 回答一个问题: 「面向对象」在机器层到底是什么? 答案朴素得惊人:对象 = 记录(数据成员就是字段, 偏移量同款);方法 = 普通函数,只是名字被编译器重整过、第一个参数被编译器暗中塞了 this 指针;构造/析构 = 在作用域进出时自动打的两个电话。学完全章你会把「类」 从神秘概念降级为「第 17 章记录 + 一些编译器纪律」。 主线例子: fraction 类(分子/分母两个 int),x.add(1) 从 C++ 语法一路走到 _ZN8fraction3addEi 的函数调用。

1. C++ 这一侧的四个新词

先立词汇,全部就地解释1:

  • 对象(object):拥有一组属性(数据)和一组方法(函数)的东西;
  • 类(class):对象的类型声明——大致是 C 的 struct,但还能把函数塞进声明里;
  • 构造函数(constructor):与类同名、无返回值的特殊成员函数,实例化对象时 编译器自动生成调用;负责把对象初始化到确定状态(如 fraction 初值 0/1);
  • 析构函数(destructor):~类名、无参无返回,程序流离开对象作用域时 编译器自动生成调用;负责释放构造函数申请的资源(本例无事可做)。

还有一层访问控制:class 里默认 private(只有本类成员函数能碰),public 段才对外开放; C++ 的 struct 关键字也能声明类,只是默认全 public2。struct fraction { private: … }; 和 class fraction { … public: … }; 等价——用哪个,作者说是个人偏好2。

2. 主线例子:incFraction 程序

清单 18-3 的 main 只有六行,每行都在「发消息」:

fraction x;      // 实例化:在栈上建一个 fraction 对象
x.display();     // 发消息:显示当前分数(0/1)
x.get();         // 发消息:从键盘读分子分母
x.add(1);        // 发消息:分数加 1
x.display();     // 再显示

点号 x.display() 的读法:向对象 x 发送 display 消息, 机器层就是「调用 x 所属类的 display 函数,并把 x 的地址递过去」3。

3. g++ 的汇编:三个机器级真相

清单 18-4 是 g++ 为这段 main 生成的汇编(作者用了 -fno-exceptions 等选项 剥掉异常信息的噪音)。逐真相拆4:

真相一:对象是栈上的记录,x 住在 −32[rbp]

序言 sub rsp, 24 给对象(8 字节:num 4 + den 4)加金丝雀留了地方; lea rax, -32[rbp] 算出对象地址——和第 17 章的 record 一模一样。 注意 −32 而不是 −24:rbx 先压了栈,作者特意提醒这一点,免得你数错格子4。

真相二:名称重整——方法名的「全名」

构造函数在 C++ 源码里叫 fraction(),机器里叫 _ZN8fractionC1Ev。 拆开看就是一套名称重整(name mangling)规则:把类名 + 函数名 + 参数类型 编成一个全局唯一的符号5。两个直接后果:

  • 不同类可以有同名的 display——_ZN8fraction7displayEv 与别的类的 display 不会撞车,类名已经在符号里;
  • 函数重载(function overloading):add(int) 重整成 …3addEi, 结尾的 Ei 记下「一个 int 参数」——同名不同参的函数因此可以共存5。

书里补了一句重要的边界:重整规则没有标准,各家编译器不同—— 所以一个程序的 C++ 代码必须用兼容的编译器/链接器整套构建5。 (第 14 章静态局部变量的 y.2319 是它的 C 版远亲。)

真相三:this 指针——源码里看不见的第一个参数

注意每次调用成员函数前都固定有两个动作:lea rax, -32[rbp]; mov rdi, rax—— 对象地址被塞进 rdi,当作第一个参数传给成员函数。 这个参数在 C++ 源码里不存在,叫 this 指针:成员函数靠它找到「当前对象是哪个」6。

证据在成员函数内部(清单 18-6):构造函数第一句 mov QWORD PTR -8[rbp], rdi 注释明写 this pointer;后面 mov DWORD PTR [rax], 0(num=0)和 mov DWORD PTR 4[rax], 1(den=1)——+0 是 num、+4 是 den,和第 17 章记录 字段偏移完全同款7。C++ 源码里写 num = 0;,编译器默默翻译成 this->num = 0;。你平时不用写 this,但它在每个成员函数的签名里。

另一个细节:main 调的是 _ZN8fractionC1Ev,编译器却把它定义成 .set _ZN8fractionC1Ev, _ZN8fractionC2Ev——C1/C2 是「完整对象/基类子对象」 两套构造入口,本例指向同一实现;析构的 D1/D2 同理。现在只需认得这个别名把戏8。

4. 编译器替你生成构造/析构?看汇编才知道它多懒

C++ Core Guidelines 的 C.45 条建议:只为初始化数据成员的默认构造函数, 别写,用类内初始化器(int num{0}; int den{1};)。作者照做,删掉构造/析构, 再看 g++ 的输出(清单 18-8)——真相有意思9:

  • 构造函数被「内联」到消失:实例化处直接两行 mov DWORD PTR -16[rbp], 0 / mov DWORD PTR -12[rbp], 1—— 没有 call,没有函数,初始化就地完成。书里说得更准: 编译器生成的是「内联构造」,而非构造函数;对比带构造函数的版本,省了 8 条指令、 含 2 次函数调用,Core Guidelines 的「更高效」在汇编层兑现了;
  • 析构什么都不做:对象只占用栈内存,结语正常清栈就是销毁, 编译器连一条指令都不用加9。

边界提醒(书里有):一旦你要带参数的构造函数,默认构造就得自己写了—— 编译器不会替你补10。

5. 用汇编手搓对象:没有魔法,全是旧零件

18.2 节把 fraction 全部用汇编写一遍,过程和零件你都认识11:

  • 属性偏移文件(清单 18-9):num=0, den=4,对象占 8 字节—— 第 17 章 aRecord 的翻版,.include "fraction" 复用同款机制;
  • 成员函数 = 普通函数 + this 约定:命名只做轻量重整(fraction_add), rdi 收 this、esi 收业务参数;
  • 调不调用别人,决定 this 住哪:fraction_construct、fraction_add 是叶函数, rdi 全程够用;fraction_display、fraction_get 要 call putInt/getInt, this 必须存栈帧(this[rbp], rdi)——第 14 章规则 9 第三次兑现;
  • 私有性没有机器级保障:.equ 的偏移量谁都能 include。 「私有」是 C++ 编译器的纪律,汇编层面全靠自觉11。

判断(我们的,不是书里的): 本章是全书「去魅」叙事的落点:第 17 章记录 → 第 18 章对象,中间只加了三件事——名称重整(区分函数)、this(找到数据)、 编译器代打电话(构造/析构)。「面向对象」作为一项机器层技术,复杂度不在运行时, 全在编译期。这也解释了为什么 C++ 的 ABI 如此脆弱(重整无标准), 以及为什么「在汇编里调用 C++ 库」远比调 C 库凶险。 如果错,会错在: 虚函数与多态没在本章出现——带虚函数的类会多一张 vtable 指针,对象的第一个字段藏一个指向方法表的指针,那才是 OOP 在机器层的第二形态。 本例全是非虚函数,别拿本章结论去推多态。

判断(我们的,不是书里的): 「编译器删除了你的构造函数」是本章最有意思的 细节:源码里「少写一个函数」,汇编里「多两行立即数赋值」——所谓编译器优化, 常常不是把代码变快,而是把抽象变没。读 -S 输出时寻找「本该有的函数不见了」, 是理解任何优化的通用入口。 如果错,会错在: 复杂构造(要锁资源、要注册回调)没法内联,Core Guidelines 的建议只覆盖「纯初始化」这种简单情形——删构造函数前先看它除了赋值还干了什么。

6. 作者的判断与证据

说法性质
对象=记录、方法=带 this 的函数、偏移量存储属性由清单 18-4/18-6 汇编实证47
名称重整实现类区分与重载;无标准、编译器各异指令集事实 + 生态事实5
编译器自动生成构造/析构调用由清单 18-4 实证(离开作用域处的 call)4
类内初始化器生成更高效代码(省去函数调用)作者实测对比:省 8 条指令含 2 call9
「编译器生成内联构造函数,而非构造函数」作者基于清单 18-8 的精确表述9
需要参数构造时,默认构造必须自己写C++ 语言规则10
「我们可能不会用汇编做 OOP,但有助于厘清 C++ 实现」作者的方法论声明11

7. 边界与局限

  • 没有虚函数、继承、多态——vtable、多重继承、RTTI 全在门外; 本章是「OOP 的机器层第一形态」。
  • 异常被 -fno-exceptions 关掉了,原因书里明说:异常元数据会干扰 对对象实现原理的观察12。真实 C++ 程序的汇编要复杂得多。
  • C++ 的调用约定细节(谁清理栈、返回值传递)与 C 有差异,书中用 extern "C" 绕开(把汇编函数声明成 C 约定)13;深入的 ABI 差异未展开。
  • 作者推荐两本 C++ 读物(Lospinoso 的 C++ Crash Course;Stroustrup 的 The Design and Evolution of C++),均为作者推荐、我们未复核14。

8. 可带走的

  1. 对象 = 一块具名内存(通常栈上),数据成员按字段偏移排布;
  2. 方法 = 普通函数 + 隐式 this 参数(this 永远是第一个参数,住 rdi);
  3. 名称重整 = 类名+函数名+参数类型编成全局符号;无标准、跨编译器不通用;
  4. 函数重载靠重整名共存;不同类的同名函数也靠它区分;
  5. 构造函数与类同名、无返回值;析构 ~类名、无参;都是编译器自动拨打的电话;
  6. 类内初始化器(int num{0};)常让编译器内联初始化、整个构造函数消失;
  7. 只占用栈的对象,析构可以免费(清栈即销毁);
  8. 私有/公有是编译器纪律,汇编层没有强制;
  9. 调 C 库函数用 extern "C" 声明,绕开 C++ 名称重整;
  10. 手搓对象的三件套:偏移量文件、this 约定、按是否调别人决定 this 存栈还是留寄存器。

9. 原文地图

主题原书章原文位置
对象/属性/方法概念第18 章 面向对象编程text/24-ch18.txt:12(搜「属性」) · text/24-ch18.txt:14(搜「方法」)
类声明、private/public第18 章 面向对象编程text/24-ch18.txt:59(搜「class fraction」) · text/24-ch18.txt:80(搜「私有作用域」)
struct 声明类第18 章 面向对象编程text/24-ch18.txt:89(搜「清单 18-2」)
实例化与发消息第18 章 面向对象编程text/24-ch18.txt:112(搜「清单 18-3」) · text/24-ch18.txt:127(搜「点号运算符」)
main 汇编:对象与名称重整第18 章 面向对象编程text/24-ch18.txt:139(搜「清单 18-4」) · text/24-ch18.txt:189(搜「_ZN8fractionC1Ev」)
this 指针作隐藏参数第18 章 面向对象编程text/24-ch18.txt:209(搜「隐藏参数」) · text/24-ch18.txt:305(搜「this pointer」)
重载与重整名的 Ei 尾第18 章 面向对象编程text/24-ch18.txt:200(搜「函数重载」) · text/24-ch18.txt:203(搜「单个 int 类型参数」)
重整无标准第18 章 面向对象编程text/24-ch18.txt:206(搜「没有标准做法」)
成员函数汇编与 .set 别名第18 章 面向对象编程text/24-ch18.txt:271(搜「清单 18-6」) · text/24-ch18.txt:315(搜「.set」)
Core Guidelines 与类内初始化第18 章 面向对象编程text/24-ch18.txt:425(搜「C++ Core Guidelines」) · text/24-ch18.txt:447(搜「num{0}」)
内联构造的真相第18 章 面向对象编程text/24-ch18.txt:468(搜「清单 18-8」) · text/24-ch18.txt:510(搜「直接初始化」) · text/24-ch18.txt:515(搜「内联构造函数」)
参数构造须自带默认构造第18 章 面向对象编程text/24-ch18.txt:519(搜「不会替我们操心」)
汇编版对象三件套第18 章 面向对象编程text/24-ch18.txt:527(搜「18.2」) · text/24-ch18.txt:532(搜「清单 18-9」) · text/24-ch18.txt:604(搜「清单 18-11」)
this 存栈帧的原因第18 章 面向对象编程text/24-ch18.txt:695(搜「保存在栈帧内」)
私有性无机器保障第18 章 面向对象编程text/24-ch18.txt:540(搜「没有哪个汇编器指令」)

Footnotes

  1. 出处:「第18 章 面向对象编程」第 24–44 段(text/24-ch18.txt:28,搜「实例化」;text/24-ch18.txt:34,搜「构造函数」;text/24-ch18.txt:39,搜「析构函数」)。 ↩

  2. 出处:「第18 章 面向对象编程」第 80–104 段(text/24-ch18.txt:80,搜「私有作用域」;text/24-ch18.txt:83,搜「公共作用域」;text/24-ch18.txt:86,搜「struct 关键字」;text/24-ch18.txt:104,搜「个人选择」)。 ↩ ↩2

  3. 出处:「第18 章 面向对象编程」第 106–128 段(text/24-ch18.txt:119,搜「fraction x;」;text/24-ch18.txt:127,搜「点号运算符」;text/24-ch18.txt:14,搜「发送消息」)。 ↩

  4. 出处:「第18 章 面向对象编程」第 129–213 段(清单 18-4,text/24-ch18.txt:149,搜「sub rsp, 24」;text/24-ch18.txt:153,搜「address of object」;text/24-ch18.txt:191,搜「正确的偏移量为−32」;-fno-exceptions 说明见第 135–137 段,text/24-ch18.txt:137,搜「关闭该特性」)。 ↩ ↩2 ↩3 ↩4

  5. 出处:「第18 章 面向对象编程」第 193–207 段(text/24-ch18.txt:196,搜「成员函数与其所属的类关联起来」;text/24-ch18.txt:200,搜「函数重载」;text/24-ch18.txt:206,搜「没有标准做法」)。 ↩ ↩2 ↩3 ↩4

  6. 出处:「第18 章 面向对象编程」第 208–210 段(text/24-ch18.txt:209,搜「隐藏参数」;text/24-ch18.txt:209,搜「不会出现在 C++代码中」)。 ↩

  7. 出处:「第18 章 面向对象编程」第 271–414 段(成员函数汇编,text/24-ch18.txt:305,搜「this pointer」;text/24-ch18.txt:307,搜「num = 0」;text/24-ch18.txt:309,搜「4[rax]」)。 ↩ ↩2

  8. 出处:「第18 章 面向对象编程」第 399–407 段(text/24-ch18.txt:401,搜「_ZN8fractionC2Ev」;text/24-ch18.txt:403,搜「.set 等同于」)。 ↩

  9. 出处:「第18 章 面向对象编程」第 422–521 段(清单 18-8,text/24-ch18.txt:481,搜「num = 0」;text/24-ch18.txt:510,搜「直接初始化数据成员」;text/24-ch18.txt:512,搜「什么都不用做」;text/24-ch18.txt:513,搜「节省了 8 个指令」;text/24-ch18.txt:515,搜「内联构造函数」)。 ↩ ↩2 ↩3 ↩4

  10. 出处:「第18 章 面向对象编程」第 518–519 段(text/24-ch18.txt:518,搜「接受参数」;text/24-ch18.txt:519,搜「不会替我们操心」)。 ↩ ↩2

  11. 出处:「第18 章 面向对象编程」第 527–764 段(汇编版对象,text/24-ch18.txt:536,搜「num,0」;text/24-ch18.txt:540,搜「没有哪个汇编器指令」;text/24-ch18.txt:695,搜「保存在栈帧内」;text/24-ch18.txt:763,搜「我们需要将 this 指」)。 ↩ ↩2 ↩3

  12. 出处:「第18 章 面向对象编程」第 130–137 段(text/24-ch18.txt:132,搜「-fno-exceptions」;text/24-ch18.txt:137,搜「关闭该特性」)。 ↩

  13. 出处:「第18 章 面向对象编程」第 55–78 段(text/24-ch18.txt:55,搜「extern "C"」;text/24-ch18.txt:78,搜「C 约定」)。 ↩

  14. 出处:「第18 章 面向对象编程」第 17–21 段(text/24-ch18.txt:18,搜「C++ Crash Course」;text/24-ch18.txt:20,搜「The Design and Evolution」)。 ↩

On this page