04 · 类与对象
定位:构造/析构、拷贝/赋值/移动、初始化列表是“C++ 六大默认函数”的考点,必考。 学完标准:能答全“六大函数什么时候被调用”;能解释为什么初始化列表更快;空类大小、explicit、delete/default 张口就来。
1. 类的六个特殊成员函数(Rule of 0/3/5)
Section titled “1. 类的六个特殊成员函数(Rule of 0/3/5)”编译器会自动生成(当需要且未被自定义时):
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11)
- 移动赋值运算符(C++11)
经验法则:
- Rule of 0:成员都用标准库类型(string/vector/智能指针),什么都不用自己写。
- Rule of 3(C++11 前):自定义了析构/拷贝构造/拷贝赋值其中任意一个,三个都要自己写(否则浅拷贝→double free)。
- Rule of 5:涉及动态资源 + 需要移动语义时,析构/拷贝构造/拷贝赋值/移动构造/移动赋值五个都写(或都 delete)。
面试金句:一旦类管理裸资源(裸指针、文件句柄),默认的浅拷贝会产生两次析构同一块内存(double free),所以必须深拷贝或禁用拷贝。
2. 构造与析构
Section titled “2. 构造与析构”2.1 构造函数初始化顺序(必背)
Section titled “2.1 构造函数初始化顺序(必背)”顺序:先基类(按继承顺序)→ 再成员(按【声明顺序】,不是初始化列表顺序)→ 最后构造函数体析构:完全相反:自身析构体 → 成员析构(逆声明顺序)→ 基类析构struct Base { Base(){ puts("Base构造"); } ~Base(){ puts("Base析构"); } };
struct Member { Member(int){ puts("Member构造"); } ~Member(){ puts("Member析构"); }};
struct Derived : Base { Member m1{1}; // 声明顺序:m1 在前 Member m2{2}; // 声明顺序:m2 在后 Derived() : Base(), m2(2), m1(1) { // 列表里写反没关系! puts("Derived构造体"); } ~Derived(){ puts("Derived析构体"); }};
int main(){ Derived d; }// 输出顺序:// Base构造 → Member构造(1) → Member构造(2) → Derived构造体// Derived析构体 → Member析构(2) → Member析构(1) → Base析构坑:初始化列表里 m2(2) 写在前面也没用,实际构造顺序按声明顺序 m1 先。编译器会警告“初始化顺序不匹配”。
2.2 为什么用初始化列表?(高频)
Section titled “2.2 为什么用初始化列表?(高频)”struct T { Member m; // 方式A:列表初始化 —— 直接以 42 构造 m,只调一次构造 T() : m(42) {}};
struct U { Member m; // 方式B:体内赋值 —— 先构造 m,再把 42 转成临时 Member 后赋值,多一步 U() { m = 42; }};必须用初始化列表的三种成员:
const成员(const 必须初始化,不能赋值);- 引用成员(引用必须初始化);
- 没有默认构造函数的成员对象(
Member m;没有无参构造 → 必须先: m(42))。
性能:列表初始化直接以目标值构造成员(一次构造);体内 m = 42 要先构造临时对象再赋值(构造 + 赋值,多一步)。对重量级成员是浪费。
2.3 析构函数要点
Section titled “2.3 析构函数要点”- 无参数、无返回值、不可重载。
- 对象离开作用域/delete 时自动调用,用于释放资源。
- 析构里不要抛异常(否则可能 terminate;异常安全要求析构 noexcept)。 !!这里不太懂
- 基类析构要 virtual(第 5 章重点)。
3. 拷贝构造 vs 赋值运算符(必考区分)
Section titled “3. 拷贝构造 vs 赋值运算符(必考区分)”struct S { S(); S(const S&); S& operator=(const S&); };
S a; // 默认构造S b = a; // 拷贝构造(新对象 b 从 a 构造)★S c(a); // 拷贝构造 ★b = c; // 拷贝赋值(b 已存在,赋新值)★
void f(S s); // 按值传参 → 拷贝构造S g() { S t; return t; } // 返回临时 → 拷贝构造(实际常被优化省略)判断口诀:“有新对象 = 拷贝构造,老对象赋值 = 拷贝赋值”。看运算符左边,是“创建”还是“已存在”。
3.1 浅拷贝 vs 深拷贝
Section titled “3.1 浅拷贝 vs 深拷贝”struct Buffer { int* data; Buffer() : data(new int[100]) {} ~Buffer(){ delete[] data; } // 不写拷贝构造 → 默认浅拷贝:拷贝指针本身};
Buffer a;Buffer b = a; // b.data 和 a.data 指向同一块内存!// 析构时:a 和 b 各自 delete[] data → 同一块内存删两次 → double free 崩溃- 浅拷贝:只复制指针值(成员逐个拷贝)。
- 深拷贝:为拷贝对象新分配资源并复制内容,让两者各自独立。
- 管理裸资源的类必须实现深拷贝(或禁用拷贝)。
3.2 正确写法
Section titled “3.2 正确写法”class Buffer { int* data; size_t size;public: Buffer(size_t n) : data(new int[n]), size(n) {} // 拷贝构造:深拷贝 Buffer(const Buffer& o) : data(new int[o.size]), size(o.size) { std::copy(o.data, o.data + o.size, data); } // 拷贝赋值:先分配新内存,成功后再释放旧资源(还要自赋值检查) Buffer& operator=(const Buffer& o) { if (this != &o) { // 自赋值检查 int* tmp = new int[o.size]; // 先分配新内存:new 失败时旧对象不受影响 std::copy(o.data, o.data + o.size, tmp); delete[] data; // 成功后再释放旧资源 data = tmp; size = o.size; } return *this; // 返回引用,支持链式 a=b=c } ~Buffer(){ delete[] data; }};现代写法:拷贝赋值可以用 copy-and-swap 惯用法(传值 + swap),更简洁且异常安全——面试提到是加分项。 !!展开说说
4. 移动语义(本章先建立直觉,第 8 章深入)
Section titled “4. 移动语义(本章先建立直觉,第 8 章深入)”- 拷贝:复制内容,源对象不变。
- 移动:把源对象的资源“偷”过来(搬走指针),源对象置空。成本 O(1) 于资源指针。
- 什么时候触发:参数是右值(临时对象、
std::move(x)结果)时,优先选移动。
class Buffer { int* data; size_t size;public: Buffer(Buffer&& o) noexcept // 移动构造 : data(o.data), size(o.size) { o.data = nullptr; o.size = 0; // 源对象置空(防双释放) } Buffer& operator=(Buffer&& o) noexcept { if (this != &o) { delete[] data; // 释放旧资源 data = o.data; size = o.size; o.data = nullptr; o.size = 0; } return *this; }};为什么移动后源对象“不能用”:std::move 只是转型,但移动构造把源指针置空了,源对象处于“有效但未指定”状态,可以析构、可以重新赋值,但内容不可依赖。
面试点:移动构造/移动赋值应尽量 noexcept——否则 vector 扩容时若类可拷贝会退化为拷贝(异常安全考虑),性能优势就没了;类不可拷贝时仍会移动。 !!没懂
5. 空类、static 成员、friend、explicit、delete/default
Section titled “5. 空类、static 成员、friend、explicit、delete/default”5.1 空类 sizeof 为什么是 1?(高频)
Section titled “5.1 空类 sizeof 为什么是 1?(高频)”class Empty {};sizeof(Empty) == 1;原因:C++ 要求不同对象地址必须不同,空对象也得占至少 1 字节来区分。
class WithVirtual { virtual void f(); };sizeof(WithVirtual) == 8; // 64位:一个 vptr 占 8 字节class EmptyA{}; class EmptyB{}; class Both : EmptyA, EmptyB {};sizeof(Both) == 1; // 不同类型的空基类可以共享同一地址(EBO);只有同类型的两个子对象必须地址不同5.2 static 成员
Section titled “5.2 static 成员”class Game { static int instanceCount; // 声明:不占对象空间,属于类};int Game::instanceCount = 0; // 定义(类外必须定义一次)!!sizeof(Game) == 1吗
- static 成员变量属于类、所有对象共享一份,存放在数据段。
- static 成员函数没有 this 指针,只能访问 static 成员。
- C++17 起可以用
inline static int x = 0;在类内直接定义。
5.3 friend
Section titled “5.3 friend”!!怎么用
- 友元函数/友元类:可以访问该类的私有成员。
- 适度使用(运算符重载常需要,如
operator<<输出流)。
5.4 explicit(单个参数构造函数必加)
Section titled “5.4 explicit(单个参数构造函数必加)”class Vec2 { public: Vec2(float x) { /* ... */ } };void setPos(Vec2 v);setPos(1.0f); // 隐式转换:1.0f → Vec2(1.0f)。常是 bug 来源!
// 加 explicit 后:class Vec2 { public: explicit Vec2(float x) { } };setPos(1.0f); // 编译错误!禁止隐式转换setPos(Vec2(1.0f)); // 必须显式构造面试点:vector<bool> 的 operator[] 返回 proxy 也是因为不能隐式转 bool 引用。explicit 同样可以修饰运算符。 !!啥意思
5.5 =delete / =default
Section titled “5.5 =delete / =default”class NoCopy {public: NoCopy() = default; // 手动要回默认构造函数(自定义了别的构造后默认被吞掉) NoCopy(const NoCopy&) = delete; // 禁止拷贝 NoCopy& operator=(const NoCopy&) = delete;};=default:显式要求编译器生成默认版本。例如自定义构造函数后默认构造函数不再自动生成,但拷贝构造/拷贝赋值仍会自动生成;=default用来把默认构造要回来或明确意图。=delete:禁用某函数;常见用途:禁拷贝、禁隐式类型转换(删掉对应构造)、禁 new(删operator new)。!!详细说说例子
6. 运算符重载要点
Section titled “6. 运算符重载要点”struct Vec2 { float x, y; Vec2 operator+(const Vec2& o) const { return {x+o.x, y+o.y}; } bool operator==(const Vec2& o) const { return x==o.x && y==o.y; }};要点:
- 推荐成员函数 vs 自由函数:二元运算符如
<<、+(可交换)用自由函数。 ++avsa++:前置返回引用(T& operator++()),后置返回值(T operator++(int))。 !!举个例子operator<<输出需要 friend。!!举例- 不能重载:
::..*?:sizeof。 - 重载不改变运算符优先级和操作数个数。
7. 高频面试题 Q&A
Section titled “7. 高频面试题 Q&A”Q1:构造/析构顺序? 先基类再成员(按声明顺序)后自身;析构反过来。初始化列表顺序由声明顺序决定,与列表书写顺序无关。
Q2:拷贝构造和赋值运算符什么时候被调用? 有新对象创建 → 拷贝构造;对已存在对象赋值 → 拷贝赋值。按值传参/返回触发拷贝构造。
Q3:为什么用初始化列表? 直接初始化成员(一次构造),比“默认构造+赋值”省一次操作;const/引用/无默认构造成员必须用它。
Q4:浅拷贝有什么问题? 管理裸资源时两个对象共享同一块内存,析构 double free。
Q5:移动构造什么时候触发?为什么移动后源对象不能用? 参数是右值时(临时对象、move 结果);移动把源资源搬走并置空源指针,源对象内容不再有效。
Q6:移动构造为什么必须 noexcept?
vector 扩容用 move_if_noexcept:移动可能抛异常且类可拷贝时会退化为拷贝;类不可拷贝时仍会移动。noexcept 让标准库放心用移动。
Q7:空类 sizeof 为什么是 1? 保证不同对象地址不同。
Q8:explicit 的作用? 禁止构造函数隐式转换,避免意外隐式转换产生错误对象。
Q9:=delete 和 =default? default 要回编译器默认实现;delete 禁用函数(禁拷贝、禁隐式转换)。
Q10:为什么析构函数不建议抛异常? 析构在栈展开时执行,抛异常可能 double throw → std::terminate 直接终止程序。
Q11:什么时候必须自己写拷贝/析构? 类持有裸资源(堆内存、文件、套接字、GPU 资源)。用 RAII 封装资源(string/vector/智能指针)就不用。
Q12:复制对象时编译器会优化掉拷贝构造吗? 会:返回局部对象时的 RVO/NRVO、按值传递临时对象时的拷贝省略(C++17 保证某些情况)。面试可提“拷贝省略”。
8. 本章自测(10 题)
Section titled “8. 本章自测(10 题)”1. 说出类的六个默认特殊成员函数。
2. 拷贝构造和拷贝赋值如何快速区分?
3. 为什么 `const` 成员必须用初始化列表初始化?
4. 写出移动构造的模板(含 noexcept 和置空源对象)。
5. 空类大小?
我的原答:含虚函数的类大小(64位)?为什么?
6. static 成员变量为什么需要在类外定义?
我的原答:(C++17 怎么解决)
7. `explicit Vec2(float)` 后 `Vec2 v = 1.0f;` 会怎样?
8. 浅拷贝导致 double free 的完整过程讲一遍。
9. `Buffer b2 = std::move(b1);` 之后 b1 的状态是什么?
10. 为什么拷贝赋值要返回引用?
📖 答案提示(先自己答完再展开)
- 默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值。
- 左边是“创建新对象”→拷贝构造;“已存在对象”→拷贝赋值。
- const 成员只能初始化不能赋值;初始化列表是“初始化”,函数体是“赋值”。
Buffer(Buffer&& o) noexcept : data(o.data), size(o.size){ o.data=nullptr; o.size=0; }- 1;8(vptr);空对象要地址唯一,vptr 占 8 字节。
- static 成员属于类不随对象构造,需要类外单独定义分配存储;C++17
inline static类内定义。 - 编译错误(禁止隐式转换)。
- a、b 共享同一 data → a 析构释放 → b 析构再释放同一地址 → double free 崩溃。
- “有效但未指定”:b1 指针被置空,可析构可重新赋值,内容不可依赖。
- 支持链式赋值
a = b = c,符合内建类型语义。
- copy-and-swap 惯用法怎么实现?(传值参数 +
std::swap,异常安全) - 什么是三/五法则(Rule of 0/3/5)?给出一个违反的例子。
- 为什么基类析构要 virtual?不做会怎样?(衔接第 5 章)
- 空基类优化(EBO)是什么?
std::tuple、标准库 allocator 怎么用它省内存?