C++ 内存模型与对象模型详解

✍️ Demo User·📅 2026年4月25日·👁 49 次阅读
c++开发
📚 系列:现代 C++ 学习之路

本文深入讲解 C++ 程序的内存布局、对象的内存结构、虚函数表机制以及智能指针的内部实现。⭐ 标记为高频重点。


一、C++ 程序的内存布局

⭐ 程序运行时的内存分区

当一个 C++ 程序运行时,操作系统为其分配的虚拟地址空间大致如下:

┌─────────────────────────────────────────┐  高地址 (0xFFFF...)
│                                         │
│            内核空间 (Kernel)             │  ← 用户程序不可访问
│                                         │
├─────────────────────────────────────────┤
│                                         │
│         栈 (Stack)        ↓ 向低地址增长 │  ← 局部变量、函数参数、返回地址
│                                         │
│  ┌─────────────────────────────────┐    │
│  │  main() 的栈帧                  │    │
│  │  ├── int a = 10;               │    │
│  │  ├── double b = 3.14;          │    │
│  │  └── 返回地址                   │    │
│  ├─────────────────────────────────┤    │
│  │  Foo() 的栈帧                   │    │
│  │  ├── int x = 42;               │    │
│  │  └── char buf[256];            │    │
│  └─────────────────────────────────┘    │
│                                         │
│              ↕ 可增长空间                │
│                                         │
│         堆 (Heap)         ↑ 向高地址增长 │  ← new / malloc 动态分配
│                                         │
│  ┌─────────────────────────────────┐    │
│  │  new int(42)                    │    │
│  │  new std::string("hello")      │    │
│  │  malloc(1024)                   │    │
│  └─────────────────────────────────┘    │
│                                         │
├─────────────────────────────────────────┤
│                                         │
│    BSS 段 (.bss)                        │  ← 未初始化的全局/静态变量
│    int g_uninit;     // 默认 0           │
│    static int s;     // 默认 0           │
│                                         │
├─────────────────────────────────────────┤
│                                         │
│    数据段 (.data)                        │  ← 已初始化的全局/静态变量
│    int g_init = 42;                     │
│    static int s = 100;                  │
│                                         │
├─────────────────────────────────────────┤
│                                         │
│    只读数据段 (.rodata)                  │  ← 字符串字面量、const 全局
│    "Hello, World!"                      │
│    const int MAX = 100;                 │
│                                         │
├─────────────────────────────────────────┤
│                                         │
│    代码段 (.text)                        │  ← 编译后的机器指令
│    main() 的机器码                       │
│    Foo() 的机器码                        │
│                                         │
└─────────────────────────────────────────┘  低地址 (0x0000...)

⭐ 各内存区域详细对比

区域管理方式生命周期增长方向大小特点
编译器自动随函数调用创建/返回销毁高→低较小(1-8 MB)速度快,无碎片
手动 new/delete手动控制低→高较大(GB级)灵活,可能碎片
全局/静态区编译器分配程序整个运行期固定自动初始化为 0
常量区编译器分配程序整个运行期固定只读
代码区编译器生成程序整个运行期固定只读,可共享

⭐ 函数调用的栈帧结构

每次函数调用,都会在栈上创建一个栈帧(Stack Frame)

高地址
┌──────────────────────────────┐
│          调用者栈帧           │
├──────────────────────────────┤
│  参数 N                      │  ← 函数参数(从右到左压栈,cdecl 约定)
│  参数 N-1                    │
│  ...                         │
│  参数 1                      │
├──────────────────────────────┤
│  返回地址 (Return Address)    │  ← call 指令自动压入
├──────────────────────────────┤  ← EBP(帧指针)指向这里
│  上一个 EBP(保存的帧指针)    │
├──────────────────────────────┤
│  局部变量 1                   │
│  局部变量 2                   │
│  ...                         │
│  临时变量                     │
├──────────────────────────────┤  ← ESP(栈指针)指向这里
│         (空闲栈空间)          │
└──────────────────────────────┘
低地址
cpp
int Add(int a, int b) {    // a, b 在参数区
    int result = a + b;    // result 在局部变量区
    return result;         // 返回值通常通过寄存器(EAX)传递
}

int main() {
    int x = Add(3, 5);    // 1. 压入参数 5, 3
                           // 2. 压入返回地址
                           // 3. 跳转到 Add
                           // 4. Add 创建自己的栈帧
                           // 5. 执行完毕,销毁栈帧,返回
}

二、C++ 对象的内存模型

⭐ 普通类对象的内存布局

cpp
class Simple {
    int a_;        // 4 字节
    double b_;     // 8 字节
    char c_;       // 1 字节
};
Simple 对象的内存布局(64 位系统):

偏移    成员         大小    说明
┌────┬──────────┬──────┬───────────────────────┐
│  0 │ a_       │ 4B   │ int                    │
├────┼──────────┼──────┼───────────────────────┤
│  4 │ (填充)   │ 4B   │ 对齐到 8 字节边界       │
├────┼──────────┼──────┼───────────────────────┤
│  8 │ b_       │ 8B   │ double                 │
├────┼──────────┼──────┼───────────────────────┤
│ 16 │ c_       │ 1B   │ char                   │
├────┼──────────┼──────┼───────────────────────┤
│ 17 │ (填充)   │ 7B   │ 总大小对齐到最大成员的倍数│
└────┴──────────┴──────┴───────────────────────┘

sizeof(Simple) = 24(而不是 13)

优化:调整成员顺序减少填充

cpp
class Optimized {
    double b_;     // 8 字节(偏移 0)
    int a_;        // 4 字节(偏移 8)
    char c_;       // 1 字节(偏移 12)
    // 3 字节填充
};
// sizeof(Optimized) = 16(节省了 8 字节)

🌈 规则:按成员大小从大到小排列,可以有效减少内存对齐造成的浪费。

⭐ 含虚函数的对象内存布局

cpp
class Base {
public:
    virtual void Func1() {}
    virtual void Func2() {}
    int x_ = 10;
};
Base 对象的内存布局:

┌────────────────────────────────┐
│  vptr(虚指针)     │ 8B(64位)│  → 指向 Base 的 vtable
├────────────────────────────────┤
│  x_                │ 4B       │
├────────────────────────────────┤
│  (填充)            │ 4B       │
└────────────────────────────────┘

sizeof(Base) = 16

Base 的 vtable(虚函数表):
┌─────────────────────────────┐
│  [0]  →  Base::Func1()      │
│  [1]  →  Base::Func2()      │
│  [2]  →  type_info (RTTI)   │
└─────────────────────────────┘

⭐ 单继承的对象内存布局

cpp
class Derived : public Base {
public:
    void Func1() override {}    // 重写 Func1
    virtual void Func3() {}     // 新增虚函数
    int y_ = 20;
};
Derived 对象的内存布局:

┌────────────────────────────────┐
│  vptr(虚指针)     │ 8B       │  → 指向 Derived 的 vtable
├────────────────────────────────┤
│  x_(继承自 Base) │ 4B       │
├────────────────────────────────┤
│  y_(Derived 自己)│ 4B       │
└────────────────────────────────┘

sizeof(Derived) = 16

Derived 的 vtable(虚函数表):
┌──────────────────────────────────┐
│  [0]  →  Derived::Func1()  ✏️   │  ← 重写了 Base 的 Func1
│  [1]  →  Base::Func2()          │  ← 未重写,仍指向 Base 版本
│  [2]  →  Derived::Func3()  🆕   │  ← 新增的虚函数
│  [3]  →  type_info (RTTI)       │
└──────────────────────────────────┘

多态调用的完整流程

Base* p = new Derived();
p->Func1();

调用过程:
  ① p 的静态类型是 Base*,但 p 实际指向 Derived 对象
  ② 从对象起始位置取出 vptr
                ↓
  ③ vptr 指向 Derived 的 vtable
                ↓
  ④ 在 vtable 的 [0] 位置找到 Derived::Func1()
                ↓
  ⑤ 调用 Derived::Func1()

  ┌──────────┐      ┌─────────────────────────┐
  │ Derived  │      │   Derived 的 vtable      │
  │ 对象      │      │                         │
  │ ┌──────┐ │      │  [0] → Derived::Func1() │ ← 找到这里
  │ │ vptr─┼─┼─────►│  [1] → Base::Func2()    │
  │ ├──────┤ │      │  [2] → Derived::Func3() │
  │ │ x_   │ │      └─────────────────────────┘
  │ ├──────┤ │
  │ │ y_   │ │
  │ └──────┘ │
  └──────────┘

⭐ 多重继承的对象内存布局

cpp
class A {
public:
    virtual void FuncA() {}
    int a_ = 1;
};

class B {
public:
    virtual void FuncB() {}
    int b_ = 2;
};

class C : public A, public B {
public:
    void FuncA() override {}
    void FuncB() override {}
    int c_ = 3;
};
C 对象的内存布局(多重继承 → 多个 vptr):

┌────────────────────────────────────┐
│  A 的子对象部分:                    │
│  ┌──────────────────────────────┐  │
│  │ vptr_A      │ 8B            │  │ → 指向 C 的 vtable_A
│  ├──────────────────────────────┤  │
│  │ a_          │ 4B + 4B 填充  │  │
│  └──────────────────────────────┘  │
├────────────────────────────────────┤
│  B 的子对象部分:                    │
│  ┌──────────────────────────────┐  │
│  │ vptr_B      │ 8B            │  │ → 指向 C 的 vtable_B
│  ├──────────────────────────────┤  │
│  │ b_          │ 4B + 4B 填充  │  │
│  └──────────────────────────────┘  │
├────────────────────────────────────┤
│  C 自己的成员:                      │
│  │ c_          │ 4B + 4B 填充  │  │
└────────────────────────────────────┘

sizeof(C) = 40

注意:多重继承时基类指针的转换涉及地址偏移!
  A* pa = &c;   // pa == &c(指向起始)
  B* pb = &c;   // pb == &c + sizeof(A 子对象)(指向 B 的子对象起始)

⭐ 菱形继承与虚继承的内存布局

普通菱形继承(A 存在两份)

虚继承(解决方案,A 只存在一份)

cpp
class A { public: int a_ = 1; };
class B : virtual public A { public: int b_ = 2; };  // 虚继承
class C : virtual public A { public: int c_ = 3; };  // 虚继承
class D : public B, public C { public: int d_ = 4; };
D 对象的内存布局(虚继承):

┌──────────────────────────────────┐
│  B 的子对象:                      │
│  ├── vbptr_B  → B 的虚基类表     │  ← 虚基类指针(指向偏移量表)
│  ├── b_                          │
├──────────────────────────────────┤
│  C 的子对象:                      │
│  ├── vbptr_C  → C 的虚基类表     │
│  ├── c_                          │
├──────────────────────────────────┤
│  D 自己的成员:                    │
│  ├── d_                          │
├──────────────────────────────────┤
│  A 的子对象(共享):              │  ← 只有一份 A!
│  ├── a_                          │
└──────────────────────────────────┘

虚基类表(vbtable)记录了从当前位置到虚基类子对象的偏移量。

三、C++ 对象的生命周期

⭐ 对象的构造与析构流程

cpp
class Base {
public:
    Base()         { std::cout << "1. Base ctor\n"; }
    virtual ~Base(){ std::cout << "6. Base dtor\n"; }
};

class Member {
public:
    Member()  { std::cout << "2. Member ctor\n"; }
    ~Member() { std::cout << "5. Member dtor\n"; }
};

class Derived : public Base {
    Member m_;
public:
    Derived()  { std::cout << "3. Derived ctor\n"; }
    ~Derived() { std::cout << "4. Derived dtor\n"; }
};

// 输出顺序:
// 构造:1 → 2 → 3
// 析构:4 → 5 → 6

⭐ 为什么构造函数中不能调用虚函数?

cpp
class Base {
public:
    Base() {
        Print();  // ⚠️ 调用的是 Base::Print,不是 Derived::Print!
    }
    virtual void Print() { std::cout << "Base\n"; }
};

class Derived : public Base {
    int value_ = 42;
public:
    void Print() override {
        std::cout << "Derived: " << value_ << "\n";  // value_ 此时还未初始化!
    }
};

Derived d;  // 输出 "Base",不是 "Derived: 42"

⚠️ 构造函数中调用虚函数不会表现出多态行为,因为此时派生类部分尚未构造完成。


四、智能指针的内部实现

unique_ptr 的实现原理

┌──────────────────────────────────────────────┐
│   unique_ptr<T>                              │
│   ┌────────────┐                             │
│   │ raw_ptr_   │ ─────→  T 对象(堆上)      │
│   └────────────┘                             │
│                                              │
│   特点:                                      │
│   • 内部只有一个裸指针,sizeof == sizeof(T*)   │
│   • 禁止拷贝(delete 了拷贝构造和赋值)        │
│   • 允许移动(移动后原指针置 nullptr)          │
│   • 析构时自动 delete                         │
│   • 零开销抽象(和裸指针一样高效)              │
└──────────────────────────────────────────────┘

简化实现:

cpp
template <typename T>
class UniquePtr {
    T* ptr_;
public:
    explicit UniquePtr(T* p = nullptr) : ptr_(p) {}
    ~UniquePtr() { delete ptr_; }

    // 禁止拷贝
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;

    // 允许移动
    UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }
    UniquePtr& operator=(UniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr_;
            ptr_ = other.ptr_;
            other.ptr_ = nullptr;
        }
        return *this;
    }

    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }
};

shared_ptr 的实现原理

shared_ptr<T> 的结构(两个指针):

┌───────────────────┐
│  shared_ptr sp1   │
│  ┌─────────────┐  │     ┌───────────────────┐
│  │ 对象指针 ptr │──┼────→│    T 对象(堆上)   │
│  ├─────────────┤  │     └───────────────────┘
│  │ 控制块指针   │──┼──┐
│  └─────────────┘  │  │
└───────────────────┘  │
                       │  ┌───────────────────────┐
┌───────────────────┐  └─→│     控制块 (Control    │
│  shared_ptr sp2   │     │       Block)           │
│  ┌─────────────┐  │     │                       │
│  │ 对象指针 ptr │──┼────→│  strong_count: 2      │ ← sp1 + sp2
│  ├─────────────┤  │     │  weak_count:   1      │ ← wp1
│  │ 控制块指针   │──┼────→│  deleter: default     │
│  └─────────────┘  │     │  allocator: default   │
└───────────────────┘     └───────────────────────┘
                       ┌──→
┌───────────────────┐  │
│  weak_ptr wp1     │  │
│  ┌─────────────┐  │  │
│  │ 对象指针 ptr │──┼──┘ (不增加 strong_count)
│  ├─────────────┤  │
│  │ 控制块指针   │──┼────→ 同一个控制块
│  └─────────────┘  │
└───────────────────┘

引用计数的变化流程

操作                          strong_count    weak_count
─────────────────────────────────────────────────────────
auto sp1 = make_shared<T>();      1              1*
auto sp2 = sp1;                   2              1
weak_ptr<T> wp = sp1;             2              2
sp2.reset();                      1              2
sp1.reset();                      0 → 析构T      2
                                                 (控制块还在)
wp 过期(expired)                                
wp 析构                           —              1
最后一个 weak 析构                  —              0 → 释放控制块

*注:控制块本身贡献 1 个 weak_count

make_shared vs shared_ptr(new T) 的内存差异

shared_ptr<T>(new T()):            make_shared<T>():

分配 #1:                           分配 #1(合并):
┌───────────┐                     ┌───────────────────────┐
│  T 对象    │                     │  控制块 + T 对象       │
└───────────┘                     │  ┌─────────────────┐  │
                                  │  │  strong_count    │  │
分配 #2:                           │  │  weak_count      │  │
┌───────────────┐                 │  │  deleter         │  │
│  控制块        │                 │  ├─────────────────┤  │
│  strong_count │                 │  │  T 的数据         │  │
│  weak_count   │                 │  └─────────────────┘  │
│  deleter      │                 └───────────────────────┘
└───────────────┘
                                  优势:
2 次内存分配                       • 只有 1 次内存分配
对象和控制块不连续                   • 缓存友好(连续内存)

五、new / delete 的底层实现

new 表达式的执行步骤

new[]delete[] 的区别

new T[5] 的内存布局:

┌──────────────────────────────────────────────────┐
│ 数组大小 (N=5) │ T[0] │ T[1] │ T[2] │ T[3] │ T[4] │
└──────────────────────────────────────────────────┘
↑ 实际分配起点     ↑ 返回给用户的指针

delete[] 时:
  ① 从返回指针前面读取 N = 5
  ② 逆序调用 5 次析构:T[4]→T[3]→T[2]→T[1]→T[0]
  ③ 释放整块内存

⚠️ new[] 必须用 delete[] 释放:
  • new T[5] + delete   → 只析构 T[0],泄漏 T[1]~T[4]!
  • new T    + delete[] → 未定义行为!

六、内存泄漏检测与防范

常见内存问题分类

⭐ 常用内存检测工具

工具类型平台检测能力
Valgrind动态分析Linux内存泄漏、越界、未初始化读取
AddressSanitizer (ASan)编译器插桩全平台越界、UAF、双重释放、泄漏
LeakSanitizer (LSan)编译器插桩Linux/macOS专注内存泄漏检测
ThreadSanitizer (TSan)编译器插桩全平台数据竞争检测
Clang Static Analyzer静态分析全平台编译期发现潜在问题
bash
# AddressSanitizer 使用示例
g++ -fsanitize=address -g main.cpp -o main
./main   # 自动报告内存问题

# Valgrind 使用示例
valgrind --leak-check=full ./main

七、堆内存分配器原理

malloc 的底层原理简述

用户调用 malloc(size)
          ↓
┌─────────────────────────────────────────┐
│  glibc 的 ptmalloc2 分配器               │
│                                         │
│  ┌──────────────────────────────────┐   │
│  │        空闲链表 (Free List)       │   │
│  │                                  │   │
│  │  小块 (< 512B):                  │   │
│  │   fastbin → 单链表, 不合并       │   │
│  │   smallbin → 双链表, 精确大小    │   │
│  │                                  │   │
│  │  大块 (≥ 512B):                  │   │
│  │   largebin → 按大小范围排序      │   │
│  │                                  │   │
│  │  超大块 (≥ 128KB):              │   │
│  │   直接 mmap() 映射              │   │
│  └──────────────────────────────────┘   │
│                                         │
│  分配策略:                               │
│   ① 先查 fastbin/smallbin               │
│   ② 再查 unsorted bin                   │
│   ③ 再查 largebin                       │
│   ④ 切割 top chunk                      │
│   ⑤ 调用 sbrk() 扩展堆                  │
│   ⑥ 超大块直接 mmap()                   │
└─────────────────────────────────────────┘

八、总结速查图


💬 评论

加载评论中...