【C++】多态

文章目录

  • 1. 多态的概念
    • 1.1 概念
  • 2. 多态的定义及实现
    • 2.1 多态构成的条件
    • 2.2 虚函数
    • 2.3虚函数的重写
    • 2.4 C++11 override 和 final
    • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
  • 3. 抽象类
    • 3.1 概念
    • 3.2 接口继承和实现继承
  • 4. 多态的原理
    • 4.1虚函数表与多态原理
    • 4.2 动态绑定与静态绑定
  • 5. 单继承和多继承关系中的虚函数表
    • 5.1 单继承中的虚函数表
    • 5.2 多继承中的虚函数表

1. 多态的概念

1.1 概念

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

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2. 多态的定义及实现

2.1 多态构成的条件

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

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

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

在这里插入图片描述

2.2 虚函数

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

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

2.3虚函数的重写

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

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
	class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	/*注意:在重写基类虚函数时,C++规定允许派生类的虚函数在不加virtual关键字时,
	虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),
	但是该种写法不是很规范,不建议这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
	 p.BuyTicket(); 
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

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

2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

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

普通调用:调用函数的类型是谁,就去调用这个对象类型的函数。
多态调用:调用指针或者引用指向的对象。指向父类就调用父类的函数,指向子类调用子类的函数。

假设派生类Student的析构函数没有重写Person的析构函数,也就是子父类析构函数不构成虚函数的重写,那么delete ptr释放对象,两次调用的都是Person类的析构函数,这种情况称为普通调用。所以结果如下:
在这里插入图片描述
只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。这种情况属于多态调用。
在这里插入图片描述

2.4 C++11 override 和 final

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

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

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

在这里插入图片描述
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

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

在这里插入图片描述

3. 抽象类

3.1 概念

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

class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 virtual void Drive()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

3.2 接口继承和实现继承

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

4. 多态的原理

4.1虚函数表与多态原理

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
	char ch = 'x';
};
int main()
{
	Base b;
	//x86环境下(32位平台)占12Byte
	cout << sizeof Base << endl;
}
//不要误以为仅仅考察的是内存对齐,在vs平台通过调试观察发现还多了一个__vfptr指针
//b对象中的__vfptr指针我们叫做虚函数表指针(v代表virtual,f代表function)
//一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
//的地址要被放到虚函数表中,虚函数表也简称虚表。

通过观察测试我们发现b对象是12bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
在这里插入图片描述


class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func(){}
private:
	int a = 0;

};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
	int b = 1;
};
void Func(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student s;
	Func(&p);
	Func(&s);
	return 0;
}

通过观察和测试,我们发现:

1.派生类s中也有一个虚表指针,s对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
2.基类p与子类s对象基表是不一样的,这里我们发现BuyTicket完成了重写,所以s的虚表中存放的是BuyTicket,所以观察底层其地址发生了改变,其本质是拷贝给子类对象,子类对象覆盖了,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.func函数被继承下来后是虚函数,所以放进了s子类对象的虚表。
4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
多态调用的具体含义:父类对象的指针或者引用指向父类对象调用父类的虚函数,指向子类对象调用子类的虚函数或者"切割"出来父类那一部分。

那么为什么只有父类的指针或者引用去调用才会形成多态,Person p = s,用父类的对象调用却不能形成多态呢?这里表示的是将子类对象拷贝给父类,而不会构成多态,将切割出子类对象中父类那一部分成员拷贝给父类,但是并不会拷贝虚函数表指针!否则,构成多态,引发一系列不可预知的结果!
例如:假如构成多态,那么以下代码,假如s重写了析构函数,然后delete p就会释放Student对象,而不是Person对象。

在这里插入图片描述

6.虚函数与虚函数表存放在哪里?
虚函数和普通成员函数一样存放在代码段,同时把虚函数地址存了一份到虚函数表。虚函数表也属于代码段(常量区)。

下面我们通过一段程序验证一下:

//程序验证,虚函数表与虚函数存放在内存哪个区域??
void funct()
{}
int main()
{
	static int a = 10;
	int b = 20;
	int* p = new int;
	const char* str = "hello world";
	Person ps;
	printf("堆区:%p\n", p);
	printf("栈区:%p\n", &b);
	printf("静态区:%p\n", &a);
	printf("代码段:%p\n", str);
	printf("虚函数表: %p\n", *((int*)&ps));//取对象头4个byte,即为指向虚表的指针
	printf("虚函数地址: %p\n", &Person::func);
	printf("普通函数地址: %p\n", funct);

	return 0;
}

在这里插入图片描述

7.满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的。不满足多态的函数调用是编译时确认好的。

4.2 动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

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

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

5.1 单继承中的虚函数表

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; }
	void func5() { cout << "void func5()" << endl; }
private:
	int b;
};
class X : public Derive
{
public:
	virtual void func3() { cout << "X::func3" << endl; }
private:
	int c;
};
int main()
{
	Base b;
	Derive d;
	X x;
	return 0;
}

将以上程序在vs平台运行起来,调试观察监视窗口发现只能看到基类的func1和func2函数,那么其他的虚函数是不是就与之前讲的冲突了呢?这不禁让我们产生了质疑:
虚函数的地址一定会被放进类的虚函数表吗?
答案是一定的,只不过这里为什么没有显示出来呢?因为编译器可视化的窗口是会欺骗人的O(∩_∩)O哈哈,开玩笑啦,真实原因取决于编译器的设计而已,监视窗口有时会隐藏一些信息,导致一些bug,我们不妨自己去验证,查看一下虚表,那么有什么方式打印虚表呢?
在这里插入图片描述

前面我们讲过打印虚函数表验证虚函数表存放在内存哪个区域,我们只要取出对象的前4个字节,就是虚表指针。
思路:以Derive对象d为例,&d即为Derive对象指针,那么如何拿到虚函数表指针,我们需要先转为int类型的指针,再解引用就可以拿到头四个字节的值,这个值就是指向虚表的指针。
在这里插入图片描述

前面我们说虚函数表本质是一个存虚函数指针的数组,这个数组最后面放了一个nullptr,我们可以写一个打印虚函数表的函数,用一个函数指针接收参数。

//打印虚表
typedef void(*VFptr)();//重命名void (*)()函数类型为VFptr

void Print_VFT(VFptr a[])
//void Print_VFT(VFptr* a)
{
	cout << "__vfptr地址:" << a << endl;
	for (size_t i = 0; a[i] != nullptr; i++)
	{
		printf("[%d] : %p ->", i, a[i]);

		VFptr f = a[i];//调用虚函数
		f();
		//以下写法等价
		//a[i]();
	}
	printf("\n");
}
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; }
	void func5() { cout << "void func5()" << endl; }
private:
	int b;
};
class X : public Derive
{
public:
	virtual void func3() { cout << "X::func3" << endl; }
private:
	int c;
};
int main()
{
	Base b;
	Derive d;
	X x;
	//不同的类型需要进行强制类型转换
	//需要再强转成VFptr*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	Print_VFT((VFptr*)*((int*)&b));
	Print_VFT((VFptr*)*((int*)&d));
	Print_VFT((VFptr*)*((int*)&x));
	return 0;
}

需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再次编译就好了!
在这里插入图片描述

所以,虚函数的地址一定会被放进类的虚函数表。同时我们也再一次深刻认识到了虚函数表。

5.2 多继承中的虚函数表

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 (*vf_ptr)();
void Print_VFT(vf_ptr a[])
{
	for (size_t i = 0; a[i] != nullptr; i++)
	{
		printf("[%d] : %p ->", i, a[i]);
		a[i]();
	}
	cout << endl;
}
int main()
{
	Derive d;
	//打印第一个虚表
	Print_VFT((vf_ptr*)*(int*)&d);

	//打印第二个虚表
	// 需要找偏移量,向后偏移sizeof Base1个字节
	//Print_VFT((vf_ptr*)*(int*)((char*)&d + sizeof(Base1)));

	//当然利用切片来写更好
	Base2 b = d;
	Print_VFT((vf_ptr*)*(int*)&b);

	return 0;
}

在多继承中,有多少

上一篇:小米路由器ax1500+DDNS+公网IP+花生壳实现远程访问-实现流程


下一篇:前端工程化-Vue3脚手架安装