effective-c++系列:public继承注意事项

public继承==is_a关系

  • 如果class D 以public形式继承class B,那么相当于告诉c++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。

    1
    2
    class Person{};
    class Student: public Person{};
  • 如果任何函数期望获得Person,那么也能接受一个Student

  • 继承带来的问题

    • 子类可能不具有父类的某些行为(emmm, 个人觉得一方面因为抽象的层次不够高,或者继承范围太模糊)
    • 父类的一些函数会破坏子类的性质(例如父类是一个矩形,子类是一个正方形,扩展长方形的长宽函数应用在子类会破坏正方形的性质。

命名遮盖规则

  • 对于下面的代码

    1
    2
    3
    4
    5
    int x;
    void dosomething(){
    double x;
    cin >> x;
    }
  • dosomething() 函数中,cin的对象是double x变量,这遵从c++的名称遮掩规则,即内层作用域会遮盖外层作用域。

  • 同理而言,在继承关系中也是这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base{
    int x;
    public:
    virtual void f1() = 0;
    virtual void f2();
    void f3();
    };
    class Derived : public Base{
    public:
    virtual void f1();
    void f4();
    };

    class Base包含了纯虚函数,虚函数和普通成员函数,以此来说明命名遮盖和只和函数名有关而与其他无关。
    假设f4()实现代码如下

    1
    2
    3
    void Derived::f4(){
    f2();
    }

    当编译器遇到f2()函数时,会查找作用域,首先看f4()函数内部,没有找到,于是朝赵class Derived作用域也没有找到,最后查找class Base作用域找到f2()。假如class Base中也没有,那就在class Base所在namespace中查找,最后在全局作用域 找。

    下面看一个更复杂的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class Base{
    int x;
    public:
    virtual void f1() = 0;
    virtual void f1(int ){
    cout << "f1 - b" << endl;
    }
    virtual void f2(){
    cout << "f2 - b" << endl;
    }
    void f3(){
    cout << "f3 - b" << endl;
    }
    void f3(double){
    cout << "f3 - b - b" << endl;
    }
    };
    class Derived : public Base{
    public:
    virtual void f1(){
    cout << "f1 - d";
    }
    void f3(){
    cout << "f3 - d";
    }
    void f4(){
    cout << "f4 - d";
    }
    };
    Derived d;
    int x = 0;
    d.f1(); //ok
    d.f1(x); //error! Base::f1()被覆盖
    d.f2(); //ok Base::f2()
    d.f3(); //ok
    d.f3(x); //error! Base::f3(int)被Derived::f3()遮盖

    子类的f3()函数将父类的f3()和f3(int)都覆盖掉。
    如果想避免这种情况,可以像下面这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
     class Base{
    int x;
    public:
    virtual void f1() = 0;
    virtual void f1(int ){
    cout << "f1 - b" << endl;
    }
    virtual void f2(){
    cout << "f2 - b" << endl;
    }
    void f3(){
    cout << "f3 - b" << endl;
    }
    void f3(double){
    cout << "f3 - b - b" << endl;
    }
    };
    class Derived : public Base{
    public:
    using Base::f1;
    using Base::f3;
    virtual void f1(){
    cout << "f1 - d";
    }
    void f3(){
    cout << "f3 - d";
    }
    void f4(){
    cout << "f4 - d";
    }
    };
    Derived d;
    int x = 0;
    d.f1(); //ok
    d.f1(x); //ok
    d.f2(); //ok Base::f2()
    d.f3(); //ok
    d.f3(x); //ok

    那假如我们只想继承父类的一部分函数呢?这时候using就不管用了,因为一旦声明using,父类同名所有函数可见。这时候可以采用转交函数(forwarding function)完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Base{
    int x;
    public:
    //virtual void f1() = 0; effective c++ 160页为纯虚函数?此处存疑。
    virtual void f1(){}
    virtual void f1(int);
    };
    class Derived : public Base{
    public:
    virtual void f1(){
    Base::f1();
    }
    };

接口继承和实现继承

  • 成员函数的接口总是会被继承。
  • 声明一个纯虚函数的目的是为了让子类只继承函数接口。纯虚函数对象不能被实例化
  • 声明一个虚函数(非纯虚函数)的目的是让子类继承该函数的接口和缺省实现。
  • 声明一个非虚函数的目的是为了让子类继承函数的接口和一份强制性实现。非虚函数是不变性(invariant)和凌驾特异性(specialization)的。所以它不该在子类中被重新定义。

默认参数的继承

  • 考虑下面默认参数继承的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Base{
    public:
    enum Option {First, Second, Third};
    virtual void f1(Option op = First) const = 0;
    };
    class D1 : public Base{
    public:
    virtual void f1(Option op = Second) const;//赋予不同的默认值,这很糟糕
    };

    class D2 : public Base{
    public:
    virtual void f1(Option op) const;
    // 如果这么写,用户以对象调用一定要指定参数值,
    // 因为静态绑定下不从基类继承默认值
    // 如果使用指针或者引用调用,可以不指定因为动态绑定下这个函数会从基类
    // 继承默认值
    };
  • 静态类型:变量声明的时候指定的类型名。

  • 动态类型:目前所指对象的类型。

    Base* b;             //静态类型为Base*,无动态类型
    Base* d1 = new D1;   //静态类型为Base*,动态类型为D1*
    Base* d2 = new D2;   //静态类型为Base*,动态类型为D2*
    b = d1;              //静态类型为Base*,动态类型为D1*
    d1 = d2;             //静态类型为Base*,动态类型为D2*
    
  • virtual函数动态绑定,默认参数却是静态绑定

    D2 -> f1(); //使用的是D2的函数,参数却是Base的。
    

    问:为什么要采用这种方式运行呢?
    答:提高运行期效率。在编译器直接决定,不在运行期动态确定(降低编译器实现难度,增加速度)。