1.继承规则
继承的本质是复用,是结构上的继承而不是内容上的继承,近似于在子类中声明了父类的成员变量。
(1)写法:class student : public person
派生类(子类),继承方式,基类(父类)
它们都分public、protected、private三种,但是含义并不相同。
(2)区分访问限定符和继承方式
访问限定符修饰的是在当前类里面是否可以访问,而继承方式是指在继承的类里面的访问方式,并且一个成员继承后的访问限定符是父类的访问限制符和子类继承方式两者间的较小权限。如果A有public成员a,而以protectd继承,因此子类成员的访问限制符是protected。
访问权限由大到小是public>protected>private,protected和private在继承语法外没有区别。比如protected成员public继承后是protected,protected成员private继承后是private,public成员public继承后是public,这都非常容易理解。
#include <iostream>
using namespace std;class A
{
public:int _a = 0;
protected:int _b = 1;
private:int _c = 2;
};class B : public A
{
public:void GetNum(){cout << _a << " " << _b << " " << _d << endl;}int _d = 3;
};int main()
{B().GetNum();return 0;
}
但是要注意private成员无论以何种方式继承,不管是public、protected还是private继承,最终子类成员的访问权限都是private且这个private成员不能被子类使用(包括访问,修改等)
看起来这个成员没有被继承,但实际上子类包含该成员。我们可以从监视窗口和类的大小双重验证这个结论。
(3)在继承里还规定友元关系不能继承,父类的友元不是子类的友元,只有子类自己声明友元
(4)形象理解:父的成员是protected意味着这些成员对外是个秘密,但在自己家庭中不算个秘密,但家庭中的每个人都有义务对外保守这个秘密,即protected无论以何种方式继承对外都不可见。
如果父成员有个private意味着这个成员是自己的秘密,不能给任何人说,比如家里有个隐藏的地下室。但是在继承给自己孩子这个房子时,孩子不知有这个地下室,但这个地下室真实存在,即private无论以何种方式继承对子对外均不可见,但这个private成员真实存在。
父的朋友不是子的朋友,即友元关系不能继承。
(5)protected和private区分
protected在继承以后才有意义。protected和private对外的功能都一样,都是不可见,它们区别在protected对整个继承体系开放(前提没有子类以private继承它),而private是只有该类能访问,其子类也不能访问。
当中途被继承为private后,对子该成员依然不可见。
(6)默认继承方式
我们也可以选择不写继承方式,这个时候class默认私有继承,struct默认以public继承,注意这和默认访问限定符一样,但这是两个概念。
2.继承的函数调用
我们先尝试解读下面的代码,这能帮助我们初步理解继承函数调用的特征:
#include <iostream>
using namespace std;class A
{
public:A(int a, int b, int c):_a(a),_b(b),_c(c){}void GetNum(){printf("%d %d %d\n", _a, _b, _c);}int _a = 0;
protected:int _b = 1;
private:int _c = 2;
};class B : public A
{
public:B():A(7, 8, 9), _d(5){}void GetNum(){printf("%d\n", _d);}int _d = 3;
};int main()
{B().A::GetNum();B().GetNum();return 0;
}
结果是
(1)构造函数
A作为父类它的构造函数正常写就是了,我们重点关注B的构造函数。
我们知道,构造函数实际构造的顺序是声明的顺序而非初始化列表的顺序。这里我们可以理解为B子类的第一个成员声明是匿名的A,在初始化时要根据A的构造函数把A当作一个整体用匿名初始化对象的方式A(7,8,9)来处理,不能直接对A的成员变量直接初始化,继承的A对象和创建一个A对象一样都不会复制函数和static变量,在需要时都是直接去A里面取,即整个继承体系共享一套函数和static变量
(2)函数的隐藏
父
子
在继承体系中是允许出现同名函数的,但是这并不构成函数重载,因为函数重载的前提是必须在同一个作用域定义两个同名函数,而这里很明显是在两个类域里定义的同名函数,所以一定不构成重载,而是构成函数的隐藏(只需要同名就构成隐藏了,要和后面的多态区分开)。
如果构成了函数隐藏的话,应该怎么区分调用这两个函数呢?很多人会以为能像函数重载的调用那样根据参数匹配程度来调用,但这个逻辑在继承里走不通,万一我就想利用隐式类型转换调用子的成员函数,但是参数却匹配了父的成员函数呢?万一是我参数写错了导致匹配错误的情况呢?这些问题都会导致歧义的发生。所以规定,只要构成函数隐藏,调用父类一定要指明类域,调用当前类的不用指明,如果不指明类域,一律只按调用当前类的函数来处理,就算此时调用匹配父类的函数参数。
我们这里还可以学到,虽然我们不能直接访问父类的私有成员_c == 9,但是我们可以选择调用父类的非private和protected函数,从而达到间接访问的目的。
(3)析构函数与构造函数
析构函数的理解相对复杂,我们先看一下下面的代码,这能帮助我们深入理解构造和析构函数:
#include <iostream>
using namespace std;class A
{
public:~A(){delete _a;}int* _a = new int(1);
};class B : public A
{
public:B():_b(_a){}~B(){A::~A();*_b = 2;}int* _b;
};int main()
{B a;return 0;
}
结果是报错,接下来我会详细分析里面的代码
为什么写作A::~A();
析构函数的名字会被统一处理为destructor(),父子类的析构函数同名构成了函数隐藏,因此想要调用父的析构函数,就必须要指定类域,这个底层细节需要我们记住。这个特殊处理的原因在多态我会提及。
为什么报错?
由于在父类的析构函数调用后间接使用了父类的成员,所以出现越界访问的情况。
我们还能发现B的构造函数借助了A的成员变量。这里要区分开的是,在子类初始化父类时只能以父类为整体去调用它的构造函数如A(7,8,9),而本质上继承的只是它的成员变量,在子类的构造函数中可以直接用父类的成员。
如此一来,我们能发现,子类的构造函数可能会依靠父类的成员变量,若父类先析构,那在子类需要用到父类成员变量时就可能会发生越界访问。所以规定,父类的构造函数一定先于子类,父类的析构一定晚于子类(构造先父后子,析构先子后父)。
在实现层面上,由于子类的第一个成员变量默认是父类A,所以我们可以不用关心,毕竟初始化顺序是按声明而不是初始化列表。但是我们要避免显式地调用父类的析构,即A::~A()不要出现在我们的继承代码中。当子类的析构函数走完后,会去自动调用父类的析构。同理,如果没有显式写构造,编译器也会首先自动调用父类的默认构造。