目录
一,继承的特点
继承基类成员访问方式的变化
基类和派生类对象赋值转换
派生类的默认成员函数
继承与友元
继承与静态成员
复杂的菱形继承及菱形虚拟继承
菱形继承
虚继承
虚继承的底层实现
多继承的例子:
多继承与组合类
多继承总结:
简单的来说,私有数据无论是哪种继承方式,继承下来的私有数据都是隐藏的,无法使用的,
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。
那么对于我们,在使用多继承的时候,最好就不要使用菱形继承(对于不是对自己的实力很相信的),我们就使用多继承就行,但不要使用这种特殊的继承。