五、类的继承与派生

738 阅读12分钟

类的继承与类的派生

继承和派生是人们认识客观世界的过程。在程序设计方法中,人们追求代码复用(这是提高软件开发效率的重要手段),将继承和派生用于程序设计方法中,从而有了面向对象程序设计的重要特点。C++对代码复用有很强的支持,“继承”就是支持代码复用的机制之一。

通过已有的类建立新类的过程,叫做类的派生。原来的类称为基类,也称为父类或一般类;新类称为派生类,也称为子类或特殊类。派生类派生自基类,或继承于基类,也可以说基类派生了派生类。派生机制是C++语言及面向对象程序设计方法的重要特征之一。派生类可以再作为基类派生新的派生类,由此基类和派生类的集合称作类继承层次结构。

继承的概念

使用基类派生新类时,**除构造函数和析构函数外,基类的所有成员自动成为派生类的成员,包括基类的成员变量和成员函数。**同时,派生类可以增加基类中没有的成员,这同样是指成员变量和成员函数。可以重新定义或修改基类中已有的成员,包括可以改变基类中成员的访问权限。当然派生类需要定义自己的构造函数和析构函数。

使用基类成员时一个重用的过程,在基类之上进行调整,不论是添加新成员还是改造已有的,都是扩充的过程。

若派生类中定义了一个与基类中同名的成员,则会出现基类与派生类有同名成员的情况,这是允许的。同名的成员既可以是成员变量,也可以是成员函数。这种情况系下,若在派生类的成员函数中访问这个同名成员,或通过派生类对象访问这个同名成员时,除非特别指明,访问的就是派生类中的成员,这种情况叫“覆盖”,即派生类的成员覆盖基类的同名成员。覆盖也称为重定义或是重写。对于成员函数来说,派生类既继承了基类的同名函数,又在派生类中重写了这个成员函数。这称为函数重定义,也称为同名隐藏。“隐藏”的意思是指,使用派生类对象调用这个名字的成员函数时,调用的是派生类中定义的成员函数,即隐藏了基类中的成员函数。

派生类的定义与大小

派生类的定义

基类和派生类的定义:

class BaseClass {                        //基类
    int v1, v2;
};
class DerivedClass : public BaseClass {  //派生类
    int v3;
};

空类也可以作为基类,也就是说,空类可以派生子类。例如,下列语句定义了空基类的派生类:

class EmptyClass {};
class SubEmptyClass : public EmptyClass {};

派生类的大小

派生类对象中包含基类成员变量,而且基类成员变量的存储位置位于派生类对象新增的成员变量之前。派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小。

**对象占用的存储空间包含对象中各成员变量占用的存储空间。**出于计算机内部处理效率的考虑,为变量分配内存时,会根据其对应的数据类型,在存储空间内对变量的起始地址进行边界对齐。可以使用sizeof()函数计算对象占用的字节数。对象的大小与普通成员变量有关,与成员函数和类中的静态成员变量无关,即普通成员函数、静态成员函数、静态成员变量、静态常量成员变量等均对类对象的大小没有影响。

int main(int argc, const char * argv[]) {
    cout << "Base = " << sizeof(BaseClass) << endl;      //Base = 8
    cout << "Derived = " << sizeof(DerivedClass) << endl;//Derived = 12
    
    return 0;
}

继承关系的特殊性

**如果基类有友元类或友元函数,则其派生类不会因继承关系而也有此友元类或友元函数。如果基类是某类的友元,则这种友元关系是被继承的。即被派生类继承过来的成员函数,如果原来是某类的友元函数,那么它作为派生类的成员函数仍然是某类的友元函数。**总之,基类的友元不一定是派生类的友元;基类的成员函数是某类的友元函数,则其作为派生类继承的成员函数仍是某类的友元函数。

#include <iostream>
using namespace std;

class Another; //前向引用声明

class Base {
private:
    float x;
public:
    void print(const Another &k);
};

class Derived : public Base {
private:
    float y;
};

class Another {
private:
    int aaa;
public:
    Another() {
        aaa = 100;
    };
    //基类的成员函数声明为本类的友元
    friend void Base::print(const Another &k);
};

void Base::print(const Another &k) {
    cout << "Base::" << k.aaa << endl;
};

int main() {
    Base a;
    Derived d;
    Another ano;//aaa初始化为100
    
    a.print(ano);//输出为:Base::100
    d.print(ano);//输出为:Base::100
    
    return 0;
};

如果基类中的成员是静态的,则在其派生类中,被继承的成员也是静态的,即其静态属性随静态成员被继承。

如果基类的静态成员是公有的或是保护的,则它们被其派生类继承为派生类的静态成员。访问这些成员时,通常用<类名>::<成员名>的方式引用或调用。无论有多少个对象被创建,这些成员都只有一个拷贝,它为基类和派生类的所有对象所共享。

#include <iostream>
using namespace std;

class Base {
private:
    float x;
public:
    static int staV;
    Base() {
        staV++;
    };
};
int Base::staV = 0;

class Derived : public Base {
private:
    float y;
public:
    Derived() {
        staV++:
    };
};

int main() {
    Base a;
    cout << a.staV << endl;//输出1
    
    Derived d;
    cout << d.staV << endl;
    //输出3,因为Derived构造前会先调用一次Bsse的构造函数

    return 0;
};

有继承关系的类之间的访问

派生类和基类中都可以定义自己的成员变量和成员函数,派生类中的成员函数可以访问基类中的公有成员变量,但不能直接访问基类中的私有成员变量。也就是说,不能在派生类的函数中,使用基类对象名.基类私有成员函数(实参),或是基类对象名.基类私有成员变量,或是基类名::基类私有成员的形式访问基类中的私有成员。

在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层,基类在外层。如果派生类声明了一个和基类某个成员同名的新成员,派生的新成员就隐藏了外层同名成员,直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用基类名和作用域分辨符来限定。

#include <iostream>
using namespace std;

class CB {
public:
    int a;
    CB(int x) {
        a = x;
    };
    void showA() {
        cout << "Class CB -- a = " << a << endl;
    };
};

class CD : public CB {
public:
    int a;//与基类a同名
    CD(int x, int y):CB(x) {//用x来初始化基类的成员变量a
        a = y;
    };
    void showA() {//与基类showA同名
        cout << "Class CD -- a = " << a << endl;
    };
    void print2a() {
        cout << "a = " << a << endl;//访问派生类的a
        cout << "CB::a = " << CB::a << endl;//访问基类的a
    };
};

int main() {
    CB cbObj(12);
    cbObj.showA();//Class CB -- a = 12
    
    CD cdObj(48, 999);
    //访问派生类的showA()
    cdObj.showA; //Class CD -- a = 999
    //访问基类的showA()
    cdObj.CB::showA();//Class CB -- a = 48
    
    cout << "cdObj.a = " << cdObj.a << endl;//cdObj.a = 999
    cout << "cdObj.CB::a = " << cdObj.CB::a << endl;//cdObj.CB::a = 48
    
    return 0;
};

protected访问范围说明符

定义类时,类成员可以使用 protected 访问范围说明符进行修饰,从而成为保护成员。保护成员的访问范围比私有成员的访问范围大,能访问私有成员的地方都能访问保护成员。此外,基类中的保护成员可以在派生类的成员函数中被访问。

在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。将基类中成员变量的访问方式修改为protected后,在派生类中可以直接访问。

多重继承

C++允许从多个类派生一个类,即一个派生类可以同时有多个基类。这称为多重继承。相应的,从一个基类派生一个派生类的情况,称为单继承或单重继承。一个类从多个基类派生的一般格式如下:

class 派生类名 : 继承方式说明符 基类名1, 继承方式说明符 基类名2,...
{
    类体    
};
  • 派生类继承了基类名1、基类名2、...、基类名n的所有成员变量和成员函数,各基类名前面的继承方式说明符用于限制派生类中的成员对该基类名中成员的访问权限,其规则与单继承情况一样。
  • 多重继承情况下如果多个基类间成员名重名时,按如下方式进行处理:
    • 对派生类而言,不加类名限定时默认访问的 是派生类的成员;
    • 而要访问基类重名成员时,要通过类名加以限定。
#include <iostream>
using namespace std;

class CB1 {
public:
    int a;//重名
    CB1(int x) {
        a = x;
    };
    void showA() {//重名
        cout << "class CB1 ==> a = " << a << endl;
    };
};

class CB2 {
public:
    int a;//重名
    CB2(int x) {
        a = x;
    };
    void showA() {//重名
        cout << "class CB2 ==> a = " << a << endl;
    };
};

class CD : public CB1, public CB2 {//多重继承,两个基类
public:
    int a;//与两个基类成员变量a重名
    CB1(int x, int y, int z):CB1(x), CB2(y) {
        a = z;
    };
    void showA() {//与两个基类成员函数showA()重名
        cout << "class CD ==> a = " << a << endl;
    };
    void print3a() {
        cout << "a = " << a << endl;
        cout << "CB1::a = " << CB1::a << endl;
        cout << "CB2::a = " << CB2::a << endl;
    };
};

int main() {
    CB1 cb1Obj(11);
    cb1Obj.showa();//class CB1 ==> a = 11
    
    CD cdObj(101, 202, 909);
    //调用派生类的showA()
    cdObj.showA();//class CD ==> a = 909
    cdObj.CB1::showA();//class CB1 ==> a = 101
    
    cout << "cdObj.a = " << cdObj.a << endl;
    //cdObj.a = 909
    cout << "cdObj.CB2::a = " << cdObj.CB2::a << endl;
    //cdObj.CB2::a = 202

    return 0;
};
  • 如果派生类中新增了同名成员,则派生类成员将隐藏所有基类的同名成员。使用派生类对象名.成员名派生类对象指针->成员名的方式可以唯一标识和访问派生类新增成员。这种情况下,不会产生二义性。
  • 如果派生类中没有新增同名成员,当满足访问权限时,使用派生类对象名.成员名派生类对象指针->成员名方式时,系统无法判断到底是调用哪个基类的成员,从而产生二义性。为了避免二义性,必须通过基类名和作用域分辨符来标识成员。
  • 当要访问派生类对象中某个变量时,添加基类::作为前缀,指明需要访问从哪个基类继承来的,从而可以排除二义性。

访问控制

设计继承类时,需要使用继承方式说明符指明派生类的继承方式。继承方式说明符可以是**public公有继承**、private私有继承或**protected保护继承**。

公有继承

各成员派生类中基类与派生类外
基类的公有成员直接访问直接访问
基类的保护成员直接访问调用公有函数访问
基类的私有成员调用公有函数访问调用公有函数访问
从基类继承的公有成员直接访问直接访问
从基类继承的保护成员直接访问调用公有函数访问
从基类继承的私有成员调用公有函数访问调用公有函数访问
派生类中定义的公有成员直接访问直接访问
派生类中定义的保护成员直接访问调用公有函数访问
派生类中定义的私有成员直接访问调用公有函数访问

类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,也称为赋值兼容规则

在公有派生类的情况下,有以下3条类型兼容规则。

  1. 派生类的对象可以赋值给基类对象。
  2. 派生类对象可以用来初始化基类引用。
  3. 派生类对象的地址可以赋值给基类指针,即派生类的指针可以赋值给基类的指针。

上述3条规则反过来是不成立的。例如,不能把基类对象赋值给派生类对象。在进行替代之后,派生类对象就可以走位基类的对象使用了,但只能使用从基类继承的成员。

私有继承

第一级派生类中第二级派生类中基类与派生类外
基类的公有成员直接访问不可访问不可访问
基类的保护成员直接访问不可访问不可访问
基类的私有成员调用公有函数访问不可访问不可访问

保护继承

保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接访问。这样,派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但在类外通过派生类的对象无法直接访问它们。

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

派生类并不继承基类的构造函数,所以需要在派生类的构造函数中调用基类的构造函数,以完成对从基类继承的成员变量的初始化工作。具体来说,派生类对象在创建时,除了要调用自身的构造函数进行初始化外,还要调用基类的构造函数初始化其包含的基类成员变量。

在执行一个派生类的构造函数之前,总是先执行基类的构造函数。派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。

构造函数和析构函数

定义派生类构造函数的一般格式如下:

派生类名::派生类名(参数表):基类名1(基类1初始化参数列表),...基类名n(基类名n初始化参数列表), 成员对象名1(成员对象1初始化参数列表),...成员对象名n(成员对象n初始化参数表) {
    类构造函数函数体
    //其他初始化操作
}

派生类构造函数执行的一般次序如下:

  1. 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
  2. 对派生类新增的成员变量初始化,调用顺序按照它们在类中声明的顺序。
  3. 执行派生类的构造函数体中的内容。

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

复制构造函数

对于一个类,如果程序中没有定义复制构造函数,则编译器会自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,对派生类新增的成员对象执行复制。

如果要为派生类编写复制构造函数,一般也需要为基类相应的复制构造函数传递参数,但并不是必须的。

多重继承的构造函数与析构函数

当创建有多个基类的派生类的对象时,按照类定义中给出的基类的顺序,依次调用它们的构造函数,再调用派生类的构造函数。对象消亡时,按照构造函数调用的次序逆向调用析构函数。

在派生类构造函数执行之前,要先执行基类的构造函数,执行次序依定义派生类时所列基类的次序而定。

类之间的关系

类与类之间的关系

  • 使用已有类编写新的类有两种方式:继承和组合。
    这也形成类和类之间的两种基本关系:继承关系和组合关系。
    (组合广西也就是包含关系)
  • 继承关系也称为is a关系或关系。
  • 组合关系也称为has a关系或关系,变现为封闭类,即一个类以另一个类的对象作为成员变量。

封闭类的派生

如果一个类的成员变量是另一个类的对象,则为封闭类。定义封闭类构造函数的一般形式如下:

类名::类名(行参表):内嵌对象1(性参表),...内嵌对象n(形参表) {
    类体
}

其中,内嵌对象1(性参表),...内嵌对象n(形参表)是初始化列表,其作用是对内嵌对象进行初始化。

互包含关系的派生

在处理相对复杂的问题而需要考虑类的组合时,很可能遇到两个类相互引用的情况,这种情况称为循环依赖。举个🌰:

class A {//类A的定义
public:
    void func(B b);//以类B对象为形参的成员函数
}

class B {//类B的定义
public:
    void method(A a);//以类A对象为形参的成员函数
}

多层次的派生

  • 在C++中,派生是可以多层次的。
    例如,Student类派生了GraduatedStudent类,而后者又可以派生DoctorStudent类等等。
    总之,类A派生类B,类B可以再派生类C,类C又能够派生类D,以此类推。
    在这种情况下,称类A是类B的直接基类,类B是类C的直接基类,类A是类C的间接基类。当然,类A也是类D的间接基类。
    在定义派生类时,只需写直接基类,不需写间接基类。
    派生类沿着类的层次自动向上继承它所有的直接和间接基类的成员。
    在C++中,类之间的继承关系具有传递性
  • 派生类的成员包括派生类自己定义的成员、直接基类中定义的成员及所有间接基类中定义的全部成员。
  • 当生成派生类的对象时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后执行派生类自身的构造函数;
    当派生类对象消亡时,会先执行自身的析构函数,然后自底向上依次执行各个基类的析构函数。

基类与派生类指针的互相转换

在公有派生的情况下,因为派生类对象也是基类对象,所以派生类对象可以赋值给基类对象。

对于指针类型,**可以使用基类指针指向派生类对象,也可以将派生类的指针直接赋值给基类指针。**但即使基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类中没有而仅在派生类中定义的成员函数。