|
| 1 | ++++ |
| 2 | +date = '2025-11-06T10:23:35+08:00' |
| 3 | +draft = true |
| 4 | +title = 'cpp学习笔记(9)' |
| 5 | ++++ |
| 6 | + |
| 7 | +## 继承,派生 |
| 8 | + |
| 9 | +### 继承初始化 |
| 10 | + |
| 11 | +在构造函数后面的 `:` 加入基类的列表初始化: |
| 12 | + |
| 13 | +```cpp |
| 14 | +class Base { |
| 15 | + private: |
| 16 | + int m_baseVar{}; |
| 17 | + |
| 18 | + public: |
| 19 | + Base(int baseVar) : m_baseVar{baseVar} {} |
| 20 | +}; |
| 21 | +class Derive : public Base { |
| 22 | + private: |
| 23 | + int m_deriveVar{}; |
| 24 | + |
| 25 | + public: |
| 26 | + Derive(int baseVar, int deriveVar) : Base{baseVar}, m_deriveVar{deriveVar} {} |
| 27 | +}; |
| 28 | +``` |
| 29 | +
|
| 30 | +### 修改派生类的访问权限 |
| 31 | +
|
| 32 | +#### 访问修饰符 |
| 33 | +
|
| 34 | +定义派生类的时候,`:` 后的访问限定符: |
| 35 | +
|
| 36 | +- `public`: 基类所有访问类型,即为派生类的访问类型。 |
| 37 | +- `protected`: 基类的 `public` 在派生类中是 `protected`。 |
| 38 | +- `private`: 基类的 `public` 和 `protected` 在派生类中是 `private`。 |
| 39 | +
|
| 40 | +基类的 `private` 在派生类中默认永远无法访问。 |
| 41 | +
|
| 42 | +#### `using`来强制改变单个成员 |
| 43 | +
|
| 44 | +可以单独在派生类中修改基类中可访问的成员在派生类的访问权限。 |
| 45 | +
|
| 46 | +```cpp |
| 47 | +class Derive : public Base { |
| 48 | + private: |
| 49 | + using Base::func; // 假设这两个在 Base 都是公开的 |
| 50 | + using Base::m_var; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +这样就把两个成员在派生类中限制在了 `private` 权限,重载函数会全部限制。 |
| 55 | + |
| 56 | +### 隐藏功能 |
| 57 | + |
| 58 | +如果基类和派生类存在同名的成员函数,那么派生类会隐藏基类的函数。 |
| 59 | + |
| 60 | +如果向派生类对象传入一个 `int`,基类里面的匹配 `int`,派生类匹配 `double`,还是会优先使用派生类的函数,然后通过数值转化把实参变成 `double`。 |
| 61 | + |
| 62 | +解决方案是在派生类强制使用 `using Base::func` 来使用基类的函数。 |
| 63 | + |
| 64 | +### 删除功能 |
| 65 | + |
| 66 | +像复制构造函数一样,可以显示删除某些不需要的基类功能,用 `= delete`。 |
| 67 | + |
| 68 | +但是仍然可以绕过派生类访问基类的功能: |
| 69 | + |
| 70 | +```cpp |
| 71 | +std::cout << derived.Base::m_value; |
| 72 | +std::cout << static_cast<Base&>(derived).m_value; |
| 73 | +``` |
| 74 | + |
| 75 | +### 虚函数 |
| 76 | + |
| 77 | +虚函数是 `RTTI`(运行时类型信息)的典型代表。 |
| 78 | + |
| 79 | +在多个派生的类中,如果有签名一致的函数,前面加上 `virtual` 关键字,如果有类型为低派生的引用或者指针指向高派生的类,那么使用该指针将会自动调用尽可能高派生的类的函数。 |
| 80 | + |
| 81 | +如果一个派类在其继承链的中只要有一个类的函数是 `virtual`,那么其所有相同签名的函数均视为虚函数。 |
| 82 | + |
| 83 | +#### 覆盖 |
| 84 | + |
| 85 | +使用 `override` 关键字在派生类中显式的覆盖相同签名的函数,如果无法覆盖会报错,这样避免了签名不一致导致未覆盖还过编译的情况。 |
| 86 | + |
| 87 | +`override` 放在成员函数的 `const` 后。 |
| 88 | + |
| 89 | +`override` 是隐式 `virtual` 的,所以只有最底层的基类型需要 `virtual` 关键字,其他都可以写 `override`。 |
| 90 | + |
| 91 | +`final` 放在 `override` 后,提示这是最后一个覆盖的派生类函数,后续派生的类若再覆盖,则报错。 |
| 92 | + |
| 93 | +#### 协变返回类型 |
| 94 | + |
| 95 | +当派生类中返回一个 `this` 指针的时候,这个指针指向的类型只跟调用它的引用/指针类型有关。 |
| 96 | + |
| 97 | +#### 虚析构函数 |
| 98 | + |
| 99 | +如果使用低层的引用/指针指向一个高层的派生类,释放的时候不会正确调用高层的析构函数。此时需要给析构函数添加 `virtual` 修饰符。 |
| 100 | + |
| 101 | +不推荐给所有的派生类添加 `virtual`,因为虚函数表会造成开销。如果不希望此类被继承,应该在定义类的时候在类名后添加 `final` 禁用对此类的继承。 |
| 102 | + |
| 103 | +#### 绑定和调度 |
| 104 | + |
| 105 | +- 早期绑定/早期调度:在编译的时候就已经确定了调用函数的地址。 |
| 106 | +- 晚期绑定:使用函数指针调用函数 |
| 107 | +- 晚期调度:调用虚函数,通过虚表查询,只有在运行时才知道真正调用的函数。 |
| 108 | + |
| 109 | +#### 纯虚函数,纯虚类,虚基类 |
| 110 | + |
| 111 | +纯虚函数指的是基类声明了有这个函数,但是实际上无法实例化,只能交由派生类实例化的函数。 |
| 112 | + |
| 113 | +在一个 `virtual` 后面加入 `= 0` 即可实现,也不需要函数体。 |
| 114 | + |
| 115 | +纯虚类(抽象类)其实相当于一个接口函数,这个类没有成员变量,只有纯虚函数,需要注意必须要有虚析构函数。此类不能被构造,一般在调用的时候,形参为其的引用/指针,并接受其派生类为实参。 |
| 116 | + |
| 117 | +虚基类在继承的 `:` 后面加入 `virtual`,目的是只有最后一个非虚的派生类创建对象,从而避免菱形继承造成的功能重复。 |
| 118 | + |
| 119 | +### 切片 |
| 120 | + |
| 121 | +当复制/移动的的目标类型是基类型时,会造成切片,只保留基类实例的部分。 |
| 122 | + |
| 123 | +### RTTI |
| 124 | + |
| 125 | +#### `dynamic_cast` |
| 126 | + |
| 127 | +用于向下转换(把一个基类型的指针转换成一个派生类型的指针),造成很大的开销,转换后需要判断是否转换成功。 |
| 128 | + |
| 129 | +当然用 `static_cast` 也可以转换,但是不会判断是否是否可以成功转换,访问的时候会造成内存问题。所以不推荐。 |
| 130 | + |
| 131 | +#### `typeid` |
| 132 | + |
| 133 | +获取表达式和类型的类型信息,用于实现C++中的自省。 |
| 134 | + |
| 135 | +重载的运算符 `==`,用于比较两个对象是否完全一致。 |
| 136 | + |
| 137 | +成员函数 `.name()`,可被打印的、修饰过的编译器名字。 |
| 138 | + |
| 139 | +### 委派重载函数 |
| 140 | + |
| 141 | +`<<` 不能够被设置成虚函数,因为不是成员函数。但是可以委派给虚函数来打印,只需要把流对象的引用传递过去即可。 |
| 142 | + |
| 143 | +## 异常处理 |
| 144 | + |
| 145 | +经典的 `try-catch` 块和 `throw` 关键字。 |
| 146 | + |
| 147 | +在主函数里面使用 `try` 包裹功能调用函数的时候,函数也可以抛出异常,如果函数本身没有处理,会清理本块的资源,然后依次交由上一级调用栈处理,这个过程叫作栈展开。如果一直到主函数都没有处理,那么就会调用 `std::terminate()` 终止程序,此时可能未完成清理。 |
| 148 | + |
| 149 | +因此,建议使用 `catch(...)` 在最后捕获所有异常,此时可以保证所有资源都被清理。 |
| 150 | + |
| 151 | +析构函数是隐式 `noexcept` 的,因为如果析构函数又抛出异常(二次异常),那么程序将会直接终止。 |
| 152 | + |
| 153 | +异常类和重载不同,对于能同时接受派生类和基类的形参,会优先选择第一个 `catch`,所以需要把最匹配的放在前面。 |
| 154 | + |
| 155 | +### 异常对象的存储机制 |
| 156 | + |
| 157 | +异常对象会在抛出的时候进行复制,所以这个对象的类必须有复制构造函数,才能正常进行异常流程。 |
| 158 | + |
| 159 | +### 在 `catch` 中抛出异常 |
| 160 | + |
| 161 | +可以在 `catch` 中抛出异常,如果希望再次抛出相同的异常向上处理,应该直接使用 `throw;` 而不是显式指定传入的名字,因为按值复制的过程会造成类型切片。 |
| 162 | + |
| 163 | +### 函数级异常 |
| 164 | + |
| 165 | +严格在函数的 `()` 后写 `try-catch` 块,此过程有隐式重抛异常,最好自己手动指定。 |
| 166 | + |
| 167 | +一般用于构造函数中。 |
| 168 | + |
| 169 | +```cpp |
| 170 | + |
| 171 | +class B : public A { |
| 172 | + public: |
| 173 | + B(int x) |
| 174 | + try |
| 175 | + : A{x} // note addition of try keyword here |
| 176 | + { |
| 177 | + } catch (...) // note this is at same level of indentation as the function itself |
| 178 | + { |
| 179 | + // Exceptions from member initializer list or |
| 180 | + // from constructor body are caught here |
| 181 | + |
| 182 | + std::cerr << "Exception caught\n"; |
| 183 | + |
| 184 | + // throw; // rethrow the existing exception |
| 185 | + } |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +### `noexcept` |
| 190 | + |
| 191 | +没有带 `noexcept` 的函数都是潜在的抛出异常的函数。 |
| 192 | + |
| 193 | +要求带有此标记的函数,必须在这个块前完成异常处理,如果超出了这个块,那么整个程序就被强行终止(使用 `std::terminate`) |
| 194 | + |
| 195 | +使用 `noexcept` 允许编译器进行深度的优化,因为不需要考虑异常的栈展开机制。 |
| 196 | + |
| 197 | +### `std::move_if_noexcept` |
| 198 | + |
| 199 | +在编译的时候确定移动构造函数是否有 `noexcept` 关键字,如果没有,就 `fallback` 到复制。 |
| 200 | + |
| 201 | +其实此函数传入的形参是可变的,如果移动构造保证无异常抛出,那么就传入将亡值,使用移动构造函数;如果没有,那么就传入左值,使用复制构造函数。 |
| 202 | + |
0 commit comments