Reading Note of CppPrimer-Chapter15

Object Oriented Programming

OOP Overview

  • 面向对象的程序设计基于三个基本概念:数据抽象 (封装)、继承、动态绑定。
    1. 通过数据抽象,可以实现将类的接口和实现分离
    2. 使用继承,可以定义相似的类型并对其相似关系建模
    3. 动态绑定,可以在一定程度上忽略相似类型的区别,而一统一的方式使用它们

Base Class & Derived Class

  • 派生类列表 (class derivation list)

    首先是一个冒号:,后面紧跟以逗号分隔基类列表,其中每个基类前面可以有以下三种访问说明符 (access specifier) 中的一种:

    • public
    • protected
    • private

    值得注意的是,当声明一个派生类的时候,不需要包含他的派生列表

    1
    2
    class Derived;                 // correct, declare a derived class
    class Derived : public Base; // error
  • 派生类经常 (但不总是) 覆盖 (overwrite) 他继承的虚函数;

    • 如果派生类没有覆盖其基类中的某个虚函数,则该函数的行为类似于普通函数,派生类会直接继承基类中的这个虚函数;
    • 当然派生类也可以覆盖他的普通成员函数 (非虚函数)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base{
    public:
    void func(){std:: cout << "base func" << std::endl;};
    virtual void func1(){std:: cout << "virtual base func1" << std::endl;};
    virtual void func2(){std:: cout << "virtual base func2" << std::endl;};
    };

    class Derived : public Base{
    public:
    // override non-virtual member function
    void func(){std:: cout << "derived func" << std::endl;};
    // override virtual function
    virtual void func1() override {std:: cout << "virtual derived func1" << std::endl;};
    };

    Derived d;
    d.func(); // derived func
    d.func1(); // virtual derived func1
    d.func2(); // virtual base func2
  • override 关键字

    C++11 新标准允许派生类显式的注明他使用某个成员函数覆盖 (overwrite) 掉了他继承的虚函数

    参数列表后或者 const 成员函数的 const 之后加上一个 override 关键字,这样可以减少很多出乎意料的 bug,比如本来想重写虚函数但是写错了参数列表导致基类虚函数被隐藏而通过动态绑定调用虚函数失败;

    only virtual member functions can be marked as override

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class BaseClass{
    public:
    virtual int sum(int a, int b);
    virtual int print(int a) const;
    };
    class DerivedClass : public BaseClass{
    public:
    int sum(int a, int b) override;
    // int sum(float a, float b) override; // error, override failed, parameters not match
    int print(int a) const override;
    };
  • 派生类构造函数

    尽管派生类对象中有从基类继承而来的成员,但是派生类并不直接初始化基类的成员,派生类必须使用基类的构造函数来初始化他的基类部分

    遵循类的接口,每个类控制他自己的成员初始化

    尽管在语法上说,我们可以在派生类的构造函数体内给基类的成员赋值来实现基类成员的初始化,但是最好不要这么做。因为每个类都有自己的接口,和类交互的时候必须使用类的接口,即使这个类是派生类的基类;

    除非特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化;派生类构造函数的初始化部分,先初始化基类,然后再按成员声明的顺序依次初始化自己的数据成员,再然后才执行构造函数体

    1
    2
    3
    4
    5
    6
    class BulkQuote : public Quote{
    BulkQuote(const std::string& book, double p, std::size_t qty, double disc):
    Quote(book, p), min_qty(qyt), discount(disc){
    // body of constructor
    }
    };
  • 静态成员的继承

    如果基类定义了一个静态成员,则在整个继承体系里面都只存在这一个静态成员的唯一定义;静态成员遵循通用的访问控制;调用静态函数的时候可以通过基类或者派生类对象和域作用符 (scope resolution operator);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Base{
    public:
    static void static_method();
    static int val = 0; // same access method with function
    };
    class Derived : public Base{
    public:
    void f(const Derived& derived_obj){
    Base::static_method(); // access via Base via using scope resolution operator
    Derived::static_method(); // access via Derived
    derived_obj.static_method(); // assess via Derived Object
    static_method(): // access via this pointer
    }
    };
  • 被用作基类的类

    如果我们希望某个类作为基类,则在继承他之前,必须有他的定义,而不仅仅是声明

    • 这是因为在派生类继承这个类的时候,派生类要知道自己继承基类的哪些非静态数据成员与函数;
    • 另外一层含义是一个类不能自己继承自己
    1
    2
    class Quote;
    class BuldQuote : Quote{...}; // error, Quote should be defined before being inherited

    一个类是基类,同时也可以是派生类

    1
    2
    3
    class Base {};
    class D1 : public Base {}; // D1 is base class of D2 and D1 is also a derived class derived from Base
    class D2 : public D1 {};
  • final 关键字

    如果不希望一个类作为基类来被继承,可以在 ** 类名后加上一个 final** 关键字;

    如果不希望一个虚函数被重写,也可以在 ** 虚函数的参数列表后使用 final** 关键字;

    1
    2
    3
    4
    5
    6
    7
    class NoDerived final{};

    class BaseClass {
    public:
    virtual void print() const final; // cannot be overrided by derived classes
    virtual int sum() const;
    };
  • 静态类型 (static type) vs 动态类型 (dynamic type)

    • 静态类型是在编译时确定的,动态类型是运行时才能确定的;动态类型是为动态绑定为生

    • 对于派生类和基类来说,一个指向基类的指针或者绑定基类的引用,他的静态类型与动态类型可能不一样;比如动态绑定,把一个派生类的对象传递给基类引用或者指针的对象 base_obj,这时 base_obj 的静态类型是基类,但是动态类型是派生类

    • 对于非基类的引用或者指针的变量或者表达式,以及基类的非引用非指针对象,其动态类型和静态类型一致

  • 派生类到基类的隐式转换

    和其他类型一样,编译器会隐式的执行派生类到基类的转换,这是因为基类是派生类的子集;这种特效,意味着我们可以把派生类对象当作基类对象使用,将基类的指针或者引用绑定到派生类的对象上去;

    派生类到基类的自动转换仅限于引用和指针

    我们可以将派生类的对象绑定到基类的引用或者指向基类的指针,但是不存在从基类到派生类的自动类型转换,即使一个基类指针或者引用绑定的是一个派生类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    BaseClass item1;
    DerivedClass item2;

    BaseClass *p = &item1;
    p = &item2; // implicit conversion of pointer
    BaseClass &r = item2; // implicit conversion of reference
    item1 = item2; // implicit conversion of normal object

    BaseClass sb = item2; // not convert, but sliced derived class

    通常情况下,要把引用或指针绑定到一个对象上,那么引用或指针的类型需要和对象的类型一致,或者存在可转换的 const 类型

    但是存在继承关系的类是一个例外:这是因为动态绑定的存在,我们并不知道引用绑定或者指针指向的对象到底是基类的类型还是派生类的类型

    和内置指针类似,智能指针也支持派生类向基类指针的自动转换

    1
    2
    const BaseClass& a = DerivedClass();
    std::shared_ptr<BaseClass> p = std::make_shared<DerivedClass>();
  • slice down

    尽管派生类到基类的隐式转换只应用于 referencepointer,但是因为类的拷贝控制操作的存在,使得直接将派生类的对象拷贝赋值或者移动赋值给基类对象是可能的,但是会有意想不到的结果,即仅会拷贝基类的部分 (slice down);

    TLDR: 因为构造函数不能是虚函数没有动态绑定,所以拷贝 / 移动构造 / 赋值函数的参数为基类的引用,是一个静态类型

    比如类的拷贝构造 / 赋值函数操作符接受 const reference 作为参数,所以派生类向基类的转换允许我们给基类的拷贝 / 赋值操作传递一个派生类对象;但是因为这些函数不是 virtual,所以将一个派生类赋值给一个基类不涉及动态绑定;这些函数只认参数的静态类型,即基类类型,而不知道传递的参数是派生类类型;因此只有基类的部分会被拷贝,派生类的部分被丢弃

    1
    2
    3
    4
    5
    // slice down: only baseclass parts are copied, derived parts are not copied
    DerivedClass derived;
    BaseClass base(derived); // copy constructor of BaseClass, derived converted to BaseClass implicitly
    BaseClass base1 = derived; // assignment operator
    BaseClass& moved_obj = std::move(derived); // move constructor of BaseClass
  • 不能隐式地把基类对象转换为派生类对象;但是可以通过 dynamic_cast 或者 static_cast 等强制转换来实现

    1. 基类到派生类的转换仅限于引用和指针
    2. 如果基类有一个或者多个虚函数,则使用 dynamic_cast 来转换,他会在运行时动态转换,如果转换不合法返回空指针或者抛出 std::bad_cast 异常 ; 参见 chapter4
    3. 如果我们已知转换是安全的,即已知基类的引用和指针确实是指向派生类的对象的时候,直接使用 static_cast 让编译器强制转换即可;
    1
    2
    BaseClass obj = DerivedClass();   // valid, slice down
    DerivedClass obj = BaseClass(); // invalid, no such conversion

Virtual Functions

  • 在 C++ 中,基类必须将他的两种成员函数区分开

    • 一种是希望派生类进行覆盖(override)的函数,此时基类应当把这些函数声明为虚函数 (virtual function);
    • 另一种是希望派生类直接继承使用而不做改变的
  • 虚函数 (virtual function)

    通过在其成员函数的声明语句之前加上 virtual 关键字将函数声明为虚函数

    • 任何构造函数之外的非静态函数都可以是虚函数;
    • virtual 关键字只能出现在类内的函数声明之前,而不能出现在类外的定义之前
    • 一般的成员函数如果没有使用可以不定义,但是虚函数不管用与不用都得定义,因为其解析发生在运行时,并不知道会调用哪个函数
    • 一旦基类中某个函数被标记为 virtual 后,他的所有派生类的这个函数都自动是 virtual 的;派生类不必一直重复写 virtual 关键字;
    • 派生类里面对此虚函数的重写应当满足参数列表返回值相同的要求;返回值有一个意外,即如果基类的 virutal 函数返回基类的引用或者指针,派生类也可以返回派生类的引用和指针,此时会涉及自动转换
  • 动态绑定 (dynamic binding)

    当我们使用基类的引用 或者 指针 调用一个虚函数时将发生动态绑定,即在运行时选择函数版本,也叫运行时绑定 (runtime binding);基类通过在函数前加上 virtual 关键字,使得该函数执行动态绑定;注意:只有引用和指针 + 虚函数可以实现动态绑定

    类的非虚成员函数是静态绑定,在编译的时候就能确定。

    In C++, dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Quote{
    virtual double price();
    };
    class BulkQuote : public Quote{
    virtual double price();
    };
    double print_total(const Quote&item){
    std::cout << item.price() << std::endl;
    }

    Quote quote;
    BulkQuote bulk;
    // dynamic binding
    print_total(quote); // call Quote::price()
    print_total(bulk); // call BulkQuote::price()
  • 绕过动态绑定

    可以显式的绕过动态绑定来调用特定版本的虚函数:通过域作用符来实现,此时编译器会在编译时解析;

    最常见的场景是在派生类或者派生类的友元函数里面调用基类的虚函数;如果不加域作用符,会出现自己调用自己的递归死循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // call BaseClass::virtual_function or DerivedClass::virtual_function depend on dynamic type of baseP
    baseP->virtual_function();

    // call the version from base class regardless of the dynamic type of baseP
    baseP->BaseClass::virtual_function();

    DerivedClass::virtual_func(){
    BaseClass:virtual_func(); // call base class version virtual func firstly
    virtual_func(); // WARNING: infinite loop
    }
    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
    class A{
    public:
    A()=default;
    virtual ~A()=default;
    public:
    virtual void func(int i = 10){
    printf("A=%d\n",i);
    }
    };

    class B :public A{
    public:
    B()=default;
    public:
    void func(int i = 20){
    A::func(); //在派生类中调用基类的版本
    printf("B=%d\n", i);
    }
    };

    // test code
    B b1;
    A* p1 = &b1;
    B* p2 = &b1;

    p1->A::func(); // print A=10, 虽然p1指向的是派生类实例,但是通过作用域限制,要求其调用基类的版本
    p2->func(); // print A=10\nB=1\n, 虽然p2是派生类指针,但是派生类的虚函数实现中,会先调用基类的实现

    // reference:https://blog.csdn.net/ykun089/article/details/106985934
  • 虚函数的默认参数问题

    如果基类的虚函数有默认参数,则派生类里面的虚函数要么定义的默认参数和基类一致,要么都不定义默认参数;否则会出现意想不到的结果;如果某次调用过程中使用了默认参数,则该实参值由本次调用的静态类型决定

    • 基类和子类都没有默认参数的时候,正常发生多态

    • 基类和子类都有默认参数的时候且默认参数不一致的时候,或者基类子类参数列表一致且基类有默认参数而子类没有默认参数的时候,派生类继承基类的默认参数

      基类中虚函数的默认参数会在编译过程就被保存,再调用子类的函数后发生多态,编译器会使用基类的默认参数,这是因为是实参值由静态类型决定,而此时的静态类型是基类的指针或引用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      class Base {
      public:
      virtual void func(int i = 10) {
      std::cout << "Base" << std::endl;
      }
      };

      class Derived : public Base {
      public:
      // 基类和子类都有默认参数的时候且默认参数不一致的时候
      void func(int i = 0) {
      std::cout << "Derived: " << i << std::endl;
      }
      // 基类子类参数列表一致且基类有默认参数而子类没有默认参数,此函数依然覆盖的是 Base 中的 func 函数
      // void func(int i) {
      // std::cout << "Derived: " << i << std::endl;
      // }
      };

      Base *p = new Derived;
      p->func(); // print "Derived 10", 10 is the default parameter inherited from base class
      p->func(1); // print "Derived 1"

      //reference: https://blog.csdn.net/weixin_44720401/article/details/106745426
    • 基类子类参数列表一致且子类有默认参数而基类没有默认参数,此时无法使用基类调用虚函数且使用子类虚函数的默认参数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      struct Base{
      void func(int a, int b){
      std::cout << "Base: " << a << b << std::endl;
      }
      };
      struct Derived : public Base{
      void func(int a, int b=1){
      std::cout << "Derived: " << a << b << std::endl;
      }
      };

      Derived d;
      Base& b = d;
      b.func(1); // error: too few arguments to function call, expected 2, have 1
    • 基类有 (默认) 参数而子类没有参数 (no parameters),则该函数 func 函数是子类 (derived class) 中的新成员,虚函数没有重写 (overwrite) 成功

      因为参数数量不一样,子类中定义的没有参数的 func 函数并不是从基类中继承而来,但是子类依旧继承了基类中有参数的 func 函数,所以用基类指针调用有参数的func 函数会发生多态,而用子类指针调用的没有参数的func 函数才是自己定义的新成员

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class Base {
      public:
      virtual void func(int i = 10) {
      std::cout << "Base" << std::endl;
      }
      };
      class Derived : public Base {
      public:
      void func() {
      std::cout << "Derived" << std::endl;
      }
      };

      Base *p = new Derived;
      p->func(); // print Base
      Derived *q = new Derived;
      q->func(); // print Derived
    • 子类中有 (默认) 参数而基类没有参数,结果和 2 中一样,原因也一样;但要注意如果用父类指针调用 func 函数并传入了参数,编译器会报错,因为父类中 func 的函数没有参数,又因为父类指针不能调用子类的非继承而来的函数,因此没有函数与之匹配。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      class Base { 
      public:
      virtual void func() {
      std::cout << "Base" << std::endl;
      }
      };

      class Derived : public Base {
      public:
      void func(int i = 10) {
      std::cout << "Derived" << std::endl;
      }
      };

      Base *p = new Derived;
      p->func();
      // p->func(3);// 如果传入参数会报错,因为父类中的 func 的函数没有参数,又因为父类指针不能调用
      // 子类的非继承而来的函数, 因此没有函数与之匹配,所以编译器会报错
      Derived *q = new Derived;
      q->func(); // print "Derived"

Abstract Base Classes

  • 纯虚函数 (pure virtual function)

    通过在函数体的位置书写 =0 就可以把一个虚函数申明为纯虚函数;=0只能出现在类内部的虚函数声明处;和普通函数不同,一个纯虚函数不需要定义

  • 虚基类

    含有纯虚函数的类是虚基类;虚基类不能直接实例化,但是可以被继承,在派生类的构造函数里面可以调用虚基类的构造函数初始化虚基类的成员变量

    1
    2
    3
    4
    class AbstractBaseClass { 
    public:
    double pure_virtual_func(std::size_t) const = 0; // pure virtual function
    };
  • 纯虚函数是可以在类的外部定义的(但是这种定义一般来说没有意义,因为派生类都会重写纯虚函数),不能在类内声明纯虚函数的时候提供函数体

    1
    2
    3
    4
    5
    // in .cpp file
    double AbstractBaseClass::pure_virtual_func(){
    std::cout << "from pure abstract func" << std::end;
    return 0.0;
    }

Access Control and Inheritance

  • protected 关键字

    某些成员,基类希望派生类或者派生类的友元可以访问,但是类的用户不可以直接访问,则可以定义为 protected 类型;protected 说明符可以看作 publicprivate 的中和

    • private 类似,类的用户不可以访问 protected 成员

    • public 类似,派生类及其友元可以访问 protected 成员

    • 派生类的成员或者友元只能通过派生类对象访问基类protected 成员,无法通过基类直接访问基类中的 protected 成员;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class BaseClass{
    public:
    BaseClass();
    protected:
    int member_protected; // can be accessed from derived class, but not class user
    private:
    int member_private; // can not be accessed from both derived class and class user
    };

    class Sneaky : public BaseClass{
    friend void clobber(Sneaky&); // 可以通过Sneaky访问BaseClass::member_protected
    friend void clobber(BaseClass&); // 不可以通过BaseClass直接访问BaseClass::member_protected,以上第三条
    };
  • 类成员的用户可以大概分为 3 级:

    • 1 级用户:类内访问 (类的实现者,如类的成员函数,友元),可以访问 所有 public,protected,private 成员
    • 2 级用户:子类,根据继承方式的不同,可以分别访问所有的 **public,protected 成员 **
    • 3 级用户:类和子类的用户 (可以理解成通过类的实例化对象来访问,即类的普通用户),只能访问 public 的成员
  • 继承派生说明符 (derivation access specifier)

    C++ 的继承派生说明符(publicprivateprotected)会影响子类的对外访问属性,判断某一句话能否被访问:

    1. 调用语句,看这句话写在子类的内部(类 / 子类内访问)、外部(类 / 子类的用户)
    2. 看子类如何从父类继承publicprivateprotected
    3. 父类中的访问级别publicprivateprotected

    ch15-access-level

    即访问派生说明符只影响类的用户对类成员的访问权限;对于子类的用户来说,只能访问访问说明符为 public 的成员

    访问派生说明符对于派生类的成员(及友元)能否访问其直接的基类成员没有什么影响;父类的公有成员 public 和保护成员 protected 由于继承方式不同,在子类中的访问说明符 (access specifier) 也不同,但是 不管是继承下来后属于哪类访问说明符,在子类的内部,这些成员(public, protected)都能被访问private 成员不论是子类还是用户均不可以访问

  • class and struct 默认继承派生说明符

    • class 默认私有继承 (private)
    • struct 默认公有继承 (public)
    • 除了默认成员访问符和默认派生访问说明符不同,structclass 没有任何区别
  • 友元与继承

    就像友元关系不能传递一样,友元关系同样不能被继承;每个类负责控制自己的成员权限,对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此

    基类的友元在访问派生类的成员时不具有特权;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class BaseClass{
    public:
    friend class Pal;
    BaseClass();
    protected:
    int member_protected; // can be accessed from derived class, but not class user
    private:
    int member_private; // can not be accessed from both derived class and class user
    };

    class Sneaky : public BaseClass{
    private:
    int j;
    };

    class Pal {
    int f1(BaseClass B){return B.member_protected;} // valid, Pal is friend class of BaseClass
    //int f2(Sneaky S){return S.j;} // invalid, Pal is not a friend class of Sneaky

    // 对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此
    int f3(Sneaky S){return S.member_protected;} // valid, Pal is friend class of BaseClass
    int f4(Sneaky S){return S.member_private;} // valid, Pal is friend class of BaseClass
    };

    派生类的友元访问基类的成员时也没有特权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class BaseClass{
    public:
    BaseClass();
    protected:
    int member_protected; // can be accessed from derived class, but not class user
    private:
    int member_private; // can not be accessed from both derived class and class user
    };

    class Sneaky : public BaseClass{
    public:
    friend class Pal;
    private:
    int j;
    };

    class Pal {
    //int f1(BaseClass B){return B.member_protected;} // invalid, Pal is not a friend class of Sneaky
    int f2(Sneaky S){return S.j;} // valid, Pal is friend class of BaseClass
    };
  • 可以使用 using 声明改变个别成员的访问级别;派生类只能对那些可以访问的成员使用 using 声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base{
    public:
    std::size_t size() const {return n};
    protected:
    std::size_t n = 0;
    private:
    int j = 0;
    };

    class Derived : private Base{
    public:
    using Base::size;
    protected:
    using Base::n; // 可以这么做是因为Derived的派生类本来就可以访问基类的protected成员
    //using Base::j; // error: 'j' is a private member of 'Base',派生类不可以访问基类的private成员
    };

    Derived d;
    std::size_t s = d.size(); // valid

Class Scope under Inheritance

  • 每个类定义自己的作用域,在这个作用域里面我们定义类的成员;

  • 当发生继承的时候,派生类的作用域嵌套在基类的作用域之内;正是因为这样的机制,我们才可以在派生类里面和使用自己的成员一样使用基类的成员,想象一下在函数体里使用全局变量;

    Base class

    int num = 0;

    Derived Class

    num = 1;

  • 名字查找优先于类型检查

    • 解析的时候,先从派生类的作用域里面找相同的成员名字,找不到就找派生类的上一层基类,一直到最顶层的基类为止;

    • 但凡找到了名字,就不再继续找了,哪怕参数列表不一致;然后再检查参数列表是否一致;

  • 基类成员数据的隐藏

    和内层的局部变量可以隐藏外层局部变量相似,派生类也能重新定义(覆盖隐藏)在其基类中存在的同名成员(函数和变量),此时派生类定义的新成员将隐藏基类中的同名成员

    当然我们可以使用域作用符 (scope resolution operator) 直接访问被隐藏的同名成员(包含数据成员和成员函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Base{
    public:
    Base(int mem_) : mem(mem_){}
    protected:
    int mem = 0; // mem of Base
    };

    class Derived : public Base{
    public:
    Derived(int mem_): Base(mem_), mem(mem_){}
    int GetBaseMem(){return Base::mem;} // return Base::mem
    int GetMem(){return mem;} // return Derived::men
    private:
    int mem = 0; // mem of Derived, which will hidden mem of Base
    };
  • 普通成员函数的重载 (overload) 和隐藏 (hidden)

    声明在内层(即派生类)的成员函数和声明在外层(即基类)的成员函数并不具有重载 (overload) 的关系

    内层同名的函数或成员会直接隐藏掉 (hidden) 外层的成员,即使函数的参数列表不一致,也不会发生重载,直接隐藏 (hidden);

    比如某个基类有多个重载的成员函数,此时派生类仅仅定义一个和基类同名的函数,就可以隐藏 (hidden) 掉所有基类的同名函数;此时如果派生类需要基类的这些重载函数对其可见,要么一个同名函数也不要定义,要么定义基类所有的重载函数(可以使用 using

    • using 关键字可以使得所有在继承过程中因为各种原因不可见的成员对于本成员可见,但前提是本身这些成员在派生类可见
      • 比如因为 privateprotected 继承导致某些成员不可见的时候
      • 比如因为继承,** 派生类把基类的成员隐藏 (hidden)** 的时候
    • 域作用符 (scope resolution operator) 也是很好的工具去直接访问被隐藏 (hidden) 的成员
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct Base{
    int mem_func(){}
    };
    struct Derived : public Base{
    // the function hidden mem_func of Base, though the argment list diffs
    int mem_func(int i){};
    };
    struct Derived1 : public Base{
    // if using Base::mem_func, the hidden function will be visible to the derived class
    using Base::mem_func;

    // the function hidden mem_func of Base, though the argment list diffs
    int mem_func(int i){};
    };

    Base b;
    Derived d;
    Derived1 d1;
    b.mem_func(); // valid
    d.Base::mem_func(); // valid, access via scope resolution operator
    d.mem_func(10); // valid
    d.mem_func(); // invalid, function hidden by int mem_func(int i){};
    d1.mem_func(10); // valid
    d1.mem_func(); // valid, because of the using Base::mem_func
  • 虚函数的重写 (overwrite) 和隐藏 (hidden)

    虚函数只有函数名和参数列表都完全匹配的情况下才算重写 (overwrite),否则基类的虚函数会被派生类的同名成员函数隐藏掉 (hidden);

    比如某个虚函数,派生类参数列表和基类的参数列表不一致,基类的虚函数就会被派生类的普通成员函数隐藏 (hidden) 掉; 此时因为派生类没有虚函数重写 (overwrite) 的发生,派生类并没有定义自己版本的虚函数也就无法通过基类的引用或者下指针访问派生类自己的虚函数 (因为本身就没有);这也是为什么我们建议基类和派生类的虚函数必须具有相同的参数列表了;

    不过,虽然不能通过基类的引用或指针加上动态绑定访问派生类的虚函数,但是可以使用基类的引用或指针加上动态绑定访问基类的虚函数

    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
    struct Base {
    virtual int fcn(){
    std::cout << "Base FCN" << std::endl;
    }
    };

    struct Derived : Base{
    // because fcn is a virtual func, then this function did not overwrite Base::fcn successfully
    // so the Derived class indeed includes 2 fcn functions:
    // 1. Base::fcn() virtual func, which is hidden and can only be accessed by dynamic binding
    // 2. Derived::fcn(int), normal member func, when we call Derived::fcn ,this one is called
    int fcn(int a){
    std::cout << "Derived FCN" << std::endl;
    }
    };

    struct Derived1 : Derived{
    // because Derived inherits the defination of virtual func: Base::fcn
    // so this class overwriten the fcn function as a virtual function
    int fcn(){
    std::cout << "Derived 1 FCN" << std::endl;
    }
    };

    Base b;
    Derived d;
    Derived1 d1;

    // static bind
    d.fcn(10); // valid
    d.fcn(); // invalid, the func is hidden by int fcn(int)

    // dynamic bind
    Base *bp1 = &b, *bp2 = &d, *bp3 = &d1;
    bp1->fcn(); // Base FCN
    bp2->fcn(); // Base FCN
    bp3->fcn(); // Derived 1 FCN

Constructor and Copy Control under Inheritance

  • 虚析构函数 (virtual destructor)

    如果派生类定义了一些需要析构函数释放的资源,而且派生类可能会使用动态绑定(使用基类的引用或者指针来访问),那么基类的析构函数需要定义为虚析构函数,此时调用基类的析构函数的其实是动态绑定后的派生类的虚构函数;一般最好都给基类定义一个虚析构函数;

    如果基类的析构函数不是虚析构函数,则 delete 一个指向派生类的基类的指针会产生未定义的结果;

    假如析构函数不是虚函数,此时使用指向派生类的基类的指针或引用不会发生动态绑定,只会调用基类的析构函数,从而在派生类中分配的资源就无法释放

    1
    2
    3
    4
    5
    class Quote {
    public:
    virtual ~Quote(){}; // 如果一个类定义了析构函数,则他的移动构造/赋值操作将不会被自动合成
    Quote(Quote&&) = default; // 如果需要使用移动操作,必须手动定义移动操作
    };
  • 派生类的合成拷贝控制

    派生类当然也可以自动合成构造赋值析构函数;这些合成的成员除了负责对自己的成员顺序初始化赋值销毁外,还负责调用直接基类的对应函数(无论是合成的还是自定义的)进行基类部分初始化赋值销毁

    整个过程从作用域的角度来看是自外向内的,从继承的角度看是从基类到派生类的;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct BaseClass{
    int base_num = 0;
    };
    struct DerivedA : BaseClass{
    int derived_a_num = 1;
    };
    struct DerivedB : DerivedA{
    int derived_b_num = 2;
    }

    此时调用 DerivedB 的构造函数的过程:

    call BaseClass::Constructor()

    initialize class member base_num

    call DerivedA::Constructor()

    initialize class member derived_a_num

    call DerivedA::Constructor()

    initialize class member derived_b_num

    正是因为这样一个机制和顺序:

    • 如果基类的默认构造函数拷贝赋值移动赋值析构函数这些是 deleteprivate,则派生类的这些成员也会被定义为 delete
    • 如果基类有一个不可访问或者删除析构函数,则派生类合成的默认构造/拷贝构造/移动构造函数也是 delete
  • 书写派生类的拷贝控制成员

    • 定义派生类的拷贝 / 移动构造函数:通常显式地调用基类的构造函数进行初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Base{};
      struct D : Base{
      D(const D& d) : Base(d){ // call Base::copy_constructor to init member of Base
      // init member of D
      }
      D(D&& d) : Base(std::move(d)){ // call Base::move_constructor to init member of Base
      // init member of D
      }
      };
    • 定义派生类的拷贝 / 移动赋值运算符:显式地调用基类的赋值运算符为基类赋值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      D& operator(const D& rhs){
      Base::operator=(rhs); // call operator= explicitly
      // handle member of the derived class
      }

      D& operator(D&& rhs){
      Base::Operator=(rhs); // call operator= explicitly
      // handle member of the derived class
      }
    • 定义派生类的析构函数派生类的析构函数只负责销毁自己分配的资源

      在析构函数体执行完后,类成员会依次进行销毁;类似的,基类成员也是隐式销毁的对象销毁的顺序与创建相反,先执行派生类的析构函数,然后再调用基类的析构函数先销毁派生类的成员,再销毁基类的成员

      1
      2
      3
      4
      5
      6
      7
      struct D : public Base{
      ~D(){
      // do this class member clean
      }
      // Base::~Base(); // the destructor of base class will be called AFTER the
      // derived class destructor body automaticly
      };
  • 在构造函数和析构函数中使用虚函数

    如我们所知,派生类的基类部分先被初始化;当执行基类的构造函数的时候,此时该对象的派生部分是未被初始化的;类似当执行基类的析构函数的时候,派生类的部分已经被销毁了;这两种情况,对象都处于未完成状态

    在未完成状态,如果构造函数或者析构函数调用了某个虚函数,则我们应该执行当前构造函数或者析构函数对应的类的虚函数版本

  • 继承构造函数

    在 C++11 新标准中,如果派生类的构造函数没有特别要处理的,派生类可以直接继承直接基类的构造函数而不用自己再显式定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct Base{
    Base(int num_, float price_): num(num_), price(price_){}
    };

    class Derived: public Base{
    // inherit constructor from direct base class
    using Base::Base;

    double func(int a){};
    };

    通常情况,using 语句只是令某个名字在当前作用域可见;但是当 using作用于构造函数的时候using 语句将令编译器生成代码

    1
    2
    3
    using Base::Base; 
    // 等价于:
    Derived::Derived(int num_, float price_): Base(num_, price_){};

    继承的构造函数无法指定 explicitconstexpr 的关键字,但是会继承基类的这些关键字

    基类构造函数的默认参数不会被继承;相反派生类会得到多个继承的构造函数:每个函数分别省略掉一个含有默认实参的形参

    如果基类有多个构造函数,则派生类会继承所有这些构造函数;派生类可以只继承一部分构造函数,不想继承的可以定义自己的版本去隐藏 (hidden) 基类相同的构造函数

    默认、拷贝、移动构造函数不可以被继承,这些函数按照正常规则去合成

Container and Inheritance

  • 容器与继承

    当我们希望使用容器保存有继承关系的对象的时候,最好的选择是保存基类的智能指针

    • 不能使用派生类对象,因为基类对象一般不能转换为派生类对象
    • 也不能保存基类对象,因为派生类对象转换为基类之后就丧生了派生类独有的那一部分成员了
    • 也不能保存基类的引用,因为引用是要在定义的时候就初始化的,无法动态添加删除,也无法赋值
    • 动态绑定除了基类引用,就剩下基类指针;在动态绑定方面,智能指针有和内置指针的性质
    1
    2
    3
    std::vector<std::shared_ptr<Base>> vec;
    vec.push_back(std::make_shared<Base>());
    vec.push_back(std::make_shared<Derived>());