---
title: "从C++到汇编：面向对象如何落地"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/cpp-to-assembly-oop-implementation/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/从C++到汇编：面向对象如何落地.md"
content_hash: 6f5aa554c438597c192f8306f49b9b145fbfb488ffa847ef7d0a3f7a290ebd37
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# 从C++到汇编：面向对象如何落地


```cpp
// hello.cpp
#include <iostream>

class Base {
public:
    int x;                    // 数据成员
    Base(int v = 0) : x(v) {} // 构造函数
    virtual void foo();       // 虚成员函数
    virtual ~Base();          // 虚析构
};

class Derived : public Base {
public:
    int y;
    Derived(int v1, int v2) : Base(v1), y(v2) {}
    void foo() override;      // 重写
};

void Base::foo()   { std::cout << "Base "   << x << '\n'; }
Base::~Base()      { std::cout << "~Base\n"; }
void Derived::foo(){ std::cout << "Derived "<< y << '\n'; }

int main() {
    Base* p = new Derived(1, 2); // 多态：基类指针指向派生对象
    p->foo();                    // 动态分派
    delete p;                    // 触发虚析构
    return 0;
}
```

## 1  编译流水线全景

| 阶段     | 产物                  | 说明（与 OO 相关的要点）                                |
| ------ | ------------------- | --------------------------------------------- |
| 预处理    | `hello.i`           | 展开头文件、宏                                       |
| **编译** | `hello.s`           | 语法/语义分析 → **生成 vtable 常量、构造/析构函数、thunk**；完成优化 |
| 汇编     | `hello.o`           | 把 `.s` 汇编为机器码 + 重定位表                          |
| **链接** | `a.out` (ELF)       | 合并段、解析外部符号；各 vtable 落在 `.rodata`              |
| **加载** | 运行时 ELF 加载器 `ld.so` | 把节映射入虚拟地址，做最后重定位；执行 `.init_array` 里的静态构造      |
| 运行     | CPU                 | 指令流 + 对象内 vptr 间接跳转实现面向对象                     |

---

## 2  对象物理布局（Derived 单继承，含虚函数）

```cpp
偏移  大小  成员 / 隐含字段
----- ----  -----------------------------
0     8     vptr → _ZTV7Derived + 16
8     4     Base::x
12    4     padding (保持 y 对齐)
16    4     Derived::y
20    4     padding (整体 8 字节对齐)
```

> * 只要类层次里**某处**出现虚函数，最派生对象必有 **单一 vptr**（单继承）。
> * vtable 前两槽是 RTTI (`typeinfo`)；+16 跳过这两槽即第一虚函数地址。

---

## 3  关键符号与 vtable 片段

```asm
# 从C++到汇编：面向对象如何落地
_ZTV4Base:      .quad 0          # [–2] (填充，供实现内部使用)
                .quad _ZTI4Base  # [–1] RTTI
                .quad _ZN4Base3fooEv    # [0]  Base::foo
                .quad _ZN4BaseD1Ev      # [1]  Base::~Base (non-deleting)
                .quad _ZN4BaseD0Ev      # [2]  Base::~Base (deleting)

_ZTV7Derived:   .quad 0
                .quad _ZTI7Derived
                .quad _ZN7Derived3fooEv # 覆写 foo
                .quad _ZN7DerivedD1Ev
                .quad _ZN7DerivedD0Ev
```

---

## 4  构造函数落地（避免覆盖的两步 vptr 写入）

```asm
## Derived::Derived(int, int) – 伪反汇编
_ZN7DerivedC2Eii:
    push   %rbp
    mov    %rbp, %rsp            # rdi=this, esi=v1, edx=v2

## (1) 调用 Base 子对象构造 —— 写入 Base vptr & 初始化 x
    mov    %esi, %esi            # 参数映射
    mov    %rdi, %rdi
    call   _ZN4BaseC2Ei          # Base::Base(int)
    #  └→ 在 Base 构造中：
    #       mov $_ZTV4Base+16,(%rdi)      ; vptr = Base
    #       mov %esi,8(%rdi)              ; x   = v1

## (2) 初始化 Derived 自身成员
    mov    %edx, 16(%rdi)        # y = v2

## (3) 覆盖 vptr 为 Derived
    mov    $_ZTV7Derived+16, %rax
    mov    %rax, (%rdi)

    pop    %rbp
    ret
```

> **为什么一定要先写 Base 然后再写 Derived？**
> C++ 规则规定：
>
> * 构造基类阶段，对象“身份”是 Base；若此时调用虚函数必须静态绑定到 Base 实现。
> * 只有所有基类构造完毕，才进入派生阶段并把 vptr 最终定向 Derived。

---

## 5  虚函数调用 = “对象 → vptr → 间接 call”

### 在 `main()` 内的 `p->foo()` 反汇编

```asm
## rdi = p
    mov    (%rdi), %rax      # rax = *(p) = vptr
    callq  *(%rax)           # 间接跳转到 vtable[0] → Derived::foo
```

* 比直接 `call _ZN7Derived3fooEv` 多一次 **内存读取**（取表），即动态分派成本。

---

## 6  delete 触发虚析构

```asm
    mov    (%rdi), %rax          # 取 vptr
    callq  *(%rax+8)             # 第 1 槽 (index = 1) 是 ~DerivedD1
    callq  _ZdlPv                # operator delete  由编译器插入
```

析构链：`Derived::~Derived()` → `Base::~Base()` → `operator delete`。

---

## 7  目标文件到机器码

* 链接阶段把所有 `call _ZN4BaseC2Ei` 之类符号解析为绝对/相对地址。
* 若函数来自共享库，机器码里是跳转到 **PLT**；首次执行由动态链接器填 **GOT**，后续直达真实地址。
* 经过加载器重定位后，最终直接执行的就是 x86-64 **机器码字节序列**（`E8 xx xx xx xx` 为 `call` 相对跳转等），CPU 毫无面向对象概念，只按地址取指令。

---

## 8  成本与扩展

| 机制            | 空间成本                     | 时间成本（热点） | 说明     |
| ------------- | ------------------------ | -------- | ------ |
| vptr / vtable | 每对象 8 B（单继承）             | 1 次 L1 读 | 支撑动态多态 |
| 构造双写 vptr     | 0                        | 构造期两次写   | 只发生一次  |
| RTTI          | 每类 1 条记录                 | –        | 存放在只读段 |
| 多重继承          | 对象多 vptr；thunk 调整 `this` | < 5 ns/跳 | 额外偏移修正 |
| 虚继承           | VTT 表、vbptr              | 同上       | 菱形继承场景 |

---

## 9  自行验证

```bash
g++ -std=c++20 -O1 -g hello.cpp          # 适度优化保留符号
objdump -dC a.out | less                 # 反汇编 + 解码 C++ 名字
readelf -r a.out | grep ZTV             # 看虚表重定位
```

聚焦：

* `Derived::Derived` 中 2 次对 `(%rdi)` 的写入；
* `p->foo` 的 `mov (%rdi), %rax / call *(%rax)`；
* `.rodata` 段里 `_ZTV*` 表头结构。

---

### 结语

> **“面向对象靠什么落地？”** —— 固定的内存布局 + 隐含指针 + 表查找 + 间接跳转。
> CPU 依然只懂“读 / 写 / 跳转”；而编译器把高层语义编排成对寄存器和内存的严格约定，从而让机器码在运行时呈现出多态、继承、封装等 OO 行为。
