面向对象(一):类与对象基础

原理

对象内存布局

对象仅存储非静态成员变量vptr(有虚函数时)。其余内容均不在对象内:

内容占对象空间?存储位置
非静态成员变量对象内,按声明顺序
vptr(有虚函数时)是(8B)对象偏移 0 处
构造函数/析构函数.text
普通成员函数.text
静态成员变量.data / .bss
静态成员函数.text

内存对齐

规则:当前偏移必须能被成员类型大小整除,否则插入 padding。对象总大小也必须能被最大对齐值整除。

64 位 Linux 常见对齐:char=1,short=2,int/float=4,double/指针/vptr=8。

class Demo {
    char  a;    // 1B, offset 0
    int   b;    // 4B, offset 4 (pad 3)
    double c;   // 8B, offset 8
    short d;    // 2B, offset 16
    virtual void f(); // vptr 8B, offset 0
    // vptr(8) + a(1)+pad(3) + b(4) + c(8) + d(2)+pad(6) = 32
};
// 注意:vptr 始终在对象开头,因此它改变了整个布局

this 指针的传递机制

每个非静态成员函数在调用时,编译器把对象地址作为第一个隐式参数传入:

  • System V ABI (Linux/macOS):this → rdi 寄存器
  • Microsoft x64 ABI (Windows):this → rcx 寄存器

在成员函数内部,this->x = val; 翻译为 mov [rdi + x_offset], esi。const 成员函数中的 this 类型为 const T* const——这就是 const 成员函数无法修改成员变量的底层原因。

构造函数的汇编层分解

; Widget w(42, "hello") 的底层过程:
; 1. 栈上分配空间
    sub   rsp, 32
; 2. 传递 this
    lea   rdi, [rsp]        ; rdi = &w = this
; 3. 传递参数
    mov   esi, 42           ; 参数1
    lea   rdx, [rip+.Lstr]  ; 参数2
; 4. 调用构造函数
    call  Widget::Widget(int, const char*)

构造/析构顺序

构造顺序:基类 → 成员变量(按声明顺序)→ 构造函数体。初始化列表先于构造函数体执行。析构顺序是完全逆序。

const/引用成员必须在初始化列表中初始化,因为它们没有默认构造后的赋值语义。

浅拷贝 vs 深拷贝

编译器默认生成的拷贝构造函数执行浅拷贝——对指针成员仅复制指针值(地址),导致两个对象内的指针指向同一块堆内存。析构时发生 double-free。深拷贝为新对象分配独立内存并逐字节复制内容。

// 深拷贝核心思路
DeepString(const DeepString& other)
    : buffer(new char[other.size + 1]) {
    memcpy(buffer, other.buffer, other.size + 1);
}

this 的隐式/显式使用规则

  • 隐式:直接写成员名时,编译器自动补 this->
  • 必须显式:参数名与成员名相同时;模板依赖名称(Base<T>:: 中的成员);链式调用返回 *this;将自身传给外部函数
template <typename T>
class Derived : public Base<T> {
    void process() {
        this->data = T{};  // 必须显式——两阶段查找规则
    }
};

语法

类定义与访问控制

class Widget {
public:     // 外部可访问
    Widget() : val(0) {}
    int getVal() const { return val; }
private:    // 仅类内部可访问
    int val;
protected:  // 派生类可访问
    static int count;  // 静态成员:所有对象共享
};

构造函数

// 默认、带参、拷贝、委托、移动
Widget() = default;                          // 编译器生成
Widget(int v) : val(v) {}                    // 带参
Widget(const Widget& other) : val(other.val) {} // 拷贝
Widget(int a, int b) : Widget(a + b) {}      // 委托(C++11)
Widget(Widget&& other) noexcept : val(other.val) { other.val = 0; } // 移动

explicit / =default / =delete

explicit Widget(int v) : val(v) {}  // 禁止隐式转换
Widget(const Widget&) = delete;      // 禁止拷贝
Widget& operator=(const Widget&) = default; // 显式要求编译器生成

析构函数

~Widget() { /* 清理资源 */ }
virtual ~Base() = default;  // 基类的析构必须是虚函数

static 成员

class Counter {
    static int total;
    static int getTotal() { return total; }
};
int Counter::total = 0;  // 类外定义

const / mutable

class Timer {
    int getVal() const { cachedVal = calc(); return cachedVal; }
    mutable int cachedVal;  // const 方法中可修改
};

实践

力扣题目:力扣去重排序(用类封装排序+去重),力扣数组题(struct/class 与比较)。

AI 自检:给 AI 以下代码,要求分析内存布局和构造函数调用顺序:

class A { int x; virtual void f(); };
class B : public A { double y; };
// 分析 sizeof(B) 的值和内部布局(含 vptr 位置)

建议先阅读03_指针与引用 — this 指针、成员指针、引用的底层理解;06_面向对象(二)继承 — 派生类构造/析构链和内存布局。