概念
继承类似于函数的复用,它是类设计层次上的复用。
再减少代码量的同时,还能在原有类的基础上扩展新的功能。
被继承的叫基类或父类,得到继承的类叫派生类或子类。
子类可以继承父类的数据和方法。
因为类的方法都存放再代码段,所以继承的概念在内存层面是数据之间的继承。
定义
语法如下
class a { //...... }; class b : public a { //...... }
public是继承方式,继承方式有如下几种
共有继承:public
保护继承:protected
私有继承:private
继承方式和访问限定符的关系如下
类成员 / 继承方式 |
public 继承 |
protected 继承 |
private 继承 |
父类的 public 成员 |
子类是public成员 |
子类是protected 成员 |
子类是private 成员 |
父类的protected 成员 |
子类是protected 成员 |
子类是protected 成员 |
子类是private 成员 |
父类的private 成 员 |
在子类中不可见 |
在子类中不可见 |
在子类中不可见 |
不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它
如果父类的成员 想被子类访问,不想被其他类访问,父类的成员就 加上protected(保护)限定符
继承方式和访问限定符取权限小的,权限关系如下:
public(共有) > protected(保护) > private(私有)
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public
赋值
子类对象可以给父类对象赋值,父类对象不能给子类对象赋值
子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用
子类是继承父类的数据,父类有的子类一定有,子类有的父类不一定有
形象示意图
这种赋值更形象化的说法叫——切片
作用域
在继承体系中父类和子类都有独立的作用域
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义,在子类成员函数中,可以使用 基类::基类成员 显示访问
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
如下是测试代码,
有兴趣的可以粘到编译器上运行一下
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆 class Person { protected : string _name = "小李子"; // 姓名 int _num = 111; // 身份证号 }; class Student : public Person { public: void Print() { cout<<" 姓名:"<<_name<< endl; cout<<" 身份证号:"<<Person::_num<< endl; cout<<" 学号:"<<_num<<endl; } protected: int _num = 999; // 学号 }; void Test() { Student s1; s1.Print(); }; // B中的fun和A中的fun不是构成重载,因为不是在同一作用域 // B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。 class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { A::fun(); cout << "func(int i)->" <<i<<endl; } }; void Test() { B b; b.fun(10); };
子类的默认成员函数
关于常用的默认成员函数可参考如下两篇文章
子类不能显式的定义,只能调用父类的构造,析构等默认成员函数
构造函数:如果父类不提供默认构造,需要在子类的初始化列表显式调用
拷贝构造:子类必须显式调用符类的拷贝构造
operator=:子类必须显式调用父类的operator=
析构:子类不能显式的调用父类的析构函数,不然会报错
子类对象初始化先调用父类构造再调子类构造
子类对象析构清理先调用子类析构再调父类的析构
继承与友元
父类与其他类或函数的友元关系不能传递给子类
继承与静态成员
父类定义了static静态成员,则 整个继承体系 里面只有 一个 这样的成员。无论派生出多少个子
类,都只有一个静态成员的实例
菱形继承
单继承::一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
多继承的语法:
class 父类类名 :继承方式 子类类名, 继承方式 子类类名...... 中间用逗号分隔
菱形继承:菱形继承是多继承的一种特殊情况
菱形继承是把同一个父类的数据继承了多次,这会造成数据冗余和二义性的问题
菱形虚拟继承
为了解决菱形继承的问题,C++用菱形虚拟继承来解决数据冗余和二义性的问题
语法:
class 父类 : virtual public 子类
virtual是关键字,说明该继承是虚拟继承
虚拟继承的位置(小编把情况搞复杂让大家理解清楚):
红线的继承需要虚拟继承,下面是虚拟继承可以出现的位置
总结:
如果一个父类要被继承两次以上,一定要用虚拟继承
虚拟继承可以传递
多次虚拟继承不影响最后的结果
原理
虚拟继承是怎么解决数据冗余和二义性的呢
问题:
子类可以继承父类的数据和方法,由于类的方法都存放在代码段,在内存层面是数据之间的继承。菱形继承导致的问题是因为父类的数据被继承了多次,在内存中一份数据就被储存了多次,同名的变量会造成二义性。
解决:
重复的父类会被子类存放在最下面,父类把数据继承给子类地同时,还会再让子类存一个指针,指针指向一张表。这个指针叫虚基表指针,这个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面本应该重复的父类。
下面是内存形象化示意图
虚拟继承就是通过上述方法让多个父类和一个子类管理一份数据而不是同名的多份数据。
小编用如下代码调试,带大家感受一下
class A { public: int _a; }; // class B : public A class B : virtual public A { public: int _b; }; // class C : public A 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; }
上述代码的示意图如下