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

原理

对象内存布局

对象仅存储非静态成员变量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{}; // 必须显式——两阶段查找规则
 }
};

对象生命周期

对象生命周期(object lifetime)从构造函数完成时开始,到析构函数开始执行时结束。生命周期内对象的内存被视为有效,生命周期外访问是未定义行为。

// 栈对象:自动管理
{
    Widget w1(1); // 生命周期开始
    Widget w2(2); // 生命周期开始
} // w2、w1 按逆序析构,生命周期结束
 
// 堐对象:手动管理
Widget* pw = new Widget(3); // 生命周期开始,栈上存指针
// ... 使用 pw ...
delete pw; // 生命周期结束
 
// 生命周期与 placement new
alignas(Widget) char buf[sizeof(Widget)];
Widget* w = new(buf) Widget(42); // 在预分配内存上构造
w->~Widget(); // 手动调用析构函数
// buf 的内存仍然有效,但 Widget 的生命周期已结束

零原则 / 三五法则 / 五法则

这是 C++ 资源管理的核心设计指南:

// 零原则(Rule of Zero):如果类不直接管理资源,不声明任何特殊成员函数
class Simple {
    int x;
    std::string name; // string 自己管理内存
    std::vector<int> data; // vector 自己管理内存
    // 编译器生成的特殊成员函数完全正确,无需手写
};
 
// 三法则(Rule of Three):如果声明了析构/拷贝构造/拷贝赋值之一,通常三者都要声明
class RawBuffer {
    int* data;
    size_t size;
public:
    ~RawBuffer() { delete[] data; }           // 析构
    RawBuffer(const RawBuffer& other);        // 拷贝构造
    RawBuffer& operator=(const RawBuffer&);   // 拷贝赋值
    // 三者缺一不可,否则资源管理不一致
};
 
// 五法则(Rule of Five,C++11 起):在三法则基础上加上移动构造和移动赋值
class MyString {
    char* data;
    size_t len;
public:
    ~MyString() { delete[] data; }
    MyString(const MyString&);
    MyString& operator=(const MyString&);
    MyString(MyString&&) noexcept;            // 移动构造
    MyString& operator=(MyString&&) noexcept;  // 移动赋值
};
// 为什么需要五法则?没有移动语义的代价:
class Expensive {
    BigObject* ptr;
public:
    Expensive(const Expensive& other)
        : ptr(new BigObject(*other.ptr)) { // 深拷贝:O(n)
    }
    // 如果没有移动构造,std::vector 扩容时每次都要深拷贝
};
 
class Efficient {
    BigObject* ptr;
public:
    Efficient(Efficient&& other) noexcept
        : ptr(other.ptr) { // 移动:O(1)
        other.ptr = nullptr;
    }
    // std::vector 扩容时使用移动,性能提升巨大
};

拷贝消除(Copy Elision)

编译器在特定场景下会消除拷贝/移动操作,直接在目标对象的内存上构造:

Widget makeWidget() {
    return Widget(42); // C++17 起强制省略拷贝(mandatory elision)
}
 
// C++17 之前:可能执行移动构造
// C++17 之后:直接在调用者的内存上构造,零拷贝
 
Widget w = makeWidget(); // 不调用任何拷贝或移动构造函数
 
// C++17 的保证:
// 1. return 语句中的 prvalue 直接构造到目标对象
// 2. prvalue 用于初始化同类型对象时无拷贝
 
// NRVO(Named Return Value Optimization):
Widget createWidget() {
    Widget w(42);    // 命名对象
    w.setData(100);
    return w;         // 可能被优化为零拷贝,但编译器可以选择(C++17 之前非强制)
}
// NRVO 在 C++17 中仍可选(non-mandatory),但大多数编译器都会做

成员对象布局详解

// 继承 + 组合下的完整内存布局
class Base {
    int a;          // offset 4 (有 vptr) 或 0 (无虚函数)
    double b;       // offset 8
};
// 无虚函数:sizeof(Base) = 16: a(4)+pad(4)+b(8)
// 有虚函数:sizeof(Base) = 24: vptr(8)+a(4)+pad(4)+b(8)
 
class Derived : public Base {
    char c;         // offset 16 (有 vptr 时)
    int d;          // offset 20 (有 vptr 时)
};
// 有虚函数:sizeof(Derived) = 24: vptr(8)+a(4)+pad(4)+b(8)+c(1)+pad(3)+d(4)
 
// 空基类优化(EBO):空基类不占派生类空间
class Empty {};
class Derived2 : public Empty {
    int x;
};
// sizeof(Derived2) == sizeof(int) == 4,Empty 不占空间
// sizeof(Empty) == 1(C++ 要求对象至少 1 字节)

语法

类定义与访问控制

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 与比较)。

练习

题号题目链接知识点
155最小栈https://www.luogu.com.cn/problem/P1001类封装辅助数据结构、RAII 设计
P1024四则运算https://www.luogu.com.cn/problem/P1024语法练习
P1023ticketshttps://www.luogu.com.cn/problem/P1023语法练习
707设计链表https://www.luogu.com.cn/problem/P1001类封装链表节点、头尾指针管理
211添加与搜索单词https://www.luogu.com.cn/problem/P1001类的组合:Trie 节点嵌套设计
380O(1) 时间插入、删除和获取随机元素https://www.luogu.com.cn/problem/P1001类封装:vector + 哈希表组合
138复制带随机指针的链表https://www.luogu.com.cn/problem/P1001拷贝构造函数设计思路
460LFU 缓存https://www.luogu.com.cn/problem/P1001类的五法则实践、复杂资源管理
355设计推特https://www.luogu.com.cn/problem/P1001类组合设计:用户、推文、时间线
102二叉树的层序遍历https://www.luogu.com.cn/problem/P1001类封装 BFS 队列状态
303区域和检索 - 数组不可变https://www.luogu.com.cn/problem/P1001类初始化列表与预计算
304二维区域和检索 - 矩阵不可变https://www.luogu.com.cn/problem/P1001类成员初始化、前缀和预计算
98验证二叉搜索树https://www.luogu.com.cn/problem/P1001类设计:递归辅助类封装
208实现 Trie (前缀树)https://www.luogu.com.cn/problem/P1001类的嵌套设计、树形结构封装