目录
简单的来说,私有数据无论是哪种继承方式,继承下来的私有数据都是隐藏的,无法使用的,
protected继承下来的数据只能用基类的成员函数访问,public继承下来的公有还是公有。
完成继承后,在初始化的时候,我们可以通过派生类来赋值给基类的对象 / 基类的指针 / 基类的引用。
我们可以理解为将派生类切片,切掉继承来的那一部分,将那一部分可以赋值给一个父类对象/父类指针/父类的引用
一句话:静态成员不算是被子类继承,子类和父类共享这一份静态成员数据。
首先我们知道单继承---一个类继承另一个类,算作是单继承:
而对于多继承来说,是一个类同时继承了一个以上的类:
多继承在上述的方式都是合理的,正常使用。
而菱形继承是一种特殊的多继承:
但是此时的菱形继承区有一个大坑:
我们知道子类会继承父类的数据,但若子类多继承的父类同时拥有上一继承的类的同一份数据?即菱形继承的最底下这个类拥有两份上上层类的数据。那么在最底下这个类访问的时候就会重现访问冲突这样的问题。
- class Person
- {
- public:
- Person(string name)
- {
- this ->name = name;
- }
- string name; // 姓名
- };
- class Student : public Person
- {
- public:
- Student(int num,string name):Person(name)
- {
- this->_num = num;
- }
- int _num; //学号
- };
- class Teacher :public Person
- {
- public:
- Teacher(int num, string name):Person(name)
- {
- this->_num1 = num;
- }
- int _num1;//职工编号
- };
- class Assistant :public Student, public Teacher
- {
- public:
- Assistant(int num, int num1, string name1, string name2, string course) :Student(num, name1), Teacher(num1, name2)
- {
- this->course = course;
- }
- string course;//学习课程
- };
- void Test()
- {
-
- string name = "张三";
- string name1 = "李四";
- string couse = "生物";
- Assistant A(001, 002, name, name1,couse);
- cout << A.name << endl;
- }
当我们成功初始化A的时候,再次调用他的name,那就会说,访问不明确。那是因为在继承的时候student与teacher类都具有name,此时直接访问就会产生二义性,编译器报错不明确的name,当然我们还是可以解决这个办法:
通过作用域确定我们访问的name:
- void Test()
- {
- string name = "张三";
- string name1 = "李四";
- string couse = "生物";
- Assistant A(001, 002, name, name1,couse);
- //cout << A.name << endl;
- cout << A.Student::name << endl;
- cout << A.Teacher::name << endl;
- }
那么二义性的问题得到了解决,但是数据冗余的问题得不到解决,那么当被继承的类若果数据够大的话,反而与用继承实现代码复用的初心背道而驰,大大浪费了空间。
那么该如何解决呢?经过祖师爷们的苦思冥想,c++在之后提供了一个关键字vitual,我们在菱形继承的第一次继承时采用虚继承的方式,这样数据冗余与二义性的问题都得到了解决:
我们将上述代码使用虚继承修改之后:
- class Person
- {
- public:
- Person(string name)
- {
- this ->name = name;
- }
- string name; // 姓名
- };
- class Student :virtual public Person
- {
- public:
- Student(int num,string name):Person(name)
- {
- this->_num = num;
- }
- int _num; //学号
- };
- class Teacher :virtual public Person
- {
- public:
- Teacher(int num, string name):Person(name)
- {
- this->_num1 = num;
- }
- int _num1;//职工编号
- };
- class Assistant :public Student, public Teacher
- {
- public:
- Assistant(int num, int num1, string name, string course) :Student(num, name), Teacher(num1, name),Person(name)
- {
- this->course = course;
- }
- string course;//学习课程
- };
- void Test()
- {
- string name = "张三";
- string course = "生物";
- Assistant A(001, 002, name, course);
- cout << A.name << endl;
- }
此时可以看到我们是可以直接访问到name的,我们对于最后继承的类也要添加最初父类的构造函数,否则报错,这是为什么呢?
这就要看我们虚继承的底层实现了。
我们首先简单的看一下name的地址:
此时发现name只有一份了,Student与Teacher的name是一样的了,即只有一份name了,我们还可以侧面证明:
- void Test()
- {
- string name = "张三";
- string course = "生物";
- Assistant A(001, 002, name, course);
- A.Student::name = "李四";
- A.Teacher::name = "王五";
- cout << A.name << endl;
- }
再次显示给stdent和teacher的name,结果是输出王五,将student与teacher给的名字交换一下,输出又变成李四,由此可以得出,name只有一份,输出的是最后name赋值的新值,也就是name最后的更新。
为了更好的分析,我们写一个简化了的菱形虚拟类:
- 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;
- }
之后再观察内存窗口:&d的内存窗口
我们的成员变量是整形,四个字节,因此一行的地址代表一个成员变量。
刚执行完_a=1;所以这里的0x0000007c710FFB60就是我们的_a,
执行完_a=2,这里的_a就是2了,这两行运行完又证明了虚继承之后_a,是只有一份的。
执行完_b=3时,此时的内存地址一跃而上来到了0x0000007c710FFB40,其值为3
执行完_c=4时,内存地址又往下走了,来到了0x0000007c710FFB50,其值为4
执行完_d=5,内存还是往下走,来到了0x0000007c710FFB58,其值为5.
之后我们去掉虚继承,再看此时的_a:
只看_a的话我们可以看到_a的地址发生了变化,即这里是两个_a,分别存储了值。
其次他的内存一直是递增的,而我们虚继承不是。
再次观察运行完之后的两者的d对象的内存:
我们发现不用虚继承时B中就是_a与_b,同理C,之后就是_d,内存是递增的。
当用虚继承时,_a的内存被单独拿出来也就是A中的内存,且被放在了这一块地址最后面,而B和C没有了_a,却多了一个不知名的地址。之后就是D,里面有一个_d。
对于这里的不知名地址又是啥呢?
对于我们之前学习继承,我们可以用子类对象去赋值父类对象,父类指针,父类引用。那是因为地址是按声明顺序存储,我们很容易切片内存而找到父类的那一部分,但此时用了虚继承之后,内存反而不是如此的顺序,切片也就更加困难,那我们该如何解决这个问题呢?
因此我们需要找到该地址的偏移量,通过偏移量来计算出此时对于这里A的地址,所以真相就是不知名的地址存放了一些信息,包括了对于这里来说就是距离A的偏移量。
除了对象d,我们创建B的一个对象bb,bb._a=1;bb._b=2;
可以看到此时B里的也是如此,f6 7f 00 00..对应的地址就是存放了距离A的偏移量。
通过存放偏移量(虽然还是少量的增加了空间消耗),实现了继承了父类成员不在数据冗杂,且不存在二义性。
对于我们的io流,实际上就实现了一个菱形继承:
istream继承ios,ostream继承ios,iostream多继承istream与ostream。
那么对于我们,在使用多继承的时候,最好就不要使用菱形继承(对于不是对自己的实力很相信的),我们就使用多继承就行,但不要使用这种特殊的继承。