C++ Primer Plus 学习之 类继承

主要介绍了类的继承、虚函数、类继承的动态内存分配问题、继承与友元函数。


公有派生

  • 基类的公有成员和私有成员都会成为派生类的一部分。
  • 基类的私有成员只能通过基类的公有或者保护方法访问。但是,基类指针或引用只能用于调用基类方法,不能调用派生类方法。(这种兼容性使得可以用派生类对象来初始化基类对象,也可以将派生类对象赋给基类对象。)
  • 基类的指针或引用可以在不显示类型转换的情况下指向派生类。
  • 派生类的构造函数。
    • 首先会创建基类的对象,派生类的构造函数应通过成员初始化列表将基类信息传递给基类的构造函数。
    • 基类有指针成员或者动态内存分配,则需要定义基类的复制构造函数。
      1
      2
      3
      4
      5
      //于b的类型为Base &,因此将调用基类的复制构造函数,如果基类使用了动态内存分配(new)的话,则需要定义基类的复制构造函数。
      Point::Point(int tk,const Base &b):Base(b)
      {
      k=tk;
      }
  • 派生类的析构函数

    • 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。

多态-动态多态

定义

  • 同一个方法在派生类和基类中的行为是不同的。
  • 方法取决于调用该方法的对象。
  • 前面所学的重载和函数模板是在编译时间便确定的联编,称为静态多态。

重写基类方法

  • 基类的方法可以在派生类中重写 – 使用classname::来说明是基类的还是派生类的。
  • 可在派生类中使用基类名作为限定符调用同名的基类函数。

    1
    2
    3
    4
    5
    void Point::show()
    {
    Base::show();
    cout<<z<<endl;
    }
  • 如果基类中的函数有多个重载,则继承过来的时候不能只重新定义一个版本的,则会导致另外的会被隐藏。

    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
    class Base
    {
    public:
    void show(int a)const
    {
    cout<<a<<endl;
    }
    void show()const
    {
    cout<<10<<endl;
    }
    void show(double a)const
    {
    cout<<a<<endl;
    }
    };
    class P : public Base
    {
    public:
    void show(int a)const
    {
    cout<<a+10<<endl;
    }
    /*void show()const
    {
    cout<<20<<endl;
    }
    void show(double a)const
    {
    cout<<a+10<<endl;
    }*/
    };
    int main()
    {
    P a;
    a.show();//invalid
    }

实现多态公有继承的两种方法

  • 在派生类重新定义基类的方法。
  • 使用虚方法。

虚函数

  • 声明前加virtual,定义不用加。
  • 如果方法是通过引用或指针而不是对象调用的,程序将根据引用或指针指向的对象的类型来选择方法。反之如果没用virtual声明,程序直接使用声明的对象的类型的方法。
  • 如果没有加virtual(一般方法),那么程序将根据引用或指针的类型来选择方法。
  • 友元函数不能是虚函数,因为友元函数不是类成员,而只有成员函数才能是虚函数。
  • 可以创建指向基类的指针数组,那么这个数组既可以指向基类,也可以指向派生类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int main() {
    Base b(10, 20);
    Point x(1, 2, 3);
    Base* bs[3];
    bs[0] = new Base(1, 2);
    bs[1] = &x;
    bs[2] = &b;
    rep(i, 0, 3) bs[i]->show(), cout << endl;
    return 0;
    }
    /*
    output:
    1 2
    1 2 3
    10 20
    */
  • 注意:基类需要声明一个虚析构函数,这样做是为了保证在释放对象时,可以调用相应对象类型的析构函数。

  • 没有重新定义
    • 重新定义一个不接受参数的show函数,那么将会隐藏同名基类的方法。
    • 两条经验规则
      • 如果重新定义继承的方法,应确保与原来的原型完全相同。
      • 如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        class Base
        {
        public:
        virtual void show(int a)const;
        ...
        };
        class Point : public Base
        {
        public:
        virtual void show()const;
        ...
        };
        Point tmp;
        tmp.show()//valid
        tmp.show(1);//invalid

静态联编和动态联编

  • 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。
  • 函数重载和函数模板C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编。
  • 虚函数,因为编译器不知道用户将选择哪种类型的对象。所以编译器必须生成能够在程序运行时选择正确的虚函数方法,这被称为动态联编。
  • C++默认选择为静态联编,因为效率更高。

虚函数的实现原理

  • 给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。
  • 该地址数组称为虚函数表。
  • 对于基类来说,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表。
  • 对于派生类来说,派生类对象也包含了一个指针,该指针指向派生类中所有虚函数的地址表。
    • 如果派生类没有重新定义某虚函数,则此表保留此虚函数基类版本的地址。
    • 如果派生类重新定义了某虚函数,则此表将更新此虚函数的新地址。
    • 如果派生类增加了新的虚函数,则此表也增加该函数的地址。
  • 不管虚函数有多少个,都只需要在对象里添加一个指针成员。
  • 调用虚函数时:
    • 程序将查看存储在对象中的虚函数表头地址。
    • 然后转身相应的函数地址表。
    • 然后根据该虚函数在类中声明的位置找到其在表中的位置。
    • 然后跳到该地址指向的函数地址,执行函数。

抽象基类(abc)

  • abstract base class
  • 使用纯虚函数提供未实现的函数 – 在声明的结尾处加 =0

    1
    2
    3
    4
    5
    6
    class Base {
    private:
    ...;
    public:
    virtual double get_area() = 0;
    };
  • 包含纯虚函数的类只用作基类 ,不能实例化 , 但是能声明(但不初始化)指针。

  • 即ABC必须至少包含一个纯虚函数。
  • 如果在基类中声明了纯虚函数,而派生类中并没有对其定义,则该函数仍为纯虚函数,派生类也为抽象类。
  • 基类中也可以定义(实现)纯虚函数,但在派生类中必需也要定义并且显示地调用(使用类名限定符)。
    • 这样可以将不同子类中公共的事务放在父类中完成。
    • 只有声明而没有定义的纯虚函数派生类是无法调用的。
    • 如果要把基类的析构函数声明为纯虚函数(有时候这么做只是为了说明此类为抽象类),则必须定义这个纯虚析构函数的实现 ,因为派生类析构时会自动调用它。
  • 纯虚函数作为一种“接口约定”, 在基于组件的编程模式中很常见。
    • 派生组件至少会实现基类组件的所有接口(纯虚函数)。

继承和动态内存分配

如果基类使用了动态内存分配 – 即在构造中使用new分配空间

  • 该基类需要声明其构造函数, 析构函数,复制构造,赋值运算符。

如果此时派生类中没有使用new分配的内存

  • 此派生类默认的复制构造会显式地调用基类的复制构造, 同时根据成员变量类型进行复制。
  • 此派生类默认的赋值运算符会显式地调用基类的赋值运算符。
  • 此派生类不需要显示定义构造函数, 析构函数,复制构造,赋值运算符。

如果子类使用new分配的内存

  • 必须为子类定义显式析构函数。
  • 必须为子类定义复制构造函数。
    • 基类的复制构造函数中的参数是基类的引用,所以可以传递进来派生类对象。
      1
      2
      3
      4
      5
      Point::Point(const Point &a):Base(a)	
      {
      z=a.z;
      }
      `
  • 必须为子类重载赋值运算符。

    • 显式调用基类的赋值运算符,以完成基类部分的赋值。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      String& String::operator=(const String& s) {    
      if(this == &s) return *this;
      Base::operator=(s); // 注意此句 -- 必须显示调用基类的赋值运算符
      delete[] str;
      len = s.len;
      str = new char[len+1];
      strcpy(str, s.str);
      return *this;
      }

继承与友元函数

  • 在派生类的友元函数中, 只能访问派生类的派生部分,而不能访问基类的私有成员。
  • 可使用基类的友元函数来负责对派生类的基类部分的访问。
  • 因为友元函数不是成员函数,不能使用作用域解析运算符,因此需要强制类型转换。