文章目录
前言
本篇博客作为C++:继承的知识总结。
一、继承的概念及定义
1. 继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,他允许程序员在保持原有类特性的基础上进行拓展,增加功能,这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,是类设计层次的复用。
如下所示:
class Person
{
public:
void func()
{
cout << "void func()" << endl;
}
protected:
string _name; // 名字
int _age; // 年龄
};
class Student : public Person
{
protected:
string id; // 学号
};
在上述示例中,Studend类继承Person类,Studend类被称为派生类/子类,Person类被称为基类/父类。在公有继承的方式下,Studend类继承Person类中的成员变量和成员函数。
2. 继承的定义
- 定义格式:
- 继承关系和访问限定符
如上图所示,继承关系与访问限定符的组合有9种。9种组合的继承类成员访问方式如下:
总结
- 基类private成员在派生类中无论以什么方式继承都是不可见的。(此处的不可见是指基类的私有成员还是被继承到了派生类对象中,但语法上限制派生类对象不管在类里面还是类外面都不能去访问它)。
- 基类private成员在派送类中是不能被访问的,如果基类成员不想再类外直接被访问,但需要在派生类中能访问,就定义为protected。
- 对于上述表格我们总结发现:基类的其它成员在派生类的访问方式等于成员 在基类的访问限定符,继承方式中权限最小的一个(public > protected > private)。
- class默认的继承方式是private,struct默认的继承方式是public。与class / struct的默认访问权限一致。
- 在实际运用中一般使用都是public继承,几乎很少使用protected / private继承。
二、基类和派生类对象的赋值转换
在以前,两个对象之间发生隐士类型转换时,会产生中间的临时变量来进行转换。
如下:
如果我们对引用去掉const,编译器就会报错。
这就是因为,隐士类型转换会产生中间的临时变量,而临时变量具有常属性
但对于继承体系而言,子类赋值给父类并不会产生中间的临时变量。
如下:
这就是因为基类和派生类对象的赋值转换规则(也叫切片,切割)。
-
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。
-
基类对象不能赋值给派生类对象
-
基类的指针或引用可以通过强制类型转换赋值该派生类的指针或引用,但必须是基类的指针指向派生类对象时才是安全的
三、继承中的作用域
-
在继承体系中,基类和派生类都有独立的作用域
-
子类和父类中有同名成员,子类成员会屏蔽父类对于同名成员的直接访问,这种情况叫做隐藏(重定义)。(在子类成员函数中,可以使用基类 :: 基类成员 显示访问)
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
-
注意在实际中,继承体系里面最好不要定义同名的成员
一个简单的试题。
对于下面代码中,A类的func成员函数 与 B类的func成员函数是什么关系? (隐藏 / 重定义)
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int i)
{
A::func();
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.func(10);
return 0;
}
四、继承中的默认成员函数
-
派生类的构造函数必须先调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认构造函数则必须在派生类构造函数的初始化列表显示调用
知道了在创建派生类对象时,编译器会先调用基类的构造函数,再调用派生类的构造函数。那么如何创建出一个不能被继承的基类呢?
只要将基类的构造函数的访问权限设置为私有(C++98) 或 用 final 修饰该类(C++11)
-
派生类的拷贝构造函数必须先调用基类的拷贝构造完成基类的拷贝初始化
-
派生类的operator=必须先调用完成后自动调用基类的operator=完成基类的复制
-
派生类的析构函数会在被调用完后自动调用基类的析构函数清理基类成员(可以保证派生类对象先清理派生类成员再清理基类成员的顺序)
如果在派生类中显示的调用基类的析构函数,那么派生类对象中父类部分会析构两次。如果此时A类对象中有资源的申请,那么程序将出错。
总结一下:对于构造函数,拷贝构造函数,赋值运算符重载函数而言,在创建派生类对象时,编译器会先调用对应的基类函数,完成对于基类那部分的操作,再调用派生类的。对于析构函数而言,编译器会先调用派生类的析构函数,再调用基类的析构函数。
五、继承,友元,静态成员的关系
1.继承与友元
友元关系不能继承,也就是说基类的友元不能访问派生类的私有和保护成员。
2.继承与静态成员
基类定义的static静态成员,则整个继承体系里面只有这一个这样的成员,无论派生类出多少个子类,都只有一个static成员实例
六、单继承,多继承,菱形继承
1. 单继承
单继承:一个子类只有一个直接父类时,称这个继承关系为单继承
2.多继承
多继承:一个子类有两个或以上的直接父类时称这个继承为多继承
3. 菱形继承
菱形继承:菱形继承是多继承的一种特殊情况
对于单继承与多继承而言,没有什么可讲的,菱形继承我们可以简单的通过上图中类A, B, C, D的关系看出,一个D类对象,会有两个A类成员。这就大有问题。
在上图的监视窗口可以看出,菱形继承有数据冗余和二义性的问题。
虽然我们可以通过指定成员的类名来解决二义性问题,但数据冗余就不好解决。
于是便有了虚拟继承。
七、虚拟继承解决二义性和冗余原理
上图就是一个虚拟继承。
这里我们将使用下面代码来理解虚拟继承的原理。
class A
{
protected:
int _a = 0;
};
class B : virtual public A
{
protected:
int _b = 0;
};
class C : virtual public A
{
protected:
int _c = 0;
};
class D : public B, public C
{
public:
void func()
{
B::_a = 1;
C::_a = 2;
_b = 3;
_c = 4;
_d = 5;
}
protected:
int _d = 0;
};
int main()
{
D d;
d.func();
return 0;
}
从下图可以分析出,D对象中将A类放在对象组成的最下面,这个A 同时属于B和C。
那么B和C如何去查找公共的A呢?这里是通过B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
如上图中A的地址是0x006FFC34,B的地址是0x006FFC24,二者的偏移量正好是20,也就是十六进制的14。
同理,C的地址是0x006FFC30,二者的偏移量正好是12,也就是十六进制的c。
这样C++通过虚拟继承就解决了菱形继承带来的二义性和数据冗余。
需要注意的是每个类公用一张虚基表。
上图中的d对象中B类的虚基表地址 与 dd对象中B类的虚基表地址相同。
八、继承与组合
-
public继承是一种is-a的关系(比如猫是动物的一种)。也就是说每一个派生类对象都是一个基类对象。
-
组合式一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
-
优先使用对象组合,而不是类继承
-
继承允许你根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常被称为白箱复用。在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类的依赖关系很强,耦合度高。
-
对象组合时继承之外的另一种复用选择。新的更复杂的功能可以通过组合或组装对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用被称为黑箱复用,因为对象内部细节时不可见的。对象只以黑箱的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用组合有助于你保持每一个类被分装。
总结
以上就是我对于继承的知识总结。感谢支持!!!