C++多态

概念

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

定义和实现

在继承中构成多态要满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  3. 子类重写父类的虚函数时,函数名、参数以及返回值都需要相同
cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

Output:

复制代码
买票-全价
买票-半价

可以看到,我们的传入参数都是一样的,但是却得到了不同的输出结果,这就是多态。

注意:子类的BuyTicket()可以不加virtual关键字,也能构成虚函数重写。

协变

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

协变是一种特例,可以使得虚函数重写时参数和返回值不同。

cpp 复制代码
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};

上面也构成虚函数重写。

析构函数重写

首先我们需要考虑的是,析构函数是否有重写的必要?先看下面代码:

cpp 复制代码
class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
private:
	int* p = new int[10];
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

Output:

复制代码
~Person()
~Person()

也就是说p没有被delete,就造成了内存泄漏。

因此析构函数确实有重写的必要,但是虚函数重写不是要求函数名一样吗?那析构函数的函数名不一样该怎么重写呢?

别急先看下面代码:

cpp 复制代码
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
private:
	int* p = new int[10];
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

Output:

复制代码
~Person()
~Student()
~Person()

也就是我们确实实现了析构函数重写,这是怎么回事呢?

实际上析构函数在编译时统一会改名成destructors,因此子类和父类的析构函数是同名函数。

override和final

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

final:修饰虚函数,表示该虚函数不能再被重写

此外,final修饰的类和函数都不可被继承。

抽象类

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

cpp 复制代码
class Car
{
public:
virtual void Drive() = 0;
};
int main()
{
	Car c;
}

尝试编译: error C2259: "Car": 无法实例化抽象类

多态的原理

先看如下代码:

cpp 复制代码
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
char c;
int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
}

按照我们以往学习的知识,这里会发生内存对齐,因此输出结果是8.

但实际输出结果是12.这是为什么呢?

在监视窗口查看变量b:

可以到b变量存储空间里面,最上面存储了一个指针,下面才是b的成员变量。

这个指针_vfptr实际上就是virtual fuction table pointer也就是所谓的虚函数表指针,它指向虚函数表。虚函数表存储的则是虚函数的函数指针。

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

监视上面代码运行过程,我们发现:

也就是说父类和子类的虚表指针是不同的,指向的虚基表内容也不同。

那么多态的原理就很显然了。如果调用的函数不构成虚函数重写,就直接调用类的成员函数。

如果构成虚函数重写,由于父类和子类的虚表指针不同。因此子类切片成父类得到的虚表指针指向的是子类的虚函数表。这时调用虚函数表里的函数指针,就会得到不同的结果。

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

单继承中的虚函数表

cpp 复制代码
class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() {cout<<"Base::func2" <<endl;}
private :
int a;
};
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

调用监视窗口:

这时我们发现子类的虚表指针内容并不完整,func3和func4的指针不在里面。因此考虑我们自行取出子类的虚表指针。

首先我们知道,在x86运行环境下,指针是4字节的对应int的大小。那是不是只要对d强转成int就能取出虚表指针。并不是。C++的强制类型转换不能将这些关联性很弱的两种类型强制类型转换。但是指针是可以任意强制类型转换成其他指针的。因此我们可以*((int*)(&d))这样取出虚表指针,并且打印虚表指针:

cpp 复制代码
typedef void(*VFPTR)();//重命名函数指针

void PrintVFT(VFPTR* vft)
{
	for (size_t i = 0; i < 4; i++)
	{
		printf("%p->", vft[i]);

		VFPTR pf = vft[i];
		(*pf)();
	}
}

int main()
{
	Base b;
	Derive d;
	VFPTR* ptr = (VFPTR*)(*((int*)&d));
	PrintVFT(ptr);
	return 0;
}

Output:

复制代码
00591492->Derive::func1
00591483->Base::func2
0059148D->Derive::func3
00591488->Derive::func4

也就是子类的虚表确实存储了四个函数指针。

多继承中的虚函数表

先看如下代码:

cpp 复制代码
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

这时我有一个问题,Derive的虚表指针应该是继承自Base1和Base2的两个虚表指针。那么func3的函数指针应该放在Base1和Base2之间的哪个虚表呢,还是说都放呢?

应用前面的方法,打印Derive的虚表:

cpp 复制代码
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}

Output:

复制代码
 虚表地址>00219B94
 第0个虚函数地址 :0X211249,->Derive::func1
 第1个虚函数地址 :0X2112f3,->Base1::func2
 第2个虚函数地址 :0X211235,->Derive::func3

 虚表地址>00219BA8
 第0个虚函数地址 :0X21136b,->Derive::func1
 第1个虚函数地址 :0X2110b4,->Base2::func2

这意味着fun3的函数指针只存储在了Base1的虚函数表中

多态相关题目

1.下面代码的运行结果是什么?

cpp 复制代码
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}

Output:

复制代码
B->1

需要注意的是,虚函数重写只是重写函数体内容,而不会重写声明内容。

2.inline函数可以是虚函数吗?

可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

3.静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

4.构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

5.对象访问普通函数快还是虚函数更快?

首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

6.虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

7.下面程序输出结果是什么?

cpp 复制代码
#include<iostream>
using namespace std;
class A{
public:
A(char *s) { cout<<s<<endl; }
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:
C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
{ cout<<s4<<endl;}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}

Output:

复制代码
class A class B class C class D
相关推荐
liuzhangfeiabc4 小时前
[luogu12541] [APIO2025] Hack! - 交互 - 构造 - 数论 - BSGS
c++·算法·题解
学习使我变快乐4 小时前
C++:迭代器
开发语言·c++·windows
好想有猫猫4 小时前
【Redis】List 列表
数据库·c++·redis·分布式·缓存·list
superior tigre6 小时前
C++学习:六个月从基础到就业——C++11/14:其他语言特性
c++·学习
天堂的恶魔9466 小时前
C++ - 仿 RabbitMQ 实现消息队列(2)(Protobuf 和 Muduo 初识)
c++·rabbitmq·ruby
休息一下接着来6 小时前
进程间通信(IPC)常用方式对比
linux·c++·进程间通讯
虾球xz6 小时前
游戏引擎学习第288天:继续完成Brains
c++·学习·游戏引擎
John_ToDebug6 小时前
Chromium 浏览器核心生命周期剖析:从 BrowserProcess 全局管理到 Browser 窗口实例
c++·chrome·性能优化
June`7 小时前
专题五:floodfill算法(图像渲染深度优先遍历解析与实现)
c++·算法·leetcode·深度优先·剪枝·floodfill
流星白龙7 小时前
【C++算法】70.队列+宽搜_N 叉树的层序遍历
开发语言·c++·算法