C++-多态

一、多态的概念

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

例如:买火车票,普通人买票就是全价,学生买票就是半价,军人可以优先买票。

二、多态的定义以及实现

2.1 多态构成的条件

在继承中构成多态的两个条件:

cpp 复制代码
1.必须是基类的指针或引用调用虚函数。
2.被调用的函数必须是虚函数,并且派生类需要对基类的虚函数完成重写。

下面就是一个多态调用:Student类继承Person类,并且完成了对Person类里面BuyTicket虚函数的重写。最后通过基类的引用调用BuyTicket虚函数。

cpp 复制代码
//Person类,基类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person::买票全价" << endl;
	}
};
//Student类,公有继承Person
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student::买票半价" << endl;
	}
};
//通过基类的引用调用多态
void func(Person& p)
{
	p.BuyTicket();
}


int main()
{
	Person p;
	Student s;
	
	func(p);	//Person::买票全价
	func(s);	//Student::买票半价
	return 0;
}

2.2 虚函数

虚函数 :被virtual关键字修饰的类成员函数被称为虚函数。

这里的虚函数和虚继承没有任何的关系,只是关键字相同。类似于&(取地址)和&(引用)。并且virtual只能修饰成员函数,无法修饰类外部的普通函数。

cpp 复制代码
class Person
{
public:
	//虚函数
	virtual void BuyTicket()
	{
		cout << "Person::买票全价" << endl;
	}
};

2.3 虚函数的重写

虚函数的重写(覆盖)的几个条件:

1.必须是继承关系。不是继承关系无法构成虚函数重写。

2.三同:虚函数的函数名要相同参数列表(参数类型、参数顺序、参数个数)要相同返回值要相同

cpp 复制代码
//Person类,基类
class Person
{
public:
	//虚函数
	virtual void BuyTicket()
	{
		cout << "Person::买票全价" << endl;
	}
};

//Student类,公有继承Person
class Student :public Person
{
public:
	//派生类完成了对基类中BuyTicket虚函数的重写
	virtual void BuyTicket()
	{
		cout << "Student::买票半价" << endl;
	}
};

3.参数列表相同,但形参的名字可以不同。

cpp 复制代码
//参数列表相同,形参名称不同依旧可以构成重写
class Person
{
public:
	virtual void func(int a,char c,float b){}
};
class Student :public Person
{
public:
	virtual void func(int k,char l,float m){}
};

虚函数重写的特殊情况

1.协变 :基类和派生类的返回值可以不同,但返回值必须也是基类和派生类的关系 ,且必须是指针或引用作为返回值

协变也能构成多态调用。

cpp 复制代码
//A和B构成继承关系
class A
{};
class B:public A
{};

class Person
{
public:
	//虚函数
	virtual A* BuyTicket()
	{
		cout << "Person::买票全价" << endl;
		return nullptr;
	}
};
class Student :public Person
{
public:
	//派生类Student的BuyTicket和基类Person的BuyTicket构成协变
	virtual B* BuyTicket()
	{
		cout << "Student::买票半价" << endl;
		return nullptr;
	}
};
void func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	//协变也能进行多态调用
	func(p);	//Person::买票全价
	func(s);	//Student::买票半价
	return 0;
}

2.派生类的虚函数不加virtual也能构成重写。但基类的虚函数必须要加virtual。

cpp 复制代码
class Person
{
public:
	//虚函数
	virtual void BuyTicket()
	{cout << "Person::买票全价" << endl;}
};
class Student :public Person
{
public:
	//派生类Student在不加vartual的情况下也完成了对BuyTicket的重写
	void BuyTicket()
	{cout << "Student::买票半价" << endl;}
};
void func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	//也能进行多态调用
	func(p);	//Person::买票全价
	func(s);	//Student::买票半价
	return 0;
}

原因析构函数正确的析构顺序是先析构派生类再析构基类特殊情况基类指针指向派生类时,基类的析构函数需要设计成虚函数并且派生类的析构函数要完成对基类析构函数的重写这样才能正确的调用析构顺序。祖师爷为了减轻使用者的负担,特殊说明派生类的虚函数可以不加virtual,这样也可以和基类的虚函数构成重写。

正确的析构函数的重写:

cpp 复制代码
class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student :public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* pp = new Person;
	delete pp;		//~Person()
	
	//当基类指针指向派生类
	pp = new Student;
	delete pp;		//~Student()\~Person()
	return 0;
}

3.虚函数重写的是实现过程 ,多态调用时,如果基类对象的指针或引用指向向派生类对象时,会使用基类虚函数的接口。简单的说:多态调用时会使用基类虚函数的缺省值

2.4 C++11的override和final

1.final:修饰虚函数表示该虚函数不能够再被重写。

cpp 复制代码
virtual void func() final
{}

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

cpp 复制代码
virtual ~Person()
{
    cout << "~Person()" << endl;
}

~Student() override
{
	cout << "~Student()" << endl;
}

2.5 重载、重定义(隐藏)、重写(覆盖)对比

2.6 破坏多态的条件继续使用多态调用的后果

普通调用:编译链接期间确定调用函数的地址。

多态调用:运行时,去虚表里面找到虚函数地址,确定地址再调用对应的虚函数。调用时指针或引用指向的对象决定调用哪个函数,指向基类,就调用基类的虚函数,指向派生类,就调用派生类的虚函数。

1.不是继承关系:直接报错,因为这两个类不再是基类和派生类的关系,只是两个毫不相关的类,没法再进行切片或切割的行为,不能进行直接赋值。

2.不是指针或引用调用:此时会变成普通调用,普通调用会直接看调用函数的类型。

3.基类虚函数不加virtual:此时派生类和基类的同名函数会构成隐藏关系。会变成普通调用。

三、抽象类

3.1 概念

纯虚函数:在虚函数后面加=0,那么这个虚函数就是纯虚函数。

抽象类只要包含纯虚函数的类就是抽象类(也叫接口类)且抽象类无法实例化出对象 。派生类继承抽象类后也无法实例化出对象,只有在派生类重写了抽象类里面的纯虚函数后才能实例化出对象。抽象类虽然无法实例化出对象,但能够定义出指针

cpp 复制代码
抽象基类Car,派生类Benz和BWM公有继承了Car类,并重写了Car里面的纯虚函数Drive。
cpp 复制代码
//抽象类
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BWM :public Car
{
public:
	virtual void Drive()
	{
		cout << "BWM-操控" << endl;
	}
};
void func(Car* c)
{
	c->Drive();
}
int main()
{
	Car* pc = new Benz;
	func(pc);	//Benz-舒适
	pc = new BWM;
	func(pc);	//BWM-操控
	return 0;
}

抽象类的意义:抽象是一系列具体事物所拥有的共同特性的集合。

cpp 复制代码
1.抽象类会有多个子类,例如人、车、动物、食物等等,都可以作为抽象类,并且会被很多派生类
继承。
2.抽象类强制派生类去重写纯虚函数。

3.2 接口继承和实现继承

1.普通函数的继承是一种实现继承 ,派生类继承了基类的函数,可以使用函数,继承的是函数的使用权

2.虚函数的继承是一种接口继承 ,派生类继承的是基类虚函数的接口,比如多态调用时会使用基类虚函数的缺省值。接口继承是为了重写函数内部的实现,达成多态,继承的是虚函数函数体的修改权

四、多态的原理

4.1 虚函数表

对于下面这个有虚函数,且成员变量只有一个int类型的类来说,这个类的大小是8个字节。

cpp 复制代码
class Base
{
public:
	virtual void func()
	{
		cout << "Base::func()" << endl;
	}
protected:
	int _b = 1;
};

监视窗口:除了有一个_b成员变量外,还有一个_vfptr。

这个_vfptr被称为虚函数表指针(v代表virtual,f代表function) ,_vfptr指向一个虚函数表,虚函数表里面用于存放虚函数的地址。一个类里面只要有虚函数,那么有至少会有一张虚表。按理来说用_vftptr来表示虚函数表指针更合适(virtual function table pointer),但VS有些地方设计的有点狗屎。

4.2 派生类的虚函数表

Base是基类,Derive公有继承Base类:

cpp 复制代码
1.Base类里面有三个虚函数:func1()、func2()、func3();还有一个成员变量_b=1。
2.Derive类里面重写了func1(),新增了一个虚函数func4();还有一个成员变量_d=2。
cpp 复制代码
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
	virtual void func3()
	{
		cout << "Base::func3()" << endl;
	}
protected:
	int _b = 1;
};
class Derive :public Base
{
public:
	//重写func1
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
	//新增func4
	virtual void func4()
	{
		cout << "Derive::func4()" << endl;
	}
protected:
	int _d = 2;
};

1.VS的监视窗口在描述续表时省略了很多东西,并不全面。

2.从内存窗口角度去观察Base基类和Derive派生类的虚表。

在Derive的虚表里,有4个类似虚函数的地址,在Base的虚表里,有3个类似虚函数的地址。

通过观察总结

1.派生类对象d中也有一个虚表指针,d由两部分构成,一部分是基类继承下来的成员(_vfptr和_a),虚表指针也存在于这部分,另一部分是自己本身的成员(_d)。

2.Base和Derive的虚表里有两个地址是完全相同的,分别是00cd1118和00cd1163,这两个地址疑似基类Base里面func2和func3的地址,另外Derive的虚表相较于Base的虚表多了两个新地址,分别是00cd1244和00cd1172,少了一个地址00cd12b2。这两个地址中应该有一个是func1重写后的地址,另一个是新增的func4的地址。

3.派生类的虚表指针和基类的虚表指针不同

原因:

cpp 复制代码
派生类生成了一个新的虚表,这个新的虚表拷贝了基类的虚表。
1.如果派生类重写了基类的虚函数,派生类会将重写后的虚函数地址覆盖新虚表中基类的虚函数地址。
2.如果派生类新增了虚函数,也会添加在新的虚表里面。

4.虚表里面不会存放普通的成员函数地址。

5.虚表本身是一个数组,虚函数指针数组,并且在这个数组的最后一个位置会存放一个nullptr。如果在内存窗口查看不到最后这个nullptr,可以试着清理一下资源后重新编译。

6.虚函数存在代码段中虚表存在于常量区(代码段)中虚函数的指针存在虚表中对象里面不会存放虚表

4.3 多态的原理

基类Person和派生类Student:

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person::买票全价" << endl;
	}
protected:
	int _a = 0;
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student::买票半价" << endl;
	}
protected:
	int _b = 1;
};

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

int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
	return 0;
}

多态的真正的原理:基类的指针或引用通过指向基类对象或派生类对象的切片,找到切片的前4/8个字节所对应的虚函表指针,然后通过虚表指针找到虚表,最后调用重写后的虚函数指针达成多态。

多态的本质原因是派生类在重写基类的虚函数后将新的虚函数地址覆盖基类的虚函数地址

4.4 动态绑定和静态绑定

1.在程序编译期间确定的程序行为 ,被称之为静态多态函数重载就是一种静态多态。

2.在程序运行期间 ,根据拿到的类型确定程序的具体行为被称之为动态多态

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

5.1 单继承中的虚函数表

基类Base和派生类Derive:

cpp 复制代码
1.Base类里面有三个虚函数:func1()、func2()、func3();还有一个成员变量_b。
2.Derive类里面重写了func1(),新增了一个虚函数func4();还有一个成员变量_d。
cpp 复制代码
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
	virtual void func3()
	{
		cout << "Base::func3()" << endl;
	}
protected:
	int _b = 1;
};
class Derive :public Base
{
public:
	//重写func1
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
	//新增func4
	virtual void func4()
	{
		cout << "Derive::func4()" << endl;
	}
protected:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

对象b和对象d在内存中的存放,以及对应类的虚表在内存中的存放情况:

打印虚表:为了更好的观察虚函数在内存中存放的具体情况,决定打印虚表。

打印虚表的代码:

cpp 复制代码
注释:
1.函数指针类型是void(*)(),重命名方式:typedef void(*名称)();
2.函数名就是函数地址获取了函数地址后可以直接加()调用:f();
3.(VFUNC*)(*(int*)&d)的解释:虚函数表在对象的头4 / 8个字节,(32位头4个,64位头8个)地
址之间可以进行强转,(int*)&d是将d的地址转换成int*, int和int* 之间也可以强转,*(int*)&d
是去头4个字节获取虚表地址,最后将虚表地址强转为VFUNC*方便Print的传参
cpp 复制代码
//因为虚表是一个函数指针数组,因此重命名函数指针会更方便
typedef void(*VFUNC)();

//打印虚表
//打印虚表前需要清理一下解决方案
void Print(VFUNC* t)
{
	//虚表内部的虚函数指针以nullptr结束
	for (int i = 0; t[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, t[i]);
		//为了确定打印的地址就是虚函数,决定打印完后调用对应的函数地址
		VFUNC f = t[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	Print((VFUNC*)(*(int*)&b));
	Print((VFUNC*)(*(int*)&d));
	return 0;
}

输出结果:

cpp 复制代码
[0]:009712B2->Base::func1()
[1]:00971118->Base::func2()
[2]:00971163->Base::func3()

[0]:00971244->Derive::func1()
[1]:00971118->Base::func2()
[2]:00971163->Base::func3()
[3]:00971172->Derive::func4()

5.2 多继承中的虚函数表

多继承中派生类会有不止一个虚表,重写的虚函数会覆盖对应虚表里原本基类的虚函数,新增的虚函数会添加到第一个继承的基类的虚表里面

cpp 复制代码
基类Base1:有func1和func2两个虚函数
基类Base2:有func1和func3两个虚函数
派生类Derive:公有继承Base1和Base2,重写func1,新增func4
cpp 复制代码
//基类Base1
class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2()" << endl;
	}
};
//基类Base2
class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "Base2::func3()" << endl;
	}
};
//派生类Derive
class Derive:public Base1,public Base2
{
public:
	//重写func1
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
	//新增func4
	virtual void func4()
	{
		cout << "Derive::func4()" << endl;
	}
};

打印Base1、Base2、Derive的虚表:

cpp 复制代码
注释:
1.Base2的虚表打印方法:Base2& b2=d就完成了对d对象中Base2那部分的切片,然后获取切片的头4
个字节就是Base2的虚表地址
2.Base1的虚表打印方法:Base1& b1=d就获取了d对象中Base1那部分的切片,然后获取头4个字节就
是Base1的虚表地址
3.Derive第一个虚表的地址可以直接&d然后取前4个字节获取
4.Derive第二个虚表的位置需要&d后强转为(char*)然后偏移sizeof(Base1)个字节获取
cpp 复制代码
typedef void(*VFUNC)();
void Print(VFUNC* t)
{
	for (int i = 0; t[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, t[i]);
		VFUNC f = t[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	Base1 b1=d;
	Base2 b2=d;

	cout << "打印Base1的虚表" << endl;
	Print((VFUNC*)*(int*)&b1);
	cout << "打印Base2的虚表" << endl;
	Print((VFUNC*)*(int*)&b2);
	cout << "打印Derive的虚表1" << endl;
	Print((VFUNC*)*(int*)&d);
	cout << "打印Derive的虚表2" << endl;
	Print((VFUNC*)*(int*)((char*) & d + sizeof(Base1)));
	return 0;
}

打印结果:

cpp 复制代码
打印Base1的虚表
[0]:00EB11C7->Base1::func1()
[1]:00EB12F8->Base1::func2()

打印Base2的虚表
[0]:00EB13CF->Base2::func1()
[1]:00EB10F5->Base2::func3()

打印Derive的虚表1
[0]:00EB1253->Derive::func1()
[1]:00EB12F8->Base1::func2()
[2]:00EB1177->Derive::func4()

打印Derive的虚表2
[0]:00EB1122->Derive::func1()
[1]:00EB10F5->Base2::func3()

六、继承和多态常见的问题

1.什么是多态

概念层面:不同对象完成同一件事会产生不同的状态。

定义层面:达成多态的三个条件1.继承关系2.派生类对基类的虚函数进行重写3.基类的指针或引用取调用重写的虚函数。

2.什么是重载、重定义、重写

重载:统一作用域,函数名相同,但参数类型不同的两个函数构成函数重载。

重定义:在基类和派生类的两个函数的函数名相同的前提下,不构成重写就一定是重定义。

重写:在基类和派生类的两个虚函数三同,函数名相同,函数参数列表相同、函数返回值相同。

3.多态的实现原理

基类的指针或引用指向基类对象或派生类对象基类那一部分的切片,找到虚函数表指针,进入虚表找到对应的虚函数进行调用,前提是派生类对基类的虚函数完成重写(覆盖)。

4.inline函数可以是虚函数吗

可以,当inline虚函数作为普通函数调用时具有内联属性,会在调用的地方直接展开;当inline虚函数进行多态调用时不具备内联属性,内联不起作用。

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

不可以,静态成员函数没有this指针,只能通过指定类域调用。

6.构造函数可以是虚函数吗

不可以,虚表指针是在构造函数阶段才初始化的,虚函数多态调用要到虚表里面找虚函数指针,如果构造函数是虚函数,此时虚表指针都还没有进行初始化。

7.析构函数可以是虚函数吗

可以,并且析构函数最好是虚函数,这样才能确保基类指针指向派生类动态申请的对象时先析构派生类再析构基类的顺序。

8.对象访问普通函数快还是虚函数快

普通函数和虚函数都是普通调用情况下:一样快。

虚函数是多态调用情况下:访问普通函数快,因为多态调用要先去虚表里面找对应的虚函数指针。

9.虚函数表在什么阶段生成的,存在哪里

虚函数表在编译阶段就生成了,虚函数表存在于代码段。

10.C++菱形继承的问题?虚继承的原理

菱形继承有两个问题:1.二义性2.数据冗余,派生类里面存了至少两份冗余的基类成员。

在直接继承造成冗余的基类的类上添加virtual关键字,会在这两个类上增加一个地址,并且这两个类不会直接存储冗余基类的成员,而是在找到地址的基础上找到这两个类到基类成员的各自偏移量,并且整个继承体系中只存储一份基类的成员。

11。什么是抽象类?抽象类的作用

包含纯虚函数的类就是抽象类。

抽象类强制继承的派生类重写纯虚函数,构成多态调用。

相关推荐
吃好睡好便好2 小时前
在Matlab中绘制质点三维运动轨迹图
开发语言·学习·matlab·信息可视化
雨落在了我的手上2 小时前
初识java(九):类和对象(⼀)
java·开发语言
SilentSamsara2 小时前
泛型与 Protocol:结构化子类型的地道写法
开发语言·python·青少年编程
玖釉-2 小时前
旋转图像:从矩阵转置、镜像到坐标变换的系统理解
c++·windows·算法·图形渲染
沐知全栈开发3 小时前
Servlet 表单数据处理指南
开发语言
超梦dasgg3 小时前
详细讲解:WebMvcConfigurer 接口
java·开发语言·spring
咩咦3 小时前
C++学习笔记23:const 成员函数
c++·学习笔记·类和对象·const·this指针·const成员函数
阿里嘎多学长3 小时前
2026-05-22 GitHub 热点项目精选
开发语言·程序员·github·代码托管