《C++面向对象程序设计(第4版)》学习笔记-7

在这里插入图片描述

此份笔记建议在完整阅读郑莉老师、董渊老师、何江舟老师所编写的《C++语言程序设计(第4版)》后食用,风味更佳!
最后,由于本人水平有限,笔记中仍存在错误但还没有被检查出来的地方,欢迎大家批评与指正。


第7章 继承与派生

7.1 类的继承与派生

7.1.1 继承关系举例

类的继承,是新的类从已有类那里得到已有的特性。

从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。

由原有的类产生新类时,新类便包含了原有类特征,同时也可以加入自己所特有的新特性。

  • 原有的类称为基类或父类,产生的新类称为派生类或子类。
  • 派生类同样也可以作为基类派生新的类,这样就形成了类的层次结构。

7.1.2 派生类的定义

1.派生类的一般定义语法
class 派生类名:继承方式 基类名1,继承方式 基类名2,...,继承方式 基类名n
{
	派生类成员声明;
}
2.单继承与多继承

(1)单继承:一个派生类只有一个直接基类

(2)多继承:一个派生类,同时有多个基类

在这里插入图片描述

3.直接基类与间接基类
  • 在派生过程中,派生出来的新类也同样可以作为基类再继续派生新的类;此外,一个基类可以同时派生出多个派生类。
  • 也就是说,一个类从父类继承来的特征也可以被其他新的类所继承;一个父类的特征,可以同时被多个子类继承。
  • 这样,就形成了一个相互关联的类的家族,有时也称做类族。
    • 在类族中,直接参与派生出某类的基类称为直接基类,基类的基类甚至更高层的基类称为间接基类
4.继承方式
  • 继承方式规定了如何访问从基类继承的成员。
  • 类的继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。
  • 继承方式关键字
    • public(公有继承)
    • protected(保护继承)
    • private(私有继承)
  • 如果不显式地给出继承方式关键字,系统的默认值就认为是私有继承。
5.派生类成员

  派生类成员是指除了从基类继承来的所有成员之外 ,新增加的数据和函数成员。

6.在派生类中,实际所拥有的成员就是从基类继承过来的成员与派生类新定义成员的总和。

7.1.3 派生类生成过程

派生新类的三个步骤:吸收基类成员、改造基类成员、添加新的成员。

吸收基类成员就是一个重用的过程,而对基类成员进行调整、改造以及添加新成员就是原有代码的扩充过程,二者是相辅相成的。

1.吸收基类成员
  • 接收全部基类中除构造函数和析构函数之外的所有函数成员。
2.改造基类成员
  • 基类成员的访问控制问题:主要依靠派生类定义时的继承方式来控制。
  • 对基类数据或函数成员的覆盖或隐藏。
    • 如果派生类声明了一个和某基类成员同名的新成员(如果是成员函数,则参数表也要相同,参数不同的情况属于重载),派生的新成员就隐藏了外层同名成员。——“同名隐藏”
3.添加新的成员
  • 由于在派生过程中,基类的构造函数和析构函数是不能被继承的,因此要实现一些特别的初始化和扫尾清理工作,就需要在派生类中加入新的构造和析构函数。

7.2 访问控制

派生类继承了基类的全部数据成员和除了构造、析构函数之外的全部函数成员;但是这些成员的访问属性在派生的过程中是可以调整的。

从基类继承的成员,其访问属性由继承方式控制。

1.基类成员的访问控制权限

基类的自身成员可以对基类中任何一个其他成员进行访问,但是通过基类的对象 ,就只能访问该类的公有成员。

  • public(公有)
  • protected(保护)
  • private(私有)
2.类的继承形式
  • public(公有继承)
  • protected(保护继承)
  • private(私有继承)
3.派生类访问基类的权限要讨论的问题
  • 派生类中的新增成员访问从基类继承的成员;
  • 在派生类外部(非类族内的成员),通过派生类的对象访问从基类继承的成员。

7.2.1 公有继承

1.具体描述

  当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问。

  • 基类的公有成员和保护成员被继承到派生类中访问属性不变,仍作为派生类的公有成员和保护成员,派生类的其他成员可以直接访问它们。
  • 在类族之外只能通过派生类的对象访问从基类继承的公有成员,而无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员。

7.2.2 私有继承

1.具体描述

  当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。

  • 基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类族外部通过派生类的对象无法直接访问它们。
  • 无论是派生类的成员还是通过派生类的对象,都无法直接访问从基类继承的私有成员。
2.讨论
  • 经过私有继承之后,所有基类的成员都成为了派生类的私有成员或不可直接访问的成员,如果进一步派生的话,基类的全部成员就无法在新的派生类中被直接访问。
  • 在私有继承情况下,为了保证基类的一部分外部接口特征能够在派生类中也存在,就必须在派生类中重新声明同名的成员。

7.2.3 保护继承

1.具体描述

  保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问。

  • 派生类的其他成员就可以直接访问从基类继承来的公有和保护成员 ,但在类外部通过派生类的对象无法直接访问它们,
  • 无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员。
2.讨论:私有继承与保护继承的区别
  • 假设 Rectangle 类以私有方式继承了 Point 类后,Rectangle 类又派生出 Square 类,那么 Square 类的成员和对象都不能访问间接从 Point 类中继承来的成员。
  • 如果 Rectangle 类是以保护方式继承了 Point 类,那么 Point 类中的公有和保护成员在 Rectangle 类中都是保护成员。Rectangle 类再派生出 Square 类后,Point 类中的公有和保护成员被 Square 类间接继承后,有可能是保护的或者是私有的(视从 Rectangle 到 Square 的派生方式不同而不同)。因而,Square 类的成员有可能可以访问间接从 Point 类中继承来的成员。

7.3 类型兼容规则

1.什么是类型兼容规则
  • 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
  • 公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。
2.类型兼容规则中所指的替代包括:
  • 派生类的对象可以隐含转换为基类对象。
  • 派生类的对象可以初始化基类的引用。
  • 派生类的指针可以隐含转换为基类的指针。
3.在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
4.实例
class B{
	...  
};
class D:public B{
    ...
};

B b1,*pb1;
D d1;

//派生类对象可以隐含转换为基类对象,即用派生类对象中从基类继承来的成员,逐个赋值给基类对象的成员
b1 = d1;
//派生类的对象也可以初始化基类对象的引用
B &rb = d1;
//派生类对象的地址也可以隐含转换为指向基类的指针
pb1 = &d1;
5.实例剖析
#include<iostream>
using namespace std;

class Base1{
public:
    void display() const {
        cout << "Base1::display()" << endl;
    }
};

class Base2:public Base1{
public:
    void display() const {
        cout << "Base2::display()" << endl;
    }
}

class Derived:public Base2{
public:
    void display() const {
        cout << "Derived::display()" << endl;
    }
}

void fun(Base1 *ptr)
{
    ptr->display();
}

int main()
{
    Base1 base1;
    Base2 base2;
    Derived derived;
    
    fun(&base1);
    fun(&base2);
    fun(&derived);
    
    return 0;
}
//输出结果
Base1::display()
Base1::display()
Base1::display()
  • 通过“对象名.成员名“或者”对象指针->成员名”的方式,就应该可以访问到各派生类中继承自基类的成员。虽然根据类型兼容原则,可以将派生类对象的地址赋值给基类 Base1 的指针,但是通过这个基类类型的指针
    ,却只能访问到从基类继承的成员。
  • 根据类型兼容规则,可以在基类对象出现的场合使用派生类对象进行替代,但是替代之后派生类仅仅发挥出基类的作用。

7.4 派生类的构造函数和析构函数

派生类的构造函数只负责对派生类新增的成员进行初始化,对所有从基类继承下来的成员,其初始化工作还是由基类的构造函数完成。

同样,对派生类对象的扫尾、清理工作也需要加入新的析构函数。

7.4.1 构造函数

1.派生类构造函数的构造过程

  在构造派生类的对象时,会首先调用基类的构造函数,来初始化它们的数据成员;然后按照构造函数初始化列表中指定的方式初始化派生类新增的成员对象,最后才执行派生类构造函数的函数体。

2.派生类构造函数的一般语法
派生类名::派生类名(参数表):基类名1(基类1初始化参数表),...,基类名n(基类n初始化参数表),成员对象名1(成员对象1初始化参数表),...,成员对象n(成员对象n初始化参数表)
{
    派生类构造函数的其他初始化操作;
}
  • 当一个类同时有多个基类时,对于所有需要给予参数进行初始化的基类,都要显式给出基类名和参数表。对于使用默认构造函数的基类,可以不给出类名。同样,对于成员对象,如果是使用默认构造函数,也不需要写出对象名和参数表。
  • 如果对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数,提供一个将参数传递给基类构造函数的途径,保证在基类进行初始化时能够获得必要的数据。
  • 当派生类没有显式的构造函数时,系统会隐含生成一个默认构造函数.该函数会使用基类的默认构造函数对继承自基类的数据初始化,并且调用类类型的成员对象的默认构造函数,对这些成员对象初始化。
3.派生类构造函数执行的一般次序

(1)调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。

(2)对派生类新增的成员对象初始化,调用顺序按照它们在类中声明的顺序。

(3)执行派生类的构造函数体中的内容。

4.需要注意

  构造函数初始化列表中基类名、对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的,无论它们的顺序怎样安排,基类构造函数的调用和各个成员对象的初始化顺序都是确定的 。

实例:

#include<iostream>
using namespace std;

class Base1{
public:
    Base1(int i){
        cout << "Constructing Base1: " << i << endl;
    }
};

class Base2{
public:
    Base2(int j){
        cout << "Constructing Base2: " << j << endl;
    }
};

class Base3{
public:
    Base3(){
        cout << "Constructing Base3: *" << endl;
    }
};

class Drived:public Base1,public Base2,public Base3{
    Drived(int a,int b,int c,int d):Base1(a),menber2(d),menber1(c),Base2(b)
    {
        
    }
    //下面这样的也行
//  Drived(int a,int b,int c,int d):Base1(a),Base2(b),menber1(c),menber2(d)
//  {
//        
//  }
private:
    Base1 menber1;
    Base2 menber2;
    Base3 menber3;
}

7.4.2 复制构造函数

  假设,Drived 类是Base类的派生类,Drived 类的复制构造函数形式如下:

Drived::Drived(const Drived &d):Base(d){
    ...
}
  • 类型兼容规则在这里起了作用:可以用派生类的对象去初始化基类的引用。因此当函数的形参是基类的引用时,实参可以是派生类的对象。

7.4.3 析构函数

1.概述

  派生类析构函数的声明方法与没有继承关系的类中析构函数的声明方法完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工做好就够了,系统会自己调用基类及对象成员的析构函数来对基类及对象成员进行清理。

2.析构函数的执行顺序——与构造函数执行相反
  • 首先执行析构函数的函数体;
    • 执行派生类析构函数体
  • 然后对派生类新增的类类型的成员对象进行清理;
    • 调用类类型的派生类对象所在类的析构函数
  • 最后对所有从基类继承来的成员进行清理。
    • 调用基类析构函数

7.5 派生类成员的标识与访问

1.在对派生类的访问中,实际上需要解决两个问题

(1)唯一标识问题

(2)可见性问题

2.二义性问题

  我们只能访问一个能够唯一标识的可见成员。如果通过某一个表达式能引用的成员不只一个,称为有二义性。

3.对于在不同的作用域声明的标识符

(1)可见性原则

  如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层仍然可见

(2)隐藏原则

  如果在内层声明了同名标识符,则外层标识符在内层不可见,这时称内层标识符隐藏了外层同名标识符,这种现象称为隐藏规则。

(3)在派生类中讨论

  • 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层。
  • 如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定。
4.讨论多继承(各个基类之间没有任何继承关系,也没有共同基类)下的可见性问题

(1)派生类的多个基类拥有同名成员,同时派生类中也有新增同名成员

  • 派生类新增同名成员将隐藏所有基类的同名成员
  • 使用“ 对象名.成员名 ”或“ 对象指针->成员名 ”方式可以唯一标识和访问派生类新增同名成员
  • 基类同名成员的访问方法
    • “ 对象名.基类名::成员名 ”或“ 对象指针->基类名::成员名 ”

(2)派生类没有新增同名成员

  • 使用“ 对象名.成员名 ”或“ 对象指针->成员名 ”方式无法唯一标识和访问来自基类的成员。
  • 基类同名成员的访问方法
    • “ 对象名.基类名::成员名 ”或“ 对象指针->基类名::成员名 ”

(3)总结

  • 派生类中新增的成员具有更小的类作用域,因此, 在派生类及建立派生类对象的模块中,派生类新增成员隐藏了基类的同名成员,这时使用“ 对象名.成员名 ”或“ 对象指针->成员名 ”的访问方式,就只能访问到派生类新增的成员。
  • 对基类同名成员的访问,只能通过基类名和作用域分辨符来实现,
5.关键字 using 的用法

(1)将一个作用域引入另一个作用域

#include<iostream>
using namespace std;

class Base1{
public:
    int var;
    void fun(){
        ...
    }
};

class Base2{
public:
    int var;
    void fun(float i){
        ...
    }
}

class Drived:public Base1,public Base2{
public:
    void fun(int j){
        ...
    }
}

int main()
{
    Drived d;
    d.var = 1;   //编译报错,有二义性
    d.fun();     //编译报错,有二义性
}

这时,我们想直接通过派生类 Drived 的基类 Base1 的数据成员 var 和函数成员 fun( ),怎么做呢?使用 using。将 Derived 类的定义修改如下:

class Drived:public Base1,public Base2{
public:
    using Base1::var;
    void fun(int j){
        ...
    }
}

那么我们就可以在主程序中这样直接访问 Base1 的数据成员 var了

int main()
{
    Drived d;
    d.var = 1;  
}

(2)实现“派生类同名函数”隐藏作用,实现基类同名函数与派生类同名函数的重载

将上面的 Derived 类的定义再修改如下:

class Drived:public Base1,public Base2{
public:
    using Base1::var;
    using Base1::fun();
    void fun(int j){
        ...
    }
}

这样,在主函数就实现了函数的重载

int main()
{
    Drived d;
    d.var = 1;   
    d.fun();    
    d.fun(10);
}
6.讨论多继承(各个基类之间没有任何继承关系,但有共同基类)下的可见性问题

  如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。

7.5.2 虚基类

1.问题的产生

  当某类的部分或全部直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称。在派生类的对象中,这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射。

  • 可以使用作用域分辨符 来唯一标识并分别访问它们;
  • 也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射。这样就解决了同名成员的唯一标识问题。
2.声明虚基类的语法
class 派生类名:vitrual 继承方式 基类名
  • 在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
3.实例
#include<iostream>
using namespace std;

class Base0{
public:    
    int var0;
    void fun0(){
        cout << "Member of Base0" << endl;
    }
};

class Base1:virtual public Base0{
public:    
    int var1;
};

class Base2:virtual public Base0{
public:    
    int var2;
};

class Drived:public Base1,public Base2{
public:    
    int var;
    void fun(){
        cout << "Member of Drived" << endl;
    }
};

int main()
{
    Drived d;
    d.var=2;
    d.fun();
    return 0;
}
//输出结果
Member of Base0
4.比较作用域分辨符和虚基类技术
  • 前者在派生类中拥有同名成员的多个副本,分别通过直接基类名来唯一标识,可以存放不同的数据、进行不同的操作;后者只维护一份成员副本。
  • 前者可以容纳更多的数据;而后者使用更为简洁,内存空间更为节省。

7.5.3 虚基类及其派生类构造函数

1.讨论问题
  • 如果虚基类声明有非默认形式的(即带形参的)构造函数,并且没有声明默认形式的构造函数,事情就比较麻烦了。
  • 这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中列出对虚基类的初始化。
2.实例
#include<iostream>
using namespace std;

class Base0{
public:
    Base0(int var):var0(var){}
    int var0;
    void fun0(){
        cout << "Member of Base0" << endl;
    }
};

class Base1:virtual public Base0{
public:    
    Base1(int var):Base0(var){}
    int var1;
};

class Base2:virtual public Base0{
public:    
    Base2(int var):Base0(var){}
    int var2;
};

class Drived:public Base1,public Base2{
public:    
    Drived(int var):Base0(var),Base1(var),Base2(var){}
    int var;
    void fun(){
        cout << "Member of Drived" << endl;
    }
};

int main()
{
    Drived d(1);
    d.var=2;
    d.fun();
    return 0;
}
3.抛出问题,上述例程中,变量 var 会不会被初始化3次?
  • 不会
  • 首先,指定 Drived 为最远派生类
  • 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类(例如,上例中的 Base1 和 Base2 类)对虚基类构造函数的调用都自动被忽略。
4.构造一个类的对象的一般顺序是

(1)如果该类有直接或间接的虚基类 ,则先执行虚基类的构造函数。

(2)如果该类有其他基类 ,则按照它们在继承声明列表中出现的次序,分别执行它们的构造函数,但构造过程中,不再执行它们的虚基类的构造函数。

(3)按照在类定义中出现 的顺序,对派生类中新增的成员对象进行初始化。 对于类类型的成员对象,如果出现在构造函数初始化列表中,则以其中指定的参数执行构造函数,如未出现,则执行默认构造函数;对于基本数据类型的成员对象,如果出现在构造函数的初始化列表中,则使用其中指定的值为其赋初值,否则什么也不做。

(4)执行构造函数的函数体。

猜你喜欢

转载自blog.csdn.net/Jason3633/article/details/91965064