Class¶
构造函数¶
构造函数不是给对象赋值,而是建立对象不变式的过程。对象不变式是指在对象的整个生命周期中,始终为真的条件。简单说,就是对象有效的状态。只要对象存在,它就必须满足这些条件,否则这个对象就处于错误或未定义的状态
初始化顺序:
- 虚基类(若有)
- 普通基类(按声明顺序)
- 成员变量(按类内声明顺序)
- 构造函数体执行
为什么必须用初始化列表:
const成员必须初始化,不能后赋值- 引用成员必须绑定,不能后改绑
- 对象成员(如
std::string)用初始化列表更高效:直接构造,避免默认构造 + 赋值
分类:
- 默认构造函数:无参数或参数全有默认值
- 带参构造函数:
- 拷贝构造函数:
T(const T&)。从同类型左值构造新对象 - 移动构造函数:
T(T&&)。从右值搬资源,减少拷贝成本 - 转换构造函数:单参数构造函数可用于隐式转换,通常要加
explicit防止意外
如果不写,编译器可能隐式生成特殊成员函数:
- 默认构造
- 析构
- 拷贝构造
- 拷贝赋值
- 移动构造
- 移动赋值
但一旦声明了某些函数,其他函数的自动生成可能被抑制:
- Rule of 0:优先让类型不自己管理裸资源,交给 RAII 类型(
std::string,std::vector,std::unique_ptr) - Rule of 3:若自定义析构/拷贝构造/拷贝赋值之一,通常三者都要考虑
- Rule of 5:C++11 后再加移动构造/移动赋值
在基类构造函数里调用虚函数,不会动态分派到派生类版本。因为派生部分尚未构造完成。因此不要在构造/析构期间依赖多态行为完成核心逻辑
构造函数抛异常时,对象未构造完成,不会调用该对象析构函数;但已完成构造的成员/基类会被正确析构
移动构造建议尽量 noexcept,标准容器(如 vector)扩容时会更倾向移动而非拷贝,性能和异常安全更好
析构函数¶
析构函数的核心不是删除对象,而是在对象生命周期结束时做资源回收与收尾,也就是 RAII 的后半部分。如果构造函数负责建立对象不变式,析构函数就负责安全地拆除不变式并释放资源
对象生命周期结束时自动调用:
- 栈对象离开作用域
- 临时对象表达式结束
delete动态对象- 容器销毁其元素
和构造相反,析构顺序是逆序:
- 先执行当前类析构函数体
- 再按成员声明逆序析构成员
- 再按继承逆序析构基类(先派生后基类)
当类直接管理资源时,通常要自定义析构函数,例如:
- 裸指针持有堆内存
- 文件句柄、socket、mutex、数据库连接
- 任何需要显式 close/release 的系统资源
如果成员本身就是 RAII 类型(string/vector/unique_ptr 等),通常不需要手写析构函数,遵循 Rule of 0 更好
虚析构函数¶
如果一个类会被当作多态基类使用(即可能用 Base* 指向 Derived,并通过基类指针 delete),基类析构函数必须是 virtual
只要类里有任何虚函数,通常就应把析构函数也设为 virtual
纯虚析构函数¶
可以把析构函数声明为纯虚,使类成为抽象类:
纯虚析构函数仍然必须提供定义(哪怕是空定义),因为销毁派生对象时基类析构阶段一定会用到它
析构函数应尽量不抛异常,实践上可视为必须不抛。因为若栈展开(异常传播)过程中析构函数再抛异常,会触发 terminate,程序直接终止
- 析构函数内部吞掉异常并记录日志
- 将可能失败的收尾动作做成显式 close/commit 接口,让调用方在正常流程处理错误
- 在语义上保持析构
noexcept(C++11 后析构默认是noexcept(true),除非被成员/基类影响)
友元¶
友元的本质是:某个函数或类虽然不是当前类的成员,但被当前类授权访问其 private 和 protected 成员。它是一个精确授权机制,不是继承关系,也不是成员关系
需要友元的场景:
- 运算符重载(尤其是输入输出流、对称二元运算符)
- 两个类需要高效协作,但不想暴露大量 public 接口
- 工厂函数、测试辅助函数需要访问内部状态
友元函数:在类里声明某个普通函数为 friend
友元类:把整个类授权给另一个类
友元成员函数:只授权某个类的某个成员函数,而不是整个类
- 友元不是成员:被声明为 friend 的函数,仍然是普通函数,不属于类作用域
- 友元关系不传递
- 友元关系不继承
- 友元关系不对称
- friend 声明位置不影响语义:写在 public/private/protected 都一样(只是阅读习惯问题)