• C++的重大特性之一:继承、菱形继承


    继承的概念和定义

    继承机制是面向对象程序设计的一种实现代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加其他的功能,在此基础上也就产生了一个新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,是类设计层次的复用。

    //以下代码就是采用了继承机制的一个场景
    class person
    {
    protected:
    	char _name[28];
    	int _age;
    	char _id[30];
    };
    //继承是代码复用的一种重要手段
    class student :public person
    {
    protected:
    	char _academy[50]; //学院
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    继承的格式
    在这里插入图片描述
    在前面的例子中,person是基类,student是派生类,继承方式是public. 这是很容易记忆的,person是基础的类,student是在person这个类的基础之上派生出来的。这就非常地像父子关系,所以基类又可以称为父类,派生类又可为子类。子类的后面紧跟着:,是:后面这个类派生出来的。


    继承关系和访问限定符

    继承的几种方式和访问限定符是相似的。
    三种继承方式:public继承、protected继承、private继承。
    三种访问限定符:public访问、protected访问、private访问。

    基类类成员的访问权限和派生类继承基类的继承方式, 关系到了基类被继承下来的类成员在派生类中的情况。ps:这句话起始很好理解地,就是这句话写起来就变得绕口和复杂了,哈哈哈😁.

    基类成员/继承方式public继承protected继承private继承
    public成员在派生类中为public成员在派生类中为protected成员在派生类中为private成员
    protected成员在派生类中为protected成员在派生类中为protected成员在派生类中为private成员
    private成员在派生类中不可见在派生类中不可见在派生类中不可见

    这里的不可见指的是:基类中的private成员也是被继承下来了的,只是在语法上,在派生类的类里和类外都不能够访问。

    记住这个特殊的点,那么其他的就可理解为“权限问题”,这里“权限只能缩小,不能放大”。例如,基类的public成员以private继承方式继承下来,为“权限小的那个”,也就是继承下来后在派生类中是private成员。

    class person
    {
    protected:
    	char _name[28];
    	char _id[30];
    private:
    	int _age;
    };
    
    class teacher :public person
    {
    public:
    	teacher()
    		:_age(0) //基类的private成员在派生类里不能访问
    	{
    	}
    protected:
    	char _jodid[20]; //工号
    };
    
    int main(void)
    {
    	teacher t1;
    	t1._age; //基类的private成员在类外不能访问
    	return 0;
    }
    
    • 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

    基类和派生类之间的赋值

    派生类的对象可以赋值给其基类的对象、基类的指针、基类的引用。
    在这里插入图片描述
    就像上面这样,取基类需要被赋值的值过去即可。
    在这里插入图片描述
    派生类赋值给基类的对象、基类的指针、基类的引用。在派生类中取基类需要的,就像把派生类给切割了一样、所以这里有一个形象的称呼:切割/切片

    class Person
    {
    protected:
    	string _name; // 姓名
    	string _sex; // 性别
    	int _age; // 年龄
    };
    class Student : public Person
    {
    public:
    	int _id; // 学号
    };
    
    int main(void)
    {
    	//可以将派生类赋值给基类的对象、指针、引用
    	Person p;
    	Student s;
    	p = s;
    	Person* Pptr = &s;
    	Person& Refp = s;
    	//注意不能将将基类对象给派生类对象
    	//s = p;
    
    	//允许将基类指针赋值给派生类指针,但是需要强制转换
    	Student* sPtr = (Student*)Pptr;
    	return 0;
    }
    
    • 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

    【注意】
    1、不允许基类对象赋值给派生类对象
    2、允许基类指针赋值给派生类指针, 但是需要强制转化。这种转化虽然可以,但是会存在越界访问的问题。

    继承中的作用域

    基类和派生类都有独立的作用域。继承下来的基类成员在一个作用域,派生类的成员在另一作用域。

    //以下代码的运行结果是什么?
    class Person
    {
    protected:
    	string _name = "杨XX"; // 姓名
    	int _num = 12138; // 身份证号
    };
    class Student : public Person
    {
    public:
    	void Print()
    	{
    		cout <<_num << endl;
    	}
    protected:
    	int _num = 52622; // 学号
    };
    void Test()
    {
    	Student s1;
    	s1.Print();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    基类中有一个_num 给了缺省值“12138”, 派生类中也有一个_name,给了缺省值“52622”,那么在派生类里直接使用_name,使用的具体是哪一个类里的?

    在这里插入图片描述
    使用的是派生类Student里的。

    总结:基类和派生类中如果有同名成员,派生类将屏蔽基类对同名成员的直接访问,这种情况称为隐藏 , 或者称为重定义

    如果想要访问,则使用基类::基类成员显示的访问。

    class Person
    {
    protected:
    	string _name = "杨XX"; // 姓名
    	int _num = 12138; // 身份证号
    };
    class Student : public Person
    {
    public:
    	void Print()
    	{
    		cout << "身份证号:" << Person::_num << endl;
    		cout << "学号:" << _num << endl;
    	}
    protected:
    	int _num = 52622; // 学号
    };
    void Test()
    {
    	Student s1;
    	s1.Print();
    };
    
    int main(void)
    {
    	Test();
    	return 0;
    }
    
    • 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

    运行结果
    在这里插入图片描述

    我们已经了解了什么是隐藏。那么来看一下下面这些代码。

    //以下的两个函数构成隐藏还是重载?
    class A
    {
    public:
    	void func()
    	{
    		cout << "func()" << endl;
    	}
    };
    class B : public A
    {
    public:
    	void func(int num)
    	{
    		cout << "func(int num)" << endl;
    	}
    };
    void Test()
    {
    	B b;
    	b.func(10);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    函数重载要求在同一作用域,而被继承下来的基类成员和派生类成员在不同的作用域,所以构成的是隐藏。

    ```cpp
    //以下代码的运行结果是什么?
    class A
    {
    public:
    	void func()
    	{
    		cout << "func()" << endl;
    	}
    };
    class B : public A
    {
    public:
    	void func(int num)
    	{
    		cout << "func(int num)" << endl;
    	}
    };
    void Test()
    {
    	B b;
    	b.func();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    因为func()函数隐藏了,在派生类的作用域内没有func()函数,所以会出现编译报错。

    派生类的默认成员函数

    类有8个默认成员函数,这里只说重点的四个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数

    如果我们不写派生类的构造函数和析构函数,编译器会做如下的事情:

    1、基类被继承下来的部分会调用基类的默认构造函数和析构函数
    2、派生类自己也会生成默认构造和析构函数,派生类自己的和普通类的处理一样

    如果我们不写派生类的赋值构造函数和拷贝构造函数,编译器会做如下的事情

    3、基类被继承下来的部分会调用基类的默认拷贝构造函数和赋值构造函数。
    4、派生类自己也会生成默认赋值拷贝构造函数和赋值函数,和普通类的处理一样。

    什么情况下需要自己写?
    1、父类没有合适的默认构造函数,需要自己显示地写
    2、如果子类有资源需要释放,就需要自己显示地写析构函数
    3、如果子类存在浅拷贝的问题,就需要自己实现拷贝构造和赋值函数解决浅拷贝的问题。


    如果需要自己写派生类的这几个重点成员函数,那么该如何写?

    //如果需要自己实现派生类的几个四个重点默认成员函数,需要如何实现?该注意什么?
    class Person
    {
    public:
    	Person(const char* name)
    		:_name(name)
    	{
    		cout << "Person(const char* name)" << endl; //方便查看它什么被调用了
    	}
    	Person(const Person& p)
    		:_name(p._name)
    	{
    		cout << "Person(const Person& p)" << endl;
    	}
    	Person& operator=(const Person& p)
    	{
    		cout << "Person& operator=(const Person& p)" << endl;
    		//首先排除自己给自己赋值
    		if (this != &p)
    		{
    			_name = p._name;
    		}
    		return *this;
    	}
    	~Person()
    	{
    		cout << "~Person()" << endl; 
    	}
    protected:
    	string _name; //姓名
    };
    class Student : public Person
    {
    protected:
    	int _id; //学号
    	int* _ptr = new int[10]; //给一个需要自己实现默认成员函数场景用以举例
    };
    
    • 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

    1、实现派生类的构造函数:需要调用基类的构造函数初始化被继承下来的基类部分的成员。如果基类没有合适的默认构造函数,就需要在实现派生类构造函数的初始化列表阶段显示调用。

    2、实现派生类的析构函数:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理被继承下来的基类成员。这样可以保证派生类自己的成员的清理先于被继承下来的基类成员。ps:析构函数名字会被统一处理成destructor(),所以被继承下来的基类的析构函数和派生类的析构函数构成隐藏。

    3、实现派生类的拷贝构造函数:需要调用基类的拷贝构造函数完成被继承下来的基类成员的拷贝初始化。

    4、实现派生类的operator=:需要调用基类的operator=完成被继承下来的基类成员的赋值。

    5、派生类对象初始化先调用基类构造再调用派生类构造。

    class Student : public Person
    {
    public:
    	Student(const char* name, int id)
    		: Person(name)
    		, _id(id)
    	{
    		cout << "Student()" << endl;
    	}
    
    	Student(const Student& s)
    		: Person(s)
    		, _id(s._id)
    	{
    		cout << "Student(const Student& s)" << endl;
    	}
    
    	Student& operator = (const Student& s)
    	{
    		cout << "Student& operator= (const Student& s)" << endl;
    		if (this != &s)
    		{
    			Person::operator =(s);
    			_id = s._id;
    		}
    		return *this;
    	}
    
    	~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    protected:
    	int _id; //学号
    };
    
    • 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

    菱形继承

    继承可分为单继承和多继承
    单继承:一个派生类只有一个直接基类
    在这里插入图片描述
    多继承:一个派生类有两个或两个以上的直接基类。
    在这里插入图片描述

    而多继承中又存在着一种特殊的继承关系,菱形继承
    在这里插入图片描述
    它们之间的继承关系逻辑上就类似一个菱形,所以称为菱形继承。菱形继承相对于其他继承关系是复杂的。

    B中有一份A的成员,C中也有一份A的成员,D将B和C都继承了,那么D中被继承下来的A的成员不就有两份了吗?不难看出,菱形继承有数据冗余和二义性的问题。

    class Person
    {
    public:
    	string _name; // 姓名
    };
    class Student : public Person
    {
    public:
    	int _num; //学号
    };
    class Teacher : public Person
    {
    public:
    	int _id; // 职工编号
    };
    class Assistant : public Student, public Teacher
    {
    public:
    	string _majorCourse; // 主修课程
    };
    
    int main()
    {
    	// 二义性、数据冗余
    	Assistant a;
    	a._id = 1;
    	a._num = 2;
    	// 这样会有二义性无法明确知道访问的是哪一个
    	a._name = "peter";
    	return 0;
    }
    
    • 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

    上面的继承关系如下:
    在这里插入图片描述
    此时Assitant中有两份_name.存在数据冗余和二义性的问题。

    二义性的问题是比较好解决的,使用::指定就可以了,但是并不能解决数据冗余的问题。

    int main()
    {
    	// 二义性、数据冗余
    	Assistant a;
    	a._id = 1;
    	a._num = 2;
    
    	a.Student::_name = "小张";
    	a.Teacher::_name = "张老师";
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    虚拟继承可以解决继承的数据冗余和二义性的问题。如上面所画的逻辑继承关系。在开始可能产生数据冗余和二义性的地方使用虚拟继承,即可解决,但是在其他地方不要去使用虚拟继承。

    虚拟继承格式

    在这里插入图片描述
    虚拟继承解决数据冗余和二义性的原理
    为了更好地研究,在这里给出一个比较简单的菱形继承体系

    class A {
    public:
    	int _a;
    };
    class B : public A{
    public:
    	int _b;
    };
    class C : public A{
    public:
    	int _c;
    };
    class D : public B, public C {
    public:
    	int _d;
    };
    int main()
    {
    	D d;
    	d.B::_a = 1;
    	d.C::_a = 2;
    	d._b = 3;
    	d._c = 4;
    	d._d = 5;
    	return 0;
    }
    
    • 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

    在这里插入图片描述
    B和C中都有一份A的数据可以看出数据的冗余。

    现在增加虚拟继承机制,解决数据冗余和二义性。

    class A {
    public:
    	int _a;
    };
    class B : virtual public A {
    public:
    	int _b;
    };
    class C : virtual public A {
    public:
    	int _c;
    };
    class D : public B, public C {
    public:
    	int _d;
    };
    int main()
    {
    	D d;
    	d.B::_a = 1;
    	d.C::_a = 2;
    	d._b = 3;
    	d._c = 4;
    	d._d = 5;
    	return 0;
    }
    
    • 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

    再次调式调用内存窗口,会发现
    在这里插入图片描述
    和没有采用虚拟继承的内存窗口有较大的变化。
    B中的地址0x00677bdc里有什么?C中的地址0x00677be4里有什么?
    在这里插入图片描述
    从内存窗口可看出,菱形虚拟继承,内存中只在对象组成的最高处地址保存了一份A,A是B、C公共的。而B和C里分别保存了一个指针,该指针指向一张表。这张表称为虚基表,而指向虚基表的指针称虚基指针。虚基表中保存的值,是到A地址的偏移量,通过这个偏移量就能够找到A了。


    继承和组合的区分与联系

    在没有学习继承之前,我们其实频繁地使用组合。

    class head
    {
    private:
    	int _eye;
    	int _ear;
    	int _mouth;
    };
    class hand
    {
    private:
    	int _arm;
    	int _fingers;
    };
    class Person
    {
    	//组合
    	//一个人由手、头等组合
    	hand _a;
    	head _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 继承是一种is-a的关系, 每一个派生类是基类,例如,Student是一个Person, Teacher 是一个Person
    • 组合是一种has-a的关系,Person组合了head, hand, 每一个Person对象中都有一个head、hand对象。
    • 如果某种情况既可以使用继承又可以使用组合,那么优先使用对象组合,而不是类继承。

    其余注意事项

    • 友元关系不能被继承,好比父亲的朋友不一定是你的朋友。
    • 如果基类中定义了静态成员,当这个基类被实例化后出现了一份,那么整个继承体系中都只有这一份实例。
  • 相关阅读:
    数据库系统概论——数据库恢复技术
    查看局域网是否有外网IP之收藏必备
    【服务器数据恢复】ext3文件系统下硬盘坏道掉线的数据恢复案例
    Golang 乐观锁实战
    图像像素值统计&图像几何形状的绘制&随机数与随机颜色
    单独使用return关键字
    【lwip】10-ICMP协议&源码分析
    【python】网络爬虫与信息提取--scrapy爬虫框架介绍
    为什么个人IP对任何行业都至关重要
    vue3实现在element Dialog 对话框中预览pdf文件
  • 原文地址:https://blog.csdn.net/qq_56870066/article/details/125536022