首页 C++ Primer Plus-第13章 类继承
文章
取消

C++ Primer Plus-第13章 类继承

    这章陆续看了三天,总算是对类继承有个差不多的概念了,之前一直稀里糊涂的QAQ

13.1 一个简单的基类

  • 通过继承可以完成的工作:

    • 可以在已有类的基础上添加新的功能

    • 可以给类添加数据

    • 可以修改类方法的行为

  • 派生类对象包含基类对象

  • 使用公有派生:

    • 基类的公有成员将成为派生类的公有成员

    • 基类的私有部分也将成为派生类的一部分,但只能通过公有和保护方法访问

  • 派生类对象具有以下特征:

    • 派生类对象存储了基类的数据成员(派生类继承了基类的实现)

    • 派生类对象可以使用基类的方法(派生类继承了基类的接口)

  • 需要在继承特性中添加:

    • 派生类需要自己的构造函数

      • 构造函数必须给新成员(如果有)和继承的成员提供数据(如果基类构造函数需要参数,那么派生类的形参就要提供(除非要使用默认构造函数就不提供))
    • 派生类可以根据需要添加额外的数据成员和成员函数

  • 派生类不能直接访问基类的私有成员,必须通过基类的公有方法进行访问

  • 有关派生类构造函数的要点:

    • 首先创建基类对象(即先调用基类构造函数再调用派生类构造函数)

    • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数

    • 派生类构造函数应初始化派生类新增的数据成员

  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象

    • 然而基类指针和引用只能调用基类方法,不能调用派生类方法

    • 不可以将基类对象的地址赋给派生类引用和指针

13.2 继承:is-a关系

  • 公有继承建立了一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行

  • 公有继承不建立has-a关系,例如午餐可能包括水果,但通常午餐不是水果,所以最容易的建模方式是将fruit对象作为lunch类的数据成员

  • 公有继承不建立is-like-a关系,即不采用明喻,律师像鲨鱼,但不是鲨鱼,不能在水下生活

  • 公有继承不建立is-implemented-as-a(作为……来实现),例如,可以使用数组实现栈名单从array类派生出stack类是不合适的

  • 公有继承不建立uses-a关系,例如,计算机可以使用激光打印机,但从computer类派生出printer类(或者反过来)是没有意义的

13.3 多态公有继承

  • 方法的行为取决于调用该方法的对象,这种复杂的行为称为多态——具有多种形态

  • 有两种重要机制可用于实现多态公有继承:

    • 在派生类中重新定义基类方法

    • 使用虚方法

  • 使用virtual虚方法(如virtual void viewacct() const;):如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法

  • 方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法

  • 如果在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例

  • 注意:关键字virtual只用于类声明的方法原型中

  • 在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法,如果没有使用作用域解析运算符,那么将会递归下去……

    1
    2
    3
    4
    5
    
    void BrassPlus::ViewAcct() const
    {
        Brass::ViewAcct();
        ...
    }
    
  • 虚析构函数作用

    • 当有new的对象时,需要delete,此时就需要虚析构函数了

    • 如果虚构函数不是虚的,那么就将调用对应于指针类型的析构函数(比如基类指针指向了一个派生类对象),这样就delete错了;如果时虚函数,那么将调用对应对象的析构函数

    • 因此,使用虚析构函数可以确保正确的析构函数被调用

13.4 静态联编和动态联编

  • 程序调用函数时,将使用哪个可执行代码呢?(因为有函数重载的情况)编译器将代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding),C/C++编译器可以在编译过程完成这种联编

  • 在编译过程中进行联编被称为静态联编 (static binding),又称为早期联编(early binding)

  • 像虚函数这样不能再编译时确定,因为编译器不知道用户将选择哪种类型的对象,所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)

  • 将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使得公有继承不需要进行显式类型转换,并且向上强制转换是可传递的

  • 相反的过程,将基类指针或引用转换为派生类的,称为向下强制转换(downcasting),必须使用显式类型转换

  • 动态联编实例:对象类型为BrassPlus,但只有在运行时程序才能确定对象的类型,所以编译器生成的代码将在程序执行时,根据对象类型将ViewAcct()关联到Brass::ViewAcct()或BrassPlus::ViewAcct()

  • 为什么有两种联编以及为什么默认为静态联编

    • 效率原因,程序要在运行阶段采取决策就需要采取方法跟踪基类指针或引用指向的对象类型,增加了开销,因此静态联编效率更高

    • C++的指导原则之一是不要为不使用的特性付出代价(内存或时间)

    • 概念模型原因,设计类时,派生类不变的不设置虚函数,既提高了效率也指出不要重新定义该函数

  • 虚函数工作原理:

    • 给每个对象添加一个隐藏成员

    • 隐藏成员中保存了一个指向虚函数地址数组的指针,数组称为虚函数表(virtual function table, vtbl)

  • 在使用虚函数时,在内存和执行速度方面有一定的成本:

    • 每个对象都将增大,增大量为存储地址的空间

    • 对于每个类,编译器都创建一个虚函数地址表

    • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址

  • 虽然非虚函数效率更高,但不具备动态联编功能

  • 虚构函数应当是虚函数,除非类不用做基类

  • 通常应该给基类提供一个虚析构函数,即使它并不需要析构函数

  • 友元不能是虚函数,因为友元不是类成员

  • 如果派生类没有重新定义函数,将使用该函数的基类版本;如果派生类位于派生链中,则将使用最新的虚函数版本

  • 重新定义产生问题:比如基类的虚函数与派生类的同名函数参数不同,虚函数的重新定义不会生成函数的两个重载版本,而是隐藏了基类版本;因此,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的),这种特性被称为返回类型协变(covariance of return type)

  • 如果基类被重载了,则应该在派生类中重新定义所有的基类版本;如果只定义一个版本,那么另外的版本会被隐藏,派生类对象将无法使用它们;当然,如果不修改,可以直接调用基类版本:void Hovel::show() const{Dewelling::show();}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    class Dwelling
    {
        public:
                virtual void show(int a) const;
                virtual void show() const;
    };
    ```cpp
    class Hovel : public Dwelling
    {
        public:
                virtual void show(int a) const;
                virtual void show() const;
    }
    

13.5 访问控制:protected

  • 关键字protected和private相似,在类外只能用公有类成员(比如公有成员函数)来访问protected的类成员

  • 派生类的成员可以直接访问基类的保护成员,但不能直接访问私有成员

  • 最好对数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据

  • 对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数

13.6 抽象基类

  • 当类声明中包含纯虚函数时,则不能创建该类的对象(理念:包含纯虚函数的类之用作基类);

  • 纯虚函数(pure virtual fuction)提供未实现的函数,纯虚函数声明的结尾处为0,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数

    1
    
    virtual double Area() const = 0;
    
  • 总之,ABC(抽象基类)描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口

  • 可以将ABC看作是一种必须实施的接口,ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则

13.7 继承和动态内存分配

  • 如果基类使用了new而派生类没有使用new

    • 那么派生类没必要定义新的显式构造函数、复制构造函数、析构函数,统统默认就行,因为默认就都用基类的了
  • 如果基类使用了new而派生类也使用new

    • 必须为派生类定义显式析构函数、复制构造函数和赋值运算符
  • 使用强制类型转换在派生类的函数中使用基类的友元函数

    1
    2
    3
    4
    5
    
    std::ostream:: operator<<(std::ostream os, const hasDMA & hs)
    {
        os<<(const baseDMA) hs;
        os<<"hasDMA";
    }
    

13.8 类设计回顾

  • 复习下特殊成员函数——默认构造函数、复制构造函数、赋值运算符

  • 复习下其他类方法——构造函数、析构函数、转换、按值传递对象和传递引用、返回对象和返回引用、使用const

  • 公有继承的相关考虑因素

    • is-a关系

    • 为什么不能继承(比如构造函数,因为要必须执行派生类构造函数(执行过程自动先执行基类构造(无论是显式调用还是隐式调用默认的)))

    • 赋值运算符

    • 私有成员、保护成员

    • 虚方法

    • 析构函数

    • 友元函数

  • 有关基类方法的说明总结:

    • 派生类对象自动使用继承而来的方法,如果派生类没有重新定义该方法

    • 派生类的构造函数自动调用基类的构造函数

    • 派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数

    • 派生类的构造函数显式地调用成员初始化列表中指定的基类构造函数

    • 派生类方法可以使用作用域解析运算符来调用共有的和受保护的基类方法

    • 派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后用该引用或指针来调用基类的友元函数

  • 成员函数属性:

    zAJO8x.jpg

本文由作者按照 CC BY 4.0 进行授权

C++ Primer Plus-第12章 动态内存和类

C++ Primer Plus-第14章 C++中的代码重用