C++ Primer(第五版)第十五章 - 面向对象程序设计。

  1. OOP

    Object-Oriented Programing

  2. OOP核心思想

    数据抽象继承动态绑定

    数据抽象:将类的接口与实现分离

    继承: 定义相似类型并对相似关系建模

    动态绑定(多态): 在一定程度上忽略相似类型的区别,以统一的方式使用它们的对象。

  3. 基类与派生类的存储

    派生类包含基类的部分。所以可以隐式的将派生类转为基类(或许应该加限制,通过引用或指针的形式)。

    所以,派生类的构造函数需要先构造基类部分,再构造派生类独有的部分。派生类析构时,先析构自己独有的,再析构基类。

    派生类只能通过基类的构造函数来啊初始化基类数据成员。同理,只应该让基类的析构函数去析构基类的成员。

  4. 基类在被继承前必须已经声明完成。

    即,不完整类型不能用作基类。这隐式地说,一个类不能自身继承自身。

  5. 静态类型与动态类型

    静态类型就是在编译时已知的类型,它是变量声明时的类型或者表达式生成的类型。

    动态类型是变量或表达式表示的内存中的对象的类型。动态类型只有在运行时才能确定。

    动态类型与静态类型不一致,当且仅当通过指向基类的指针或引用来调用虚函数时才有可能发生。即发生动态绑定时,动态类型与静态类型可能不一致。

  6. 继承类与基类的转换

    不存在基类想继承类的转换,因为基类只是是继承类的一部分。

    不存在继承类对象向基类对象的转换。只有通过引用或者指针的形式!

  7. 何时才可能发生动态绑定

    只有通过指针或者引用调用虚函数时,才会在运行时解析该调用。

    故以下两种不可能发生动态绑定——

    1. 通过对象调用虚函数。

    2. 通过引用或指针调用非虚函数。

  8. 针对虚函数的final修饰符与override修饰符

    第一,finial和override修饰符只针对虚函数!

     // clang 的报错
     error: only virtual member functions can be marked 'final'
     error: only virtual member functions can be marked 'override'
    

    一个虚函数定义为final后,继承类就再也不能重写它了。

    一个虚函数在继承类中被用override修饰,是为了让编译器帮助检查我们是否真的覆盖了这个虚函数,而不是仅仅定义了一个同名的函数(隐藏了父类的虚函数)。

     ‘void DS::print() const’ marked override, but does not override
    

    final与override之间没有顺序关系,但是二者都必须放在其他函数修饰符之后(const,引用修饰符?)。

     virtual void print() const final override ;
    

    最后,final、override是声明修饰符,定义时不能出现!

  9. 含有纯虚函数的类(抽象类)不能实例化对象

    即使这个纯虚函数在外部有定义,还是不能实例化。

    一般来说,抽象类是用于定义接口的,不是用于实例化的。

  10. 派生类列表中的访问控制符只影响后续的派生类及类的使用者

    就是这样一种情况:

    class Derived : [public/protect/private] Base
    {}
    

    然后首先说明三种用户:

    1. 类自身(自身成员) + 友元

    2. 类的继承者们

    3. 类的使用者

    然后,在派生类表的限制访问符,对用户1,即类自身是毫无影响的!在Derived内部,能否访问Base的成员,完全取决于Base中成员的可访问性。但是,对于类的继承者和类的使用者,对于基类成员的访问就受到了该控制符的影响。具体的就是继承者只能访问protected以上,外部只能使用public。

  11. 派生类向基类的转换

    这里似乎仍然与上面相同。对于类自身,均可转换;对于继承者,只有protected以上;用户代码只能public。

    看下面的代码:

    // 定义基类,以及3个不同访问权限的派生类
    class Base
    {
        int a = 10;
    };
    
    class Pub_D : public Base
    {};
    
    class Prot_D : protected Base
    {
        public:
        void set(Base &b){ b = *this; }
    };
    
    class Priv_D : private Base
    {
            
    };
    
    // 测试转换访问权限
    int main(int argc , char *argv[])
    {
        Pub_D pubd;
        Prot_D protd;
        Priv_D privd;
        Base &b = pubd; // OK
        protd.set(b); // OK , 在类内部转换
    
        Base *pb = &pubd; // OK , public的
        pb = &prot; // Error, 外部不可访问
        pb = &privd; // Error,外部不可访问
        return 0;
    }
    
  12. 对基类的友元 对基类成员的访问由基类本身决定

    尽管我们知道友元关系是不可继承的,但是基类的友元对于派生类中基类成员的访问仍然是完全可行的。

    即,尽管基类的友元与派生类没有关系,但是还是可以访问派生类中基类的部分!

    这个看起来有些奇怪,不过这也说明了一点:派生类中基类的部分真的是完全分开的!尽管是融合在一起了,但是访问权限依然还在。所以,可以认为派生类就是在内部有个到基类的指针。然后派生类自己可以任意控制指针的访问权限,但是外部类要访问基类就需要过这个权限限制了。然后派生类与基类的本身的成员是相互独立的,权限各自独立。

    当然,基类的友元能不能从派生类中访问到基类,还是首先要看基类数如何被继承的。

  13. 继承中的作用域

    这个很关键,名字查找就是跟作用域有关嘛。

    结论就是,派生类的作用域嵌套在基类中。 就是 基类在外层,派生类在内层。这种关系是在派生类中能够访问基类成员的本质原因(当然,有派生访问权限控制能否向上访问基类作用域)。

    因为作用域不同,所以派生类与基类间不存在重载关系。对于非虚函数,只能有隐藏基类的关系。对虚函数,可能有覆盖(当函数签名相同),或者隐藏(签名不同,则隐藏)。

  14. 重点:继承中的名字查找

    编译器在解释一个类调用(翻译为下层表示)时,如 obj.mem_func() / obj.mem_var, 会按照如下规则

    1. 确定obj的静态类型。其必须是一个类类型,才能够有调用或访问成员的能力

    2. 根据该静态类型找到该类的声明,然后从当前类的声明中找调用或访问的成员名字。如果找到了,跳到下一步;如果没有找到,就按照继承链往上找,直到找到、或者到达最顶层基类、或者因为访问控制而停止。如果最终没有找到,那么编译报错,退出。

    3. 找到了名字,检查成员的类型与调用处是否可匹配。对函数,就是返回类型、实参与形参的匹配;对普通变量,就是类型(当然,函数完全也可以看做变量了)。如果不匹配(包括重载检查吧),编译报错。否则到第4步。

    4. 如果是变量,直接生成相应的代码;如果是函数,需要判断

      1. 如果是虚函数,且对象是个指针或者引用,那么生成动态绑定的代码

      2. 否则,直接生成静态调用的代码。

    以上的过程带来为什么派生类中同名但签名不同的函数为什么会覆盖基类中所有同名的函数。这是因为一旦找到名字,立刻就停止查找了,作用域就限制住了!因为查找是先完成的,并不会边查找,边匹配函数签名。

  15. 对于基类的有重载函数(不论虚、非虚),派生类要么全部重写该重载函数的所用签名形式(可以直接用一条using语句),要么不覆盖任何一个。

    原理就是上面的名字查找过程。基类的所有重载,都是一个名字,然后对应不同的签名。如果派生类出现了该名字,那么查找过程立即在派生类就停止了。然后下一步查找签名的过程就只能在派生类的作用域下完成了。所以要么让名字查找停留到基类上,要么在派生类下重写(覆盖或隐藏)所有重载签名,否则在派生类下的重载形式就和基类不同了。

    注意,可以简单的使用一条 using Base::func即可。注意,一定要把基类的作用域写上。

    以下是一个冗长的实例:

    class Base
    {
        public :
        int func() const { return 10; }
        int func(int a) const { return a; }
    };
    class Derived : public Base
    {
        public:
            int func() const { return 5; }
            using Base::func; // using `using`
    };
    int main(int argc , char *argv[])
    {
        Derived d ;
        d.func(11);
        return 0;
    }
    
  16. 如果用作多态,那么基类一定要定义虚析构函数。

    否则,派生类的成员不能被正确地释放。

    如果没有动态内存成员,可以直接使用default:

    virtual ~Base() = default;
    
  17. 如果基类定义了虚析构函数,那么就不会有合成的移动构造、移动赋值函数。

  18. 派生类的拷贝构造、拷贝赋值、移动构造、移动赋值都需要正确地、显式地操作基类的成员

    所谓正确的,就是对(拷贝、移动)构造函数,因为在初始列表中调用基类的(拷贝、移动)构造函数;如果不在初始化列表中显示调用,则派生类将堆基类隐式调用默认初始化。

    对赋值函数,因为在函数内部调用基类的相应赋值运算符。

    Base::operator=(rhs);
    
  19. 派生类的析构函数不需显示调用基类的析构函数

    因为派生类先析构,然后才是基类析构。不要显示调用基类的析构函数。

  20. 使用using声明继承直接基类的构造函数

    非常奇怪的一点,我们可以使用

    using Base::Base;
    

    来得到派生类的构造函数(组),这些自动继承的构造函数与每一个基类的构造函数参数一致,即得到如下的形式:

    Derived(param) : Base(args){}
    

    一个特别的例子是,如果基类有默认参数的构造函数,其继承的构造函数会很奇怪:

    Base::Base(int a, int b=5, string c="default");
    -----
    using Base::Base; 
    ==> 
        Derived(int a): Base(a, 5, "default"){};
        Derived(int a, int b) :Base(a, b, "default"){};
        Derived(int a, int b, string c): Base(a, b, c){};
    

    这是Primer上的意思,其实感觉根本不用管,从使用上来说,可以认为和原始的一样就可以了。

    最后,得到的继承构造函数,性质与基类的构造函数一致。即,访问控制符、是否explicit、是否constexpr。

  21. 要想在容器中同时放基类与派生类,只能把基类指针(或智能指针)放里面

  22. 派生类与基类是IS_A的关系