继承概念:
首先引入一个生活例子,普通人是一个类对象,学生是一个类对象,普通人拥有的属性学生一定会有,学生拥有的属性普通人不一定有。类比一下,把普通人抽象为A对象,学生抽象为B对象,A对象的成员B对象均有。
于是在定义类的时候,我们并不希望在AB中输入两次重复的代码,更希望通过一种简便的方式快速植入共同成员,这种方法就叫做继承
class A{};
class B : public A{};
上面的写法表示B是A的公有继承
A我们称之为基类,B称之为派生类
继承方式:
继承分为公有继承、保护继承、私有继承三类,其中最常用的是公有继承,它们的关键字与访问限定符一致,分别是public、protect、private。
不同的继承方式所对应的结果见下图👇:
注:在派生类中不可见不代表派生类不继承基类的private成员,而是在派生类中无法直接访问这些成员
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
整个继承体系里面只有一个static成员
赋值转换:
class A{
protected:
int _a = 1;
};
class B : public A{
protected:
int _b = 1;
};
B obj;
A& ref = obj; //①
A* ptr = &obj; //②
上面的代码会编译出错吗?
答案是不会,很奇怪,按理来说命名A B是两个类型,为什么就可以初始化A&和A*类型呢?
这里涉及到一个类似 切片 的过程,编译器会自动把ref和ptr指向B中基类所拥有的成员,而忽略B中独有的成员。
(vs2019监视现象)
这里并不会涉及到隐式类型转化,原因就在于我们上述代码没有报错,加入有隐式类型转化,那么必然会生成具有常性的临时变量,会造成权限放大的错误。
成员隐藏:
class A{
protected:
int _num=1;
};
class B : public A{
public:
void print(){cout<<_num<<endl;}
};
很简单的一段代码,甚至可以猜到输出内容是1,B继承了A中的_num,
但是如果我们在B中也设置一个成员变量,也叫_num(缺省值设置为2),
那么输出结果又是什么?
结果是2
可以解释为编译器自动地把这一块部分识别成当前类域的变量,这种现象我们就叫做成员构成隐藏,如果想要使得打印结果成为1,需要加上访问限定符,cout<<A::_num<<endl;
成员函数的隐藏与此同理,特别需要区分隐藏和函数重载的不同,隐藏是不同作用域下的(只需要函数名相同就构成隐藏),而重载的作用域必须相同
默认成员函数:
派生类在实例化的时候会先调用基类中的默认成员函数来处理共同部分,而后在处理独有部分
以构造函数为例子:
class A{
public:
A(int x)
:_num1(x){}
protected:
int _num1;
};
class B : public A{
public:
B(int x,int y)
:A(x)
,_num2(y){}
protected:
int _num2;
};
菱形继承:
C++的缺陷之一:多继承引发的菱形继承
菱形继承最大的问题就是数据冗余和二义性问题,很容易导致编译出错。
实际工作或学习中我们应该避免多继承,避免菱形继承。
为了解决菱形继承引发的一系列问题,C++引入了虚拟继承,由于菱形继承实用性非常非常底下,不做过多介绍。