【C++ —— 多态】

C++ ------ 多态

多态的概念

在C++中, 多态性(Polymorphism) 是面向对象编程中的一个重要概念,它允许以统一的方式 处理 不同类型的对象 ,从而提高代码的灵活性和可扩展性。多态性基于继承和虚函数实现,主要有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。

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

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

多态演示:

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

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

void Func(Person* p)
{
	p->BuyTicket();
}

void test1()
{
	Person lt;
	Student hcx;

	Func(&lt);		// "全价买票"
	Func(&hcx);		// "半价买票"

}

多态的定义和实现

多态的构成条件

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

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

虚函数: 即被virtual修饰的类成员函数称为虚函数。

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

例如上述的 Person 类的 BuyTicket 函数被 virtual 修饰,所以他是一个虚函数!

注意: 这里的 virtual 和菱形继承那一块的 virtual 的作用不同,二者作用完全不同,只是关键字的名字一样而已。

虚函数的重写

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

代码演示:

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

class Student :public Person
{
	virtual void BuyTicket()		//派生类重写基类的虚函数
	{
		cout << "半价买票" << endl;
	}
};

注意: 这里的派生类的 virtual 关键字不写也构成虚函数的重写,但是这种写法并不推荐!

虚函数重写的两个例外

  1. 协变 (基类与派生类虚函数返回值类型不同)
  2. 析构函数的重写 (基类与派生类析构函数的名字不同)
协变:

派生类重写基类虚函数时,与基类虚函数返回值类型不同

即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

cpp 复制代码
//基类
class A {};

//派生类
class B : public A {};

//基类
class Person
{
public:
	virtual A* f()			//基类的f()函数返回值是A*
	{
		cout << "A* Person::f() " << endl;
		return new A;
	}
};

//派生类
class Student : public Person
{
public:
	virtual B* f()			//衍生类f()函数的重写,其返回值是B*
	{
		cout << "B* Student::f()" << endl;
		return new B;
	}
};

void Func(Person* p)
{
	p->f();
}

void test2()
{
	Person p;
	Student s;
	Person* ptr1 = &p;
	Person* ptr2 = &s;

	Func(ptr1);		//A* Person::f()
	Func(ptr2);		//B* Student::f()
}
析构函数的重写

在多态中,析构函数(destructor) 的调用有一个特殊的问题。如果不将基类的析构函数声明为虚函数(virtual) ,那么当使用 基类指针 删除 派生类对象 时,只会调用基类的析构函数而不会调用派生类的析构函数。 这可能导致派生类中的资源没有被正确释放,造成内存泄漏或其他问题。

类似下面这个问题:

cpp 复制代码
class Person  
{  
public:  
    ~Person()  
    {  
        cout << "~Person()" << endl;  
    }  
};  
  
class Student : public Person  
{  
public:  
    ~Student()  
    {  
        cout << "~Student()" << endl;  
    }  
};  
  
void test3()  
{  
    Person* ptr1 = new Person;  
    Person* ptr2 = new Student;  
  
    delete ptr1; 	//~Person()
    // 调用 Person 的析构函数  
    
    delete ptr2; 	//~Person()
    // 如果 Person 的析构函数不是虚函数,则只调用 Person 的析构函数  
}

如果 Person 的析构函数不是虚函数 ,当 执行 delete ptr2; 时,只有Person 的析构函数会被调用,而 Student 的析构函数则不会被调用。但是 Student 类 可能包含一些额外的资源(如动态分配的内存、打开的文件句柄等)需要清理。

为了避免这种情况,您应该将 Person 的析构函数声明为虚函数:

cpp 复制代码
class Person  
{  
public:  
    virtual ~Person() // 声明为虚析构函数  
    {  
        cout << "~Person()" << endl;  
    }  
};

现在,当执行 delete ptr2; 时,首先会调用 Student 的析构函数(因为它被首先创建),然后调用 Person 的析构函数(因为它是基类)。这就是所谓的析构函数链(destructor chaining),它确保了对象的所有部分都被正确地清理。

C++11 override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

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

比如下面的基类Car的虚函数Drive被final修饰后就不能再被重写了,子类若是重写了基类的Drivet函数则编译报错。

cpp 复制代码
//基类
class Car
{
public:
virtual void Drive() final {}
};

//衍生类
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};

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

下面这个衍生类Drive()的成员函数被override所修饰,所以在编译时将检查是否派生类虚函数是否重写了基类的这个虚函数,没有的话就会报错。

cpp 复制代码
//基类
class Car {
public:
	virtual void Drive() {}
};

//衍生类
class Benz :public Car {
public:
	virtual void Drive() override
	{
		cout << "Benz-舒适" << endl;
	}
};

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

抽象类

概念

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

派生类继承后也不能实例化出对象 ,只有重写纯虚函数 ,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

cpp 复制代码
class Car						//包含纯虚函数所以叫抽象类
{
public:
	virtual void Drive() = 0	//纯虚函数
	{}
};

class BMW :public Car
{
	void Drive()				//派生类必须重写纯虚函数
	{
		cout << "操控" << endl;
	}
};

class Benz :public Car			
{
	void Drive()				//派生类必须重写纯虚函数
	{
		cout << "舒适" << endl;
	}
};

void Func(Car* c)
{
	c->Drive();
}

void test1()
{
	BMW b1;
	Benz b2;
	Car* c1 = &b1;				
	Car* c2 = &b2;				
	Func(c1);					//操控
	Func(c2);					//舒适

	cout << endl;
	c1->Drive();
	c2->Drive();
}

这里的Car类 中包含了纯虚函数 Drive ,所以 Car类 是抽象类,其衍生类必须重写纯虚函数,才能实例化对象。

接口继承和实现继承

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

多态的继承

虚函数表

先来看一道面试题:

这里的 sizeof(Base) 等于多少呢?

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

答案是:

那为什么是12呢?

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

那这个虚函数表指针的原理是什么呢?

下面Base类当中有三个成员函数,其中Func1和Func2是虚函数,Func3是普通成员函数,子类Derive当中仅对父类的Func1函数进行了重写。

cpp 复制代码
#include <iostream>
using namespace std;
//父类
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:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过调试可以发现,父类对象b和基类对象d当中除了自己的成员变量之外,父类和子类对象都有一个虚表指针,分别指向属于自己的虚表。

通过对调试的观察,我们发现这几个小点:

  1. 实际上这个虚表存储的就是虚函数的地址,可以通过观察b对象的虚表内容可知。方便存储的是Func1Func2的地址。
  2. 派生类对象d中也有一个虚表。这个虚表指针实际上是就是继承了基类的虚表,只不过在派生类对象d中重写了Func1所以,两个虚表就有所不同。所以d的虚表是继承了b的 Func2 地址和重写 Func1 地址。所以这也就是为什么函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。此外,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr

总结一下派生类的虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

多态的原理

还记得之前实现的Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket

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 Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicketmike的虚表中找到虚函数是Person::BuyTicket。观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicketjohson的虚表中找到虚函数是Student::BuyTicket

这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
cpp 复制代码
//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

void test2()
{
	Student s;		//实例化对象s
	Person p = s;	//切片
	p.BuyTicket();	//通过对象直接调用,不满足多态,即静态绑定

}

此时直接调用函数,不满足多态条件,不构成多态,通过反汇编观察为静态绑定。

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

此时通过指针调用函数,满足多态条件,构成多态,通过反汇编观察为动态绑定。

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

单继承中的虚函数表

单继承即只有一个基类:

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;
};

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

cpp 复制代码
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
void test3 ()
{
	Base b;
	Derive d;
	
	VFPTR * vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
}

多继承中的虚函数表

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;
};
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;
}

void test4()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

相关推荐
前行的小黑炭43 分钟前
设计模式:为什么使用模板设计模式(不相同的步骤进行抽取,使用不同的子类实现)减少重复代码,让代码更好维护。
android·java·kotlin
Java技术小馆1 小时前
如何设计一个本地缓存
java·面试·架构
XuanXu2 小时前
Java AQS原理以及应用
java
风象南4 小时前
SpringBoot中6种自定义starter开发方法
java·spring boot·后端
mghio13 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室18 小时前
java日常开发笔记和开发问题记录
java
咖啡教室18 小时前
java练习项目记录笔记
java
鱼樱前端19 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea19 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq