Cpp中关于多态那些事

c++进阶多态

一.多态的简介

虚表在编译的时候生成,虚表指针在创建函数时
就已经生成。(布鲁品)

1.1:多态的概念

完成某个行为时,不同的对象去完成时会产生出不
同的状态

  • 例如:两个老师先后审阅同一份试卷,但是两个老师给出的得分却不相同,这种状态就叫做多态
1.2:多态的构成条件
  • 1.调用函数的对象必须是指针或者引用(传引用和传对象的目的是避免传值时调用构造函数重新创造对象)
  • 2.被调用的函数必须是虚函数,且完成了虚函数的重写

在这里插入图片描述

【原则】

  • 非多态看类型

指明调用函数的地址

  • 多态看对象

未指明调用函数的地址,根据对象的虚表指针,虚表调用对象所指定函数完成功能

1.3:虚函数

虚函数:就是在类的成员函数的前面加virtual关键字

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

再次强调

  • 多态发生的前提:继承
  • 调用函数的类型必须是指针或者引用
  • 被调用函数必须为虚函数,并且被重写
  • 虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数,
  • 完全相同是指:函数名、参数、返回值类型都相同。另外虚函数的重写也叫作虚函数的覆盖
#include<iostream>

using namespace std;

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
//Student类调用时发生欺骗操作
//此处类型为基类的原因是:基类对象不能赋值给子类对象,存在访问越界的风险,
//但是子类对象可以赋值给基类对象(发生切片操作)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}
  • 由上例可以看出基类和派生类中有虚函数的重写
  • 派生类在调用Func时(参数为Person类型的引用)发生了欺骗操作,Student类型的对象调用参数为Person类型的函数调用成功。
  • 可以看出不同类型的对象分别调用的是属于自己类型的代码段
1.5虚函数重写的例外:协变

在C++中,只要原来的返回类型是指向类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。这样的类型称为协变返回类型(Covariant returns type).

  • 重写的虚函数的返回值可以不同,但是返回值类型必须分别是基类指针和派生类指针或基类引用和派生类引用
//下述代码中基类和派生类函数重写构成协变
class Person {
public:
	virtual A* f() {return new A;}
};
class Student : public Person {
public:
	virtual B* f() {return new B;}
}

【注意】:不规范的重写行为

  • 在派生类中重写的成员函数可以不加virtual关键字,也可构成重写.因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,我们只是重写了它。但是这是非常不规范的,我们平时不要这样使用
1.5:析构函数的重写问题
  • 基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。
  • 这里他们的函数名不相同,看起来违背了重写的规则,其实不是这样的,
  • 这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
  • 这也说明基类的析构函数最好写成虚函数。
#include<iostream>

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

  • 运行结果

  • 可以从该程序的运行结果中看出~Person()被调用了两次,
  • 第一次调用:是在new Person的时候调用默认构造函数Person()的时候,从而引发的~Person函数的调用
  • 第二次调用:是在new Student的时候调用默认构造函数Student()的时候掉用了Person(),从而引发的~Person函数的调用

那么基类中的虚构函数没有写生虚函数,会怎么样呢?

  • 执行如下代码
#include<iostream>
using namespace std;
class A{
public:
A(){
cout<<"A"<<endl;
}
~A(){
cout<<"~A"<<endl;
}
};
class B:public A{
public:
B(){
cout<<"B()"<<endl;
}
~B(){
cout<<"~B()"<<endl;
}
};
int main(){
A* a=new B;
delete a;
return 0;
}
  • 运行结果

在这里插入图片描述
可以看出,基类析构函数不使用virtual关键字修饰的时候出现了如下问题

将执行派生类的基类指针delete之后,不会调用派生类的析构函数,造成内存泄漏

  • 接口继承和实现继承

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

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

  • 上图中将三者的特点做了一定的对比,可以帮助大家更清楚的认识他们各自的特点
二:抽象类
2.1:简单介绍
  • 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),

纯虚函数格式

virtual hanshuming()=0;
  • 抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数(基类中的纯虚函数在派生类中重写的时候不能重写成纯虚函数),派生类才能实例化出对象。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

【简单总结】

  • 包含重虚函数的抽象类不能实例化对象,其派生类只有重写纯虚函数才能实例化对象
#include<iostream>

using namespace std;

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 c;//提示不允许使用抽象类类型Car的对象
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

int main(){
	Test();
	return 0;
}
  • 在派生类中重写的函数若还是纯虚函数,那么派生类还是抽象类,依然不能实例化出对象
2.2:补充(c++ 11 override 和 final)
  • c++11提供了override和final来修饰虚函数

  • 实际中建议多使用纯虚函数+ overrid的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义

  • override: 强制重写

声明为override的函数必须重写父类的一个函数

class Car{
public:
	virtual void Drive(){}
};
// 2.override 修饰派生类虚函数强制完成重写
class Benz :public Car {
public:
	virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

fianl修饰基类的虚函数不能
被派生来继承

class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() {cout << "Benz-舒适" << endl;}
	//提示错误无法重写"final函数" "Car::Drive"
};
  • 注意】final 修饰基类的虚函数时,虚函数不能被派生类重写
三:多态的实现的原理
3.1:虚函数表

【知识点补充】

  • 虚表指针:存在于对象中,位于对象的前4/8个字节(与平台有关),指向虚表的首地址
  • 虚表:存放函数指针,本质为指针数组,虚表本身存在(vs)代码段
  • 子类继承父类的虚表,对于子类重写的函数,覆盖掉父类对应的虚函数
    -== 虚表中存放虚函数的地址,不存放普通函数的地址单继承虚表结构==
// 这里常考一道笔试题:sizeof(Base)是多少?
#include<iostream>

using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main(){
	Base b;
	cout<<sizeof(b)<<endl;
	return 0;
}

该程序中虚表指针与虚表的关系图

在这里插入图片描述

  • 通过调试我们发现 sizeof(Base) 大小为8,那么除了一个int类型的变量之外的四个字节来自哪里呢?
  • 原来还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

虚函数表指针

_ vfptr: vritual function pointer
存在于对象模型的第一个位置


  • 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

监视结果如下图所示

  • 对上面程序的改进(在子类中重写Func1()函数)
#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:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

  • 监视结果

在这里插入图片描述

  • 虚基表中只存放该类中虚函数的地址,例如基类中的Fun3函数的地址就不存在该类的虚基表中

若我们将派生类中做如下改动,再看监视结果

class Derive : public Base
{
public:
	
private:
	int _d = 2;
};
  • 监视结果

在这里插入图片描述

我们发现派生类继承的虚基表中,两个虚函数的地址和基类中的一模一样,没有发生任何改变
那么我们可以根据上述两个测试得出结论:派生类在继承的时候生成了自己的虚基表,但是派生类虚基表中存放的任然是基类虚基表中虚函数的地址,只有在重写基类中的虚函数的时候,派生类中的虚函数地址才会发生改变.

总结

  • 1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,除了虚表指针之外的内容是派生类自己的内容

  • 2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法

  • 3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但其不是虚函数,所以不会放进虚表。

  • 4.虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

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

    • a.先将基类中的虚表内容拷贝一份到派生类虚表中
    • b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 6.这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的

    答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然。

    注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的指针存到了虚表中另外对象中存的不是虚表,存的是虚表指针

那么虚表存在哪的呢?

#include<iostream>

using namespace std;

class Parent{
public:
	virtual void Fun1(){
		cout << "Parent::Fun1()" << endl;
	}
	virtual void Fun2(){
		cout << "Parent::Fun2()" << endl;
	}

	virtual void Fun3(){
		cout << "Parent::Fun3()" << endl;
	}
private:
	int _p = 2;
};

class Child :public Parent
{
public:
	virtual void Fun1(){
		cout << "Child::Fun1()" << endl;
	}
private:
	int _c=1;
};
int global = 3;
int main(){
	Child c;
	int tmp = 2;
	int  *p = new int;
	cout <<"栈上:"<< &tmp << endl;
	cout << "堆上:"<<p << endl;
	cout <<"数据段:"<< &global << endl;
	printf("代码段:%p\n", &Parent::Fun3);

	printf("虚表:%p\n",*((int *)&c));
	return 0;
}
  • 运行结果

在这里插入图片描述

  • 实际我们去验证一下会发现vs下是存在代码段的
3.2:多态的原理
#include<iostream>

using namespace std;
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;
}
  • 调用关系

我们可以看到两个不同类型的对象通过调用相同的函数Func调用了不同的BuyTicke函数,没有因为Func函数的参数类型而影响最终结果。原因是他们在各自的虚基表中存储的是不同的虚基表函数地址从而调用了不同的虚函数

  • 监视结果

  • 运行结果

  • 当p是指向mike对象时,p->BuyTicket在mick的虚表中找到虚函数是Person::BuyTicket
  • 当p是指向johnson对象时,p->BuyTicket在johnson的虚表中找到虚函数是Student::BuyTicket
  • 这样的话就可以实现不同的对象去完成同一行为时,展现出不同的形态
3.3:动态绑定与静态绑定
  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

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

如何实现多态

  • 多态的对象模型:只要类中有虚函数,对象模型中就会存在一个虚表指针,虚表指针指向虚表,虚表本质上为函数指针数组,虚表在vs下存在代码段。
  • 1.本质上是通过虚表实现:程序在运行的时候,根据引用或者指针指向的对象,访问对象模型中的虚表指针,获取虚表中对应位置的函数指针,调用对应的函数
四.单继承和多继承关系的虚函数表
4.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;}
private :
	int b;
};

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数.

  • 监视结果如图所示

在这里插入图片描述

那么我们如何查看d的虚表呢?

  • 解决办法

  • 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

  • 1.先取b的地址,强转成一个int的指针

  • 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

  • 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

  • 4.虚表指针传递给PrintVTable进行打印虚表

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

#include<iostream>

using namespace std;

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

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 PrintVF(VFPTR vfpt[]){
printf("虚表:%p\n",vfpt);
//虚表以nullptr结尾
while(*vfpt!=nullptr){
VFPTR vf=*vfpt;
printf("0X%p-->",vf);
vf();
cout<<endl;
++vfpt;
}
}
*/
int main()
{
	Base b;
	Derive d;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}
  • 运行结果

  • 由程序的运行结果来看,在派生类中重写了虚函数fun1从而发生了覆盖现象。而派生类中没有重写fun2函数,所以派生类中的fun2函数是继承基类的(由图知基类和派生类中fun2函数地址并没有改变)。
4.2:多继承中的虚函数表
  • 验证程序
#include<iostream>

using namespace std;

typedef void(*VFPTR)();

void PrintVF(VFPTR vfpt[])
{
	printf("虚表: %p\n", vfpt);
	//虚表以nullptr
	while (*vfpt != nullptr)
	{
		VFPTR vf = *vfpt;
		printf("0x%p-->", vf);
		vf();
		//cout << endl;
		++vfpt;
	}
}



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

int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	Base2* pb2 = &d;
	PrintVF((VFPTR*)*(int*)&b1);
	PrintVF((VFPTR*)*(int*)&b2);
	cout << "Derived: " << endl;
	PrintVF((VFPTR*)*(int*)&d);
	PrintVF((VFPTR*)*(int*)pb2);
	PrintVF((VFPTR*)*(int*)((char*)&d + sizeof(Base1)));
	cout << sizeof(Derive) << endl;

	return 0;
}

  • 运行结果

在这里插入图片描述

  • 调用情况图示

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

【推荐链接】:

包含C++ 虚表函数解析

五:【总结】
  • 多态:不同对象执行同一种行为产生不同的状态

  • 构成多态的条件

  • 1.调用函数的类型必须为指针或者引用

  • 2.函数必须为虚函数,并且完成虚函数的重写

  • 重写(覆盖)

  • 1.分别存在于子类和父类,

  • 2.函数名相同,参数相同,返回值类型相同

  • 重定义(隐藏)

  • 1.分别存在于子类和分类中,函数名相同

  • 重载

  • 1.同一作用域,函数名相同,参数不同(个数、顺序、类型)

  • 虚函数:virtual 函数

  • 纯虚函数: virtual 函数()=0;

  • 抽象类:包含纯虚函数的类,不能实例化对象,反映的是接口继承

  • 多态的原理:通过虚表实现多态

  • 虚表:存放虚函数指针,本质为指针数组,虚表本身在(vs)代码段

  • 父类有虚表

  • 子类继承父类的虚表,对于子类重写的函数,覆盖掉父类对应的虚函数。虚表中存放虚函数的地址,不存放普通函数的地址

  • 虚函数:存在于代码段

  • 虚表指针:存在于对象中,位于对象的前4/8个字节

  • 单继承虚表结构

  • 首先存放的是父类虚函数地址,子类定义的虚函数,对应的地址按顺序依次存放在虚表的末尾

  • 多继承

  • 虚表的个数和直接父类的个数一致,子类定义的虚函数,对应的地址按顺序依次存放在第一个直接父类的虚表末尾

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值