2. 类与对象(封装 / 构造析构 / 深浅拷贝 / this)¶
⭐ 这一章是全卷第一考点。"看程序写结果"里出现最多的就是"构造和析构按什么顺序打印"。 把这一章吃透,及格线就稳了一半。
2.1 类和对象:图纸与房子¶
先建立最基本的直觉:
类 (Class) = 图纸。规定了"这种东西有哪些数据、能做哪些事"
对象 (Object) = 按图纸盖出来的房子。每盖一栋都有自己独立的数据
例:Rectangle 类规定"所有矩形都有宽和高,都能算面积"
一个宽 5 高 10 的具体矩形,就是一个对象
class Rectangle { // 图纸
int width, height; // 数据(成员变量)
public:
int area() { return width * height; } // 行为(成员函数)
};
Rectangle r; // r 是一个对象(实例 instance)
- 成员变量 / 属性:类里的数据(width、height)
- 成员函数 / 方法:类里的操作(area)
- 实例化 instance:用类创建对象的过程
2.2 封装:把数据藏起来,只留接口¶
封装 (Encapsulation) = 把"数据"和"操作数据的逻辑"捆在一起,并对外隐藏内部细节。外面只能通过你开放的接口来访问。
C++ 用三个访问修饰符控制谁能访问:
class Stack {
private: // 藏起来:外面不能直接动 data 和 top
int data[100];
int top = 0;
public: // 开放接口
void push(int x) { data[top++] = x; }
int pop() { return data[--top]; }
};
封装的好处:
⚠️ 必考冷知识:
struct和class在 C++ 里本质完全一样,都是类。 唯一区别:默认访问权限不同。 -class默认private-struct默认public
2.3 构造函数 Constructor:让对象"出身即合法"¶
为什么需要构造?¶
C 里 malloc 出来的内存是未初始化的随机值(脏数据)。如果不初始化就用,会读到垃圾,产生诡异 bug。构造函数的任务就是:把这块原始内存按你的逻辑初始化,让对象一出生就合法可用。
构造函数的三条规则(判断题高频)¶
class Player {
public:
Player() { score = 0; cout << "Default Constructor\n"; } // 默认构造(无参)
Player(int s) { score = s; } // 带参构造
int score;
};
Player p1; // 调默认构造,score = 0
Player p2(100); // 调带参构造,score = 100
默认构造函数的"消失"规则(⭐必考判断题)¶
判断题原题:Default constructor must be defined, if parameterized constructor is defined and the object is to be created without arguments. → T(对)
如果你既想要带参构造,又想要默认构造,用 = default 让编译器把默认构造"加回来":
class Player {
public:
Player() = default; // 提示编译器:给我生成默认构造
Player(int s) : score(s) {}
int score;
};
初始化列表(⭐高频,必须会写)¶
Player(int s) : score(s) {} 里冒号后面那部分叫初始化列表。它直接构造成员变量。
Player(int s) : score(s) {} // ✅ 初始化列表:直接用 s 构造 score
Player(int s) { score = s; } // 函数体内赋值:先默认构造 score,再赋值(多一步)
两条必须记的理由:
class C {
const int x; // const 成员
int& r; // 引用成员
public:
C(int v, int& ref) : x(v), r(ref) {} // 只能用初始化列表,函数体内赋值会报错
};
2.4 析构函数 Destructor:对象临终前的"清理工"¶
对象生命结束时自动调用,负责释放它占用的资源(堆内存、文件句柄等)。
析构函数的规则¶
class Buffer {
public:
int* data;
Buffer(int size) { data = new int[size]; } // 构造:申请内存
~Buffer() { delete[] data; } // 析构:归还内存
};
析构什么时候被触发?(⭐看程序写结果的核心)¶
输出:
2.5 ⭐核心题型:构造析构顺序¶
这是必考的"看程序写结果"。先记住三类对象的时机规律:
全局对象 最先构造(main 之前)、最后析构(程序结束)
static 局部 第一次执行到声明时才构造、程序结束才析构(只构造一次)
普通局部 执行到声明时构造、离开作用域立即析构
通用规律 "后构造的先析构"(像叠盘子,后放的先拿走)
经典原题(2013-2014 真题,⭐⭐必背)¶
class Obj {
char c;
public:
Obj(char cc) { c = cc; cout << "Obj::Obj for " << c << endl; }
~Obj() { cout << "Obj::~Obj for " << c << endl; }
};
void f() { static Obj b('b'); } // static 局部对象
void g() { Obj c('c'); } // 普通局部对象
Obj a('a'); // 全局对象
int main() {
cout << "inside main()" << endl;
f(); // 第一次进 f,构造 b
g(); // 构造 c,g 结束析构 c
f(); // 第二次进 f,b 已存在,不再构造
g(); // 又构造一个 c,g 结束析构 c
cout << "leaving main()" << endl;
return 0;
}
慢动作逐行推导:
程序启动 → 全局对象 a 最先构造 → "Obj::Obj for a"
进入 main → "inside main()"
f() 第一次:static b 第一次构造 → "Obj::Obj for b"
g() 第一次:局部 c 构造 → "Obj::Obj for c"
g() 结束,c 离开作用域析构 → "Obj::~Obj for c"
f() 第二次:b 已经构造过了,啥也不发生 →(无输出)
g() 第二次:又一个局部 c 构造 → "Obj::Obj for c"
g() 结束析构 → "Obj::~Obj for c"
→ "leaving main()"
main 结束,开始析构存活的对象:
static b 析构(后构造的先析构) → "Obj::~Obj for b"
全局 a 析构(最先构造,最后析构) → "Obj::~Obj for a"
完整答案:
Obj::Obj for a
inside main()
Obj::Obj for b
Obj::Obj for c
Obj::~Obj for c
Obj::Obj for c
Obj::~Obj for c
leaving main()
Obj::~Obj for b
Obj::~Obj for a
三个记忆钩子: - static 对象只构造一次(第二次进 f 没有输出) - 全局对象最先生、最后死 - 同一批对象后构造先析构
2.6 new 和 delete:C++ 的动态内存¶
C 用 malloc/free(只管内存)。C++ 用 new/delete,它们在分配/释放内存的同时,还会自动调用构造/析构函数。
Buffer* p1 = new Buffer(10); // 申请内存 + 调用构造函数
delete p1; // 调用析构函数 + 归还内存
int* p2 = new int[4]; // 数组形式
delete[] p2; // ⚠️ 数组必须用 delete[]
两条铁律(判断题高频):
⚠️ 判断题原题:用
delete[]释放new[]的数组,会对每个元素都调用析构函数 → T(对)。 而"只对第一个元素调析构" → F(错)。 如果错用了delete(没中括号)去删数组 → 未定义行为,可能只析构第一个、漏掉其余 → 内存泄漏。
2.7 ⭐深拷贝 vs 浅拷贝:double free 的根源¶
这是必考重点。当一个对象拷贝给另一个对象时,如果成员里有指针,问题就来了。
浅拷贝(Shallow Copy)—— 危险¶
逐个成员"按二进制原样复制"。如果成员是指针,只复制了地址,两个对象的指针指向同一块内存:
浅拷贝后的内存:
对象 m1 对象 m2
┌────────┐ ┌────────┐
│ data ──┼───┐ │ data ──┼───┐
└────────┘ │ └────────┘ │
▼ ▼
┌──────────────────────┐
│ 同一块堆内存 │ ← 两个对象指向同一块!
└──────────────────────┘
后果:m1 和 m2 析构时,各 delete 一次 → 同一块内存被释放两次
→ double free → 程序崩溃 💥
深拷贝(Deep Copy)—— 安全¶
不仅复制成员,还为指针成员重新申请一块新内存,把内容也复制过去:
深拷贝后的内存:
对象 m1 对象 m2
┌────────┐ ┌────────┐
│ data ──┼───┐ │ data ──┼───┐
└────────┘ │ └────────┘ │
▼ ▼
┌───────────┐ ┌───────────┐
│ 各自的内存 │ │ 各自的内存 │ ← 两块独立的内存
└───────────┘ └───────────┘
各自析构各自的内存,互不干扰 ✅
2.8 拷贝构造函数:用一个对象造一个新对象¶
用一个已存在的对象,去初始化一个新对象时调用。函数签名固定:
正确的深拷贝写法(以 Buffer 为例,必须会默写):
Buffer(const Buffer& other) : size(other.size) {
data = new int[size]; // ⚠️ 重新申请一块内存(深拷贝关键)
for (int i = 0; i < size; i++)
data[i] = other.data[i]; // 把内容逐个复制过去
}
拷贝构造的三个触发场景(⭐高频)¶
ClassA obj2 = obj1; // ① 用 obj1 初始化 obj2(注意:这是拷贝构造,不是赋值!)
ClassA obj2(obj1); // ② 同上,另一种写法
func(obj1); // ③ 以值传递方式向函数传对象(拷贝一份进去)
⚠️ 第①个最坑:
obj2 = obj1里有等号,但因为 obj2 正在被创建,所以调的是拷贝构造,不是拷贝赋值。区别看下一节。判断题:Copy constructors are overloaded constructors. → T(对)(拷贝构造是构造函数的一种重载)
2.9 拷贝赋值运算符:把一个已有对象的值赋给另一个已有对象¶
注意和拷贝构造的区别:两个对象都已经存在了,现在把右边的值赋给左边。函数签名固定:
正确写法(比拷贝构造多了"先释放旧资源"和"防自赋值",必须会默写):
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this; // ① 防止自赋值(a = a 时别把自己删了)
delete[] data; // ② 先释放自己原有的旧资源
size = other.size;
data = new int[size]; // ③ 申请新内存 + 深拷贝
for (int i = 0; i < size; i++)
data[i] = other.data[i];
return *this; // ④ 返回 *this,支持链式 a = b = c
}
拷贝构造 vs 拷贝赋值 —— 一句话区分:
这就是为什么拷贝赋值要先
delete[] data—— 因为 b 之前已经持有资源了,不先释放就泄漏。
2.10 this 指针:指向"调用这个函数的对象自己"¶
每个非静态成员函数内部,都隐藏着一个 this 指针,指向当前正在调用该函数的那个对象。
Buffer(const Buffer& other) : size(other.size) {
this->data = new int[size]; // this->data 就是当前对象的 data
for (int i = 0; i < this->size; i++)
this->data[i] = other.data[i];
}
• 成员函数里访问成员变量,编译器都会偷偷加上 this->(你不写也有)
• this 其实是成员函数的"隐藏第一个参数"(学过 Python 的话,类似 self)
• this 的类型是 ClassName* const(指针本身不能改,但能改它指向的对象)
this == &other 那行就是用 this 来判断"是不是在自己给自己赋值"。
2.11 本章自测¶
1. struct 和 class 唯一的区别? → 默认访问权限(struct public, class private)
2. 构造函数有返回值吗? → 没有,连 void 都不是
3. 定义了带参构造后还能 A a; 吗? → 不能,除非写 A() = default
4. const 成员只能怎么初始化? → 初始化列表
5. 浅拷贝为什么会崩溃? → 两对象指向同一内存,析构时 double free
6. ClassA b = a; 调的是拷贝构造还是赋值? → 拷贝构造(b 正在被创建)
7. 拷贝赋值为什么要先 delete[]? → b 已存在持有旧资源,不释放会泄漏
8. 全局、static、局部对象谁先构造谁先析构? → 全局最先构造最后析构;后构造的先析构