• C++——面向对象三大特性之继承


    在这里插入图片描述

    C++作为一门面向对象的语言,当然要说到面向对象的三大特性,封装,继承,多态(inheritance, encapsulation, polymorphism),那么下面就是关于继承的详解。

    继承概念

    继承的定义

    继承的核心是代码的复用,继承机制就是面向对象语言对于类保持原来的结构,并对其进行扩展的重要手段。在继承里面有这样两个类,基类(父类),派生类(子类),根据名称可以看出他们之间的关系。

    继承就是子类继承父类,子类的结构相当于是在父类的结构上面的一个扩展。继承是类在设计层次的复用。

    继承的语法格式

    了解继承的使用先来看一段代码

    class Person
    {
    public:
    	void Print()
    	{
    		cout << "name:" << _name << endl;
    		cout << "age:" << _age << endl;
    	}
    protected:
    	string _name = "peter"; // 姓名
    	int _age = 18; // 年龄
    };
    //person是这里的父类,student和teacher都是这里的子类
    //子类继承父类之后会子类的结构内会有父类的可见的成员变量和成员函数
    //因此在子类对象里面也可以访问到print,同样也可以访问到父类的成员变量
    //这里是成员函数的复用和成员变量的复用
    class Student : public Person
    {
    protected:
    	string _stuid = "202111050974"; // 学号
    };
    class Teacher : public Person
    {
    protected:
    	string _jobid = "202122090987"; // 工号
    };
    int main()
    {
    	Student s;
    	Teacher t;
    	s.Print();
    	t.Print();
    
    	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
    • 32
    • 33
    • 34
    • 35

    ![image.png](https://img-blog.csdnimg.cn/img_convert/18cb05394e66cdbe85a45fc323035f54.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=459&id=ueaf60aba&margin=[object Object]&name=image.png&originHeight=688&originWidth=1455&originalType=binary&ratio=1&rotation=0&showTitle=false&size=29609&status=done&style=none&taskId=u39666505-1b02-4ab3-9861-90e99374fb3&title=&width=970)
    这里就是继承语法的基本格式。派生类名后面加一个冒号( :)然后决定继承方式,在加上继承基类的名称。

    继承方式

    继承方式有三种,就是与访问限定符的那三个是一样的
    ![image.png](https://img-blog.csdnimg.cn/img_convert/9f06fb17426ec2bfc9662304ade64158.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=205&id=ucd5b208e&margin=[object Object]&name=image.png&originHeight=308&originWidth=554&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36526&status=done&style=none&taskId=uc183bdc8-df78-4f0a-acdc-779953d7a3d&title=&width=369.3333333333333)![image.png](https://img-blog.csdnimg.cn/img_convert/9b5dd8d2fa5794c666be0d01b848e250.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=220&id=u132ece6f&margin=[object Object]&name=image.png&originHeight=330&originWidth=595&originalType=binary&ratio=1&rotation=0&showTitle=false&size=34921&status=done&style=none&taskId=u4877fd7d-e3e1-4711-bd60-528218e7a7d&title=&width=396.6666666666667)
    继承方式也是可以省略的,class省略默认是private继承,struct默认是public继承。那么这三种不同的继承有什么区别呢?

    继承方式的汇总

    三种不同的方式其实是决定了继承后父类的成员函数与成员变量在子类中属于什么类型的成员,到底是public还是protected亦或者private下面这个表格详细给出。

    类成员/继承方式public成员protected成员private成员
    public继承子类的public成员子类的protected成员子类的private成员
    protected继承子类的protect成员子类的protected成员子类的private成员
    private继承在子类中不可见在子类中不可见在子类中不可见

    1.比较特殊的就是第四行private继承之后,虽然这个成员在子类中存在(已经继承下来了)但是在子类中是不可见的,不可见就是不仅在子类中访问不到父类的private成员,在类外也访问不到,只有父类自己这个类里面可以访问到private成员。
    2.其他的继承可以总结一个规律那就是权限的缩小。public > protected > private。继承到子类里的成员属于什么类型取决于继承方式和这些成员以前在父类里面是什么类型。由着两种类型的权限小的哪一个决定。

    之前在访问限定符哪里,protected和private是一样的。但是在继承这就不同了。
    3.如果想要基类的成员在类外访问不到,但是在继承的子类中可以访问到,那么就将父类的这个成员设置成protected类型。可以看出保护成员限定符是因继承才出现的

    **4.**class省略默认是private继承,struct默认是public继承。但是最好不要省略
    5.实际运用中还是public继承使用最多。protected和private继承很少用,也不提倡使用。因为继承下来的对象只能在类内访问,也增加了维护的复杂度。

    注:到了继承这里
    父类成员:public成员和private成员
    子类继承方式:public继承

    赋值兼容规则

    必须是public继承才可以使用

    基类对象和派生类对象的赋值转换

    赋值规则:
    1.继承类对象可以赋值给基类对象基类指针,基类引用
    2.基类对象不可以赋值给继承类对象(强转也不行)
    3.基类对象的指针可以通过强制类型转换赋值给继承类的指针变量,但是存在安全问题(越界访问)。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(多态的时候详细介绍)

    这里的继承类赋值给基类,就是赋值转换,也叫做切片、切割

    其实为什么子类对象可以赋值给父类对象很好理解,因为子类不仅有从父类继承下来的成员,还具有自己的成员。因此赋值给父类的时候只需要舍弃子类的成员即可。因此也叫做切割或者切片。如下图
    ![image.png](https://img-blog.csdnimg.cn/img_convert/5728beee2abbe01bc97af4fc89f27c46.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=315&id=ueda130ce&margin=[object Object]&name=image.png&originHeight=473&originWidth=885&originalType=binary&ratio=1&rotation=0&showTitle=false&size=33055&status=done&style=none&taskId=u9db38cf4-9c89-45c0-8a90-09cb29ad0cc&title=&width=590)

    下面是关于赋值转换使用的代码

    class Person
    {
    public:
    	void Print()
    	{
    		cout << "name:" << _name << endl;
    		cout << "age:" << _age << endl;
    	}
    protected:
    	string _name = "peter"; // 姓名
    	int _age = 18; // 年龄
    };
    
    class Student : public Person
    {
    public:
    	void Print()
    	{
    		cout << "student_id:"<<_stuid << endl;
    	}
    protected:
    	string _stuid = "202111050974"; // 学号
    };
    
    class Teacher : public Person
    {
    protected:
    	string _jobid = "202122090987"; // 工号
    };
    
    int main()
    {
    	Student s;
    	Teacher t;
    	Person p;
    
    	//s = (Student)p;//父类赋值给子类对象是不可的。
    	
    
    	//子类对象赋值给父类对象,指针,引用完全没有问题。
    	p = s;
    	p.Print();
    	Person& ref = s;
    	ref.Print();
    	Person* ptr = &s;
    	ptr->Print();
    	//
    
    	Student* ps1 = (Student*)&p;
    	//ps1->Print();//这里访问的是Student里面的Print造成了越界导致程序崩溃。
    	//引用经过强转之后也是可以从父类赋值给子类
    	Student& rs1 = (Student&)p;
    	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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    子类赋值给父类的时候完全没有问题,也不需要类型转换。相当于将子类中父类的那一部分切割出来赋值给父类对象。
    父类对象直接赋值给子类时会报错。只有父类的指针或父类的引用经过强制类型转换之后才可以赋值给子类的指针和引用。但是容易越界访问。
    如果是父类的private成员,子类public继承之后,虽然private成员不可见,但是依旧可以切片赋值父类之后,这个private成员依然是有效的。

    继承中的作用域

    1.父类和子类都有各自独立的作用域
    2.父类的成员和子类的成员同名的时候,子类的成员会屏蔽父类成员的直接访问,这种情况叫做隐藏或者重定义,如果要访问父类被隐藏的成员需要 基类::基类成员名 显示访问(限定作用域)
    3.父类和子类中只要是同名的函数就构成
    隐藏
    ,注意没有构成重载,因为重载必须是两个函数在同一个作用域,并且参数的类型或者是个数不同,父类和子类是两个不同且独立的作用域,所以不构成重载
    4.尽量不要在继承这里定义同名的成员。

    class Person
    {
    public:
    	int test = 1;
    };
    
    class Student : public Person
    {
    public:
    	int test = 2;
    };
    
    class Teacher : public Person
    {
    public:
    	int test = 3;
    };
    
    int main()
    {
    	Person p;
    	Student stu;
    	Teacher tea;
    
    	cout << p.test << endl;
    	cout << stu.test << endl;
    	cout << tea.test << endl;
    	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

    这里同时定义了三个同名的变量,子类的test和父类的test构成隐藏,所以直接访问的时候只能访问到自己这个类的test,想要访问父类里面的test只能显式的指定(限定作用域)
    ![image.png](https://img-blog.csdnimg.cn/img_convert/cd537fed832c37c017498a0a7a6dfcf2.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=389&id=u622f70b4&margin=[object Object]&name=image.png&originHeight=584&originWidth=1312&originalType=binary&ratio=1&rotation=0&showTitle=false&size=68157&status=done&style=none&taskId=uef34731c-2c35-4e0f-a9b1-45eec36a44c&title=&width=874.6666666666666)

    派生类的默认成员函数

    首先需要复习一下什么是默认成员函数,就是我们不写编译器会自动生成
    默认构造函数就是不用参数就可以调用的构造函数
    ![image.png](https://img-blog.csdnimg.cn/img_convert/1fabc01cad61e47287f9421525c474e1.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=261&id=u950b6f35&margin=[object Object]&name=image.png&originHeight=391&originWidth=1241&originalType=binary&ratio=1&rotation=0&showTitle=false&size=134840&status=done&style=none&taskId=ud54c478c-3f89-4433-a3dc-439d93d0242&title=&width=827.3333333333334)

    派生类这里默认成员函数的规则是:
    1.编译器自动生成的构造函数和析构函数:对于父类会调用父类的默认构造函数,对于子类不处理。析构函数会自动调用父类的析构函数(先完成父类的析构)
    这点与普通类相似
    普通类对于内置类型不处理,对于自定义类型会去调用他的默认构造。
    2.operator=函数也是一样的,对父类会调用父类的operator=完成父类成员的赋值,对于子类成员默认使用值拷贝,也就是浅拷贝。
    3.拷贝构造函数对于父类的成员调用父类的拷贝构造,对于子类成员,不写就是浅拷贝,值拷贝。
    **总结:**继承下来的成员调用父类的默认成员函数处理,子类的成员按照普通类的基本规则

    这里有三个问题
    1.什么时候需要自己写成员函数?
    2.子类的成员函数如何写?
    先看代码:

    class Person
    {
    public:
    	Person()
    	{
    		cout << "Person()" << endl;
    	}
    	Person(const Person& x)
    	{
    		cout << "Person(const Person& x)" << endl;
    	}
    	Person& operator=(const Person& x)
    	{
    		cout << "Person& operator=(const Person& x)" << endl;
    		return *this;
    	}
    	~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    	int test = 1;
    };
    
    class Student : public Person
    {
    public:
    	int test = 2;
    };
    
    class Teacher : public Person
    {
    public:
    	int test = 3;
    };
    
    int main()
    {
    	Person p;
    	Student stu1;
    	Teacher tea1;
    
    	Student stu2(stu1);
    	Teacher tea2;
    	tea2 = tea1;
    	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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    运行结果:
    ![image.png](https://img-blog.csdnimg.cn/img_convert/36219cf5539602dea3ec776b363f1bdb.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=302&id=u807e3a22&margin=[object Object]&name=image.png&originHeight=453&originWidth=1098&originalType=binary&ratio=1&rotation=0&showTitle=false&size=74183&status=done&style=none&taskId=u8611777e-a972-4107-8707-e6bc8e02186&title=&width=732)
    我们不写,编译器默认生成的子类的默认成员函数会自动调用父类的默认成员函数

    下面来看一下我们自己写该是如何

    class Person
    {
    public:
    	Person()
    	{
    		cout << "Person()" << endl;
    	}
    	Person(const Person& x)
    	{
    		cout << "Person(const Person& x)" << endl;
    	}
    	Person& operator=(const Person& x)
    	{
    		cout << "Person& operator=(const Person& x)" << endl;
    		return *this;
    	}
    	~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    	int test = 1;
    };
    
    class Student : public Person
    {
    public:
    	Student()
    		:Person()
    	{
    		cout << "Student()" << endl;
    	}
    	//显示调用父类拷贝构造,传子类对象,切片
    	Student(const Student& st)
    		:Person(st)
    	{
    		cout << "Student(const Student& st)" << endl;
    	}
    	Student& operator=(const Student& st)
    	{
    		Person::operator=(st);
    		cout << "Student& operator=(const Student& st)" << endl;
    		return *this;
    	}
    	//注意析构代码是错误的
    	~Student()
    	{
    		Person::~Person();
            cout<<"~Student()"<<endl;
    	}
    	int test = 2;
    };
    
    
    • 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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    int main()
    {
    	Student stu1;
    	Student stu2(stu1);
    	Student stu3;
    	stu3 = stu1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在构造和拷贝构造函数需要先在初始化列表里面调用父类的构造和拷贝构造。拷贝构造这里为什么可以用子类对象调用父类的拷贝构造呢?这就是前面说到的切片。
    对于operator=这种函数没有初始化列表,就在函数体内调用,因为父类和子类的赋值运算符重载的这个函数名是一样的。所以构成了隐藏,子类的会隐藏掉父类的。因此要指定作用域显式调用父类的operator=
    这里为什么说析构函数哪里是错误的呢?
    我们来看一下运行结果:
    ![image.png](https://img-blog.csdnimg.cn/img_convert/8ad34bd638be699dafaeb255f568786c.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=439&id=ud6341704&margin=[object Object]&name=image.png&originHeight=659&originWidth=1299&originalType=binary&ratio=1&rotation=0&showTitle=false&size=95279&status=done&style=none&taskId=u680d42b0-23b7-4edd-acd6-59e1a8dc816&title=&width=866)
    这里三个对象却调用了六次析构,现在这个函数没有崩溃是因为成员里面没有资源需要清理,一旦有资源需要清理,这段代码就会因为对同一块空间释放多次造成程序崩溃。
    那么这里为什么会调用两次父类的析构函数呢?
    因为子类构造函数调用的时候会在初始化列表内先构造出父类的成员,然后再函数体内构造出子类的成员,因为系统的栈式后进先出的。所以在析构的时候就需要子类的成员先析构,因此编译器在这里做了处理,在子类析构函数结构的时候会自动调用父类的析构函数,防止我们先调用父类的析构打乱了析构顺序。
    ![image.png](https://img-blog.csdnimg.cn/img_convert/d1c445e431fa216ab497b4bae8dc1896.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=457&id=ubdc65531&margin=[object Object]&name=image.png&originHeight=686&originWidth=907&originalType=binary&ratio=1&rotation=0&showTitle=false&size=15403&status=done&style=none&taskId=u6a1a694c-d23d-428a-a41b-e68ffcb639b&title=&width=604.6666666666666)
    如图所示结构,析构的时候需要先析构子类成员

    	~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4

    这才是析构的正确代码,在子类析构内,不需要主动调用父类的析构。

    其实上面的析构代码还表现出一个问题

    	~Student()
    	{
    		Person::~Person();
            cout<<"~Student()"<<endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里的两个类的析构函数函数名是不同的,明明不构成隐藏为什么调用父类的析构还需要显式指定作用域呢?并且不指定就会报错。

    答案是:在编译阶段,析构函数的名字会被统一替换成destructor()(ps:多态领域的知识)因此,子列和父类的析构函数就构成隐藏了。

    继承与友元

    很简单:友元关系不能继承。父类的友元函数或者友元类与子类没有任何关系。
    就像是你爸爸的朋友不一定是你的朋友一样的道理。

    继承与静态成员

    静态成员也是可以继承的,并且子类和父类共享的是同一个静态成员。

    class Person
    {
    public:
    	Person()
    	{
    		++k;
    	}
    	int test = 1;
    	static int k;
    };
    int Person::k = 0;
    class Student : public Person
    {
    public:
    	int test = 2;
    };
    
    class Teacher : public Person
    {
    public:
    	int test = 3;
    };
    
    int main()
    {
    	Student stu1;
    	cout << Student::k << endl;
    	Student stu2;
    	cout << Student::k << endl;
    	Person p;
    	cout << Person::k << endl;
    	
    	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
    • 32
    • 33
    • 34

    ![image.png](https://img-blog.csdnimg.cn/img_convert/2a60207048e8daeb32800580b2eab654.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=325&id=u7638ba11&margin=[object Object]&name=image.png&originHeight=487&originWidth=1092&originalType=binary&ratio=1&rotation=0&showTitle=false&size=63942&status=done&style=none&taskId=uca54d15e-51a3-4a3c-a6aa-a8b47a8225e&title=&width=728)
    运行结果显示子类对象和父类对象都是共用同一个静态对象。

    菱形继承

    菱形继承是多继承里面的一种特殊情况。
    首先上面的代码使用的都是单继承。就是子类只有一个直接继承的父类。
    ![image.png](https://img-blog.csdnimg.cn/img_convert/37d0dea9d3b825413b4d64a7030c239a.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=238&id=u1d778812&margin=[object Object]&name=image.png&originHeight=357&originWidth=594&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23894&status=done&style=none&taskId=u836ea828-966d-4c27-898a-fada2416db4&title=&width=396)
    多继承:一个子类有两个及以上的直接继承的父类(最少两个最多没试过,测试三个也是可以的)
    ![image.png](https://img-blog.csdnimg.cn/img_convert/69156e11a3450d5b8fb9e1ad9475f4fc.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=191&id=ud42a2fe0&margin=[object Object]&name=image.png&originHeight=286&originWidth=675&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30848&status=done&style=none&taskId=udc3a556a-9b40-493d-9b7b-b859599b063&title=&width=450)

    菱形继承就是三层继承,一个类继承的两个直接父类,这两个直接的父类又同时继承了一个父类。如下图
    ![image.png](https://img-blog.csdnimg.cn/img_convert/f33eba89c9f4338d8d2a198716d05d5a.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=328&id=u5810b6d6&margin=[object Object]&name=image.png&originHeight=492&originWidth=962&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53439&status=done&style=none&taskId=ubf7a7e8d-b6a8-44d6-998b-e1e84547bed&title=&width=641.3333333333334)
    这个就是菱形继承。

    菱形继承有一个问题,就是最下面的派生类,相当于拥有两份最上面的基类的成员。
    ![image.png](https://img-blog.csdnimg.cn/img_convert/db4e6a500cd60ef43ac27d7fbc25e3d6.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=439&id=ua5c09a2e&margin=[object Object]&name=image.png&originHeight=658&originWidth=657&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65979&status=done&style=none&taskId=u8f87bf3b-9408-4408-933f-d4080f387a4&title=&width=438)
    这就引出了菱形继承的两大问题:数据冗余和二义性
    数据冗余就是同时拥有两份最上面基类的成员
    二义性就是在最下面的派生类的对象访问最上面的基类,比如_name的时候编译器就会不知道是访问Teacher里面的_name还是访问Student里面的_name 这就是二义性。

    代码演示:

    class Person
    {
    public:
    	string _name; // 姓名
    };
    class Student : public Person
    {
    protected:
    	int _num; //学号
    };
    class Teacher : public Person
    {
    protected:
    	int _id; // 职工编号
    };
    class Assistant : public Student, public Teacher
    {
    protected:
    	string _majorCourse; // 主修课程
    };
    
    int main()
    {
    	Assistant a1;
        //这里会报错
    	cout << a1._name << endl;
    	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

    ![image.png](https://img-blog.csdnimg.cn/img_convert/7f127a17674d99a7b66ddf6bfe66ee78.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=357&id=u6810be23&margin=[object Object]&name=image.png&originHeight=536&originWidth=1128&originalType=binary&ratio=1&rotation=0&showTitle=false&size=67373&status=done&style=none&taskId=u63f9b2cd-6f67-4e22-ba10-43646b48476&title=&width=752)
    这就是二义性的问题。可以同过显式指定作用域来解决,如下

    int main()
    {
    	Assistant a1;
    	cout << a1.Student::_name << endl;
    	cout << a1.Teacher::_name << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但是数据冗余问题就需要特殊解决了。这里有人可能会问,数据冗余不解决不行吗,反正就多了一个string成员,没有多大的空间浪费,在这段代码这里确实是这样的,但是万一基类有一个int arr[10000]的成员呢?这时候数据冗余就会使得最下面的派生类对象变大两倍左右。

    虚继承

    虚继承就是用来解决菱形继承里面的数据冗余和二义性的

    虚继承语法格式

    class Person
    {
    public:
    	string _name; // 姓名
    };
    class Student : virtual public Person
    {
    protected:
    	int _num; //学号
    };
    class Teacher : virtual public Person
    {
    protected:
    	int _id; // 职工编号
    };
    class Assistant : public Student, public Teacher
    {
    protected:
    	string _majorCourse; // 主修课程
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在中间这一层的类的继承方式前面加上了一个virtual。这就是虚继承
    要注意必须是直接继承基类的派生类这里加上virtual
    ![image.png](https://img-blog.csdnimg.cn/img_convert/4002f97a6c4fd5368f2e84bd13073425.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=309&id=ud22e32af&margin=[object Object]&name=image.png&originHeight=463&originWidth=1097&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64073&status=done&style=none&taskId=u457fae83-30fd-4835-9241-af6f72c752a&title=&width=731.3333333333334)
    还有一种情况
    ![image.png](https://img-blog.csdnimg.cn/img_convert/8e2982d1db555a90b45a655c186a85c5.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=447&id=u08e65ea7&margin=[object Object]&name=image.png&originHeight=671&originWidth=982&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9285&status=done&style=none&taskId=ua39c280a-0c77-42e6-bfdd-a7eecc00d19&title=&width=654.6666666666666)
    这种情况下,virtual也是添加在直接继承基类的这一层这里,主要是要明白那两个类这里存在数据冗余和二义性。这里B和C同时直接继承了A。所以之类的B和C的子类如果被同一个类继承就会出现数据冗余问题,解决问题就该在这个源头这里解决。

    虚继承的原理

    虚继承是如何解决数据冗余和二义性呢?
    主要是通过虚基表来解决
    虚继承是解决数据冗余和二义性的方案,虚继承也只能用在这里。

    下面来看一下,没有使用虚继承的这段代码在内存中是如何存储的。

    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

    ![image.png](https://img-blog.csdnimg.cn/img_convert/d124fc099b4d69054d2fbe085fbdcb7f.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=322&id=u418b9cf8&margin=[object Object]&name=image.png&originHeight=483&originWidth=543&originalType=binary&ratio=1&rotation=0&showTitle=false&size=51644&status=done&style=none&taskId=u1d99666c-3610-460b-b26d-fd619ecdc5c&title=&width=362)
    在这内存图中可以看到,在d这个对象内部分区,因为D类继承的时候先继承的是B类,所以B类先出现在低地址处,然后是C类成员。最下面才是D类对象自己的那一部分。可以看到B类和C类的分区里面都有一个A类的成员_a这就是没有虚继承的D类的内存。

    下面代码和内存演示虚继承如何解决数据冗余和二义性

    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

    ![image.png](https://img-blog.csdnimg.cn/img_convert/9fc156443ec1e1f3dac07b4428024cd6.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=301&id=u5f9df317&margin=[object Object]&name=image.png&originHeight=451&originWidth=1019&originalType=binary&ratio=1&rotation=0&showTitle=false&size=85949&status=done&style=none&taskId=u42fccc1f-95e6-4d4a-b4f7-04e40c96374&title=&width=679.3333333333334)
    这里的B类分区和C类分区里面的第一个位置存放的是一个地址,这个地址指向的就是一个虚基表
    虚基表里面存放的是从B分区的第一个位置到公共部分_a的地址偏移量。
    当然虚基表的第一个位置的数值其实也是一个偏移量,这个是虚函数的偏移量,在多态领域会学到虚函数。这里不需要关注。

    有了虚基表的偏移量,我们在d对象中的B部分或者C部分找_a就可以通过偏移量计算找到那个公共地址处的_a

    那么B和C部分为什么要存一个虚基表地址保存偏移量去找_a呢?我们我是不是可以默认_a就在那个固定的位置,每次找a都在哪里呢?答案是不可以

    如果是这种情况,我们必须要知道偏移量。

    D d;
    B b = d;
    C c = d;
    
    • 1
    • 2
    • 3

    就是切片,这时候我们需要将d里面的B分区和公共部分a切割给b对象,这时候就需要我们只需将b的那一部分切割过去,再通过偏移量计算a的位置,一同切割过去即可。

    ![image.png](https://img-blog.csdnimg.cn/img_convert/3c23d9fdbb5a8cd566f05519f4084f5e.png#clientId=uc6aaa233-8832-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=499&id=u46fd1539&margin=[object Object]&name=image.png&originHeight=748&originWidth=988&originalType=binary&ratio=1&rotation=0&showTitle=false&size=103738&status=done&style=none&taskId=u646eded6-6c52-4c54-bcef-2aac54b209d&title=&width=658.6666666666666)
    这是菱形继承的详细图解。

    继承的总结

    继承可以说是C++语法复杂的一个方面,因为有了多继承,所以就会存在菱形继承,为了解决菱形继承又有了菱形虚拟继承。不仅底层实现复杂,对性能也有影响。所以一般不能设计出菱形继承。大多数情况都是使用单继承。像是java只有单继承。

    继承和组合

    开始的时候说过,继承就是一种代码的复用方式,那这里的组合就是代码的另一种复用方式。

    继承和组合之间是不同的复用方式
    继承是is-a,所有的子类都是父类
    组合是has-a的关系,子类里面有父类。

    //轮胎
    1class Tire{
     protected:
     string _brand = "Michelin"; // 品牌
     size_t _size = 17; // 尺寸
     };
     
     class Car{
     protected:
     string _colour = "白色"; // 颜色
     string _num = "陕ABIT00"; // 车牌号
     Tire _t; // 轮胎
     };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里的这段代码就是has-a的关系。汽车里面包含了轮胎。
    组合的语法形式就是在一个类的成员里面包含另一个类的对象。

    一般来讲推荐使用组合不使用继承。但是还是要看情况

    如果类之间的关系是is-a的关系就使用继承,has-a就使用组合。两种都可以优先使用组合。

    组合优于继承的原因
    继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
    (white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,**基类的内部细节对子类可见 **。
    继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关 **
    系很强,耦合度高。 **
    对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
    象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为
    黑箱复用
    (black-box reuse),
    因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 **组合类之间没有很强的依赖关系, **
    耦合度低。优先使用对象组合有助于你保持每个类被封装。
    实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
    合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就
    用组合。

    组合的耦合性低也是一种相对的说法,比如如果基类的成员都是公有成员,那么组合和继承就差不多了,修改成员都是会很大程度影响到派生类。

    对于黑箱和白箱,在软件测试方面还有黑箱测试和白箱测试。
    黑箱测试就是根据软件的功能依次进行测试,测试人员看不到内部代码的实现。
    白箱测试是测试人员可以看到内部实现代码,可以根据代码实现有针对性的进行测试。找出代码的缺陷。一般来说白箱测试是比黑箱测试更加严格的。

  • 相关阅读:
    Android studio2022.3项目,禁止页面左右
    筑梦远航 势不可挡|和数研究院四周岁啦
    优先级队列(priority_queue)
    DN-DETR源码讲解
    Linux_用户组管理
    ssm在线教学质量评价系统毕业设计源码141550
    echrat 的tooltip轮播播放高亮
    如果再来一次,你还会选择互联网么?
    监控服务器体系
    用node.js完成
  • 原文地址:https://blog.csdn.net/qq_62745420/article/details/126770524