继承,派生 #
继承初始化 #
在构造函数后面的 : 加入基类的列表初始化:
class Base {
private:
int m_baseVar{};
public:
Base(int baseVar) : m_baseVar{baseVar} {}
};
class Derive : public Base {
private:
int m_deriveVar{};
public:
Derive(int baseVar, int deriveVar) : Base{baseVar}, m_deriveVar{deriveVar} {}
};
修改派生类的访问权限 #
访问修饰符 #
定义派生类的时候,: 后的访问限定符:
public: 基类所有访问类型,即为派生类的访问类型。protected: 基类的public在派生类中是protected。private: 基类的public和protected在派生类中是private。
基类的 private 在派生类中默认永远无法访问。
using来强制改变单个成员
#
可以单独在派生类中修改基类中可访问的成员在派生类的访问权限。
class Derive : public Base {
private:
using Base::func; // 假设这两个在 Base 都是公开的
using Base::m_var;
}
这样就把两个成员在派生类中限制在了 private 权限,重载函数会全部限制。
隐藏功能 #
如果基类和派生类存在同名的成员函数,那么派生类会隐藏基类的函数。
如果向派生类对象传入一个 int,基类里面的匹配 int,派生类匹配 double,还是会优先使用派生类的函数,然后通过数值转化把实参变成 double。
解决方案是在派生类强制使用 using Base::func 来使用基类的函数。
删除功能 #
像复制构造函数一样,可以显示删除某些不需要的基类功能,用 = delete。
但是仍然可以绕过派生类访问基类的功能:
std::cout << derived.Base::m_value;
std::cout << static_cast<Base&>(derived).m_value;
虚函数 #
虚函数是 RTTI(运行时类型信息)的典型代表。
在多个派生的类中,如果有签名一致的函数,前面加上 virtual 关键字,如果有类型为低派生的引用或者指针指向高派生的类,那么使用该指针将会自动调用尽可能高派生的类的函数。
如果一个派类在其继承链的中只要有一个类的函数是 virtual,那么其所有相同签名的函数均视为虚函数。
覆盖 #
使用 override 关键字在派生类中显式的覆盖相同签名的函数,如果无法覆盖会报错,这样避免了签名不一致导致未覆盖还过编译的情况。
override 放在成员函数的 const 后。
override 是隐式 virtual 的,所以只有最底层的基类型需要 virtual 关键字,其他都可以写 override。
final 放在 override 后,提示这是最后一个覆盖的派生类函数,后续派生的类若再覆盖,则报错。
协变返回类型 #
当派生类中返回一个 this 指针的时候,这个指针指向的类型只跟调用它的引用/指针类型有关。
虚析构函数 #
如果使用低层的引用/指针指向一个高层的派生类,释放的时候不会正确调用高层的析构函数。此时需要给析构函数添加 virtual 修饰符。
不推荐给所有的派生类添加 virtual,因为虚函数表会造成开销。如果不希望此类被继承,应该在定义类的时候在类名后添加 final 禁用对此类的继承。
绑定和调度 #
- 早期绑定/早期调度:在编译的时候就已经确定了调用函数的地址。
- 晚期绑定:使用函数指针调用函数
- 晚期调度:调用虚函数,通过虚表查询,只有在运行时才知道真正调用的函数。
纯虚函数,纯虚类,虚基类 #
纯虚函数指的是基类声明了有这个函数,但是实际上无法实例化,只能交由派生类实例化的函数。
在一个 virtual 后面加入 = 0 即可实现,也不需要函数体。
纯虚类(抽象类)其实相当于一个接口函数,这个类没有成员变量,只有纯虚函数,需要注意必须要有虚析构函数。此类不能被构造,一般在调用的时候,形参为其的引用/指针,并接受其派生类为实参。
虚基类在继承的 : 后面加入 virtual,目的是只有最后一个非虚的派生类创建对象,从而避免菱形继承造成的功能重复。
切片 #
当复制/移动的的目标类型是基类型时,会造成切片,只保留基类实例的部分。
RTTI #
dynamic_cast
#
用于向下转换(把一个基类型的指针转换成一个派生类型的指针),造成很大的开销,转换后需要判断是否转换成功。
当然用 static_cast 也可以转换,但是不会判断是否是否可以成功转换,访问的时候会造成内存问题。所以不推荐。
typeid
#
获取表达式和类型的类型信息,用于实现 C++ 中的自省。
重载的运算符 ==,用于比较两个对象是否完全一致。
成员函数 .name(),可被打印的、修饰过的编译器名字。
委派重载函数 #
<< 不能够被设置成虚函数,因为不是成员函数。但是可以委派给虚函数来打印,只需要把流对象的引用传递过去即可。
异常处理 #
经典的 try-catch 块和 throw 关键字。
在主函数里面使用 try 包裹功能调用函数的时候,函数也可以抛出异常,如果函数本身没有处理,会清理本块的资源,然后依次交由上一级调用栈处理,这个过程叫作栈展开。如果一直到主函数都没有处理,那么就会调用 std::terminate() 终止程序,此时可能未完成清理。
因此,建议使用 catch(...) 在最后捕获所有异常,此时可以保证所有资源都被清理。
析构函数是隐式 noexcept 的,因为如果析构函数又抛出异常(二次异常),那么程序将会直接终止。
异常类和重载不同,对于能同时接受派生类和基类的形参,会优先选择第一个 catch,所以需要把最匹配的放在前面。
异常对象的存储机制 #
异常对象会在抛出的时候进行复制,所以这个对象的类必须有复制构造函数,才能正常进行异常流程。
在 catch 中抛出异常
#
可以在 catch 中抛出异常,如果希望再次抛出相同的异常向上处理,应该直接使用 throw; 而不是显式指定传入的名字,因为按值复制的过程会造成类型切片。
函数级异常 #
严格在函数的 () 后写 try-catch 块,此过程有隐式重抛异常,最好自己手动指定。
一般用于构造函数中。
class B : public A {
public:
B(int x)
try
: A{x} // note addition of try keyword here
{
} catch (...) // note this is at same level of indentation as the function itself
{
// Exceptions from member initializer list or
// from constructor body are caught here
std::cerr << "Exception caught\n";
// throw; // rethrow the existing exception
}
}
noexcept
#
没有带 noexcept 的函数都是潜在的抛出异常的函数。
要求带有此标记的函数,必须在这个块前完成异常处理,如果超出了这个块,那么整个程序就被强行终止(使用 std::terminate)
使用 noexcept 允许编译器进行深度的优化,因为不需要考虑异常的栈展开机制。
std::move_if_noexcept
#
在编译的时候确定移动构造函数是否有 noexcept 关键字,如果没有,就 fallback 到复制。
其实此函数传入的形参是可变的,如果移动构造保证无异常抛出,那么就传入将亡值,使用移动构造函数;如果没有,那么就传入左值,使用复制构造函数。