【C++ 程序设计】第 5 章:类的继承与派生

news2024/11/25 18:47:26

目录

一、类的继承与类的派生

(1)继承的概念

(2)派生类的定义与大小

① 派生类的定义

② 派生类的大小

(3)继承关系的特殊性

(4)有继承关系的类之间的访问

(5)protected 访问范围说明符

(6)多重继承 

二、访问控制

(1)公有继承

(2)类型兼容规则

(3)私有继承

(4)保护继承  

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

(1)构造函数和析构函数 

(2)复制构造函数 

(3)多重继承的构造函数与析构函数 

四、类之间的关系

(1)类与类之间的关系   

(2)封闭类的派生  

(3)互包含关系的类 

五、多层次的派生

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




一、类的继承与类的派生

  • 继承和派生是人们认识客观世界的过程。
  • 在程序设计方法中,人们追求代码复用(这是提高软件开发效率的重要手段),将继承和派生用于程序设计方法中,从而有了面向对象程序设计的重要特点。
  • C++ 对代码复用有很强的支持, “继承” 就是支持代码复用的机制之一。
  • 通过已有的类建立新类的过程,叫作类的派生。
  • 原来的类称为基类,也称为父类或一般类;新类称为派生类,也称为子类或特殊类。
  • 派生类派生自基类,或继承于基类,也可以说基类派生了派生类
  • 派生机制是C++语言及面向对象程序设计方法的重要特征之一。
  • 派生类可以再作为基类派生新的派生类,由此基类和派生类的集合称作类继承层次结构。

(1)继承的概念

  • 使用基类派生新类时,除构造函数和析构函数外,基类的所有成员自动成为派生类的成员,包括基类的成员变量和成员函数。
  • 同时,派生类可以增加基类中没有的成员,这同样是指成员变量和成员函数。
  • 可以重新定义或修改基类中已有的成员,包括可以改变基类中成员的访问权限
  • 当然派生类需要定义自己的构造函数和析构函数。
  • 使用基类成员是一个重用的过程,在基类之上进行调整,不论是添加新成员还是改造已有的,都是扩充的过程。
  • 若派生类中定义了一个与基类中同名的成员,则会出现基类与派生类有同名成员的情况,这是允许的。
  • 同名的成员既可以是成员变量,也可以是成员函数。
  • 这种情况下,若在派生类的成员函数中访问这个同名成员,或通过派生类对象访问这个同名成员时,除非特别指明,访问的就是派生类中的成员,这种情况叫 “覆盖” ,即派生类的成员覆盖基类的同名成员。
  • 覆盖也称为重定义或是重写。
  • 对于成员函数来说,派生类既继承了基类的同名成员函数,又在派生类中重写了这个成员函数。
  • 这称为函数重定义,也称为同名隐藏。
  • “隐藏”的意思是指,使用派生类对象调用这个名字的成员函数时,调用的是派生类中定义的成员函数,即隐藏了基类中的成员函数。

(2)派生类的定义与大小

① 派生类的定义

【示例一】 基类与派生类的定义

【示例代码】C++ 类继承示例,其中 DerivedClass 公有继承了 BaseClass :

class BaseClass // 定义基类 BaseClass
{
    int v1; // 基类的私有成员变量 v1
    int v2; // 基类的私有成员变量 v2
};

class DerivedClass : public BaseClass // 定义派生类 DerivedClass,公有继承自 BaseClass
{
    int v3; // 派生类的私有成员变量 v3
};

【代码详解】

  1. BaseClass 类有两个私有成员变量 v1 和 v2 ,这些变量只能被 BaseClass 类的成员函数访问。
  2. DerivedClass 类公有继承了 BaseClass ,意味着 DerivedClass 可以访问 BaseClass 的公有成员和保护成员(如果有的话)。
  3. DerivedClass 类有一个私有成员变量 v3 ,这个变量只能被 DerivedClass 自身的成员函数访问。
  4. 由于 DerivedClass 公有继承了 ​​​​​​​BaseClass ,因此它可以访问 ​​​​​​​BaseClass 的公有成员变量和函数,这个访问权限是通过编译器在编译时生成的指针实现的。

特别注意】

  • 在派生关系中,子类可以使用父类(基类)的公有成员和保护成员,但不能使用父类的私有成员。
  • 在这个示例中,v1 ​​​​​​​和 ​​​​​​​​​​​​​​v2 ​​​​​​​只能被 ​​​​​​​​​​​​​​BaseClass ​​​​​​​的成员函数访问。
  • 而 ​​​​​​​​​​​​​​v3 ​​​​​​​只能被 ​​​​​​​​​​​​​​DerivedClass ​​​​​​​的成员函数访问。
  • 此外,子类也可以在自己的成员函数中重载父类的函数,即在子类中定义一个函数和父类的函数名称一致,但函数体和参数不同,这就是函数重载。
  • 这些都是类继承中很重要的概念。

【示例二】空类也可以作为基类,也就是说,空类可以派生子类

  • 派生类可以改变基类中成员的访问权限

【示例代码】C++ 中的类继承示例,下列语句定义了空基类的派生类:

class emptyClass{ }; //空基类
class subemptyClass : public emptyClass{ }; //派生类

【代码详解】

  • emptyClass ​​​​​​​​​​​​​​是一个空的基类,它没有定义任何成员变量和成员函数。
  • 而 ​​​​​​​​​​​​​​​​​​​​​subemptyClass ​​​​​​​​​​​​​​是一个公有继承自 ​​​​​​​​​​​​​​​​​​​​​emptyClass ​​​​​​​​​​​​​​的派生类,也是一个空类。
  • 在这个定义中,subemptyClass ​​​​​​​​​​​​​​继承了 ​​​​​​​​​​​​​​​​​​​​​emptyClass ​​​​​​​​​​​​​​的公有成员和保护成员(如果有的话),但是由于 ​​​​​​​​​​​​​​​​​​​​​emptyClass ​​​​​​​​​​​​​​本身没有任何属性,因此这个继承没有任何实际意义。

【特别注意】

  • 空类仍然具有 ​​​​​​​​​​​​​​sizeof ​​​​​​​​​​​​​​的值,这是由 ​​​​​​​​​​​​​​C++ ​​​​​​​​​​​​​​标准规定的一个特例。
  • 空类的 ​​​​​​​​​​​​​​sizeof ​​​​​​​​​​​​​​通常是 ​​​​​​​​​​​​​​1 ​​​​​​​​​​​​​​。
  • 因为对于空类,编译器仍然需要为它分配一个唯一的地址。
  • 所以一个空类实际上至少会占用一个字节。

② 派生类的大小

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

【示例】基类与子类占用空间及字节对齐

【示例代码】C++ 中的类继承示例,通过 ​​​​​​​class ​​​​​​​关键字定义了两个类:基类 ​​​​​​​BaseClass ​​​​​​​和派生类 ​​​​​​​DerivedClass ​​​​​​​,再通过输出每个类 ​​​​​​​sizeof 的值,来观察它们占用内存的大小:

#include<iostream>  // 引入iostream标准库
using namespace std; // 使用命名空间std

class BaseClass // 定义基类 BaseClass
{ 
    int v1;      // 基类私有成员变量 v1
    int v2;      // 基类私有成员变量 v2
    char v4;     // 基类私有成员变量 v4

public:         // 基类公有函数部分
    // 基类公有函数 templ,返回值类型为 int
    int templ()   
    {
        // 函数体略
    }
}; 

class DerivedClass : public BaseClass //定义派生类 DerivedClass,继承自 BaseClass
{ 
    int v3;     // 派生类私有成员变量 v3
    int* p;     // 派生类私有指针变量 p

public:         // 派生类公有函数部分
    // 派生类公有函数 temp, 返回值类型为 int
    int temp()
    {
        // 函数体略
    }
};

int main()  // 主函数
{ 
    // 输出"Base="和sizeof(BaseClass)的值
    cout << "Base=" << sizeof(BaseClass) << endl;  // 12,输出结束后换行
    // 输出"Derived="和sizeof(DerivedClass)的值
    cout << "Derived=" << sizeof(DerivedClass) << endl;   // 20,输出结束后换行
    return 0;   // 返回0,即正常退出程序
}

【代码详解】

  1. 首先我们引入了 ​​​​​​​​​​​​​​iostream ​​​​​​​标准库,并使用 ​​​​​​​​​​​​​​using namespace std ​​​​​​​进行命名空间引入。
  2. 接着我们定义了一个基类 ​​​​​​​​​​​​​​BaseClass ​​​​​​​,其中有两个私有成员变量 ​​​​​​​​​​​​​​v1 ​​​​​​​和 ​​​​​​​​​​​​​​v2 ​​​​​​​和一个私有的 ​​​​​​​​​​​​​​char ​​​​​​​类型的变量 ​​​​​​​​​​​​​​v4BaseClass ​​​​​​​类中还定义了一个公有函数 ​​​​​​​templ(),返回类型为int
  3. 接着我们定义了一个派生类 ​​​​​​​​​​​​​​DerivedClassDerivedClass ​​​​​​​继承自 ​​​​​​​​​​​​​​BaseClass,其中有一个私有成员变量 ​​​​​​​​​​​​​​v3 ​​​​​​​和一个私有的整型指针变量 ​​​​​​​​​​​​​​p ​​​​​​​。DerivedClass ​​​​​​​类中也定义了一个公有函数 ​​​​​​​​​​​​​​temp(),返回类型为 ​​​​​​​int
  4. 在 ​​​​​​​​​​​​​​main ​​​​​​​函数中,我们输出了 ​​​​​​​​​​​​​​sizeof(BaseClass) ​​​​​​​和 ​​​​​​​​​​​​​​sizeof(DerivedClass) ​​​​​​​的值,分别为 ​​​​​​​12 ​​​​​​​和 ​​​​​​​20,并在每个输出后换行。
  5. 最后我们返回 ​​​​​​​0 ​​​​​​​,并结束程序的执行。

【特别注意】

  • 由于派生类 ​​​​​​​​​​​​​​​​​​​​​DerivedClass ​​​​​​​​​​​​​​继承了 ​​​​​​​​​​​​​​​​​​​​​BaseClass ​​​​​​​​​​​​​​,因此在派生类中,不仅有自己的成员变量和成员函数,还包含了基类中的成员变量和成员函数。
  • 因此 ​​​​​​​​​​​​​​​​​​​​​sizeof(DerivedClass) ​​​​​​​​​​​​​​的值为 ​​​​​​​​​​​​​​​​​​​​​BaseClass ​​​​​​​​​​​​​​成员变量和 ​​​​​​​​​​​​​​​​​​​​​DerivedClass ​​​​​​​​​​​​​​成员变量的总大小,这是 ​​​​​​​​​​​​​​20 ​​​​​​​​​​​​​​字节(因为在 ​​​​​​​​​​​​​​​​​​​​​Windows ​​​​​​​​​​​​​​系统下指针变量占用 ​​​​​​​​​​​​​​4 ​​​​​​​​​​​​​​字节)。

(3)继承关系的特殊性

  • 如果基类有友元类或友元函数,则其派生类不会因继承关系而也有此友元类或友元函数
  • 如果基类是某类的友元,则这种友元关系是被继承的
  • 即被派生类继承过来的成员函数,如果原来是某类的友元函数,那么它作为派生类
  • 的成员函数仍然是某类的友元函数
  • 总之,基类的友元不一定是派生类的友元;基类的成员函数是某类的友元函数,则其作为派生类继承的成员函数仍是某类的友元函数
  • 如果基类中的成员是静态的,则在其派生类中,被继承的成员也是静态的,即其静态属性随静态成员被继承。
  • 如果基类的静态成员是公有的或是保护的,则它们被其派生类继承为派生类的静态成员。
  • 访问这些成员时,通常用 “<类名>::<成员名>” 的方式引用或调用。
  • 无论有多少个对象被创建,这些成员都只有一个拷贝,它为基类和派生类的所有对象所共享。

【示例一】基类与子类占用空间及字节对齐

【示例代码】C++ 程序示例,展示了如何定义类、继承类、使用成员变量和成员函数,并在主函数中输出类中成员变量所占用的空间大小:

#include <iostream> // 引入头文件 iostream
using namespace std; // 使用命名空间 std

class BaseClass // 定义基类 BaseClass
{ 
    int v1;      // 基类私有成员变量 v1
    int v2;      // 基类私有成员变量 v2
    char v4;     // 基类私有成员变量 v4

public:         // 基类公有函数部分
    // 基类公有函数 templ,返回值类型为 int
    int templ()   
    {
        // 函数体略
    }
}; 

class DerivedClass : public BaseClass // 定义派生类 DerivedClass,继承自 BaseClass
{ 
    int v3;     // 派生类私有成员变量 v3
    int* p;     // 派生类私有指针变量 p

public:         // 派生类公有函数部分
    // 派生类公有函数 temp, 返回值类型为 int
    int temp()
    {
        // 函数体略
    }
};

int main()  //主函数
{ 
    cout << "Base=" << sizeof(BaseClass) << endl;         // 输出 "Base=12"
    cout << "Derived=" << sizeof(DerivedClass) << endl;   // 输出 "Derived=20"
    return 0;   // 返回 0,程序正常退出
}

【代码详解】

  1. 引入头文件 iostream,通过 using namespace std 使用命名空间 std。
  2. 定义了一个基类 BaseClass 和一个派生类 DerivedClass,其中基类有三个私有成员变量 int v1、int v2 和 char v4,一个公有成员函数 int templ();派生类继承了基类,并增加了两个私有成员变量 int v3 和 int* p,以及一个公有成员函数 int temp()。
  3. 主函数中,打印出两个类的大小,即基类 BaseClass 和派生类 DerivedClass 所占用的字节数,分别用关键字 sizeof 获取它们的大小,然后通过 cout 输出各自标签并加上大小信息。
  4. 最后,程序使用返回值 0 的 return 语句结束程序。

【特别注意】

  • 这段代码主要运用了 C++ 面向对象编程的核心思想,即定义类、继承类和使用类中的成员变量和成员函数。
  • 同时,利用 sizeof 运算符获取了类中各个成员变量所占用的空间大小,并使用 cout 操作符将信息输出到控制台上。
  • 这样的代码可以帮助我们更好地掌握类和继承的用法,对于 C++ 面向对象的编程入门是很有帮助的。
  • BaseClass 和 DerivedClass 实际上仅仅定义了类的结构,它们并未实现任何方法,因此在类的定义中,所有函数体后面用的是略字 ...
  • 在 main 函数中,我们使用了 cout 方法输出字符串和变量的值,注意使用了 << 运算符将字符串和变量合并为一条输出语句。于是,最后程序输出了两行文字,分别是 Base=12 和 Derived=20

【示例二】静态成员变量和类的继承

【示例代码】C++ 程序示例,定义了一个基类 Base,以及一个继承自 Base 的派生类 Derived。同时,代码还定义了一个名为 staV 的静态成员变量,用于记录 Base 和 Derived 类的实例个数:

#include <iostream> // 引用标准输入输出库 iostream
using namespace std; // 使用命名空间 std

class Base // 定义基类 Base
{
private: // 声明私有变量
    float x; // 基类私有成员变量 x

public: // 声明公有函数和变量
    static int staV; // 基类公有静态成员变量 staV
    Base() // 声明默认构造函数
    { 
        staV++; // 每次实例化对象时,静态成员变量 staV 值加1
    }
};

int Base::staV = 0; // 初始化静态成员变量 staV

class Derived : public Base // 定义派生类 Derived,继承自 Base
{
private: // 声明私有变量
    float y; // 派生类私有成员变量 y

public: // 声明公有函数和变量
    Derived() // 声明默认构造函数
    { 
        staV++; // 每次实例化对象时,静态成员变量 staV 值加1
    }
};

int main() // 主函数
{
    Base a; // 实例化一个 Base 类对象 a
    cout << a.staV << endl; // 输出 Base 类对象 a 的 staV 值,输出 1
    Derived d; // 实例化一个 Derived 类对象 d
    cout << d.staV << endl; // 输出 Derived 类对象 d 的 staV 值,输出 3
    return 0; // 返回值 0,表示程序正常退出
}

【代码详解】

  1. #include <iostream> 引入标准库 iostream,以便使用输入输出相关的对象和函数。
  2. using namespace std; 指示使用命名空间 std,方便调用 std 命名空间下的函数和对象。
  3. class Base {...}; 定义类 Base,其中包含了一个名为 x 的私有变量和一个名为 staV 的公有静态成员变量。在构造函数中,每次实例化 Base 类的对象时,都会让 staV 自加 1。
  4. int Base::staV = 0; 定义 Base 类中的静态成员变量 staV 并初始化为 0,用于记录 Base 和 Derived 类的实例个数。
  5. class Derived: public Base {...}; 定义继承类 Derived,基类为 BaseDerived 类中包含了一个名为 y 的私有变量,在构造函数中实例化 Derived 类的对象时,都会让 staV 自加 1。
  6. int main() {...} 主函数入口,包含以下语句:
  • Base a; 定义 Base 类的对象 a,由于此时 staV 的值为 1,因此会让 staV 增加 1。
  • cout<< a.staV << endl; 输出调用 a 的 staV,此时输出为 1,因为此时只有 a 一个 Base 类对象。
  • Derived d; 定义 Derived 类的对象 d,由于此时 staV 的值为 2,因此会让 staV 增加 1,总计增加了 3 次。
  • cout<< d.staV << endl; 输出调用 d 的 staV,此时输出为 3,因为有 3 个类 Base 和 Derived 的对象。

【特别注意】

  • 这段代码的主要目的是演示静态成员变量和类的继承。
  • 具体实现是定义一个名为 staV 的静态成员变量,在实例化对象时不管是基类还是派生类,都会计数器加 1。
  • 因此可以通过打印 staV 的值来查看当前有多少个类 Base 和 Derived 的对象已经被实例化。
  • 静态成员变量属于类而非对象,它与类的特定实例无关,这就意味着可以通过类名和作用域解析运算符 :: 来调用它,而不需要实例化对象。
  • 在类定义时,可以通过在静态成员变量前面加上 static 来使其变为静态成员变量,它将被类和类的所有实例所共享。

(4)有继承关系的类之间的访问

  • 派生类和基类中都可以定义自己的成员变量和成员函数,派生类中的成员函数可以访问基类中的公有成员变量,但不能直接访问基类中的私有成员变量。
  • 也就是说,不能在派生类的函数中,使用 “基类对象名.基类私有成员函数(实参)”,或是 “基类
  • 对象名.基类私有成员变量” ,或是 “基类名::基类私有成员”的形式访问基类中的私有成员。
  • 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。
  • 二者的作用范围不同,是相互包含的两个层,派生类在内层,基类在外层。
  • 如果派生类声明了一个和基类某个成员同名的新成员,派生的新成员就隐藏了外层同名成员,直接使用成员名只能访问到派生类的成员。
  • 如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。
  • 如果要访问被隐藏的成员,就需要使用基类名和作用域分辨符来限定。 

【示例】继承和访问控制

【示例代码】C++ 程序示例,主要是对继承和访问控制进行讲解的,其中基类 CB 包含一个成员变量 ​​​​​​​a 和一个成员函数 ​​​​​​​showa(),派生类 ​​​​​​​CD 公有继承自 ​​​​​​​CB,同时又定义了一个同名的成员变量 ​​​​​​​a 和一个同名的成员函数 ​​​​​​​showa(),并新增了一个成员函数 ​​​​​​​print2a() 来打印出基类和派生类中的同名变量:

#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; 
    CD(int x,int y):CB(x) 
    { 
        a=y;
    }
    void showa() 
    { 
        cout << "Class CD--a=" << a << endl; 
    }
    void print2a()
    { 
        cout << "a=" << a << endl; 
        cout << "CB::a=" << CB::a << endl; 
    }
};

int main()
{
    CB CBobj(12);  // 创建基类对象CBobj,调用基类的构造函数CB(int x),其中x为12,用于初始化a的值
    CBobj.showa();  // 调用CB类的成员函数showa(),输出CBobj的成员变量a
    CD CDobj(48, 999);  // 创建派生类对象CDobj,传入的参数用于初始化基类的a和派生类的a
    CDobj.showa();  // 调用CD类的成员函数showa(),输出CDobj的成员变量a
    CDobj.CB::showa();  // 调用CB类的成员函数showa(),输出CBobj的成员变量a,使用作用域限定符
    cout << "CDobj.a=" << CDobj.a << endl;  // 输出CDobj的a
    cout << "CDobj.CB::a=" << CDobj.CB::a << endl;  // 调用CB类的成员变量a,使用作用域限定符
}

【代码详解】

  1. 第 3-12 行:声明并定义基类 CB,有一个公有成员变量 a 和一个构造函数 CB(int x),它接受一个整数参数x并将其赋值到类成员变量 a 上,还有一个公有成员函数showa(),用于输出成员变量 a 的值。

  2. 第 14-23 行:声明并定义派生类 CD,它继承了基类 CB 的所有成员和成员函数。派生类中也有一个成员变量 a ,它与基类成员变量的 a 同名。在派生类构造函数中,用参数 x 来初始化基类成员变量 a ,用参数 y 来初始化派生类中被隐藏的同名成员变量a。另外,由于派生类中已经有了名为 ​​​​​​​showa() 的成员函数,这里通过重写的方式覆盖了基类中的同名成员函数 ​​​​​​​showa(),并用于输出派生类中的成员变量 ​​​​​​​a 的值,同时新增了一个成员函数 print2a(),通过作用域限定符分别输出基类和派生类中的同名成员变量。

  3. 第 25-35 行:主函数 main(),在其中定义了基类对象 CBobj 和派生类对象 ​​​​​​​CDobj,分别使用传入的参数进行初始化。然后,依次调用基类 ​​​​​​​CB 和派生类 ​​​​​​​CD 的各种成员函数,输出基类和派生类的各种成员变量。

【特别注意】

  • 这段代码主要考察了继承和访问控制的相关知识点。
  • 在派生类中,同名成员变量或成员函数会将基类中的同名成员隐藏,此时可以通过使用作用域限定符 (::) 指明访问基类中的同名成员。
  • 此外,在派生类中也可以通过重写来覆盖基类的成员函数。
  • 该代码没有考虑到派生类和基类中同名成员变量的命名冲突和覆盖带来的问题,在实际编程中应特别注意这一点。
  • 另外,派生类中对于与基类同名的成员变量的初始化建议通过构造函数的初始化列表来完成,避免出现不必要的错误和混乱。

【执行结果】 该段代码的输出结果是符合预期的:

  • 第 1 行和第 3 行:分别是输出基类对象 CBobj 和派生类对象 ​​​​​​​CDobj 的成员变量 ​​​​​​​a ,分别是 12 和 999,符合预期;
  • 第 2 行:是输出派生类对象 CDobj 的同名成员变量 ​​​​​​​a,值为 999,符合预期;
  • 第 4 行:是调用派生类对象 CDobj 的基类 ​​​​​​​CB 中 ​​​​​​​a 的值,值为 48,符合预期;
  • 第 5 行和第 6 行:分别输出派生类对象 CDobj 的同名成员变量 a 和其在基类 CB 中的同名成员变量 a,分别是 999 和 48,符合预期。
Class CB--a=12
Class CD--a=999
Class CB--a=48
CDobj.a=999
CDobj.CB::a=48

(5)protected 访问范围说明符

  • 定义类时,类成员可以使用 protected 访问范围说明符进行修饰,从而成为 “保护成员” 。
  • 保护成员的访问范围比私有成员的访问范围大,能访问私有成员的地方都能访问保护成员。此外,基类中的保护成员可以在派生类的成员函数中被访问。
  • 在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。
  • 将基类中成员变量的访问方式修改为 protected 后,在派生类中可以直接访问。

(6)多重继承 

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

【格式】一个类从多个基类派生的一般格式如下:

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

【示例】多重继承和名称空间限定符的使用

【示例代码】C++ 程序示例,演示了 C++ 中的多重继承和名称空间限定符的使用:

#include<iostream>
using namespace std;

class CB1
{
public:
    int a; // 基类成员变量a
    CB1(int x)
    {
        a = x;
    }
    void showa() // 基类成员函数showa
    {
        cout << "Class CB1==>a=" << a << endl;
    }
};

class CB2
{
public:
    int a; // 基类成员变量a
    CB2(int x)
    {
        a = x;
    }
    void showa() // 基类成员函数showa
    {
        cout << "Class CB2==>a=" << a << endl;
    }
};

class CD: public CB1, public CB2
{
public:
    int a; // 派生类成员变量a,与两个基类成员变量a重名
    CD(int x, int y, int z): CB1(x), CB2(y)
    {
        a = z;
    }
    void showa() // 派生类成员函数showa,与两个基类成员函数showa重名
    {
        cout << "Class CD==>a=" << a << endl;
    }
    void print3a()
    {
        cout << "a=" << a << endl; // 访问当前类成员变量a
        cout << "CB1::a=" << CB1::a << endl; // 访问基类CB1的成员变量a
        cout << "CB2::a=" << CB2::a << endl; // 访问基类CB2的成员变量a
    }
};

int main()
{
    CB1 CB1obj(11);
    CB1obj.showa(); // 输出 Class CB1==>a=11
    CD CDobj(101, 202, 909);
    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
}

【代码详解】

  1. CB1 类和 CB2 类都有成员变量 a 和成员函数 showa。
  2. CD 类公有继承了 CB1 类和 CB2 类,并且自己也定义了成员变量 a 和成员函数showa,与两个基类重名。
  3. 在 CD 类中,成员函数 print3a() 使用名称空间限定符 (::) 访问了当前类和基类 CB1、CB2 的成员变量 a ,以及当前类和基类 CB1 的成员函数 showa。
  4. 在 main 函数中,首先创建了一个 CB1 类对象 CB1obj 来测试基类成员函数的访问。然后创建了一个 CD 类对象 CDobj ,并分别测试了当前类的成员函数 showa() 和基类CB1 的成员函数 showa() 的访问,以及当前类和基类的成员变量 a 的访问。

【特别注意】

  • 当访问当前类和基类的成员变量 a 时需要使用名称空间限定符,以区分它们

【执行结果】

  • 首先输出了 CB1obj.showa() 中打印出的字符串 “Class CB1==>a=11”,接着输出了CDobj.showa() 中打印出的字符串 “Class CD==>a=909”。
  • 这表明派生类 CD 的成员函数 showa() 的优先级高于两个基类 CB1 和 CB2 的同名成员函数showa()。
  • 接着输出了 CDobj.CB1::showa() 中打印出的字符串 “Class CB1==>a=101”,这表明可以使用名称空间限定符访问基类的成员函数。
  • 最后输出了 CDobj.a 和 CDobj.CB2::a 的值,分别为 909 和 202 。
  • 这表明使用名称空间限定符可以访问当前类和基类的成员变量。
Class CB1==>a=11
Class CD==>a=909
Class CB1==>a=101
CDobj.a=909
CDobj.CB2::a=202


二、访问控制

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

(1)公有继承

 


(2)类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,也称为 赋值兼容规则
⚫ 在公有派生的情况下,有以下3条类型兼容规则:
  1. 派生类的对象可以赋值给基类对象。
  2. 派生类对象可以用来初始化基类引用。
  3. 派生类对象的地址可以赋值给基类指针,即派生类的指针可以赋值给基类的指针。
⚫ 上述 3 条规则反过来是不成立的:
  • 例如,不能把基类对象赋值给派生类对象。
  • 在进行替代之后,派生类对象就可以作为基类的对象使用了,但只能使用从基类继承的成员。
  1. 如果类 为基类,类 为类 的公有派生类,则类 中包含了基类 中除构造函数、析构函数之外的所有成员。
  2. 这时,根据类型兼容规则,在基类 B 的对象可以出现的任何地方,都可以用派生类 D 的对象来替代。
  • 假设有以下的声明:
class B{…}
class D : public B{…}
B b1, *pb1;
D d1;
  • 这时,派生类对象可以隐含转换为基类对象,即用派生类对象中从基类继承来的成员变量的值,逐个为基类对象的成员变量的值进行赋值。
b1=d1;
  • 派生类的对象也可以用来初始化基类对象的引用,即
B &rb=d1;
  • 派生类对象的地址可以隐含转换为指向基类的指针,即派生类对象的地址赋给基类指针:
pb1=&d1;
  • 由于类型兼容规则的引入,对于基类及其公有派生类的对象,可以使用相同的函数统一进行处理。因为当函数的形参为基类的对象(或引用、指针)时,实参可以是派生类的对象(或指针),从而没有必要为每一个类设计单独的模块,大大提高了程序的效率。

【示例一】类的公有派生和对象的赋值

【示例代码】C++ 程序示例,演示了 C++ 中的类的公有派生和对象的赋值操作:

#include <iostream>
using namespace std;

class A
{
    int an; // 成员变量

public:
    A() {} // 默认构造函数
    A(int n) // 带参数构造函数
    {
        an = n;
    }
};

class B : public A // 公有派生
{
    int bn; // 成员变量

public:
    B(int n) : A(2 * n) // 构造函数初始化表,初始化基类的数据成员an
    {
        bn = n;
    }
};

int main()
{
    A a(10); // 创建基类A对象a,传入参数10
    B b(20); // 创建派生类B对象b,传入参数20
    a = b; // 派生类对象b的值赋给基类对象a
    return 0;
}

【代码详解】

  1. 定义了基类 A 和派生类 B。

  2. 基类 A 包括一个私有成员变量 an,一个默认构造函数和一个带参数构造函数。其中,带参数构造函数会将参数 n 赋值给成员变量 an。

  3. 派生类 B 公有派生自基类 A,包括一个私有成员变量 bn 和一个构造函数。构造函数使用初始化表的方式,调用基类 A 的带参数构造函数,并将2 * n的值传递给它来初始化基类的数据成员 an。同时,构造函数也将参数n的值赋值给成员变量bn。

  4. 在 main 函数中,创建了 A 类和 B 类的对象 a 和 b,并分别传递参数10和20进入对象的构造函数。因为派生类 B 公有派生自基类 A,所以可以将 B 类型的对象赋给 A 类型的对象。赋值语句 a = b 可以将派生类对象 b 的值复制到基类对象 a 中。

  5. 最后,main 函数返回0,结束程序。

【执行结果】

  • 这段代码没有任何输出,因此没有执行结果输出。

【示例二】类的公有派生和函数重载、以及派生类对象赋值给基类对象

【示例代码】C++ 程序示例,演示了 C++ 中的类的公有派生和函数重载、以及派生类对象赋值给基类对象的操作:

#include <iostream>
using namespace std;

class A
{
    int an; // 成员变量

public:
    A() {} // 默认构造函数
    A(int n) // 带参数构造函数
    {
        an = n;
    }
    void print() // 输出函数
    {
        cout << "A的对象:";
        cout << "an:" << an;
    }
    void print(int k) // 重载的输出函数
    {
        cout << "an:" << an;
    }
};

class B : public A // 公有派生
{
    int bn; // 成员变量

public:
    B(int n) : A(2 * n) // 构造函数初始化表,初始化基类的数据成员an
    {
        bn = n;
    }
    void print()
    {
        cout << "\nB的对象:";
        A::print(1); //调用基类A的print函数
        cout << ",bn=" << bn << endl;
    }
};

int main()
{
    A a(10); // 创建基类A对象a,传入参数10
    B b(20); // 创建派生类B对象b,传入参数20
    a.print(); // 调用基类 A 的输出函数
    b.print(); // 调用派生类 B 的输出函数
    a = b; // 派生类对象b的值赋给基类对象a
    a.print(); // 调用基类 A 的输出函数
    b.print(); // 调用派生类 B 的输出函数
    return 0;
}

【代码详解】

  1. 定义了基类 A 和派生类 B。

  2. 基类 A 包括一个私有成员变量 an,一个默认构造函数和一个带参数构造函数。其中,带参数构造函数会将参数 n 赋值给成员变量 an。同时,类 A 还包括一个输出函数 print() 和一个重载的输出函数 print(int k)。

  3. 派生类 B 公有派生自基类 A,包括一个私有成员变量 bn 和一个构造函数。构造函数使用初始化表的方式,调用基类 A 的带参数构造函数,并将2 * n的值传递给它来初始化基类的数据成员 an。同时,类 B 还包括一个输出函数 print(),在其中调用了基类 A 的输出函数 print()。

  4. 在 main 函数中,创建了 A 类和 B 类的对象 a 和 b,并分别传递参数10和20进入对象的构造函数。a.print() 输出基类对象 a 中的成员变量 an,b.print() 输出派生类对象 b 中的成员变量 an 和 bn。

  5. 执行语句 a = b,将派生类对象 b 复制到基类对象 a 中,由于派生类继承了基类的所有成员,因此成员变量 an 得到了正确的赋值,但是成员变量 bn 的值没有被传递。

  6. a.print() 输出赋值之后基类对象 a 中的成员变量 an 的值,b.print() 输出派生类对象 b 中的成员变量 an 和 bn。

【执行结果】

  • 从结果可以看出,基类对象输出了自己的成员变量 an 的值;
  • 派生类对象输出了自身的成员变量 an 和 bn 的值;
  • 派生类对象赋值给基类对象后,基类对象中的 an 调整为了来自派生类对象的值,但是没有 bn 值。
A的对象:an:10
B的对象:A的对象:an:40,bn=20

A的对象:an:40
B的对象:A的对象:an:40,bn=20

(3)私有继承


(4)保护继承  

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


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

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

(1)构造函数和析构函数 

定义派生类构造函数的一般格式如下:
派生类名::派生类名(参数表):基类名1(基类1 初始化参数表),…,基类名m(基类
m初始化参数表),成员对象名1(成员对象1 初始化参数表),…,成员对象名n(成员
对象n 初始化参数表)
{
    类构造函数函数体  //其他初始化操作
}
派生类构造函数执行的一般次序 如下:
  1. 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
  2. 对派生类新增的成员变量初始化,调用顺序按照它们在类中声明的顺序。
  3. 执行派生类的构造函数体中的内容。
构造函数初始化列表中基类名、对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的,无论它们的顺序怎样安排,基类构造函数的调用和各个成员变量的初始化顺序都是确定的。

【示例一】类的继承

【示例代码】C++ 类的继承示例,它定义了一个基类 BaseClass,一个公有派生的派生类 DerivedClass,以及一个主函数,主函数中创建了两个对象,一个是 BaseClass 类的对象 baseCla,另一个是 DerivedClass 类的对象 derivedCla。我们可以看到,它们分别都调用了不同的构造函数:

#include<iostream>
using namespace std;

class BaseClass // 基类
{
protected:
    int v1, v2;

public:
    BaseClass(); // 默认构造函数
    BaseClass(int, int); // 带参数构造函数
    ~BaseClass(); // 析构函数
};

BaseClass::BaseClass()
{
    cout << "BaseClass 无参构造函数" << endl;
}

BaseClass::BaseClass(int m, int n)
{
    v1 = m;
    v2 = n;
    cout << "BaseClass 2个参数构造函数" << endl;
}

BaseClass::~BaseClass()
{
    cout << "BaseClass 析构函数" << endl;
}

class DerivedClass : public BaseClass // 公有派生的派生类
{
    int v3;

public:
    DerivedClass(); // 默认构造函数
    DerivedClass(int); // 带1个参数构造函数
    DerivedClass(int, int, int); // 带3个参数构造函数
    ~DerivedClass(); // 析构函数
};

DerivedClass::DerivedClass()
{
    cout << "DerivedClass 无参构造函数" << endl;
}

DerivedClass::DerivedClass(int k) : v3(k)
{
    cout << "DerivedClass 带1个参数构造函数" << endl;
}

DerivedClass::DerivedClass(int m, int n, int k) : BaseClass(m, n), v3(k)
{
    cout << "DerivedClass 带3个参数构造函数" << endl;
}

DerivedClass::~DerivedClass()
{
    cout << "DerivedClass 析构函数" << endl;
}

int main()
{
    cout << "无参对象的创建" << endl;
    BaseClass baseCla; // 创建一个基类对象,调用默认构造函数
    DerivedClass derivedCla; // 创建一个派生类对象,调用默认构造函数
    return 0;
}

【代码详解】

  1. 这两行代码导入了 iostream 库,使得我们可以在程序中进行输入输出操作:

    #include<iostream>
    using namespace std;
  2. 这里定义了一个基类 BaseClass,包含了两个 protected 成员变量 v1 和 v2,以及三个访问说明符函数:一个默认构造函数 BaseClass(),一个带参数构造函数 BaseClass(int, int),一个析构函数 ~BaseClass():

    class BaseClass // 基类
    {
    protected:
        int v1, v2;
    
    public:
        BaseClass(); // 默认构造函数
        BaseClass(int, int); // 带参数构造函数
        ~BaseClass(); // 析构函数
    };
  3. 此处为默认构造函数,实现为输出一行字符串,表示调用了该构造函数:

    BaseClass::BaseClass()
    {
        cout << "BaseClass 无参构造函数" << endl;
    }
  4. 此处为带参数构造函数,它有两个参数 m 和 n,将这两个参数赋值给基类的成员变量 v1 和 v2,然后输出一行字符串,表示调用了该构造函数:

    BaseClass::BaseClass(int m, int n)
    {
        v1 = m;
        v2 = n;
        cout << "BaseClass 2个参数构造函数" << endl;
    }
  5. 此处为析构函数,它就是在对象销毁时执行的函数。这里实现为输出一行字符串,表示调用了该析构函数:

    BaseClass::~BaseClass()
    {
        cout << "BaseClass 析构函数" << endl;
    }
  6. 这里定义了一个公有派生的派生类 DerivedClass,它从基类 BaseClass 公开继承。同时,DerivedClass 类有一个私有成员 v3,三个构造函数和一个析构函数:

    class DerivedClass : public BaseClass // 公有派生的派生类
    {
        int v3;
    
    public:
        DerivedClass(); // 默认构造函数
        DerivedClass(int); // 带1个参数构造函数
        DerivedClass(int, int, int); // 带3个参数构造函数
        ~DerivedClass(); // 析构函数
    };
  7. 此处为默认构造函数,与基类的默认构造函数类似,输出一行字符串,表示调用了该构造函数:

    DerivedClass::DerivedClass()
    {
        cout << "DerivedClass 无参构造函数" << endl;
    }
  8. 这里为带 1 个参数的构造函数,它有一个参数 k,用于初始化派生类自己新增的私有成员 v3。在构造函数中,输出一行字符串,表示调用了该构造函数:

    DerivedClass::DerivedClass(int k) : v3(k)
    {
        cout << "DerivedClass 带1个参数构造函数" << endl;
    }
  9. 这里为带 3 个参数的构造函数,它有三个参数 mn 和 k,其中 m 和 n 用于初始化从基类继承而来的成员变量 v1 和 v2,而 k 用于初始化派生类新增的私有成员变量 v3。在构造函数中,输出一行字符串,表示调用了该构造函数:

    DerivedClass::DerivedClass(int m, int n, int k) : BaseClass(m, n), v3(k)
    {
        cout << "DerivedClass 带3个参数构造函数" << endl;
    }
  10. 与基类的析构函数类似,此处为派生类的析构函数。在对象销毁时执行,输出一行字符串,表示调用了该析构函数:

    DerivedClass::~DerivedClass()
    {
        cout << "DerivedClass 析构函数" << endl;
    }
  11. 主函数中,输出一行字符串,表示将创建无参对象。接着,创建了 BaseClass 类和 DerivedClass 类的对象。由于它们都是无参对象,因此分别调用了两个类的默认构造函数。程序执行完毕后,返回 0:

    int main()
    {
        cout << "无参对象的创建" << endl;
        BaseClass baseCla; // 创建一个基类对象,调用默认构造函数
        DerivedClass derivedCla; // 创建一个派生类对象,调用默认构造函数
        return 0;
    }

【执行结果】

  • 首先输出了程序中的字符串 “无参对象的创建”,然后按照创建的先后顺序调用了两个类的构造函数,接着按照销毁的逆序调用了两个类的析构函数。
  • 因此,输出结果中先输出了 DerivedClass 类的析构函数,再输出了 BaseClass 类的析构函数。
无参对象的创建
BaseClass 无参构造函数
DerivedClass 无参构造函数
DerivedClass 析构函数
BaseClass 析构函数

【示例二】类的继承

【示例代码】C++ 类的继承示例,其中定义了两个类 Base 和 Derived

#include<iostream>
using namespace std;

class Base
{
private:
    int Y;
public:
    // 默认构造函数
    Base(int y = 0)
    {
        Y = y;
        cout << "Base(" << y << ")" << endl;
    }
    // 析构函数
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    // 成员函数
    void print()
    {
        cout << Y << " ";
    }
};

class Derived : public Base
{
private:
    int Z;
public:
    // 带参数构造函数
    Derived(int y, int z) : Base(y)
    {
        Z = z;
        cout << "Derived(" << y << "," << z << ")" << endl;
    }
    // 析构函数
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
    // 成员函数
    void print()
    {
        Base::print();
        cout << Z << endl;
    }
};

int main()
{
    Derived d(10, 20);
    d.print();
    return 0;
}

【代码详解】

  1. Base 类有一个私有成员变量 Y,一个默认构造函数,一个析构函数和一个用于输出成员变量 Y 值的成员函数。
  2. 在主函数中,首先创建了一个 Derived 对象 d,该对象以参数 10 和 20 调用了 Derived 类的构造函数,初始化了继承自 Base 类的成员变量 Y 和新增的私有成员变量 Z。接着,调用 d.print() 函数输出了 Y 和 Z 的值。
  3. Derived 类从 Base 类公有继承,即 Derived 类包含了 Base 类的全部成员,同时还有一个新增的私有成员 ZDerived 类有一个带有两个参数的构造函数,可以用于初始化从 Base 类继承而来的成员变量 Y 和私有成员变量 Z,以及一个析构函数和一个用于输出 Y 和 Z 值的成员函数。

  4. 最后,程序返回 0,结束执行。

【执行结果】

  • 程序按照对象的创建和销毁顺序输出了调用的构造函数和析构函数,我们可以发现程序中 Base 类的构造函数和析构函数的调用顺序与继承顺序完全相反,而 Derived 类则是按照构造函数和析构函数定义顺序的相反顺序依次调用的。
  • 此外,在 Derived 类的成员函数 print() 中,通过调用 Base::print() 函数输出了继承自 Base 类的成员变量 Y,然后输出了私有成员变量 Z:
Base(10)
Derived(10,20)
10 20
~Derived()
~Base()

(2)复制构造函数 

  • 对于一个类,如果程序中没有定义复制构造函数,则编译器会自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,对派生类新增的成员对象执行复制。
  • 如果要为派生类编写复制构造函数,一般也需要为基类相应的复制构造函数传递参数,但并不是必须的。

【示例一】构造函数和拷贝构造函数的使用、继承和重写函数

【示例代码】主要演示了 C++ 中的构造函数和拷贝构造函数的使用,以及继承和重写函数的相关操作:

#include<iostream>
using namespace std;

class A
{
public:
    // 默认构造函数
    A()
    {
        i = 100;
        cout << "类A默认构造函数" << endl;
    }
    // 复制构造函数
    A(const A& s)
    {
        i = s.i;
        cout << "类A复制构造函数" << endl;
    }
    int getValue(); // 取值
    void setValue(int); // 设置值

private:
    int i;
};

int A::getValue()
{
    return i;
}

void A::setValue(int k)
{
    i = k;
}

class B : public A // 公有派生类
{
private:
    float f;
public:
    B()
    {
        f = 20.1;
        cout << "类B默认构造函数" << endl;
    }
    B(const B& v) : A(v), f(v.f)
    {
        cout << "类B复制构造函数" << endl;
    }
    // 重写基类函数,改变了返回值类型
    float getValue();
    int getValue1()
    {
        return A::getValue();
    }
};

float B::getValue()
{
    return f;
}

int main()
{
    A a; // 调用类A默认构造函数
    B b; // 调用类A默认构造函数、类B默认构造函数
    B bb(b); // 调用类A复制构造函数、类B复制构造函数
    return 0;
}

【代码详解】

  1. #include<iostream>:包含头文件 iostream,用于进行输入输出操作。

  2. using namespace std:使用命名空间 std,避免每次都要打 std::

  3. class A:定义了一个类 A,其中包含一个默认构造函数和一个复制构造函数。同时,类 A 中还定义了一个 getValue() 函数和一个 setValue(int) 函数。

  4. class B : public A:定义一个公有派生类 B,它继承于类 A,并包含了一个默认构造函数和一个复制构造函数。类 B 中还定义了一个重写基类函数 getValue() 和一个函数 getValue1() 调用基类的 getValue()

  5. int A::getValue():定义了类 A 的 getValue() 函数,其返回值为类 A 的私有变量 i

  6. void A::setValue(int k):定义了类 A 的 setValue(int) 函数,用于设置类 A 的私有变量 i

  7. B::B():定义了类 B 的默认构造函数,其中初始化成员变量 f = 20.1,并输出字符串 “类B默认构造函数”。

  8. B::B(const B& v):定义了类 B 的复制构造函数,它会先调用基类的复制构造函数 A(v),再将类 B 的成员变量 f 赋值为 v.f,并输出字符串 “类B复制构造函数”。

  9. float B::getValue():定义了类 B 的重写函数 getValue(),它的返回值是类 B 的私有变量 f

  10. int main():主函数开始。

  11. A a:定义对象 a,调用了类 A 的默认构造函数。

  12. B b:定义对象 b,调用了类 A 的默认构造函数和类 B 的默认构造函数。

  13. B bb(b):定义对象 bb,将 b 作为参数调用类 B 的复制构造函数。

  14. return 0:程序结束,返回值为 0。

【执行结果】

  • 首先执行了 main() 函数,创建了一个 A 类对象 a 和一个 B 类对象 b
  • 创建 a 对象时,调用了 A 的默认构造函数;而创建 b 对象时,先调用了 A 的默认构造函数,再调用 B 的默认构造函数。
  • 接着,创建了一个 B 类对象 bb,并以 b 为参数进行了初始化,这里调用了 A 的复制构造函数和 B 的复制构造函数。
  • 最后,程序返回 0,结束执行。
  • 可以看到,在类 B 的复制构造函数中,先调用了 A 的复制构造函数,再初始化了 B 的私有成员变量 f
  • 需要注意的是,B 类重写了 A 类的成员函数 getValue,因此 B 类对象的 getValue 函数返回的是 f 的值,而不是 A 类对象的 i 值:
类A默认构造函数
类A默认构造函数
类B默认构造函数
类A复制构造函数
类B复制构造函数

【示例二】派生类调用基类复制构造函数以及重载基类的赋值运算符的使用

【示例代码】主要演示了 C++ 中的派生类调用基类复制构造函数以及重载基类的赋值运算符的使用:

#include<iostream>
using namespace std;

class CBase
{
public:
    CBase() {} // 默认构造函数
    // 复制构造函数
    CBase(const CBase& c)
    {
        cout << "CBase::复制构造函数" << endl;
    }
    // 重载赋值运算符
    CBase& operator=(const CBase &b)
    {
        cout << "CBase::operator=" << endl;
        return *this;
    }
};

class CDerived : public CBase
{
public:
    CDerived()
    {
        cout << "CDerived::复制构造函数" << endl;
    }
};

int main()
{
    CDerived d1, d2;
    CDerived d3(d1); // d3 初始化过程中会调用类 CBase 的复制构造函数
    d2 = d1; // 会调用类 CBase 重载的"="运算符
    return 0;
}

【代码详解】

  1. #include<iostream>:包含头文件 iostream,用于进行输入输出操作。

  2. using namespace std:使用命名空间 std,避免每次都要打 std::

  3. class CBase:定义了一个类 CBase,其中包含一个默认构造函数,一个复制构造函数和一个重载赋值运算符。

  4. CBase(const CBase& c):定义了类 CBase 的复制构造函数,用于在复制对象时调用,输出字符串 “CBase::复制构造函数”。

  5. CBase& operator=(const CBase &b):定义了类 CBase 的重载赋值运算符,输出字符串 “CBase::operator=”,并返回一个 CBase 对象的引用。

  6. class CDerived : public CBase:定义了一个公有派生类 CDerived,它继承于类 CBase,并包含了一个默认构造函数。

  7. CDerived():定义了类 CDerived 的默认构造函数,输出字符串 “CDerived::复制构造函数”。

  8. main():主函数开始。

  9. CDerived d1, d2:定义对象 d1 和 d2,并调用类 CDerived 的默认构造函数。

  10. CDerived d3(d1):定义对象 d3,并将 d1 作为参数,初始化过程中会调用类 CBase 的复制构造函数。

  11. d2 = d1:将对象 d1 复制给对象 d2,会调用类 CBase 重载的"="运算符。

  12. return 0:程序结束,返回值为 0。

【执行结果】

  • 在 main() 函数中,首先创建了两个 CDerived 类对象 d1 和 d2,分别调用了 CDerived 类的构造函数 CDerived::复制构造函数 完成对象的初始化。
  • 接着,通过 CDerived d3(d1) 方式创建了 d3 对象,实际上会先调用 CBase 的复制构造函数 CBase::复制构造函数,然后调用 CDerived 的构造函数 CDerived::复制构造函数,完成对象的初始化并分配内存。
  • 需要注意的是,注意到 d1 对象的引用限定了 CBase 类的一些成员,CDerived 类利用这些成员在 d3 对象中生成了其基类部分的拷贝。
  • 所以,当派生类对象作为参数进行复制构造函数或赋值运算时,其基类部分也要进行复制或赋值(因为基类部分在内存中的位置不同)。
  • 最后,通过 d2 = d1 方式实现了对象 d2 初始化,会先调用 CBase 类重载的赋值运算符 CBase::operator=,完成对象的初始化。
CDerived::复制构造函数
CBase::复制构造函数
CBase::operator=

(3)多重继承的构造函数与析构函数 

  • 当创建有多个基类的派生类的对象时,按照类定义中给出的基类的顺序,依次调用它们的构造函数,再调用派生类的构造函数。
  • 对象消亡时,按照构造函数调用的次序的逆,调用析构函数。
  • 在派生类构造函数执行之前,要先执行两个基类的构造函数,执行次序依定义派生类DerivedClass时所列基类的次序而定。

【示例】 定义派生类时最前面的语句是:

class DerivedClass : public BaseClass1, public BaseClass2
所以,先执行基类 BaseClass1 的构造函数,再执行基类 BaSeClass2 的构造函数,然后执行派生类 DerivedClass 的构造函数。


四、类之间的关系

(1)类与类之间的关系   

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

(2)封闭类的派生  

  • 如果一个类的成员变量是另一个类的对象,则为封闭类。
  • 定义封闭类构造函数的一般形式如下:
    类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),…
    {
        类体
    }
  • 其中, “内嵌对象1(形参表),内嵌对象2(形参表),…”是初始化列表,其作用是对内嵌对象进行初始化。

(3)互包含关系的类 

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

【示例】互包含关系

【示例代码】C++ 例子中,类 classA 和类 classB 互相包含了对方,这种情况下,通常需要使用前置声明来解决:

// 前置声明 classB,使得在定义 classA 时可以使用 classB 类型的参数
class classB;

// 定义类 classA
class classA
{
public:
    // 定义类中的一个函数 f,它的参数是类 B 对象 b,无返回值
    void f(classB b);
};

// 定义类 classB
class classB
{
public:
    // 定义类中的一个函数 g,它的参数是类 A 对象 a,无返回值
    void g(classA a);
};

【代码详解】

  1. 这段代码主要是定义了两个类 ​​​​​​​classA 和 classB,并且它们互相调用对方的类型作为参数,这个时候可以用前置声明来解决交叉依赖的问题,避免两个类相互依赖而导致编译不通过。

  2. 其中在 ​​​​​​​classA 中声明了公有函数 ​​​​​​​f,它的参数是类 classB 对象 ​​​​​​​b,函数没有返回值,而在 ​​​​​​​classB 中声明了公有函数g,它的参数是类 classA 对象 a,函数没有返回值。

  3. 对于前置声明,其实它就像是一个 “承诺”,承诺在之后会有一个类 ​​​​​​​classB 被定义出来,然后就可以在此之前定义类 classA

  4. 最终目的就是为了让编译器先知道类的存在,以便后面出现的类能够使用。

  5. 如果没有前置声明,当编译器处理到后面的类 ​​​​​​​classA 时,它并不知道还有一个以后会出现的类 classB,所以就会出现编译错误。

【执行结果】

  • 这段代码本身只是定义了两个类 classA 和 classB,以及 classA 中的一个以 classB 对象为参数的函数 f,和 classB 中的一个以 classA 对象为参数的函数 g,并没有对这些类进行实例化或者调用。
  • 因此,在代码执行过程中并没有任何输出结果,只是编译器通过了编译,生成了可执行文件。


五、多层次的派生

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


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

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

【示例】派生类和基类之间的关系

【示例代码】C++ 例子中,主要演示了派生类和基类之间的关系:

#include <iostream>
using namespace std;

//定义类 CBase
class CBase {
protected:
    int n;
public:
    // 构造函数
    CBase(int i) : n(i) {}
    // 成员函数
    void print() {
        cout << "CBase:n=" << n << endl;
    }
};

// 定义类 CDerived 继承于 CBase
class CDerived : public CBase {
public:
    int v;
    // 构造函数
    CDerived(int i) : CBase(i), v(2 * i) {}
    // 成员函数
    void Func() {};
    void print() {
        cout << "CDerived:n=" << n << endl;
        cout << "CDerived:v=" << v << endl;
    }
};

int main() {
    // 定义 CDerived 类和 CBase 类的对象
    CDerived objDerived(3);
    CBase objBase(5);
    // 使用基类指针指向派生类对象
    CBase* pBase = &objDerived;
    // 使用派生类指针指向派生类对象
    CDerived* pDerived;
    pDerived = &objDerived;
    cout << "使用派生类指针调用函数" << endl;
    // 调用的是派生类中的函数
    pDerived->print();
    // 基类指针=派生类指针,正确
    pBase = pDerived;
    cout << "使用基类指针调用函数" << endl;
    // 调用是基类中的函数
    pBase->print();
    // 错误,通过基类指针不能调用派生类函数
    // pBase->Func( );
    // 错误 派生类指针=基类指针
    // pDerived = pBase;
    // 强制类型转换,派生类指针=基类指针
    pDerived = (CDerived*)pBase;
    cout << "使用派生类指针调用函数" << endl;
    // 调用的是派生类中的函数
    pDerived->print();
    return 0;
}

【代码详解】

  1. 它首先定义了两个类 CBase 和 CDerived,其中 CDerived 是从 CBase 派生而来的,它多了一个 v 成员变量和一个 Func() 成员函数。

  2. 在 main() 中,先定义了一个 CDerived 类和一个 CBase 类的对象,然后使用基类指针指向派生类对象,再使用派生类指针指向派生类对象,最后进行强制类型转换。

  3. 一个需要注意的地方是,使用基类指针时只能使用基类中有的成员函数,不能使用派生类中特有的成员函数,需要强制类型转换成派生类指针才能使用。

  4. 此外,尽管可以将基类指针转换成派生类指针,但是这么做需要非常小心,因为如果强制类型转换不正确,会导致程序出错。

【执行结果】

  • 结果表明,在使用基类指针指向派生类对象时,调用的是基类中的函数,使用派生类指针时调用的是派生类中的函数,而在进行指针类型转换后,派生类指针可以调用派生类中的函数:
使用派生类指针调用函数
CDerived:n=3
CDerived:v=6
使用基类指针调用函数
CBase:n=3
强制类型转换,派生类指针=基类指针
使用派生类指针调用函数
CDerived:n=3
CDerived:v=6

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/684625.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多线程单例模式

1、单例模式 顾名思义&#xff0c;单例模式能保证某个类在程序中只存在唯一一份示例&#xff0c;而不会创建出多个实例。就像java的JDBC编程只需要创建一个单例类DataSourece从这个DataSorce中获取数据库连接。没必要创建多个对象。 单例模式具体实现方式分为“饿汉”和“懒汉…

java编译与反编译

参考&#xff1a; Idea 使用技巧记录_source code recreated from a .class file by intell_hresh的博客-CSDN博客 深入理解Java Class文件格式&#xff08;一&#xff09;_昨夜星辰_zhangjg的博客-CSDN博客 实践详解javap命令&#xff08;反编译字节码&#xff09;_天然玩家…

【运筹优化】元启发式算法详解:迭代局部搜索算法(Iterated Local Search,ILS)+ 案例讲解代码实现

文章目录 一、介绍二、迭代局部搜索2.1 总体框架2.2 随机重启2.3 在 S* 中搜索2.4 ILS 三、获得高性能3.1 初始解决方案3.2 Perturbation3.2.1 扰动强度3.2.2 自适应扰动3.2.3 更复杂的扰动方案3.2.4 Speed 3.3 接受准则3.4 Local Search3.5 ILS 的全局优化 四、ILS 的精选应用…

Windows PE怎么修复系统?使用轻松备份解决!

​什么是Windows PE? Windows预先安装环境&#xff08;英语&#xff1a;Microsoft Windows Preinstallation Environment&#xff09;&#xff0c;简称Windows PE或WinPE&#xff0c;是Microsoft Windows的轻量版本&#xff0c;主要提供个人电脑开发商&#xff08;主要为OEM厂…

electron+vue3全家桶+vite项目搭建【20】窗口事件广播,通用事件封装

引入 electron中的渲染进程与主进程之间的数据交互需要利用ipc通信&#xff0c;互相订阅/通知来实现&#xff0c;我们不妨封装一个通用事件广播&#xff0c;利用自定义的事件名称来让主进程遍历窗口挨个推送对应内容&#xff0c;来实现事件的广播。 demo项目地址 实现思路 …

【计算机视觉】MaskFormer:将语义分割和实例分割作为同一任务进行训练

文章目录 一、导读二、逐像素分类和掩码分类的区别2.1 逐像素分类2.2 掩码分类2.3 区别 三、DETR四、MaskFormer五、MaskFormer用于语义和实例分割六、总结 一、导读 目标检测和实例分割是计算机视觉的基本任务&#xff0c;在从自动驾驶到医学成像的无数应用中发挥着关键作用。…

模拟电路系列分享-运放的关键参数5

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示&#xff1a;这里可以添加技术概要 例如&#xff1a; 实际运放与理想运放具有很多差别。理想运放就像一个十全十美的人&#xff0c;他学习100 分&#xff0c;寿命无限长&#xff0c;长得没挑剔&#xff0c;而实…

【c++11】移动构造的性质 和 与拷贝构造的比较(详解)

文章目录 定义性质移动构造的定义实例代码分析移动构造 与 拷贝构造的比较移动赋值 和 拷贝赋值 应用场景 定义 移动构造&#xff08;Move Constructor&#xff09;是一种特殊的构造函数&#xff0c;它通过接收一个右值引用参数来创建新对象&#xff0c;并从传入的对象中“移动…

操作系统——Windows 线程的互斥与同步

一、实验题目 Windows 线程的互斥与同步 二、实验目的 (1) 回顾操作系统进程、线程的有关概念&#xff0c;加深对 Windows 线程的理解。 (2) 了解互斥体对象&#xff0c;利用互斥与同步操作编写生产者-消费者问题的并发程序&#xff0c;加深对 P (即 semWait)、V(即 semSig…

[Spec] WiFi P2P Discovery

学习资料&#xff1a;Android Miracast 投屏 目录 学习资料&#xff1a;Android Miracast 投屏 P2P discovery Introduction Device Discovery procedures Listen State Search State Scan Phase Find Phase 总结 P2P discovery Introduction P2P发现使P2P设备能够快速…

WiSA Technologies开始接受WiSA E多声道音频开发套件的预订

美国俄勒冈州比弗顿市 — 2023年6月13日 — 为智能设备和下一代家庭娱乐系统提供沉浸式无线声效技术的领先供应商WiSA Technologies股份有限公司&#xff08;NASDAQ股票代码&#xff1a;WISA&#xff09;宣布&#xff1a;该公司现在正在接受其WiSA E开发套件的预订。WiSA E使用…

论文不详细解读(一)——MoCo系列

1. MoCo v1 论文名称&#xff1a; Momentum Contrast for Unsupervised Visual Representation Learning 开源地址&#xff1a;https://github.com/facebookresearch/moco 大佬详细解读&#xff1a;https://zhuanlan.zhihu.com/p/382763210 motivation 原始的端到端自监督方…

听说软件测试岗位基本都是女孩子在做?

“听我一朋友说&#xff0c;测试岗位基本都是女孩子做。” 不知道是不是以前“软件测试岗”给人印象是“不需要太多技术含量”的错觉&#xff0c;从而大部分外行认为从业软件测试的人员中女生应占了大多数。比如有人就觉得&#xff1a;软件测试主要是细心活&#xff0c;所以女生…

Python多任务执行方式

一、多任务的执行方式 并发&#xff1a;在一段时间内交替去执行任务&#xff08;单核CPU&#xff09;并行&#xff1a;CPU核数大于任务数 二、进程&#xff08;实现多任务&#xff09;——操作系统调度 进程是操作系统进行资源分配的基本单元一个程序至少有一个进程&#xf…

极致呈现系列之:EchartsK线图的数据量化

目录 什么是K线图K线图的特性及应用场景K线图的特性K线图的应用场景 Echarts中K线图的常用属性Vue3中创建K线图 什么是K线图 K线图是一种用于展示金融市场中股票、期货、外汇等交易品种价格走势的图表形式。它由一根根的垂直线条和水平线组成&#xff0c;能够直观地显示出一段…

OJ #378 字符串括号匹配2

题目描述 ​ 给出一个字符串&#xff0c;判断其中的左右括号是否匹配。 ​ 注&#xff1a;需同时判断左右圆括号 ( 和 ) &#xff0c;左右中括号 [和]&#xff0c;左右大括号 {和}。 ​ 不需要考虑括号之间的优先级的问题&#xff0c;也就是说&#xff0c;小括号包含大括号&…

NodeJS应届毕业生财务管理系统-计算机毕设 附源码82886

基于VueNodeJS应届毕业生财务管理系统 摘 要 随着互联网大趋势的到来&#xff0c;社会的方方面面&#xff0c;各行各业都在考虑利用互联网作为媒介将自己的信息更及时有效地推广出去&#xff0c;而其中最好的方式就是建立网络管理系统&#xff0c;并对其进行信息管理。由于现在…

合宙Air724UG Cat.1模块硬件设计指南--看门狗

概述 Air724UG 内部已经自带了看门狗&#xff0c;4秒进行一次喂狗&#xff0c;如果主芯片异常死机&#xff0c;自带的看门狗15秒左右会硬件复位主芯片。 另外主芯片死机情况下&#xff0c;reset键也可以硬重启。 通常情况下不需要外加硬件看门狗&#xff0c;如果对系统稳定性有…

FreeRTOS和uC/OS:选择入手哪个RTOS更合适?

FreeRTOS和uC/OS是两个流行的实时操作系统&#xff08;RTOS&#xff09;&#xff0c;用于嵌入式系统开发。它们有一些区别&#xff0c;但选择哪个先入手取决于你的需求和项目要求。 复杂度&#xff1a;FreeRTOS是一个相对较简单的RTOS&#xff0c;它专注于提供基本的实时调度和…

西门子Mendix 入门 3

导航页面&#xff1a;用于在应用程序中添加或修改其他页面 创建查看查看和添加公司不同部门的页面 打开导航页面&#xff0c;添加新项目 选择TaskTracke新建一个名为Department_Overview的页面&#xff0c;并选择List作为模板 创建成功 现在转到 Department_Overview页面 链接数…