深蓝学院C++基础与深度解析笔记 第 12 章 类进阶
1. 运算符重载
● 使用 operator 关键字引入重载函数:
– 重载不能发明新的运算,不能改变运算的优先级与结合性,通常不改变运算含义
– 函数参数个数与运算操作数个数相同,至少一个为类类型
– 除 operator() 外其它运算符不能有缺省参数
– 可以选择实现为成员函数与非成员函数
● 通常来说,实现为成员函数会以 *this 作为第一个操作数(注意 == 与 <=> 的重载)
● 根据重载特性,可以将运算符进一步划分参考资料:
– 可重载且必须实现为成员函数的运算符( =,[],(),-> 与转型运算符)
– 可重载且可以实现为非成员函数的运算符,(理论上都可以)
– 可重载但不建议重载的运算符( &&, ||, 逗号运算符)
● C++17 中规定了相应的求值顺序(之前没有规定求值顺序),但没有方式实现短路逻辑
– 不可重载的运算符(如 ? : 运算符)
C++中不可重载的运算符列表总结:
- 作用域解析运算符 ::
- 成员指针运算符 .*
- 成员对象指针运算符 ->*
- 条件运算符(三元运算符) ?:
- 大小运算符 sizeof
- 成员选择运算符 .(点操作符)
- 成员访问运算符 .*(星号点操作符)
- 对象创建和销毁运算符 new 和 delete
- 对象创建和销毁运算符 new[] 和 delete[]
- 对象创建和销毁运算符 ::new 和 ::delete
- 对象创建和销毁运算符 ::new[] 和 ::delete[]
- 运行时类型信息运算符 typeid
- 数组下标访问运算符 []
- 函数调用运算符 ()
● 对称运算符通常定义为非成员函数以支持首个操作数的类型转换
● 移位运算符一定要定义为非成员函数,因为其首个操作数类型为流类型
● 赋值运算符也可以接收一般参数
● operator [] 通常返回引用
● 自增、自减运算符的前缀、后缀重载方法,括号内为空为前缀++:
后缀:
后缀比前缀会有更大开销,但是编译器可能会优化掉,不保证优化掉
● 使用解引用运算符( * )与成员访问运算符( -> )模拟指针行为
– 注意“ .” 运算符不能重载
– “→” 会递归调用 操作 “→”
opreater这只能返回:1、另外的一个类 或结构体 2、另外的一个类 或结构体指针
● 使用函数调用运算符构造可调用对象:
指在C++中,可以通过重载函数调用运算符(operator())来创建一个可调用对象,使其表现得像一个函数一样。
#include <iostream>
class MyCallable {
public:
void operator()(int value) const { // 重载了函数调用运算符 operator()
std::cout << "Called with value: " << value << std::endl;
}
};
int main() {
MyCallable myObj;
myObj(42); // 使用函数调用运算符调用对象
return 0;
}
通过函数调用运算符的重载,我们可以自定义可调用对象的行为,使其在使用时更加灵活和符合特定需求。这种方式可以用于函数对象、函数指针、Lambda 表达式等各种类型的可调用实体。
● 类型转换运算符
– 函数声明为 operator type() const ,通常会加const
– 与单参数构造函数一样,都引入了一种类型转换方式
– 注意避免引入歧义性与意料之外的行为
● 通过 explicit 引入显式类型转换, 只能通过显式调用来进行类型转换的构造函数。
– explicit bool 的特殊性:用于条件表达式时会进行隐式类型转换
● C++ 20 中对 == 与 <=> 的重载
– 通过 == 定义 !=, 反之不行
– 通过 <=> 定义多种比较逻辑
– 隐式交换操作数
– 注意 <=> 可返回的类型: strong_ordering, week_ordering, partial_ordering
2. 类的继承
● 通过类的继承(派生)来引入 是一个 的关系:(is - a,抽象不断具体化)
– 通常采用 public 继承( struct V.S. class )
– 注意:继承部分不是类的声明,声明不加继承方式,加了报错
– 使用基类的指针或引用可以指向派生类对象
– 静态类型 V.S. 动态类型
– protected 限定符:派生类可访问
使用基类的指针或引用可以指向派生类对象:
● 类的派生会形成嵌套域:
– 先构造基类,再构造子类
– 派生类所在域位于基类内部
– 派生类中的名称定义会覆盖基类
– 使用域操作符显式访问基类成员
– 在派生类中(含派生类的构造函数)调用基类的构造函数
● 通过虚函数与引用(指针)实现动态绑定
– 使用关键字 virtual 引入
– 非静态、非构造函数可声明为虚函数
– 虚函数会引入vtable结构
● dynamic_cast
示意图:
● 虚函数在基类中的定义
– 引入缺省逻辑
– 可以通过 = 0 声明纯虚函数,相应地构造抽象基类 ,可以但是不建议
● 虚函数在派生类中的重写( override )
– 函数签名保持不变(返回类型可以是原始返回指针 / 引用类型的派生指针 / 引用类型)
– 虚函数特性在子类中保持不变
– override 重写 关键字 final不再重写关键字
● 由虚函数所引入的动态绑定属于运行期行为,与编译期行为有所区别
– 虚函数的缺省实参只会考虑静态类型,编译期就完成了
– 虚函数的调用成本高于非虚函数
● final 关键字**
– 为什么要使用指针(或引用)引入动态绑定
– 在构造函数中调用虚函数要小心
– 派生类的析构函数会隐式调用基类的析构函数
– 通常来说要将基类的析构函数声明为 virtual 的
– 在派生类中修改虚函数的访问权限
2. 类的继承——继承与特殊成员函数
● 派生类合成的……
– 缺省构造函数会隐式调用基类的缺省构造函数
– 拷贝构造函数将隐式调用基类的拷贝构造函数
– 赋值函数将隐式调用基类的赋值函数
● 派生类的析构函数会调用基类的析构函数
● 派生类的其它构造函数将隐式调用基类的缺省构造函数
● 所有的特殊成员函数在显式定义时都可能需要显式调用基类相关成员
● 构造与销毁顺序
– 基类的构造函数会先调用,之后才涉及到派生类中数据成员的构造
– 派生类中的数据成员会被先销毁,之后才涉及到基类的析构函数调用
补充知识:
● public 与 private 继承参考资料
– public 继承:描述 是一个 的关系 “ ”;子类和父类的权限是一致的
– private 继承:描述 根据基类实现出 的关系 “ ”, 仅类内部可见
– protected 继承:几乎不会使用,基类
● using 与继承
– 使用 using 改变基类成员的访问权限
● 派生类可以访问该成员
● 无法改变构造函数的访问权限
– 使用 using 继承基类的构造函数逻辑
– using 与部分重写
● 继承与友元:友元关系无法继承,但基类的友元可以访问派生类中基类的相关成员;友元是单向的
● 通过基类指针实现在容器中保存不同类型对象,可以放置子类的类型
● 多重继承与虚继承 :将继承的访问方式改成 virtual
● 空基类优化 与 [no_unique_address] 属性 :
当派生类继承一个空基类时,根据空基类优化,编译器可以选择不为派生类分配额外的空间来存储空基类的对象。相反,派生类可以共享基类的内存空间,从而减少派生类对象的总体大小。
[[no_unique_address]]
属性是C++ 20 标准提供的一种属性(attribute),用于指示编译器在内存布局中不分配额外的空间来存储特定的成员变量。它可以应用于非静态数据成员,包括非静态数据成员的嵌套。
使用[[no_unique_address]
]属性有助于减小类的大小,尤其是在存在多个成员变量且某些成员变量具有相同类型且不需要独立的地址时。这种情况下,编译器可以选择共享存储空间,从而减少对象的总体大小。