IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++中的多态 -> 正文阅读

[C++知识库]C++中的多态

系列文章目录

C++中的继承

C++入门

C++类和对象(上)

C++类和对象(中)

C++类和对象(下)

C/C++内存管理

C++string类

C++vector类

C++list类

C++stack和queue

C++双端队列

C++模板进阶

C++IO流

C++中的继承



一、多态是什么?

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

二、多态的定义及实现

1.多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person-全票" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student-半票" << endl;
	}
};

void func(Person& p)
{
	p.BuyTicket();
}
void test1()
{
	Person p;
	Student s;
	func(p);
	func(s);
}

被virtual关键字修饰的函数即为虚函数。

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

在子类中重写基类的虚函数是如果不加virtual关键字,也会被判定为是虚函数。但是这样做不规范,因此并不建议。

2.虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

  1. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

另外,基类的析构函数最好写成虚函数,否则可能出现delete时不调用派生类的析构函数

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person-全票" << endl;
	}
	~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student-半票" << endl;
	}
	~Student()
	{
		cout << "~Student" << endl;
	}
};

void func(Person& p)
{
	p.BuyTicket();
}
void test1()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
}

结果如下:
在这里插入图片描述
但是如果用virtual修饰虚构函数:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person-全票" << endl;
	}
	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student-半票" << endl;
	}
	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
};

void func(Person& p)
{
	p.BuyTicket();
}
void test1()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
}

结果如下:
在这里插入图片描述
这时delete就能正常调用派生类的虚构函数了。

3. override 和 final

C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被继承
class Car
{
	virtual void Drive() final
	{
		cout << "Drive" << endl;
	}
};
class Benz : public Car
{
	virtual void Drive()
	{
		cout << "Benz Drive" << endl;
	}
};

结果如下:
在这里插入图片描述

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car
{
	//virtual void Drive()
	//{
	//	cout << "Drive" << endl;
	//}
};
class Benz : public Car
{
public:
	virtual void Drive() override
	{
		cout << "Benz Drive" << endl;
	}
};

这里我们注释掉了基类的虚函数:
在这里插入图片描述

4.重载、重写(覆盖)与重定义(隐藏)

在这里插入图片描述

三、抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0
	{
		cout << "Car : Drive" << endl;
	}
};
class Benz : public Car
{
public:
};
void test3()
{
	Car c;
	Benz b;
}

结果如下:
在这里插入图片描述
所以,如果派生类继承的是抽象类的话,必须要重写虚函数。

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

四、多态的原理

1.虚函数表

我们先来看一个例子:

class Base
{
public:
	virtual void func()
	{
		cout << "Base" << endl;
	}
protected:
	int _b;
};

void test4()
{
	cout << sizeof(Base) << endl;
}

打印结果是什么呢?让我们来看一下:
在这里插入图片描述
为什么是8呢?不是只有一个整型变量吗。

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

这里我们定义一个Base类型对象,通过监视窗口来观察一下对象内到底是什么:

class Base
{
public:
	virtual void func()
	{
		cout << "Base" << endl;
	}
protected:
	int _b;
};

void test4()
{
	Base b1;
	b1.func();
	//cout << sizeof(Base) << endl;
}

我们通过监视窗口来看一下:
在这里插入图片描述
这个_vfptr就是虚函数表指针,简称虚表指针。

那接下来,我们再定义一个派生类,并重写func函数:

class Base
{
public:
	virtual void func1()
	{
		cout << "Base: func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base: func2()" << endl;
	}
protected:
	int _b;
};
class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive: func1()" << endl;
	}
protected:
	int _d;
};

void test4()
{
	Base b1;
	Derive d1;
}

再让我们看一下监视窗口:
在这里插入图片描述
我们可以发现,在上图中,在我们打√的地方,虚函数函数是一样的,打×的地方是不同的,这是因为,我们重写(语法层面上)了基类的第一个虚函数,所以在虚表中,新的虚函数地址覆盖(内存层面上)了原先的地址。

注意:

  1. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  2. 另外Func2继承下来后是虚函数,所以放进了虚表。如果不是虚函数,则不会放进虚表中。
  3. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
  4. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  5. 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表同样也是存在代码段中的。

2.多态的原理

还记得之前的那个例子吗:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person-全票" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student-半票" << endl;
	}
};

void func(Person& p)
{
	p.BuyTicket();
}
void test1()
{
	Person p;
	Student s;
	func(p);
	func(s);
}

我们用基类的指针或者引用去接受基类或者派生类,会根据传入参数的不同,实现多态,形成不同的效果。原因就是因为,基类和派生类的虚表是不同的,调用虚函数是也是根据其虚表查找的。

所以,我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的。不满足多态的函数调用时编译时确认好的。

3.动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

五、单继承和多继承关系的虚函数表

单继承关系中:

class Base
{
public:
	virtual void func1()
	{
		cout << "Base: func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base: func2()" << endl;
	}
protected:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive: func1()" << endl;
	}
protected:
	int _d = 2;
};

typedef void (*Vfunc)();
void PrintVTable(Vfunc* table)
{
	//从虚表中依此取虚函数指针打印并调用
	cout << "虚表地址:" << table <<endl;
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("Vtable[%d]: %p->", i, table[i]);
		table[i]();
	}
}

void test4()
{
	Base b1;
	Derive d1;

// 1.先取b的地址,强转成一个int*的指针
 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
 // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
 // 4.虚表指针传递给PrintVTable进行打印虚表
 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面
//  没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

	Vfunc* table1 = (Vfunc*)(*(int*)&b1);
	PrintVTable(table1);

	Vfunc* table2 = (Vfunc*)(*(int*)&d1);
	PrintVTable(table2);

}

结果如下:
在这里插入图片描述

多继承关系中的虚表:

class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1: func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base1: func2()" << endl;
	}
protected:
	int _b1 = 1;
};
class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2: func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base2: func2()" << endl;
	}
protected:
	int _b2 = 2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive: func1()" << endl;
	}
	virtual void func3()
	{
		cout << "Derive: func3()" << endl;
	}
protected:
	int _d = 3;
};

void test5()
{
	Derive d;

	Vfunc* table1 = (Vfunc*)(*(int*)&d);
	PrintVTable(table1);
	cout << endl;
	
	//找到第二个基类类在派生类中虚表指针的位置
	Vfunc* table2 = (Vfunc*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(table2);
}

结果如下:
在这里插入图片描述

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-21 18:44:48  更:2022-05-21 18:45:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/13 18:26:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码