Loading

欢乐C++ —— 15. 继承

参考:

https://www.cnblogs.com/QG-whz/p/5132745.html

简述

还记得我们之前写的编译期多态与运行期多态吗?继承是运行期多态的基石。下一节则是模板,编译期多态的基础。

运行期多态
  • 优点
    1. OO设计中重要的特性,对客观世界直觉认识。
    2. 能够处理同一个继承体系下的异质类集合。
  • 缺点
    1. 运行期间进行虚函数绑定,提高了程序运行开销。
    2. 庞大的类继承层次,对接口的修改易影响类继承层次。
    3. 由于虚函数在运行期在确定,所以编译器无法对虚函数进行优化。
    4. 虚表指针增大了对象体积,类也多了一张虚函数表,当然,这是理所应当值得付出的资源消耗,列为缺点有点勉强。

OOP核心思想在C++中表现是三部分:数据抽象,继承和动态绑定。通过使用数据抽象,可以将类的接口和实现分离(也就是类);使用继承,可以定义相似的对象类型,并对其关系建模;使用动态绑定,在一定程度上忽略了相似类型间的区别,统一了相似类的接口。

首先通过小例子来看看为什么继承。

  • 通过前面的学习我们得知了类的概念,现在考虑这样一种应用场景,要编写一些管理学生的类,分别管理中学生,大学生,研究生。

    按照面对对象的思想,我们应该给这几个不同的学生都要对应一个类。现在问题来了,学生这一个群体,它有一些共同属性,例如年龄,姓名,性别等等,这些属性及其所对应的方法没必要在每个类中单独写一遍,这样既不利于编写代码(多个类万一有的属性没打对),也不利于后期维护(如果发现哪个方法错误,要修改的话所有类都需要修改一遍)。

    到了使用继承的时候了,我们可以将这些共同的属性统一组织到一个学生类(基类),然后其它的中学生,大学生等(派生类)都可以通过继承这个类,获得它所有的属性。这样修改时只需要修改这个基类,减少代码量和方便维护。

    后面我们会进一步介绍,这种表示一个基类是派生类共同属性,派生类是基类的扩展 这种继承关系是public 继承 ,学生类是 抽象基类

  • 还有一些类它们间的关系和上面的例子又不太相似。考虑这样一种关系:发动机和汽车。直接按上面的思想来继承的话就会有根本思想的错误,发动机不具有汽车的通有属性,发动机更像是汽车的一个组成部分,实际上对于这种关系我们也采用继承。

    这种继承关系是private 继承,表达基类是派生类一部分的概念。

继承的概念不是很难,但在实际应用中正确使用比较困难,本文只介绍基础应用,后期会再做补充扩展。

继承基础

静态类型和动态类型

表达式的静态类型在编译期间是已知的,它是变量声明时的类型。动态类型则是内存中对象的类型,动态类型只有运行时才能确定。

只有基类指针或引用的动态类型和静态类型 才可能 不一致,从根本说,这才是C++支持多态的基础。

静态绑定,动态绑定 所谓动态绑定是相对静态绑定而言,静态绑定就是我们要调用哪个函数是在编译期就已经绑定决定好,而动态绑定是在运行时决定的。

可以将派生类的指针和引用传递给基类的指针和引用,当通过基类的指针或引用调用虚成员函数时,那么会根据这个指针或引用所指向的对象是 基类对象还是 派生类对象,从而调动基类或派生类的虚成员函数。

引用的底层就是指针,所以在这它和指针的作用等价。

声明

声明一个派生类,不需要加上派生列表 class Deriver;

并且如果想继承某个类,则基类必须在之前已经定义过。没有定义则为不完整类型,无法继承。

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只有该成员的唯一定义。

拷贝和赋值

当派生类对象赋值给基类对象,或作为参数拷贝构造基类对象,调用的都是基类的函数。因为基类对象无法转化为派生类对象。而派生类对象可以只使用基类部分。

虚函数

基类必须区分两类函数,一类是希望派生类直接继承而不要改变的函数,一类是希望派生类对其进行覆盖的函数。对于后者,基类应将其声明为虚函数(virtual)。当我们使用指针或引用调用虚函数时,调用将会被动态绑定。这样带来的好处是我们可以以同一种形式调用不同版本的函数。根据指针或引用的动态类型不同而调用不同的函数,简化编程。

虚函数适用于公有继承,因为跟公有继承的思想符合。

任何构造函数之外的非静态函数都可以是虚函数。virtual 声明只能出现在类内。

如果派生类想覆盖基类的虚函数,就必须函数名,形参,返回类型 都相同(而函数重载不要求返回类型)。

必须为每一个虚函数提供定义,因为编译器在编译时不知道将来会运行哪个版本的虚函数。

override 和final 可以控制成员函数在继承中关系,都是添加在形参后

  • final 指定一个函数(包括虚函数与非虚函数)不可在派生类中被覆盖

    假如有学生类(基类,有年龄属性),那么不管是哪个阶段的学生(不同的派生类),其查询年龄的函数都是一样的,为了避免派生类不小心覆盖这个函数,就可以在学生类中将此函数声明为final

  • override 指定这个函数是覆盖基类的虚函数

    主要用于排除一些人为的错误,如果指定一个函数为override 说明想用这个函数覆盖基类的虚函数,假如此时不小心形参没写对,或者返回类型错误等等,则编译器会直接指出该函数未能正确覆盖。

虚函数的默认实参:通常使用 引用或指针的静态类型中虚函数默认实参,所以尽量不要有同一个虚函数基类子类默认实参不同的情况。

处于派生体系中间的类即是基类也是派生类,如果指针或引用的静态类型是该类,则虚函数默认实参是该类指定的默认实参,

class Base {
public:
	virtual void show( int x=10) {
		cout <<x<< "666" << endl;
	}
};
class D1 :public Base{
public:
	void show(int x=20 )override {
		cout << x<<"D1" << endl;
	}
};
class D2 :public D1 {
public:
	void show(int x = 30)override {
		cout << x<<"D3" << endl;
	}
};
int main( ) {
	D1 * x = new D1( );
	D1 * y = new D2( );
	x->show( );
	y->show( );
	return 0;
}
/*结果
20D1
30D3
*/

如果想避免虚函数动态绑定机制,指定域名访问父类中的虚函数

BaseClass *p = new DerivedClass();	//基类有fun 虚函数,并且派生类已覆盖
p->fun();				//调用派生类fun
p->BaseClass::fun();	//调用基类fun
虚析构函数

只要有虚函数,则析构函数应该设置为虚函数,这样当delete 动态类型和静态类型不一致的指针对象时,可以正常先调用派生类的析构函数,再调用基类。

底层实现

其底层实现是通过虚函数指针和虚函数表来实现,具体感兴趣可进一步搜索研究

纯虚函数

纯虚函数就是没有定义的虚函数,具有纯虚函数的类是抽象基类,通过 =0 声明一个虚函数使其变为纯虚函数。

= 0 实际上将该函数的指针指向NULL

抽象基类

类是描述对象的模板,而有些东西,它是一个抽象的概念,例如汽车,游戏,电脑,这种表示一类物品的抽象概念。例如游戏,游戏都具有发布时间,运行平台等属性(这些属性就应该放在基类)。当我们说游戏,世界上所有的游戏产品都是游戏这个概念下的实体,例如王者荣耀,它是游戏这个类的派生类的一个具体对象。当我说创建一个游戏时,无法直接创建游戏这两个字所代表的特定对象。

所以,这些抽象类,它不对应现实世界中的实体,我们无法直接获得它的对象,只能通过子类进一步描述后再获取对象。

为了实现这样的功能,c++提供了抽象基类,抽象基类不可以定义对象。从实现的角度来说具有纯虚函数的类称为抽象基类。子类中没有实现纯虚函数的类依然是抽象基类。

一个抽象基类中所有函数都是纯虚函数称为接口类。

访问权限与继承关系

访问权限

在前面 类与对象的基础 中我们已经看到了类内两种访问权限 public private ,这块,我们看一个新的访问限制符 受保护的成员protected,这个访问限制只有在继承中表现与private不同。

如果基类中成员定义为protected 则基类,派生类中都可以访问它,而限制通过基类对象和派生类对象访问。

继承限制

类内的访问限制符是限制用户通过对象访问类 ,而在继承中访问限制符是 控制派生类用户对基类的访问权限

如果想更改继承的权限可以在派生类中使用 using

继承关系
  • 公有继承表示派生类是基类,派生类是对基类更详细的描述的概念。(如动物类为基类,人类为派生类)
  • 私有继承表示基类是派生类一部分的概念。(如发动机类是基类,汽车类是派生类)
  • 保护继承不太常用,它的特点不是很鲜明。
派生类向基类的转换的可访问性

当操作者是用户时,只有派生类公有继承基类才可以转换。

当操作者是派生类时,派生类向基类的转换都是可以的。

其它相关知识点

类作用域

重载 指的是函数名相同,形参不同。在继承中,派生类只要和基类中 **函数名相同 **就会发生 同名隐藏 现象。

而通过using 可以解除同名隐藏现象。此时可进行 基类与派生类 函数重载。

class Base {
public:
	void show( int x) {
		cout << "666" << endl;
	}
};
class D1 :public Base{
public:
	void show(string str) {
		cout << str << endl;
	}
};
class D2 :public D1 {
public:
    //using Base::show;
	void show( ) {
		cout << "7777" << endl;
	}
};
int main( ) {
	D2 d;
	d.show(2);  //x	D2 没有接受int 的成员函数,Base 和 D1 的show 方法都被隐藏了。如果添加第15行,则正常
    
    D1 dd;
    dd.show();  //x D1 没有无参的show 函数
    
	return 0;
}

除了覆盖基类的虚函数之外,不要定义和基类同名的成员。

构造函数与拷贝控制

派生类构造函数必须调用基类构造函数

派生类赋值运算符也需要调用基类赋值运算,控制基类的赋值。

派生类析构函数不用调用基类析构函数,其会自动调用。

继承构造函数

可以通过using 声明来继承 基类的构造函数

继承与智能指针与容器

当在容器中存放派生类或基类对象时,通过存放指针,可以混用。这种集合称为异质对象集合。

多重继承与虚继承

多重继承

在派生类的派生类列表中继承多个基类。例如以下继承体系,Panda 既继承Bear 又继承 Endangered ,就称为多重继承。

image-20200705094320763
class ZooAnimal {
public:
	int z = 1;
};

class Bear :public ZooAnimal {
	int b = 2;
};

class Endangered {
	int e = 4;
};

class Panda :public Bear, public Endangered {
	int p = 5;
};

int main( ) {
	Panda temp;

	return 0;
}

构造顺序

在多重继承的情况下,派生类会在构造函数初始化列表中,按照派生列表中基类出现的顺序,初始化所有的基类。(当有虚继承时,存在例外)

虚继承

多重继承体系中,有些继承体系得到的结果却不是我们所期望的。假如如下继承体系:

image-20200705092305415

先看看多重继承:

class ZooAnimal {
public:
	int z = 1;
};

class Bear :public ZooAnimal {
	int b = 2;
};
class Raccoon :public ZooAnimal {
	int r = 3;
};
class Endangered { 
	int e = 4;
};

class Panda :public Bear, public Raccoon, public Endangered { 
	int p = 5;
};

int main( ) {
	Panda temp;
	temp.z = 6;	//error z 不明确
    temp.Raccoon::z = 6;
    
	return 0;
}

这样的思路是没有问题的,但是实际上实现的过程中有点问题。但为什么 z 不明确呢?实际上只要明白这种情况下的内存分布就很好理解。

可以看到,对于 Panda 对象来说,它实际上有两个 ZooAnimal 成员,一个是由 Bear 继承而来,另一个是由Raccoon 继承而来。两个ZooAnimal 对象的数据没有关系。

这显然不是我们想要表示的继承关系,在我们想表示的继承关系中,熊猫 就是属于 动物园动物,而不是说它属于两个动物园动物。这种情况下,就要考虑使用虚继承。

虚继承的就是令某个类愿意共享它继承的基类(被共享的基类称为虚基类),所以虚继承一般出现在在继承体系的中间部分(即除过最底层的基类和最顶层的派生类)。无论一个派生类的派生体系中出现过多少次虚基类,它都只有一份虚基类对象。

//... 其它部分都不改变,只是添加两个virtual 关键字
class Bear :virtual public ZooAnimal {	//表示愿意与其它类共享ZooAnimal
	int b = 2;
};
class Raccoon :virtual public ZooAnimal {
	int r = 3;
};
//...

image-20200705100236395

底层实现

其底层是通过虚继承表 和虚继承指针 实现,具体感兴趣可进一步搜索研究

构造顺序

由最底层的派生类,按照继承列表的顺序,先构造所有虚基类,无论是直接还是间接。之后再按照顺序构造普通基类,最后再构造自身。而析构顺序与构造顺序相反。虚基类总会优先于普通基类构造。

posted @ 2020-07-06 14:09  沉云  阅读(138)  评论(0编辑  收藏  举报