跳过正文
  1. 文章/

现代 C++ 笔记:继承、虚函数与异常处理

·2271 字·5 分钟
目录
CPP 学习 - 这篇文章属于一个选集。
§ 9: 本文

继承,派生
#

继承初始化
#

在构造函数后面的 : 加入基类的列表初始化:

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: 基类的 publicprotected 在派生类中是 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 到复制。

其实此函数传入的形参是可变的,如果移动构造保证无异常抛出,那么就传入将亡值,使用移动构造函数;如果没有,那么就传入左值,使用复制构造函数。

CPP 学习 - 这篇文章属于一个选集。
§ 9: 本文

相关文章