面向对象 OOP:把数据和操作打包
C 的 struct 只能装数据。C++ 的 class 既能装数据、又能装操作这些数据的函数——把"是什么"和"能干嘛"绑成一个整体。封装、继承、多态是三大件,这一章讲清楚。
class 与 struct,构造与析构
一个最小的类
运行结果
辨class 和 struct 的唯一区别
没有本质区别,唯一差别是默认访问权限:class 默认 private,struct 默认 public。习惯上:纯数据打包用 struct,带行为和封装用 class。
拷贝 vs 移动:深拷贝与偷资源
这是 C++ 最容易翻车的地方。当你的类管着一块堆内存(比如一个动态数组),默认的"浅拷贝"只会复制指针值,结果两个对象指向同一块内存,析构时 free 两次——double free,直接崩。C++11 的"移动"则是"把资源偷过来",不复制。
| 特殊函数 | 什么时候调 |
|---|---|
| 拷贝构造 | 用一个旧对象造一个新对象,深拷贝(复制一份资源)。 |
| 拷贝赋值 | 已有对象 = 另一个对象。 |
| 移动构造(C++11) | 用一个即将销毁的临时对象造新对象:把它的资源偷过来,不复制。 |
| 移动赋值(C++11) | 同上,对象已存在时。 |
std::move(x) | 告诉编译器:"把 x 当临时对象,可以偷它的资源。" |
继承与多态
多态的意思是:同一个调用,不同的对象有不同的表现。靠虚函数 virtual 实现。C++11 加了 override(显式声明我在重写父类函数)和 final(不许再被重写),让多态更安全。
虚函数 + override:动物叫出不同声
提纯虚函数、抽象类、菱形问题
纯虚函数:virtual void draw() = 0;——只定接口不给实现,含它的类就是抽象类,不能实例化,只能被继承。相当于"规定派生类必须实现这个方法"。
菱形继承:A 是爷爷,B、C 都继承 A,D 又同时继承 B 和 C——D 里就有两份 A,容易乱。解决办法是虚继承(virtual public)让共同祖先只留一份。实际项目里设计得当尽量别踩菱形。
静态成员与友元
静态成员是"所有对象共享的一份",不归属某个具体对象,相当于类级别的全局变量。友元则是"破格让某个函数/类访问我的私有成员",用多了破坏封装,慎用。
静态成员:统计造了几个对象
继承中的构造析构顺序
派生类对象出生时,先造基类、再造派生类;销毁时反过来,先析构派生类、再析构基类。记住这个"洋葱"顺序,继承复杂对象时才不会乱。
| 阶段 | 顺序 |
|---|---|
| 构造 | 基类构造 → 成员对象构造 → 派生类自己的构造体。 |
| 析构 | 反过来:派生类析构 → 成员析构 → 基类析构。 |
如果用 基类指针 delete 一个派生类对象,而基类析构不是 virtual,派生类的析构根本不会被调用——派生类的资源(比如它 new 出来的东西)泄漏。只要一个类可能被当基类用,就把它的析构函数声明为 virtual。这是铁律。
完美转发 std::forward 与拷贝省略(RVO/NRVO)
写泛型包装函数时,你想把收到的参数"原封不动"转发给下一个函数:值就是值、左值就是左值、右值就是右值。但普通传参会把它们都"降级"成左值。std::forward<T> 就是干这个的——配合"转发引用"T&&(注意是模板里的,不是右值引用)。
std::forward:泛型工厂函数里"原样转交"参数
和 std::move 的区别一句话:std::move 无条件把东西转成右值(告诉编译器"可以偷");std::forward 是有条件——参数本来是右值才转成右值,本来是左值就保持左值。写泛型转发用 forward,别处想显式移动用 move。
拷贝省略(copy elision / RVO / NRVO):很多时候你"看着像拷贝",其实编译器偷偷根本不拷贝:
结论:别为了"怕拷贝"而到处传引用、写智能指针——现代编译器的 RVO/NRVO 会帮你优化掉。先按"值语义、简洁"写,遇到性能问题再局部改成移动/引用。