一、多态的概念

多态,顾名思义,即多种形态。多态分为编译时多态(静态多态)运行时多态(动态多态),我们重点讲解运行时多态
编译时多态主要就是我们前面讲的函数重载和函数模板,它们传不同的参数可以调不同的函数,通过参数不同达到多种形态,而它们实参传给形参的参数匹配是在编译时完成的,因此叫编译时多态
运行时多态,就是对于某个函数,传不同的对象就会完成不同的行为,就达到多态形态。
举个例子:比如说买票,成人会买成人票,儿童会买儿童票,军人则是优先买票;再比如,一个函数是动物叫,传猫对象过去,就是“(> ^ ω ^ <)喵”,传狗过去就是“汪汪”。

二、多态的定义及实现

1. 虚函数

类成员函数前面加virtual关键字修饰,那么这个成员函数被称为虚函数。注意:非成员函数不能加virtual关键字修饰。

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

2. 多态实现的条件

多态是继承关系下的类对象,实现条件:

  • 必须是基类的指针或者引用调用虚函数
  • 被调用的必须是虚函数,并且完成了虚函数重写/覆盖

说明:实现多态,第一,必须是基类的指针或引用;第二,必须对虚函数完成重写/覆盖

3. 虚函数重写/覆盖

重写/覆盖需要派生类有一个跟基类完全相同的虚函数,要满足三个条件**(三同)**:

  1. 返回值相同
  2. 函数名字相同
  3. 参数列表相同

注意:在重写基类虚函数时,派生类虚函数在不加virtual关键字时,也可以构成重写(继承时虚函数被继承下来,仍然保持虚函数的属性),但这种写法并不规范,不建议这么写

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
} 
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

4. 多态的一道题

以下程序输出结果是什么
A. A->0 B. B->1 C. A->1 D. B->0 E.编译出错 F. 以上均不对

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

我们先来看看func函数,派生类与基类函数名,返回值,参数类型均相同,它是满足三同特征的,因此func函数构成动态多态,然后再看p,p去调用A继承下来的函数test,因此test内的this是A*类型,满足多态函数传基类指针的要求,因为传入的p原本就是B*类型,所以func会回到B内调用,那我们来看看最终答案是什么:
在这里插入图片描述
答案竟然是B->1,这是为什么呢?我们要理解清楚:多态函数重写的本质是,基类声明+派生类定义,因此func就变成了下面这样:

virtual void func(int val = 1)
{ std::cout<<"B->"<< val <<std::endl;}

这一点在 《Effective C++》条款37有提到:“绝不重新定义继承而来的缺省参数值” ,因此我们在写程序时应尽量避免这种情况

5. 多态其他问题

5.1 协变(了解即可)

派生类重写虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象指针或引用,派生类对象返回派生类指针或引用时,称为协变。其实际意义并不大,我们了解即可。

5.2 虚构函数的重写(重点)

如果基类虚构函数为虚函数,此时派生类虚构函数只要有定义,无论是否加virtual关键字,都与基类虚构函数构成重写,虽然它们看起来不符合重写规则,但是编译器在处理析构函数时,它们的析构函数名统一被处理成destructor,所以基类虚构加了virtual,派生类析构就构成重写
我们看以下案例:

class A
{ 
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:"<<_p<< endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下面的delete对象调⽤析构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

p1,p2都是A*类型,假设A类虚构函数不加virtual,使虚构函数不构成多态,那么在析构时,p1,p2都会去调用A的析构,而p2指向的B对象,调用A的析构会导致B中申请的内存无法被释放,引发内存泄漏,因此,基类的虚构函数建议设计成虚函数

6. override和final关键字

6.1 override

C++11提供了override关键字,它的作用是检测函数是否重写,如果没有重写,编译器就会报错

// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}

6.2 final

final关键字可以阻止函数被重写

// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{ 
public:
	virtual void Drive() final {}
};
class Benz :public Car
{ 
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}

7. 重载/重写/隐藏的对比

这三者都是描述函数之间的关系,我们来梳理一下它们的特点
在这里插入图片描述

三、纯虚函数和抽象类

1. 定义

在虚函数后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(它本身没有意义,需要被派生类重写,但语法上可以实现),只要声明即可。包含纯虚函数的类叫抽象类,抽象类不能被实例化出对象,如果派生类不重写纯虚函数,那么派生类也是抽象类。纯虚函数就相当于是强制派生类重写虚函数。

2. 示例

基本定义:

class Base {
public:
    virtual void func() = 0;  // 纯虚函数
};
// Base 是抽象类
Base b;  // ❌ 编译错误

派生类继承了纯虚函数但未覆盖:

class Base {
public:
    virtual void func() = 0;
};

class Derived : public Base {
    // 没有实现 func()
};
// Derived 仍然是抽象类!
Derived d;  // ❌ 编译错误

纯虚函数的默认实现不影响抽象性质:

class Base {
public:
    virtual void func() = 0;
};
void Base::func() {  // 纯虚函数有实现
    std::cout << "Base::func()" << std::endl;
}
Base b;  // ❌ 仍然是抽象类,不能实例化

多重继承下的混合情况:

class A {
public:
    virtual void f1() = 0;
};
class B {
public:
    void f2() {}
};
class C : public A, public B {
    // 没有实现 f1()
};
C c;  // ❌ 抽象类,因为 f1() 仍是纯虚

四、多态的原理

1. 虚函数表指针

看下面一个程序:

class Base
{ 
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

以上程序运行结果为12,我们来看看b对象的结构:
在这里插入图片描述
我们可以看到b对象除了_b和_ch成员,还有一个void类型的 __vfptr,这个指针我们叫做虚函数表指针**,一个含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有的虚函数的地址都要放到这个类对象的虚函数表中(虚函数表是一个函数指针数组 , 存放在对象里的指针是函数指针数组指针 ),虚函数表也简称虚表

2. 实现原理

我们先定义三个类:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
	string _name="张三";
	int _age=18;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
	int _id = 1;
};
class Soldier : public Person
{
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
};

我们来看看它们的结构:
在这里插入图片描述
可见,每个对象的基类部分都有一个__vfptr指针,各自的__vfptr指向数组所存储的则是对象自己的虚函数
在这里插入图片描述
在这里插入图片描述
每个指针指向谁就调用谁的虚函数,ps指向Person对象,就调用Person虚函数;st指向Student对象,就调用Student虚函数

3. 动态绑定与静态绑定

  • 对不满足多态条件的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
  • 满足多态条件的函数是在运行时绑定,也就是运行时编译器会到虚函数表里去找虚函数,叫做动态绑定

我们来看看静态绑定与动态绑定在汇编层面的区别:
动态绑定(动态多态):

ptr->BuyTicket();
00E94442  mov         eax,dword ptr [ptr]  
00E94445  mov         edx,dword ptr [eax]  
00E94447  mov         esi,esp  
00E94449  mov         ecx,dword ptr [ptr]  
00E9444C  mov         eax,dword ptr [edx]  
00E9444E  call        eax  
00E94450  cmp         esi,esp  
00E94452  call        __RTC_CheckEsp (0E913E8h)  
00E94457  nop  

静态绑定(静态多态):

ptr->BuyTicket();
005D43B2  mov         ecx,dword ptr [ptr]  
005D43B5  call        Person::BuyTicket (05D1663h)  
005D43BA  nop  

我们可以看到,对于BuyTicket函数,动态绑定的汇编语言会比静态绑定复杂很多,动态绑定会去call eax);而静态绑定会去call Person::BuyTicket,这个函数在编译时,已经有固定的地址(05D1663h)。

4. 虚函数表

  • 基类对象虚函数表中存放基类所有虚函数的地址。同类型对象共用一张续表,不同对象各自有独立的续表,因此基类和派生类有各自独立的虚表
  • 派生类由两部分构成,继承而来的基类和自己的成员,一般情况下,继承下来基类有虚表指针,自己就不会生成虚表指针。但是这里继承下来的基类部分虚表指针和基类对象的虚表指针不是同一个,就像基类对象成员和派生类对象中的基类成员也是独立的
  • 对于派生类重写的虚函数,派生类中虚函数表对应的虚函数就会被覆盖成派生类重写的虚函数
    在这里插入图片描述
  • 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派⽣类自己的虚函数地址三个部分
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记(这个与编译器有关)。
  • 虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
  • 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};

class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }

protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);

	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}

在这里插入图片描述
这样我们就能轻易地得到上述结论。

更多推荐