- 三种继承关系与三种访问限定符的关系
- 父类和子类对象赋值转换(切片)
- 隐藏的定义及和重载的区别
- 子类的默认构造函数怎么写
- 子类构造函数
- 子类拷贝构造函数
- 子类赋值运算符重载
- 子类析构
- 杂项(了解即可)
- 菱形继承
- 数据冗余和二义性
- 虚继承作用及操作
- 虚继承原理
- 组合和继承
总结一下:说了3个东西,知道的可以跳过
- 只用public继承
- 父类不要用private成员,因为子类无法直接访问
- 继承与访问限定符的关系是权限缩小
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
通过这张表可以得出一个规律,继承关系与访问限定符的关系是min的关系。
ps:其实计算机很多设计都是设计成权限缩小的。在读写问题上也是如此。因此这个规律也很好记。
举个例子:public成员被protected继承,min(public, protected) = protected,因此派生类继承下来的父类成员在子类是protected的。
还有一个重点:我们看到父类的private成员不管是什么继承,在子类都是看不见的。看不见只是不可以使用的意思,并不是没有继承下来。
如图:
ps:子类是不能直接访问父类的私有成员,但不是不能访问,父类可以拿一个函数接口来获取父类的私有成员。
这是语法规定,好处是可以保证封装性。同时我们发现,protected成员在public和protected继承下,分别在子类是public和protected的,可以被子类访问。
如图:
注:protected和private在非继承情况下都是类外不可访问,没有区别。protected和private只有在继承的情景下才有区别。在继承情景下:父类的private成员子类是不可以直接访问的,父类的protected成员子类在public和protected继承方式下是可以直接访问的。
可以认为protected就是为了继承情景才产生的。
所以一般父类成员一般都是public或者protected,很少用private。
父类和子类对象赋值转换(切片)
总结一下:这里知道四个东西即可,知道的可以跳过
1.对象切片
2.指针切片
3.引用切片
4.子类切片给父类(父类不能给子类)
所谓切片,肯定是大的切给小的,因此是子类切片给父类。(父类不能给子类)
切片有三种形式,对象切片,指针切片,引用切片。
总结一下:若知道的可以跳过
- 隐藏指父子类成员重名时,子类只能看见自己的成员
- 隐藏是不同作用域成员名相同,重载是相同作用域函数名相同
- 父子类成员不要同名,也就是不要出现隐藏。
隐藏是指父类和子类成员名(函数或者变量)相同,子类继承下来后子类只会看到子类自己的成员。
若想调用父类的func,要加作用域限定符::
这里和函数重载做一下区别:
虽然它们参数不同,但是两个func并不构成重载。因为重载的前提是在同一个作用域。由于这是两个不同的作用域,因此是隐藏。
总结:这里不要跳过,全部看完
- 子类构造函数组成部分:显示调用父类构造+子类成员初始化
- 子类拷贝构造组成部分:显示调用父类拷贝构造+子类拷贝自己的成员
- 子类赋值运算符重载组成部分:显示调用父类运算符(加作用域限定符)+子类拷贝自己成员
- 子类析构函数组成部分:只析构子类的成员,父类析构不准调用,因为C++自己会帮你调用
- 对于子类对象,先构造父类部分,再构造子类部分,先析构子类部分,再析构父类部分。(创建对象就是压栈,析构对象就是出栈)
- 背住后自己写一遍这四个默认构造函数就会了。
父类
person(string _s)
{
name = _s;
}
子类
stu(string s, int _id)
:person(s)
,id(_id)
{}
ps:person是自定义类型,如果person没有默认构造函数,必须用初始化列表初始化。
子类拷贝构造函数父类
person(const person& p)
{
name = p.name;
}
子类
stu(const stu& s)
:person(s)
,id(s.id)
{}
ps:拷贝构造中,person是自定义类型,必须要用初始化列表初始化。因为person的拷贝构造不可能是默认构造函数。
子类赋值运算符重载父类
person& operator=(const person& p)
{
if (this != &p)
{
name = p.name;
}
}
子类
stu& operator=(const stu& s)
{
person::operator=(s);
id = s.id;
}
注意:这里子类调用父类的赋值运算符重载函数必须要指定作用域,否则子类会隐藏父类的operator=,导致自己调用自己,陷入死递归。
子类析构父类
~person(){}
子类
~stu(){}
不允许写person::~person(),这样会对父类的内存空间析构两次。
杂项(了解即可)- 友元关系不能被继承
- 父类的静态成员子类继承后两个静态成员是同一个
- 如果要写一个不能被继承的类,让父类的构造函数是private就可以了。因为子类无法调用父类的构造函数,因此子类也无法创建对象
- 可以用静态成员来计算子类一共创建了多少次,只要在父类的构造函数中对这个静态成员++。每次创建一个子类对象,子类对象去调用父类对象的构造函数,会让静态成员++。(虽然这种写法没什么卵用)
总结关键点:知道的跳过即可,不知道的重点复习+自己写一个菱形继承来验证。
- 菱形继承的缺点:数据冗余+二义性
- 虚继承的作用和如何写虚继承
- 虚继承原理
数据冗余和二义性
此处简易自己写一个菱形继承来验证一下。
- 数据冗余很好理解,就是student有一个name,teacher也有一个name。
- 二义性就是当我访问name这个成员的时候,编译器不知道我访问的是student的name还是teacher的name。
二义性可以用作用域限定符来解决。但是数据冗余暂时没有办法解决。
虚继承作用及操作关键字:virtual。虚继承可以解决二义性和数据冗余问题。
在哪里写virtual呢?在菱形的腰部加上virtual
总结一下:看得懂这句话和这个图就可以跳过了。看不懂建议自己写代码看底层结构。
虚继承的类会存多一个指针的地址,这个指针指向一个内存,这个内存被称为虚基表。虚基表里面存的是虚继承的类离父类成员变量的地址偏移量(单位是字节)
关系:
底层:
验证一下上面的底层结构是否如此:
关系结构代码如下:就是上面那个图的关系。
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.a = 1;
d.b = 2;
d.c = 3;
d.d = 4;
}
打开内存,看&d的内存分布。如下:
我们说了,红色圈起来的是指向虚基表的指针,现在去看一下这两个指针。
可以发现,14和0c就是对应的偏移量,在10进制下,分别是20和12.
自己去算一下,刚好就是B到A成员的偏移量和C到A成员的偏移量大小
ps:第一行是空着的,和多态+菱形继承+虚继承有关。这一行是为了存自己的类的虚表指针的偏移量。一般都是向上挨着的,因此一般是-4,也就是往上4个字节。
如果想了解,建议写代码去调试看。
如果不想了解,背住这个是存离自己的虚表指针的偏移量,一般为4即可。
写代码验证的时候,A类写一个虚函数,B类写一个虚函数,C类写一个虚函数,D类写一个虚函数。然后调试你就可以看到这个完整的过程了。(重不重写虚函数对现象没有影响,毕竟重写只是覆盖虚表里面的函数指针而已)
知道以下几点即可:
- 关系是is-a就用继承,关系是has-a就用组合
- 关系不明确的就用组合就用组合
- 组合无法访问其他对象的保护成员,有封装性和高内聚。继承可以访问父类的保护成员,耦合性提高了。



