面向对象(一):类与对象基础
原理
对象内存布局
对象仅存储非静态成员变量和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 | 语法练习 |
| P1023 | tickets | https://www.luogu.com.cn/problem/P1023 | 语法练习 |
| 707 | 设计链表 | https://www.luogu.com.cn/problem/P1001 | 类封装链表节点、头尾指针管理 |
| 211 | 添加与搜索单词 | https://www.luogu.com.cn/problem/P1001 | 类的组合:Trie 节点嵌套设计 |
| 380 | O(1) 时间插入、删除和获取随机元素 | https://www.luogu.com.cn/problem/P1001 | 类封装:vector + 哈希表组合 |
| 138 | 复制带随机指针的链表 | https://www.luogu.com.cn/problem/P1001 | 拷贝构造函数设计思路 |
| 460 | LFU 缓存 | 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 | 类的嵌套设计、树形结构封装 |