我使用类也有一段时间了,慢慢觉得我们做一件事时,就是要先描述,例如写一个管理系统登记学校成员的信息,我们就要先对在学校内的老师和学生做描述,学生要有年龄,班级,姓名,学号和身份证,描述老师要有年龄,姓名,工号和身份证,分别用一个类来将这些信息封装起来,描述完后,实例化出一个对象可以存一个学生的信息,存多个学生那就new一块大的空间来存,然后就可以写成员函数来对一个个学生信息做增删查改,我们可以发现在描述学生和老师的时候,他们有一些信息是重合的,那在老师类和学生类中都要写对应的接口函数对这些相同的信息做处理,这就有些冗余,所以我们希望能把这些公共信息分离出来,再封装入类,命名为person类,然后我们学生类想要添加person类的成员函数和成员变量就继承person类,这就是继承的由来,当时我有点疑惑,为什么不直接在student类中定义一个person类的成员变量,后来才知道这叫做组合。
组合和继承的区别我理解比较深刻的就是继承中,子类和父类的关联度更高,因为父类(person类)的公有和保护成员,子类在类内都可以使用,但是组合就只能使用persong类的公有成员,也就只会被公用成员的修改影响,只能说最好就是用组合,如果要实现多态就必须用继承(或许实际工作中我们可以理解地更深刻)。
class A
{
public:
int _a;
};
class B
{
public:
void Print()
{
cout << "B:Print()" << endl;
}
int _b;
A a1;
};
class C: public B ,public A C要继承B,就在类名后加冒号(:),然后写继承方式,public表示公有继承,
再加类名B,要继承多个类就用逗号隔开,C称为子类,A,B称为父类,
{
public:
int _c;
};
void test1()
{
C c1;
c1.Print();//继承
}
继承一个父类就对应写一个继承方式,如果不写会有有默认的继承方式,如果子类是class定义的,那继承方式默认为private,如果为struct定义的子类,继承方式默认为public, 等会会统一总结继承方式带来的区别。(如下图)
三种继承方式遇到基类(或称父类)的三种成员,使得基类成员的访问方式有九种变化,其实理解记忆也很好记。
基类的私有成员任意继承方式在派生类(或称子类)都是不可见,不可见就是在子类类内都无法访问,但在内存中还是存在,也就是用地址可以强制访问。
而基类的protected和public成员,与不同继承方式也有个规律,就是取权限小的,例如protected成员被public继承,那它就是子类的保护成员,被私有继承那就是子类的私有成员。
是不是都记住了呢?理解记忆即可,根本就不用死记硬背。
子类和父类对象是不同的类型,是如何支持赋值的呢?答案是切片
子类对象是有继承父类对象成员的,当要赋值给父类对象时,就把属于父类的切出来赋值给父类对象。
而对于父类的指针p1来说,它仅仅指向子类中的父类对象,
父类的引用也只是子类中属于父类的那一部分的别名。
那好如果反过来呢,父类对象能赋值给子类对象(又称向下转换)吗?父类的指针和引用可以(会存在越界访问),对象不行,可能是大佬觉得父类对象赋值给子类对象更危险,因为剩下的子类成员不知道该放什么数据。
注意:切片就意味着要访问子类中的父类成员,如果是私有和保护继承,那在赋值的时候会报错。
当子类和父类的成员函数重名时,此时子类的成员函数就会对成员函数进行隐藏。也就是当子类对象调用一个同名成员时,会优先调用子类的,除非显示调用。
class C
{
public:
void Print()
{
cout << "C:Print()" << endl;
}
int _c=3;
};
class E : public C
{
public:
void Print()
{
cout << "E:Print()" << endl;
}
int _c=4;
};
void test2()
{
E e1;
cout<
运行结果:
这里用的都还是原来类的成员函数的知识,如果那部分没掌握,对于继承时成员函数更不容易理解。
由于子类其实是把继承来的父类成员当成一个整体,当子类对象调用自己的构造函数的时候,初始化列表会先调用父类的构造函数(不写就调用默认构造,显示就自己决定)初始化子类中的父类成员,再初始化子类的。拷贝构造函数也是如此,所以拷贝构造函数我们要自己在初始化列表显示调用父类的拷贝构造函数,不然就会调成默认构造函数了。父类被继承认为声明在子类成员之前,而我们之前在类和对象中学过了,先声明的会先在初始化列表初始化。
子类赋值成员函数必须要先调用父类的赋值成员函数对子类中的父类成员做处理(这个顺序性是为了保证父类提供给子类的一定是初始化好,或者赋值好的),而且是显示调用,因为赋值成员构成隐藏,如果不显示调用父类的,那就会一直调用子类的,导致栈溢出。
而析构函数是被编译器做了处理的,名字统一为destructor(原因在多态),所以也构成隐藏,但却不需要我们显示调用父类的,因为编译器需要先用子类的析构函数清理子类资源,再自己调用父类的析构函数。
析构顺序原因有二:
1.遵循后定义的先析构
2.子类中可以使用父类成员,如果先调用父类析构函数,我们又在该函数后访问父类成员会出问题,所以要防止这种情况出现。
父类的友元函数继承时是不会变成子类的友元函数的,就像是父辈的朋友不一定是你朋友一样,除非在子类内对该函数进行友元声明。
至于静态成员则更加特殊,在没有提及继承之前,静态成员是属于整个类的,就一份,那继承给子类后会多一份吗?上代码!
class C
{
public:
void Print()
{
cout << "C:Print()" << endl;
}
static int _c;
};
int C::_c = 3;
class E : public C
{
public:
void Print()
{
cout << "E:Print()" << endl;
}
int _e=4;
};
void test2()
{
E e1;
C c1;
cout << "C:" << c1._c << endl;
cout << "E:" << e1._c << endl;
e1._c = 4;
cout << "C:" << c1._c << endl;
cout << "E:" << e1._c << endl;
c1._c=5;
cout << "C:" << c1._c << endl;
cout << "E:" << e1._c << endl;
}
当我们用子类和父类对象去打印这个静态成员的时候,他们值是相同的,但这不能说明他们是一个变量,有可能是复制了父类的值,所以我们用子类和父类对象去改这个变量,结果如下图,结论是子类和父类共用这个静态成员。但要强调的一点是,e1,c1对象内都不包含静态成员变量,因为我用sizeof发现子类E继承父类C后大小不变。
多继承这个部分的知识点不少,就先来说说多继承的弊端。
上图是多继承中的菱形继承,代码如下。
class A
{
public:
int _a;
};
class B:public A
{
public:
int _b;
};
class C:public A
{
public:
int _c;
};
class E:public C,public B
{
public:
int _e;
};
从内存窗口看,我们可以发现e1中有两份A类型,一份是在继承C得来的,一份是继承B得到的。
特别要说明的是内存排列顺序,E是从左往右继承,所以先继承的C,再继承B,而C显示在上,可以认为先继承的显示在上,显示在上意味着什么呢?这就要再说结构体成员的内存分布了,&e1时的地址是整个结构体的最低地址,这应该是规则,所以显示在上意味着C位于结构体内存块的低地址处。这说明结构体内部是先使用低地址,再使用高地址的,但栈帧是先用高地址。
既然e1中有两份A,那是不是有点冗余呢?当然啦,之前说子类继承父类,是为了更好的描述,如果继承的成员有重复,那对自己的描述不就出现重复了吗,就像有两个身份证号,用哪个呢?二义性不就来了吗?
继承真正的难点才刚刚开始,请做好心理准备。
先来看看虚拟继承的使用
class A
{
public:
int _a=1;
};
class B:virtual public A
{
public:
int _b=2;
};
B虚拟继承A类内存图
如果B是正常继承A,内存窗口如下。
想必大家发现了,监视窗口是没有发生变化的,这是因为监视窗口是编译器想让你看到的,并不是实际的,内存窗口才是真实的,怪不得有句话说大佬的境界是这样的:看代码不是代码,而是内存。
总结虚拟继承对子类内存空间的改变
1.虚拟继承的成员放在存储空间的地址最低处,公共的东西当然放最开始或者最后面了。
2.增加了一个指针,称为虚基表指针,它指向虚基表。
这是对应的虚基表。
虚基表内容后面详谈。
然后我们再来看看一开始提的多继承弊端中的菱形继承如何通过虚拟继承改进。
class A
{
public:
int _a;
};
class B: virtual public A
{
public:
int _b;
};
class C: virtual public A
{
public:
int _c;
};
class E:public C,public B
{
public:
int _e;
};
如下图,当C,B类均被E继承时,他们的公共部分A会只保留一份,如果仅仅只是B虚拟继承A类,对于B类对象还没什么变化,但是当两份虚拟继承而来的成员再被E继承时,此时在E类对象只会保留一份,这就是虚拟继承的作用。
大部分的博客都说在继承腰部的位置加个virtual关键字,可是腰部在哪,什么是腰部?下面这个图还好说,其它情况呢?
如下图,解决A类的数据冗余和二义性,应该在哪加virtual关键字?
要回答这个问题我们就要回顾刚刚说的虚拟继承的作用了,我理解就是虚拟继承是将继承而来父类成员都放到一块地方,当出现同样的成员时,取其中一份即可,这两份的数据一定是相同的,因为我们现在是在继承类的声明,不是继承某个类对象,所以要想解决A类的冗余,在B,C继承A时加virtual关键字即可,如果是在继承D,C时加,那就把B,C类的所有成员(包括B,C自己的成员和继承而来的A类的成员)变成公共的了,可我们只需要将A类变为公共的即可,个人认为不要做多余的事。
第一行内容,有的说是虚表指针指向自己这个指针的距离,所以为零,但我尝试中却发现该值并不都是0,最后我认为是虚表指针距离子类最低地址(&e1)的距离,比如上面那个虚基表,当B虚拟继承A,就会在B类型对象内增加一个虚表指针,此时虚基表第一行就是距离B最低地址的距离,所以为零,当我设计其它场景时,代码如下:
class B
{
public:
int _b=2;
};
class C
{
public:
int _c=3;
};
class E :virtual public C, public B
{
public:
int _e=4;
};
从下面的内存窗口可以看出,虚拟继承的C类成员虽然先继承,但虚拟继承的成员统一放在最后面,至于讨论虚基表指针和B类成员谁先先后是没什么意义的,在这里我觉得只需知道一般继承而来的父类成员放子类对象中的低地址处,然后放子类成员,最后放虚拟继承而来的成员就可以啦。我们可以看到的是虚基表中的第一行变成了-4(内存显示的数据是补码), 那虚基表第一行是虚基表指针指向自己的距离就是不成立的,可以我们发现-4不就是虚基表指针距离子类E最低地址的距离吗。(我在此只写了两个例子证明,实际上我自己写了不少更复杂且无用的继承场景来证明,如果你觉得我是错的,可以私聊,非常欢迎)
第一行以外的都是虚基表指针距离父类成员的偏移量。
我写这么多探究虚基表第一行内容是什么,是觉得其他人的博客说虚基表第一行是虚基表指针距离自己的距离不太对,才打算写出来反驳一下他们。
最后我再谈谈对一些问题的看法,例如,为什么要用一个表去存偏移量,而不是直接存偏移量到对象呢?首先偏移量是指向虚继承中的父类成员的,如果子类虚继承了多个父类,那偏移量是不止一个的,如果把偏移量都塞进每个子类对象中,一百个子类对象就有一百份偏移量,而如果放在虚基表中,而虚表指针大小是固定的,并且让所有子类对象都可以共用这张表,对,我说的是共用,每个对象的大小是一样的,结构是一样的,当然可以共用这张表格,空间上来说当偏移量超过两个,就可以提现出这种设计是节省空间。
下图是两个子类对象的内存窗口图,他们的虚基表地址都是一样的。
至于为什么存偏移量,这我也不知道啊,大佬就是这么设定的,可能以后随着学习的深入就理解了。