C++多态


一、多态的概念及条件

1.1 多态的概念

多态:顾名思义为多种形态,详细解释为一个接口,多种实现,即方法的实现应取决于调用该方法的对象。其中可分为两种,静态多态和动态多态。

  • 静态多态:就是我们常见的函数重载和函数模板。在编译的时候就能确定该接口是哪种实现方法,所以也是编译时多态。
  • 动态多态:运行时多态称为动态多态,具体引用的接口在运行时才能确定。是本文要讲述的重点,也是面向对象语言中的一个难点。

1.2 形成多态的条件

在任何时刻,如何区分是否形成多态,都要遵循这两个条件。

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

先来简单看一下多态的实现:

#include<iostream>
#include<string>
using namespace std;

class Animal
{
public:
	Animal(string na = "动物")
		:_name(na)
	{}
	virtual void print()
	{
		cout << "animal : " << _name << endl;
	}
protected:
	string _name;
};
class Dog : public Animal
{
public:
	Dog(string _na = "小狗")
		:Animal(_na)
	{}
	virtual void print()
	{
		cout << "Dog : " << _name << endl;
	}
};

void show(Animal& a)
{
	a.print();
}
int main()
{
	Animal a("老虎");
	Dog d("哈士奇");

	show(a);
	show(d);

	return 0;
}

在这里插入图片描述

二、虚函数的重写

2.1 虚函数

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

virtual void print()
{
	cout << "animal : " << _name << endl;
}

2.2 虚函数的重写

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

在派生类中一个函数要实现重现,四个条件

  1. 父类中是虚函数
  2. 返回值相同
  3. 函数名字相同
  4. 参数列表相同

若一个函数,父类中是虚函数,子类中不写也算虚函数,但是一般不建议这样写。

在上述条件中,有两种例外的情况

  1. 协变 (两个虚函数的返回值是父子关系的引用或指针)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
父类中
virtual Animal& print()
{
	cout << "animal : " << _name << endl;
	return *this;
}

子类中
virtual Dog& print()
{
	cout << "Dog : " << _name << endl;
	return *this;
}

其他具有继承关系的类也行

class A
{};
class B : public A
{};
父类中
virtual A* print()
{
	cout << "animal : " << _name << endl;
	return nullptr;
}
子类中
virtual B* print()
{
	cout << "Dog : " << _name << endl;
	return nullptr;
}
  1. 析构函数的重写 (基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

    这样做的原因是为了完整的释放空间,调用析构函数。
    比如:
    new -> 本质上是 先调用操作符operator new 开辟空间,再调用构造函数
    delete -> 本质上是 先调用析构函数,再调用操作符operator delete 释放空间

Animal* p1 = new Animal;
Animal* p2 = new Dog;// 父类指针是能够接受子类对象的

delete p1; 
delete p2;
// p2是父类Animal的指针,需要释放子类空间,先得调用子类的析构函数,那就需要构成多态

p2是父类Animal的指针,需要释放子类空间,先得调用子类的析构函数,那就需要构成多态,所以就把两个析构函数名最终处理为同名,使得满足多态条件。

2.3 C++11 override 和 final

这是C++11新增的两个关键字

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

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

在这里插入图片描述
final修饰类,表示不能被继承
一个类不能被继承的常规做法,是将该类的构造函数写成私有的,这样的话子类就不能实例化,因为无法初始内容。
在这里插入图片描述
但是这样也导致了父类A不能初始化
在这里插入图片描述
我们知道私有成员,类外不能访问,类内可以访问,那么就可以实现一个函数来调用构造函数。但要注意这里函数需要是静态的,因为没有对象,只能调用静态成员,就像先有鸡还是先有蛋这个问题,这里可以先弄出一只鸡来,即静态函数,可以使用类名调用。
在这里插入图片描述
C++11增加关键字final以后就可以直接在类后面添加,用来表示该类不能被继承。更加方便。
在这里插入图片描述

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

在这里插入图片描述

2.4 重载、重写(覆盖)、重定义(隐藏)

在这里插入图片描述

三、抽象类

3.1 概念

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

纯虚函数一般不实现,但也是可以实现的。

class Animal
{
public:
	Animal(string na = "动物")
		:_name(na)
	{}
	virtual void print() = 0; // 纯虚函数一般只定义不实现,因为实现也没有价值
	virtual ~Animal(){}
protected:
	string _name;
};
class Dog : public Animal
{
public:
	Dog(string _na = "小狗")
		:Animal(_na)
	{}
	virtual void print() override
	{ 
		cout << "Dog : " << _name << endl;
	}
};

为什么纯虚函数实现了也没有价值呢? 这里解释一下,需要先把多态的原理看了,再来看这里。

虽然抽象类不能创建对象,但是可以创建类的指针(空指针),去调用位于代码段的成员函数,但是却调不了纯虚函数。因为对于纯虚函数而言,它的调用地址被放到了虚函数表里面,而虚函数表指针是放在对象里面的,所以如果要找到虚表指针,就要对对象指针解引用,对空指针解引用是错误的,所以无法调用到该函数。

那又有一个问题,如果这个指针不是空指针,而是子类对象的地址,那能调到父类的该纯虚函数呢? 这是不能的,因为子类继承抽象类后 需要重写纯虚函数,一旦发生重写,虚函数表里面原来的指针将被改为子类重写的函数的地址,所以也调不了父类的纯虚函数。

class Animal
{
public:
	Animal(string na = "动物")
		:_name(na)
	{}
	virtual void print() = 0 // 纯虚函数一般只定义不实现,因为实现也没有价值
	{
		cout << "A::print()" << endl;
	}
	void f()
	{
		cout << "A::f()" << endl;
	}
protected:
	string _name;
};
class Dog : public Animal
{
public:
	Dog(string _na = "小狗")
		:Animal(_na)
	{}
	virtual void print() override
	{ 
		cout << "Dog : " << _name << endl;
	}
};

int main()
{
	// 1.
	//Animal* p = nullptr;
	//p->f();   发生访问错误,对空指针的解引用
	//  因为需要在对象里面通过虚表指针找到虚表,然后找到函数地址
	//p->print(); 可以调到,因为该函数是普通成员函数,可以直接找到函数地址

	// 2.
	// 定义一个子类
	Dog d;
	Animal* pa = &d;
	pa->print(); // 由于多态,调到了类重写的函数

	return 0;
}

3.2 接口继承和实现继承

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

四、多态的原理

4.1 虚函数表

引出多态原理之前,先来计算一个包含虚函数的类的大小
在这里插入图片描述
答案是 8,按常理来说包含一个整型变量,在win32下,应该是 4字节,多出来的四字节用来干什么了呢?
通过监视窗口看一下
在这里插入图片描述
原来多了一个指针,刚好是四字节大小。这个指针有什么作用,是指向哪里的呢?
对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
在这里插入图片描述
来看一下继承之后的虚表
在这里插入图片描述
总结:

  1. 派生类对象d中也有一个虚表指针,由父类继承得来。
  2. 基类 a 对象和派生类 d 对象虚表是不一样的,这里我们发现 print() 完成了重写,所以d 的虚表中存的是重写的子类的 print() 函数,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 不是虚函数不会将地址放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段(常量区)的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

4.2 多态的原理

可以总结为: 基类的指针/引用,指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用
现在再来使用一下多态

看一下对应的汇编代码
在这里插入图片描述
传入参数为子类
在这里插入图片描述
它们的汇编代码是一样,根据传入参数的不同,找到对应的虚函数表,这样访问传入参数类的函数,达到我们想要的目的。

4.3 动态绑定与静态绑定

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

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

我们可以写一个函数来打印一下虚函数表。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<vector>
using namespace std;

typedef void(*VF_PTR)(); // 将函数指针类型重定义为 VF_PTR

void Print(VF_PTR* table) // 参数是一个函数指针数组, 也就是虚表
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		table[i](); // 调用该函数
	}
	cout << endl;
}

5.1 单继承的虚函数表

单继承

class A {
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
protected:
	int _a;
};

class B : public A {
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func3() { cout << "B::func3" << endl; }
protected:
	int _b;
};

这里B重写了func1函数,有加了 func3新的函数。
可以画一下B的对象模型,B的虚表指针,是继承A的。在VS中虚表指针是放在对象空间开头的。
在这里插入图片描述
打印虚表

int main()
{
	A a;
	Print((VF_PTR*)(*(void**)&a)); 
	// 先将a的地址强转为void**,因为对象里面存放虚表指针
	// 再解引用为void* 即一个无类型指针,这样就拿到了虚表指针的值
	// 再将这个指针转成函数指针的指针,因为虚表里面存放的是函数指针
	// 这样的话也可以使用 Print((*(VF_PTR**)&a));  更加简洁
	B b;
	Print((VF_PTR*)(*(void**)&b));
	return 0;
}

在这里插入图片描述

5.2 多继承下的虚函数表

多继承下有多份虚表
例如 C继承A和B

class C : public A, public B {
public:
	virtual void func1() { cout << "C::func1" << endl; }
	virtual void func3() { cout << "C::func3" << endl; }
	virtual void func4() { cout << "C::func4" << endl; }
protected:
	int _c;
};

在这里插入图片描述

int main()
{
	C c;
	Print((VF_PTR*)(*(void**)&c));
	Print((VF_PTR*)(*(void**)((char*)&c + sizeof(A))));
// 先将c的地址转成char*,一个字节的指针,再向后偏移A的大小,就可以取到继承与B的虚表
	return 0;
}

在这里插入图片描述

最后说一句 多态牛逼


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_persist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值