---继承---

引入

✳️面向对象三大特性:
封装、继承、多态

封装:将数据和方法放到一起,用访问限定符去限制,想给你访问的设置成公有,不想给你访问的设置成私有,这样是一种更好的类的设计管理;比如我们之前说的栈的实现,如果不进行封装,那么可以随便修改它的成员 ,代码素养高的人还好,但是低的人就不会去遵守你设想的来使用,这样会导致很乱。但C++封装了之后就很规范,你想访问栈,访问栈顶的元素,你只能调用我的top函数,而不是来用我成员变量访问(这是对比C语言来看);
什么是封装、继承、多态;它们的特点是什么?这个问题是没有标准答案的,就好比说如何做红烧肉好吃,每个地方都说自己做的好吃 ;其实是没有标准答案的。
(从狭义角度)C++Stack类的设计和C设计Stack对比,封装更好,访问限定符 + 类
(从纯C++来看)封装再广义一点点,迭代器的设计;若没有迭代器,容器的访问只能暴露底层结构,我们容器出来,以前数据结构,C去实现数据结构,都去写Print,但是Print不够好呀,那我想对你每个数据++呢,我不给你写Print你就只能暴露底层结构,暴露底层结构使用复杂且使用成本很高 ,对使用者要求也很高
我们C++封装了容器的底层结构,不暴露底层结构的情况,提供统一的访问容器的方式,将低使用成本,简化使用。
Stack/queue/priority_queue的设计—适配器模式,我不规定一定要用链表,数组,双端队列等,我给个container默认是vector来适配它,从你用栈的角度你不知道底层是啥,可以适配出想要的东西。
请添加图片描述

引出–继承

要设计图书管理系统,则要设计角色:学生、代课老师、行政老师、保安、保洁、后勤…
class Student class Teacher
{ {
string _name
string _study_card; …
string _tel;
string _adress;
… …
}; }

✳️所以有些数据和方法每个角色都有的—设计重复了
有写数据和方法每个角色是独有的

✳️所以这时候就用继承来解决重复
class Person class Student :public Person
{ {
string _name
string _study_card;
string _tel;
string _adress;
… … .
}; }

✳️继承本质是类设计角度的复用!
请添加图片描述

继承概念以及定义

概念

✳️继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

定义格式

✳️下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
请添加图片描述
来看一份代码

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
	// protected/private成员对于基类 -- 一样的,类外面不能访问,类里面可以访问
	// protected/private成员对于派生类 -- private成员不能用 protected成员类里面可以用

//protected:
private:
	string _name = "peter"; // 姓名
	int _age = 18;  // 年龄
	// ...
};

class Student : public Person
{
public:
	void func()
	{
		Print();
		//_age = 0; // 不可见
	}
protected:
	int _stuid; // 学号
};

class Teacher : public Person
{
protected:
	int _jobid; // 工号
};

int main()
{
	Student s;
	cout << sizeof(s) << endl;

	Teacher t;
	//s._age = 0;
	//s.Print();
	//t.Print();

	return 0;
}

✳️学生Student继承了Person,Person的成员变量在我Student定义的对象里面,但是Person定义的方法函数不在里面,因为对象里面不存方法!

继承关系和访问限定符

在这里插入图片描述
✳️基类将自己的成员变量设计为private,子类去继承,虽然基类将其成员变量设置为private,子类是看不见且用不了里面的成员变量,但是子类还是继承了他的成员变量的!用sizeof就能验证!

✳️现在protect和private是有区别的!以前还没学继承protect和private类外面都不能使用,但是现在学了继承,他们是有区别的,protect是能让子类去使用父类的成员变量。
protect/private成员对于基类—一样的,类外面不能访问,类里面可以访问
protect/private成员对于派生类—privare成员不能被使用,protect成员类里面可以使用!

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

✳️派生类对象可以赋值给基类的对象/基类的指针/基类的引用。(切割/切片
有一个子类Student,和父类Person,那么如何赋值给父类呢?你想想父类有的数据我都有,就相当于把子类给切开,把父类当中有的值赋值过去。那么就又用到了内置类型值拷贝,自定义类型调用赋值拷贝构造了。
有的人也说是向上转换。

这里的赋值是天然的,和其他地方的赋值有点不一样。比如说,不同类型之间赋值,中间会产生临时对象,因为类型转换都会产生临时对象。

✳️但是子类对象给父类对象/指针/引用-----语法天然指针,没有类型转换。(后面的多态也是以这个地方赋值兼容(或叫做切割/切片)作为基础。)

✳️这里的引用赋值和指针赋值如何理解?对象赋值的话就是我们说的内置类型值拷贝,自定义类型调用赋值拷贝构造。 在引用这里可以这样理解:假如Person& rp,rp是子类部分的别名;指针可以这样理解:Person* prp,我prp指向的是父类,我是多大空间呢?我看的是切开过去的那一部分,因为我的类型也决定了我只看那一部分。
如果是对象那就把那一部分赋值过去;引用就是你变成我子类当中是父类变量成员的别名;指针就是指向子类当中属于父类的那一部分。
即若为引用:rp._age++则会改变子类当中的_age的值!同样指针也是!

int main()
//{
//	Person p;
//	Student s;
//	s._name = "张三";
//	s._age = 18;
//	s._sex = "男";
//	//-----➡️子类对象给父类 对象/指针/引用 -- 语法天然支持,没有类型转换 
//	p = s;
//	Person& rp = s;
//	Person* ptrp = &s;
	rp._age++;------➡️前提得是public继承才能修改
//	ptrp->_age++;

//	cout << &s << endl;
//	cout << &rp << endl;
//	cout << ptrp << endl;
✳️它们三个地址是一样的!因为你子类的对象的起始地址是第一个成员变量的地址,那么你子类前3个成员变量都是继承父类的,那么引用/指针切片的时候就从你的第一个开始切过去!

s = (Student)p; ----➡️父类是不可以赋值给子类的!
//但是呢指针是可以的,要涉及到后面的知识。世纪当中也很少父类转子类,都是子类转父类

请添加图片描述

继承中的作用域/同名函数构成隐藏关系

✳️1.当子类和父类的成员变量同名后,我们从子类当中访问同名变量,访问的是子类的变量,会先从子类当中去查找。就好像是屏蔽了对父类的同名变量访问了。若实在要访问就得加上父类的类域去访问。尽量不要设为同名

/class Person
//{
//protected:
//	string _name = "小李子"; // 姓名
//	int _num = 111; 	   // 身份证号
//};
//
//class Student : public Person
//{
//public:
//	void Print()
//	{
//		cout << " 姓名:" << _name << endl;
//		cout << " 学号:" << _num << endl;
//		cout << " 身份证号:" << Person::_num << endl;
			-----➡️只会去访问子类当中的_num成员,不会去访问父类的!
			若真想要访问,只能显示的去访问:Person::_num这样才可以
//	}
//protected:
//	int _num = 999; // 学号
//};

✳️2.成员函数同名:一个是继承父类得来的,一个子类自己的,它会优先调自己的!若实在要访问父类的,可以加作用域。两个函数之间构成的关系是隐藏。只要是函数名相同就是构成隐藏,即调用自己的函数,不一定是子类的!

1.
//class A
//{
//public:
//	void fun()
//	{
//		cout << "A::func()" << endl;
//	}
//};
//class B : public A
//{
//public:
//	void fun()
//	{
//		cout << "B::func()"<< endl;
//	}
//};

	b.fun();---➡️会优先调用自己B里面的
	b.A::fun();---➡️实在要访问加上作用域
✳️两个函数之间构成”隐藏关系“, A::fun 和 B::fun 的关系 -> 隐藏。


2.
// ✳️函数重载要求在同一作用域。一定要记住同一作用域才讨论重载!!且函数名相同,函数参数不同
	同样这里的A::fun 和 B::fun 的关系 -> 隐藏。
	只要继承这里只要函数名相同就构成隐藏了!
//class A
//{
//public:
//	void fun()
//	{
//		cout << "A::func()" << endl;
//	}
//};
//class B : public A
//{
//public:
//	void fun(int i)
//	{
//		cout << "B::func()" << endl;
//	}
//};

3.
//	//b.fun();  // 构成隐藏,编译报错-----➡️会报错,因为b只会调用自己本类的,构成隐藏!
//	b.A::fun();-----➡️必须得显示的去调用!
//	b.fun(1);

请添加图片描述

派生类的默认成员函数

✳️父类构造对象和我们以前讲的普通类的默认成员函数没有区别。主要是子类会有些区别。

/class Person
//{
//public:
//	Person(const char* name)
//		: _name(name)
//	{
//		cout << "Person()" << 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
//{
//public:
	🌟1.Student(const char* name = "", int num = 0)-----➡️❌编译错误
//		:_num(num)
		,_name(name)-----➡️显示的初始化父类对象不是这样的!要像🌟3那样。
//	{
//		cout << "Student(const char* name = "", int num = 0)" << endl;
//	}
	🌟2.Student(const char* name = "", int num = 0)-----➡️✅编译通过了,并且发现会发现会调用父类的构造函数,
因为_name会在初始化列表调用父类的构造函数。
若父类没有提供全缺省/无参的构造函数,你是调不动且会报错!

所以C++处理原则是:父类的成员变量一定要调用父类的构造函数初始化!
 
若父类没有提供全缺省/无参的构造函数/你想显示的去初始化父类的成员变量
你必须得像🌟3的Student构造函数那样,在初始化列像构造匿名对象传参数!
//		:_num(num)
//	{
		_name = name;
//		cout << "Student(const char* name = "", int num = 0)" << endl;
//	}
//	🌟3.Student(const char* name = "", int num = 0)
//		:_num(num)
//		, Person(name)----若自己想显示的去初始化
	✳️:走初始化列表的时候,也会先初始化父类的,因为初始化列表是按照声明的顺序来初始化,会默认父类声明在子类所有成员之前。
//	{
//		cout << "Student(const char* name = "", int num = 0)" << endl;
//	}
//
//	Student(const Student& s)
//		:Person(s)
//		, _num(s._num)
//	{
//		cout << "Student(const Student& s)" << endl;
//	}
//
//	// s1 = s3
//	Student& operator=(const Student& s)
//	{
//		if (this != &s)
//		{
//			Person::operator=(s);
//			_num = s._num;
//		}
//
//		cout << "Student& operator=(const Student& s)" << endl;
//
//		return *this;
//	}
//
//	// 父子类的析构函数构成隐藏关系
//	// 原因:下一节多态的需要,析构函数名统一会被处理成destructor()
//
//	// 为了保证析构顺序,先子后父,
//	// 子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
//	~Student()
//	{
//		//Person::~Person();
//
//		cout << "~Student()" << endl;
//	}// -》自动调用父类析构函数,不需要我们显示去调用
//protected:
//	int _num; //学号
//	//string _addrss = "西安";
//};

// 子类构造函数原则:
// a、调用父类构造函数初始化继承自父类成员
// b、自己再初始化自己的成员 -- 规则参考普通类
// 析构、拷贝构造、复制重载也类似

int main()
[
Student s;----➡️即使Student一开始什么都没有定义,也会调用Person的构造函数和析构函数

Student s2(s);---➡️拷贝构造:子类自己的成员与普通类调用拷贝构造一样,但父类的成员一定会调用父类的拷贝构造;
	若是显示的在拷贝构造函数去初始化:_name(name)是❌的!不能这样写
}

✳️我子类的成员由两部分构成:一部分是父类继承的,一部分是自己的。
子类构造函数原则:a.调用父类构造函数初始化继承自父类的成员
b.自己再初始化自己的成员(相当于父类之外的成员变量相当于普通类的构造函数了)
析构、拷贝构造、赋值重载也是类似

❓那么我们在子类显示的在自己构造函数里面初始化继承自父类的成员可以吗?
✳️可以在自己构造函数初始化。但是若你想初始化父类的成员,若是在初始化列表阶段初始化的话:你得像构造匿名对象那样:Person(name)进行传参;
或者你也可以不在初始化列表,在{}括号里面对父类的成员变量赋值。
但是,你不能在初始化列表这样对父类成员显示初始化:_nama(name);

✳️:若父类没有默认的构造函数:如父类没有提供全缺省/无参的构造函数,那么子类就得在初始化列表像构造匿名对象那样进行显示的传参初始化。否则会报错
因为C++的原则是:父类的成员变量一定要调用父类的构造函数!

✳️拷贝构造:子类自己的成员与普通类调用拷贝构造一样(内置类型…,自定义成员…),但父类的成员一定会调用父类的拷贝构造;
若是显示的在拷贝构造函数去初始化:_name(name)是❌的!不能这样写

❓我们拷贝构造时候,父类的成员变量要调用父类的拷贝构造,那我就要将父类的对象传过去,那我怎么样才能从子类对象中的父类对象拿出来通过拷贝构造传过去呢?(就是你想要显示的去写)
这样就行了:在初始化列表里面这样写就行Person(s),就可以了,不要想的那么复杂。
因为我一个子类Student对象可以传给父类Person对象,即符合切片的原理。 · 其中拷贝构造的参数都是引用传参:因为你引用的是我子类对象父类的那一部分,父类根据引用就能把我的成员取出来进行拷贝。

✳️赋值重载:要分成两个部分去看待,赋值自己和父类的。父类的那一部分不要自己去赋值,而是显示调用父类的赋值重载去值operator=(s),显示的去调用父类的赋值重载operator=也是一个切片,将s赋值过去,父类那里的赋值重载是引用传参,所以就能引用子类对象s。重中之重的是:一定要指定作用域去调用父类的operator=函数,因为继承会导致父类与子类的同名函数构成隐藏关系!
Student& operator=(const Student& s)
// {
// if (this != &s)
// {
// Person::operator=(s);-----➡️必须得指定作用域调用父类的重载函数!因为父类和子类同名函数构成隐藏关系!隐藏之后都是调用自己的。
// _num = s._num;
// }

✳️析构函数:有了前面的几个函数的经验,若要显示的去调用析构函数,一定要显示的去调用。若是这样显示的去写:~Person();会报错。因为父子类的析构函数构成隐藏关系!
原因是因为:解下来因为多态的需要,析构函数名统一会被处理成destructor()的样子。是因为要达到重写的效果,但构造函数不会统一成construct(),不需要重写。
若不写~person();反而还是对的。但若要显示调用也必须得加上作用域,即
Person:~Person();因为他们构成了隐藏关系。
但是若正确显示调用后,会发现调用析构函数的次数变多了,变得不正常了!屏蔽掉显示调用反而是正常的!
✳️为了保证析构顺序,顺序为先子后父。
子类析构函数完成后会自动调用父类的析构函数,所以不需要我们显示调用。
❓若我们显示调用析构函数会不会有问题??
答:可能会有。若有实质性的释放空间就会导致多释放出现问题,没有实质性的释放空间则不会。

❓如何设计一个不能被继承的类?
a.父类的构造函数弄成私有,若你自己父类要构造对象则还需要加一个创建对象的函数:
A CreatObj()
{
return A();//-----➡️当然你也可new,然后要记得释放就行。
}

✳️但还是不太行!因为你要有了对象才能调用函数,所以你还得将其弄为静态的成员函数!因为static成员不需要有this指针去访问!若要有this指针那么就肯定要先有对象才行。
static A CreatObj()
{
return A();
}
然后你可以 A a = A::CreatObj();-----➡️就可以获取到父类对象了。

若你可以懂A父类,你可以将子类B变成A的友元类,也可以调用A的CreatObj(0函数了,但你可以动我A类,还不如直接都变成public,你说是不是,所以前提肯定是不能动A类。

继承与友元

✳️友元关系不能继承!也就是说:你函数(同理友元类)是基类友元,但你并不代表就是其子类的友元,所以你就不能访问子类私有和保护成员。

class Student;
class Person
{
public:
 friend void Display(const Person& p, const Student& s);
protected:
 string _name; // 姓名
};

class Student : public Person
{
protected:
 int _stuNum; // 学号
};

void Display(const Person& p, const Student& s) {
 cout << p._name << endl;
 cout << s._stuNum << endl; -----❌你不是子类的友元函数,所以你也就不能访问子类的保护/私有成员!
 }
void main()
{
 Person p;
 Student s;
 Display(p, s);
}

继承与静态成员

✳️基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class Person
{
public :
 Person () {++ _count ;}
protected :
 string _name ; // 姓名
public :
 static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
 int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
 string _seminarCourse ; // 研究科目
};
void TestPerson()
{
 Student s1 ;
 Student s2 ;
 Student s3 ;
 Graduate s4 ;
 cout <<" 人数 :"<< Person ::_count << endl;-------➡️只要加上类域就能过突破类域去访问静态成员变量
 Student ::_count = 0;
 cout <<" 人数 :"<< Person ::_count << endl; }

输出结果是:4 0
✳️父类有static的东西,继承下来都是同一个!即即使是Student的子类Graduate创建的对象也会让_count++;

多继承及复杂菱形继承

单继承/多继承概念

请添加图片描述
请添加图片描述

菱形继承

请添加图片描述

class Person
{
public :
 string _name ; // 姓名
};
12345
比特科技
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承
Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Student : public Person
{
protected :
 int _num ; //学号
};
class Teacher : public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 // 这样会有二义性无法明确知道访问的是哪一个
 Assistant a ;
 a._name = "peter";
 
 // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
 a.Student::_name = "xxx";
 a.Teacher::_name = "yyy"; }

我是研究生助教,所以我即使学生,也是老师的角色。但是现在有一个问题就是:我学生类有一个Person继承下来的信息,我老师类也有一个Person继承下来的信息 。最后导致在我的对象中有两份Person的基类。就会有数据冗余和二义性。
就是现在我有两份的Person的name,但是你现在要访问哪一份呢?那么现在就不明确了,这个就叫做二义性。
但是二义性可以通过类域指定的访问两个父类的成员来解决。
a.Student::_name = “xxx”;
a.Teacher::_name = “yyy”;
但是无法解决数据冗余,导致空间浪费的情况。

菱形继承的根源是多继承。前人栽树,后人乘凉,所以java就没有多继承了。

虚继承

✳️虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
虚继承之后,Visual监视窗口就不一定准了。
虚继承是在腰部的位置加上“vitural”:即在父类继承其父类的时候,加上virtual继承其父类。

class Person
{
public :
 string _name ; // 姓名
};
class Student : virtual public Person----➡️腰部加上virtual
{
protected :
 int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 Assistant a
 a._name = "peter"; 
 }
研究虚继承是如何解决问题的
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;
}

我们上面说了,监视窗口不准了,但是内存窗口一定是准的。
✳️下面是没有➕virtual的时候的程序结果
请添加图片描述
第一行:01 00 00 00是d.B::_a = 1;
第二行:03 00 00 00是d._b = 3;
第三行:02 00 00 00是d.C::_a = 2;
第四行:04 00 00 00是d._c= 4;
第五行:05 00 00 00是d._d = 5;
我们发现第一行和第三行是数据冗余和二义性的部分。我们可以看到数据在内存里面也是挨着放的,若我是先继承C那么C就在前面;现在写的代码是先继承B所以B相关的数据地址在前面。所以我们先继承谁,谁就在前面。

✳️下面是➕了virtual的程序结果:
没有将A类里面的_a数据放在上面的地址了,而是放在比较下面的位置 ,d.B::_a也是那个位置,d.C::_a = 2也是同一个位置。
相当于是这样的,它现在既没有把_a放在B里面,也没有放在C里面。因为B和C里面是公共的_a的话,那么到底属于谁?那么我干脆谁都不放,我放到公共的区域里面。请添加图片描述
它不再是B里面存一份,C里面存一份。而是放在后面地址的公共区域里面。但这只是一种设计,你也可以将公共区域放在最前面。放哪个位置不是最重要的,但肯定不能是B里面和C里面。放在单独的区域里面。
经过虚拟继承,调整了对象模型。

❓经过没加virtual和加了virtual相比较,有人从图片中发现,空间变大了?
加了virtual后增加了5c cd c1 00 和 bc cb c1 00。
你虽然省下了4个字节,只存储一份冗余的数据_a,但是增加了两个指针。给人一种空间变大的感觉。
是因为此处是只有4个字节的冗余空间,若冗余的数据空间很大呢?那么多出来的指针是不是就能节省空间了!

5c cd c1 00—>00 c1 cd 5c(后面写的才是由高到低,前面的是由低到高地址) 这是一个指针,那肯定是指向一段空间,空间是这样的请添加图片描述
bc cb c1 00—>00 c1 cb bc指向的空间是这样的请添加图片描述
发现00c1cd5c和00c1cbbc都是指向00 00 00 00,所以直接指向的位置都是0;
然后再看下面的值为 14 00 00 00(14为16进制的不是14而是20)和0c 00 00 00(c为12)。那么20 和 12有什么用呢?
我们不知道这两个指针是干啥的,我们把这两个指针指向的空间给拿出来看了,但是指针的指向的位置值为0,但是下面指向的值为20 和 12.

✳️20 和 12是偏移量。那么偏移量用来干嘛?----➡️距离存储公共数据位置的偏移量
偏移量用来找A类数据_a!你看20 和 12分别存在B类的空间 和 C类空间都是用来找A( 不要局限于_a,只是这里A的数据简单只有_a)数据的。从005BF7C0加4到005BF7C4、加8到…、加12到…、加16到…、加20正好指向005BF7D4的位置是公共数据A的位置。
同理C类里面存的指针指向的空间的下面一个值0c 00 00 00,也是正好指向公共数据_a!

❓那么有人会说,我直接把公共数据位置放在最后面不就好了,还要存储什么偏移量呢?
想想有没有可能有这么一个场景,我这里有一个B对象:B b;有一个C对象:C c;也有一个D对象:D d;有没有一种可能d要传给b即b = d 和 d要传给c:c = d;
比如我在d给b的时候,我要从D对象里面找B类的成员,我只找到了_b是3 ,那么我还要找A,但是我怎么知道A在哪个位置呢?是不是可能要切片呀!
切片/切割的时候,通过偏移量计算A的位置。
因为我要把东西给b,我不仅要有B的数据,也要有A的数据。那我怎么找呢?
是这样做的,我先找到B数据的起始地址, 然后通过指针,找到偏移量,从自己指针的位置加上偏移量,便是A位置的值,然后切片赋值过去。
若是c = d,那么除了要有C类的数据,也同样还得找到A类的数据,那也就要通过指针找到偏移量,继而找到A。
所以没有规定必须得是最下面,有可能是最上面呢 ?则我们模型依然有用,也能通过偏移量找到你!

还会有这样的场景:
B* ptrb = &d,我有一个B类型的指针,指向了d。那我指向d,就是将你d当中B部分切片出来给prtb;
那我还要进行:ptrb->_a = 1呢?那么赋值是怎么操作的?
你的ptrb不是已经通过切片使你已经指向我d中B类数据的起始地址了嘛,你要要A类数据,就直接通过指针加上偏移量,就能找到A类的数据!
这个存储模型,就是菱形继承的代价,影响了访问数据的效率。虽然有点损失,但其实也还好。
所以Java这么一看,直接就算了不要多继承了。不要多继承影响也不大。

❓那么有人会问,为什么指针指向的位置,不直接是偏移量呢?而是数据0,它下面一个位置才是偏移量?
第一个位置是预留的。预留给其他地方用。有了多态之后会用到,其实是偏移量数组。
有些地方会把指针指向的位置,称作虚基表,将A称作虚基类。这两个表(指针指向的第一个位置和下面一个位置)叫做虚基表指针。

❓有人会问,D对象是怎么找到D类型的数据呢?编译器知道不同类型数据地址是怎么分配的,它先拿到自己的起始地址,先将B的大小除掉,C的大小除掉,就能找到了。所以编译器可以通过去算。

✳️A、B、C、D类型的数据都是存在D对象里面的,然后编译器通过规则➕运算就可以去找到。这里虚继承最大问题就是,A类型的数据并没有存储在B/C自己对应的区域,而是通过偏移量去找。

❓有人说将虚基类里面的数据弄成静态的static不就好了吗?不就没这么麻烦了吗? 若是弄成静态staatic成员,那么所有你创建的对象,你子类的子类等都是同一个变量,如果我想要每个对象都有自己的独立变量呢?

✳️这里当作了解范畴。如果有人问你C++有什么缺陷,那么你可以说这里很复杂,第二个多继承会产生菱形继承,菱形继承会导致什么?数据冗余和二义性!数据冗余和二义性怎么解决?在腰部的位置虚拟继承。菱形虚拟继承是如何解决的?你可以讲讲看虚基类怎么找,要通过子类的数据区域的指针去找虚基表。我要B里面A找出来,把C里面的A找出来,我必须得根据偏移量来找。我们是B类型指针或引用,只能看到B那一块,看不到整体,所以要借助偏移量虚基类。但是你是D对象你是可以看整体的!虚基类我有但是不在我B那块了,它属于我但不在我里面,因为它既属于B,也属于C,我在这里用指针或引用切片到B那块或C那块的时候怎么办?那我想去访问A的时候怎么办?可是我的那块没有A呀,所以我只能通过指针找到偏移量继而找到A。这就是刨根问底的问题
看下代码
B b;
b._a = 10;
b._b = 20;

B* prt1 = &d;
B* ptr2 =&b;

cout << ptr1->_a << endl;
cout << ptr2->_a << endl;
cout << prt1->_b << endl;
cout << prt2->_b << Lendl;

我指针去访问B,B肯定是在我对象里面。在这块空间上面我去算它在哪块位置,好算。那A怎么访问?很难受,因为这个地方,你的指针有可能指向B对象,也有可能指向D对象。
若是指针指向D对象(既 B* ptr1 = &d),访问A怎么办?我是指针,一开始指向B嘛,A的数据在不在,不在,就根据偏移量往后面去算。
若我是B对象呢?(既上面代码 B b;)很神奇的是,经过virtual虚继承后,现在B对象的存储结构也被改了。它存储的时候也符合有个虚基表!
我在内存窗口:&b就是00B3F780,B对象的b给_a的值为10,b的值为20;所以00B3F788地址对应的值为0a 00 00 00(就是10),00B3F784地址对应的值为14 00 00 00(就是20);然后&b即00B3F780地址指向的是一个指针0047cc1c,这个指针指向的是两个虚基表,第一个表的值是0,第二个表的值是8, 所以ptr2访问A也要通过偏移量8来找A了!(若B没有虚拟继承A则存储模式不会是这样的,而是我们最前面代码演示没有virtual那样!)
ptr1访问A和ptr2访问A是不一样的概念,ptr1和ptr2都是一个B类型指针,他不知道自己指的是B对象还是D对象,因为它既可以指向B对象然后B指向B嘛(就是B b;B* ptr2 =&b);也可以B指针指向D对象(就是B* ptr1 = &d),但是指针从代码的角度,看到这一块的时候,我知不知道是指向的B还是D,我不知道。那我怎么保证访问到A的数据呢?那我们都用同一个存储模式(即B类用虚拟继承A类后,存储A数据的模式改变)。因为从代码角度我不知道我指向的是B还是D,那A距离我B的位置是不一样的。
我ptr1和ptr2是不知道自己指向的是B还是D对象。因为它们都是B类型的指针,C++运行的时候,是不知道,识别不了它指向的哪种类型对象。
在D对象里面,A成员跟B的距离偏离远是20,在B对象里面,A成员跟B的距离近是8,我不知道我是D对象还是B对象,那我取A的时候去哪里取呢?难道都加8加到下面吗?不是。因为D对象是20,所以不统一!
那么这里既然虚拟继承后,那么我原本B类型存储A的模式结构要发生改变,不管我指针指向B还是D都使用同样的模式。
ptr1和ptr2无法知道,也不关心自己指向的谁,都使用同样的方式找到A成员。那用什么样的方式呢?都先找到虚基表中的偏移量,然后计算A的位置!
因为涉及切片嘛 !
实践当中尽量不涉及多继承,更不要涉及菱形继承,更更不要涉及虚拟菱形继承!
请添加图片描述

✳️为什么要在这里存偏移量,我们解释了很多。为什么D不用找A,B要去找
A呢?因为你的B类型有可能是被切片的B,也有可能本身就是B。切片的B和本身独立的B,那么A距离它的偏移量是不一样的,所以你不能用不同的方式,否则这里就会出问题了。我在同样的位置找A找不到,那不是bug吗?因为我指针不知道是切片的B,还是本身就是B。

✳️继承这个章节可以这么理解:我们大佬早期设计的时候想复杂了,所以这章节也偏复杂。实际当中用的继承反而用的很简单。所以继承设计语法偏复杂。用的角度一定要偏简单一点!
我们基本都是用公有继承,父类没特殊要求不用private权限。
继承的作用域了解就好,同父类和子类同名变量构成隐藏,同名函数也构成隐藏。但我们自己避开隐藏!避坑!
派生类默认成员函数:半重点去掌握一下。写派生类和以前写普通类差不多的。父类的你要调父类的去完成,不要自己弄。
复杂的另行继承及菱形虚拟继承:了解菱形继承的问题,解决起来也复杂,如何解决要了解一下。我们自己设计尽量不要用菱形继承。

✳️继承和组合:

class A
{
	//....
};
class B : public A---➡️继承
{};

class C
{
	//....
};
class D
{
C _c;-------➡️组合,那么我也可以用你的函数了
};
他们是有区别的:但他们复用的程度是不一样的;有些关系天生适合用继承或者组合。
不排除有些场合继承也可以,组合也可以。
继承下来A里面的成员对B来说是透明的相当于白箱,除了private,但我们说了尽量不用私有。我了解你是怎么实现的。A对象公有成员B可以直接用;A对象保护成员B也可以直接用
但是对于组合来讲是黑箱,我复用你C,但是部分,有些你对我是屏蔽的,我看不见。C对象公有成员D可以直接用;C对象保护成员D不能直接用。
所以继承一定程度破坏封装,依赖关系更大,耦合关系越高。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值