【C++ 笔记中篇】类与对象(中级核心概念)
上一期我们聊了类的基本定义、访问控制、成员变量/函数的初步使用。这一期重点进入中级阶段最常考、最实用、也最容易出错的几个核心机制:
- this 指针
- 构造函数的6大默认成员函数(尤其是生成规则)
- 拷贝构造 & 移动构造(深浅拷贝、三五零法则)
- 析构函数的调用时机 & RAII思想
- 静态成员 & const 成员
- 初始化列表 vs 赋值初始化
- explicit / mutable 等修饰符
1. this 指针(最基础却最容易混淆的概念)
class Point {
int x, y;
public:
void set(int x, int y) { // 形参x,y 遮蔽了成员x,y
this->x = x; // 必须写this->才能访问成员
this->y = y;
}
Point& move(int dx, int dy) {
x += dx;
y += dy;
return *this; // 返回自身引用,支持链式调用
}
};
this 的本质:
- 类型:
类类型* const(指针常量,不能改变指向) - 每个非静态成员函数隐式第一个参数就是
this - 只有非静态成员函数才有
this - 静态成员函数没有
this(因为不依赖具体对象)
经典面试题:
Point& Point::operator=(const Point& rhs) {
if (this == &rhs) return *this; // 防止自赋值
// ...
}
2. 类的6大默认特殊成员函数(C++11/14/17/20 视角)
编译器可能自动生成以下函数(但有生成条件):
| 序号 | 函数名 | 默认生成条件(什么时候编译器会帮你写) | 是否 trivial(平凡的,可 memcpy) |
|---|---|---|---|
| 1 | 默认构造函数 | 用户没写任何构造函数时生成(C++11前) 用户没写任何构造函数且没声明 =default/=delete 时 | 是(成员都是 trivial 的情况下) |
| 2 | 拷贝构造函数 | 用户没声明时生成 | 是(同上) |
| 3 | 拷贝赋值运算符 | 用户没声明时生成 | 是 |
| 4 | 析构函数 | 用户没声明时生成(总是生成,除非用户 =delete) | 是 |
| 5 | 移动构造函数(C++11) | 用户没声明拷贝/移动构造/析构/拷贝赋值时,且有移动语义成员才生成 | 否(通常) |
| 6 | 移动赋值运算符(C++11) | 同上 | 否 |
现代 C++ 重要规则(2025-2026 面试最爱问):
- 规则0(零法则):尽量使用 RAII 类管理资源(如
std::unique_ptr、std::shared_ptr、std::string、std::vector),自己什么都不写。 - 规则3(三法则):如果你需要自定义析构函数、拷贝构造或拷贝赋值中的任意一个,通常需要同时定义全部三个(经典指针成员场景)。
- 规则5(五法则):如果你需要自定义析构、拷贝构造、拷贝赋值,通常也应该同时定义移动构造和移动赋值(C++11+ 推荐)。
- 规则5 的简化版:要么全用编译器默认(零法则),要么全自己写/禁用(五法则)。
3. 拷贝构造 vs 移动构造(最容易写错的地方)
class Widget {
std::string name;
int* data; // 拥有资源
public:
// 拷贝构造(深拷贝)
Widget(const Widget& other)
: name(other.name), data(new int(*other.data)) {}
// 移动构造(窃取资源)
Widget(Widget&& other) noexcept
: name(std::move(other.name)),
data(other.data) {
other.data = nullptr; // 重要!置空防止双重释放
}
~Widget() { delete data; }
};
移动语义关键点:
- 参数必须是
T&&(右值引用) - 函数要标记
noexcept(否则容器可能不使用移动) - 移动后要把源对象置于“合法但未指定状态”(通常把指针置 nullptr)
4. 成员初始化列表(必须掌握)
class Person {
std::string name;
const int id; // const 成员
int& ref_age; // 引用成员
public:
Person(std::string n, int i, int& a)
: name(std::move(n)) // 效率更高
, id(i) // const 必须在这里初始化
, ref_age(a) // 引用必须在这里绑定
{
// 这里不能初始化 const / 引用成员
}
};
必须用初始化列表的场景(面试必考):
- const 成员
- 引用成员
- 没有默认构造函数的成员对象
- 基类构造函数需要参数
- 想避免先构造再赋值的两步开销(性能)
5. 静态成员 & const 成员
class Singleton {
private:
static Singleton* instance; // 声明
Singleton() = default;
public:
static Singleton& getInstance() {
if (!instance) instance = new Singleton;
return *instance;
}
static int count; // 所有对象共享
};
Singleton* Singleton::instance = nullptr; // 定义(必须类外)
int Singleton::count = 0;
// const 静态成员可以在类内初始化(C++11+)
class Config {
static const int MAX = 100; // 整型 const static 可类内初始化
static constexpr double PI = 3.14159; // C++11 constexpr 更强
};
6. const 成员函数 & mutable
class TextBlock {
std::string text;
mutable size_t textLength; // mutable 允许 const 函数修改
mutable bool lengthValid = false;
public:
size_t length() const { // const 成员函数
if (!lengthValid) {
textLength = text.size();
lengthValid = true;
}
return textLength;
}
};
小结:中级阶段最常踩的 10 个坑
- 忘记写移动构造/赋值 → 容器退化成拷贝
- 移动后没把源对象指针置空 → 双重 delete
- const 成员 / 引用成员没用初始化列表
- 自赋值没判
if(this == &rhs) - 析构函数里 delete 了 nullptr(其实是安全的,但很多人紧张)
- 写了析构就忘了写拷贝构造 → 浅拷贝灾难
- 没把移动函数标记 noexcept
- static 成员忘记类外定义
- 在 const 函数里改了非 mutable 成员
- 滥用友元(friend)破坏封装
下一期可以继续聊继承与多态(虚函数、虚继承、override/final、纯虚函数、抽象类等),或者深入运算符重载、类型转换(explicit/single-argument ctor)、RAII+智能指针实战。
你现在更想先深入哪个方向?
- 继承 & 多态 & 虚函数表
- 运算符重载全攻略
- 现代 C++ 资源管理(智能指针 + RAII)
- 模板初步(类模板 / 函数模板)
或者你项目里最近在用哪些类设计模式 / 痛点,也可以直接抛出来,我们针对性拆解~