一、继承简介
众所周知,C++是面向对象(OOP)的一门语言,而面向对象的三大特性为封装,继承,多态。由此可见,继承是面向对象程序设计中的重要特性之一,本篇中将介绍一下C++中的继承特性及用法。
概念
继承机制是面向对象程序设计中实现代码复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生的新类,称为派生类 或 子类,而原有特性的旧类称为基类 或 父类
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,此前我们接触的复用都是函数复用,而继承是类设计层次的复用
#include<iostream>
using namespace std;
class Base //基类
{
public:
void method()
{
cout << "Base::method()" << endl;
}
protected:
int B_var;
};
class Derived : public Base //派生类
{
protected:
int D_var;
};
int main()
{
Derived d; //实例化一个派生类对象
d.method(); //用这个派生类对象调用基类中的method()方法
cout << sizeof(Base) << endl; //打印基类的大小
cout << sizeof(Derived) << endl; //打印派生类的大小
return 0;
}
输出结果:
Base::method()
4
8
根据上述例子可以看出,继承后的基类成员变量和方法都会变成派生类的一部分,在public继承权限下可以通过派生类对象进行调用基类的方法
定义
通过上面例子,我们对继承有了一定的了解,下面是对继承的定义格式:
class 派生类名 : 继承权限 基类名
{…};
继承又分为单继承和多继承
单继承为一个派生类只有一个基类,派生类继承了该基类的成员,其定义格式:
class Base //基类
{};
class Derived1 : public Base //派生类1
{};
class Derived2 : protected Base //派生类2
{};
class Derived3 : private Base //派生类3
多继承为一个派生类可以有多个基类,继承了多个基类的成员,其定义格式:
class Base1 //基类1
{};
class Base2 //基类2
{};
class Base3 //基类3
{};
class Derived : public Base1, protected Base2, private Base3 //派生类同时继承3个基类
{};
此处的继承权限为任意给出,按照具体实现功能给出,在稍后会进行详细讲解
对象模型
通过对象模型能够更加深刻的认识到单继承和多继承的具体继承方式:
单继承
#include<iostream>
using namespace std;
class Base
{
public:
int _b;
};
class Derived : public Base
{
public:
int _d;
};
int main()
{
Derived d;
d._d = 1;
d._b = 2;
return 0;
}
通过上图可以看出Derived公有继承Base,其对象模型为:
派生类继承基类的_b,并且将继承自基类成员置于自己特有成员之上,这一点,在VS调试内存窗口下可以进行验证
多继承
#include<iostream>
using namespace std;
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derived : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derived d;
d._d = 1;
d._b1 = 2;
d._b2 = 3;
return 0;
}
派生类分别继承自基类Base1和Base2,其对象模型如下所示:
在内存窗口下验证,得出该对象模型正确
注意:**在派生类的继承列表中继承基类成员的次序和对象模型中基类成员的上下次序有关,**此处,我们先继承Base1后继承Base2,所以在对象模型中继承自Base1的成员在最上,而继承自Base2的成员在下面(相较于继承自基类的成员来说),若是在继承列表中颠倒次序,那么对象模型中继承自基类的成员也会颠倒次序进行存放。
二、继承权限
类成员、继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
ps:
有人可能会发现,这三种继承方式有似曾相识的感觉,没错,但是,它们是在访问限定符出现过,此处,需要区别继承方式和访问限定符的区别和应用场景
继承权限:派生类继承基类时对基类中的成员(变量和函数)是否可以在派生类中进行访问修改操作进行限制
访问权限:对类中定义的成员(变量和函数)在程序的其他地方是否可以进行访问操作进行限制
总结:
1、基类中的派生类无论以何种方式继承在派生类中都是不可见的。这里的不可见是指基类的私有成员确实被继承到了派生类对象中,但是语法上限制派生类对象不管在类内还是类外都不能进行访问
2、比较一下private和protected会发现,private继承方式基类成员在派生类中不可见,在类外不能进行访问,protected继承方式基类成员在派生类中能够被访问,在类外同样不能进行访问。若想实现在派生类中的基类成员可以访问但是在类外不能进行访问,可以用protected继承方式
3、从整个表格中可以发现:此处姑且对成员的访问权限进行一个排序:public > protected > private,通过某种继承方式继承后的基类成员在派生类中的访问权限为继承方式和原访问权限的最小值,此处可与上表一一验证求证该结论
4、使用关键字class时默认的继承方式为private,使用struct时默认的继承方式为public,建议显式写出继承方式
ps:默认访问权限和默认继承权限一致,class为private,struct为public,因为struct要兼容C
5、在实际使用中,一般都是使用public继承,因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
三、基类和派生类对象赋值转换
1、派生类对象可以赋值给基类的对象 、基类的指针、基类的引用
2、基类对象不能赋值给派生类对象
3、基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针指向派生类的对象时才是安全的。
#include<iostream>
using namespace std;
class Base
{
public:
void method()
{
cout << "Base::method()" << endl;
}
protected:
int B_var1;
int B_var2;
};
class Derived : public Base
{
public:
int D_var;
};
int main()
{
Derived derived;
//派生类对象可以赋值给基类对象、指针、引用,编译成功
Base base = derived;
Base* pb = &derived;
Base& rb = derived;
//基类对象不可以赋值给派生类对象,报错
derived = base;
//基类的指针可以通过强制类型转换赋值给派生类的指针
pb = &derived;
Derived* pd1 = (Derived*)pb; //这种情况转换可以
pd1->D_var = 10;
pb = &base;
Derived* pd2 = (Derived*)pb; //这种情况转换虽然可以,但是存在访问越界的问题
pd2->D_var = 10;
return 0;
}
继承中的作用域
1、在继承体系中基类和派生类都有独立的作用域
2、同名隐藏(重定义):派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的访问(在派生类成员函数中,可以使用 基类::基类成员 显示访问)
3、需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏
#include<iostream>
using namespace std;
//Base中的method()和Derived中的method()不构成重载,因为不在同一作用域
//Base中的B_var和Derived中的B_var构成同名隐藏
//Base中的method()和Derived中的method()构成同名函数隐藏
class Base
{
public:
void method()
{
cout << "Base::method()" << endl;
cout << B_var << endl;
}
protected:
int B_var = 0;
};
class Derived : public Base
{
public:
void method()
{
cout << "Derived::method()" << endl;
cout << B_var << endl;
}
protected:
int B_var = 1;
int D_var;
};
int main()
{
Derived d;
d.method();
return 0;
}
输出结果:
Derived::method()
1
派生类的默认成员函数
构造函数
>> 基类如果没有显示定义构造函数,派生类也可以不用定义
>> 基类具有无参或全缺省的构造函数,派生类也可以不用定义(如果派生类没有定义,编译器将会生成默认的构造函数)
>> 如果基类具有带参数的构造函数(不是全缺省),则派生类的构造函数必须显示提供,而且要在其初始化列表的位置显示调用基类的构造函数
拷贝构造函数
>> 基类如果没有定义,派生类也可以不用定义,此时,两个类都采用默认拷贝构造函数(前提:类中不会涉及资源管理等的深浅拷贝问题)
>> 基类如果显示定义了自己的拷贝构造函数,派生类也必须显式定义,而且要在其初始化列表的位置显式调用基类的拷贝构造函数
赋值运算符重载
>> 如果基类没有定义,派生类也可以不用提供,除非派生类需要在赋值期间做其他操作,视具体情况而定
>> 如果基类定义了赋值运算符重载,一般情况下派生类也要提供,并且要在其中调用基类的赋值运算符重载
析构函数
>> 派生类的析构函数会在被调用完成后自动调用完成后的基类的析构函数清理基类成员。以此保证先清理派生类对象成员后清理基类成员的顺序
在继承体系中,基类和派生类的构造和析构的调用次序
构造次序:
先进入派生类函数体内的构造函数初始化列表位置,再调用初始化列表中基类的构造函数进行初始化
析构次序:
先进入派生类函数体内释放派生类的成员资源,当派生类成员资源全部释放完成后,再调用基类的析构函数,释放基类资源
其总的次序概括来说为(执行次序):基类构造->派生类构造->派生类析构->基类析构
友元的继承
友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员
ps:
友元只是能访问指定类的私有和保护成员的自定义函数,不是被指定类的成员,自然不能继承
静态成员的继承
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例
在继承关系中,派生类有各自的成员变量组成的数据段,但是成员函数和静态成员的内存是公有的
菱形继承
菱形继承又称为钻石继承,即将单继承和多继承结合起来
现有如下代码表示菱形继承,它的继承关系如图所示:
#include<iostream>
using namespace std;
class Base
{
public:
int _b;
};
class C1 : public Base
{
public:
int _c1;
};
class C2 : public Base
{
public:
int _c2;
};
class Derived : public C1, public C2
{
public:
int _d;
};
int main()
{
return 0;
}
如图所示,这就是菱形继承的对象模型,很明显地,它存在着问题,即二义性和代码冗余:
二义性----如果用派生类Derived定义出来的对象来直接调用继承自基类中的成员变量,由于Derived中分别继承了C1和C2中的_d,它的空间中存储了两份基类Base中的成员,编译器不知道要调用哪个而报错
代码冗余----因为Derived中继承了两份Base中的成员,这两份内容一模一样,造成代码冗余,空间浪费,而且如果继承自基类中的成员达到一定的数量级后,会严重的占用系统资源
解决办法
1、让其访问明确,即加上访问限定符::
比如上述例子,d.C1::_b或d.C2::_b,但是这个办法并没有解决代码冗余的问题
2、想办法让基类Base中的成员只在Derived中存储一份,由此,引入了菱形虚拟继承
菱形虚拟继承
虚拟继承可以解决菱形继承的二义性和代码冗余的问题,如上述的继承关系,只需在C1和C2的继承列表中继承方式之前加上virtual即可。需要注意的是,虚拟继承在现阶段的唯一作用就是解决多重继承的冗余和二义性问题,问,菱形虚拟继承的对象模型是什么样的?大小多少?
#include<iostream>
using namespace std;
class Base
{
public:
int _b;
};
class C1 : virtual public Base
{
public:
int _c1;
};
class C2 : virtual public Base
{
public:
int _c2;
};
class Derived : public C1, public C2
{
public:
int _d;
};
int main()
{
Derived d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
cout << sizeof(d) << endl;
return 0;
}
//打印出来的d的大小24
下图是菱形虚拟继承中派生类的对象模型:如图所示,对派生类Derived实例化的对象在内存中进行分析,发现结果或许不如预期的一样,首先打印出来的大小为24,其次在内存窗口中打开对象空间中的内容时,发现在菱形继承中基类的成员数值位置变成了两个地址,分别打开这两个地址,发现每个地址的指向分别都为八个字节的数值。
结论:这两个表为虚基表(虚表),存储指向这两块空间地址的指针称为虚基表指针。虚基表中存的是偏移量,前四个字节为相对于自身的偏移量,后四个字节为相对于基类Base成员的偏移量,通过偏移量可以找到Base的_b
(其中,虚基表中的后四个字节14和0c为十六进制,分别对应十进制的20和12)
关于上述例子的菱形虚拟继承的原理解释: