【C++】多态

文章目录


一、什么是多态

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

就像我们日常生活中去买火车票,普通人去买火车票一般是原价,而学生去购票可能价格会所不同,学生买火车票一般会打折。

二、多态的定义

1.多态的构成条件

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数(只能是类的非静态成员函数) 。

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

3.虚函数的重写

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

派生类重写虚函数可以不加virtual(不推荐,建议加上)

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

class Student:public Person
{
	virtual void Buy()const  //虚函数重写:函数名、返回值、参数列表完全相同(三同)
	{
		cout << "买票:半价" << endl;
	}
};

虚函数重写的特例:

①协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指

针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(实际开发中不经常使用)

cpp 复制代码
#include<iostream>

using namespace std;

class Person
{
public:
	virtual Person* Buy()const  //虚函数
	{
		cout << "买票:全价" << endl;
		return 0;
	}
};

class Student:public Person
{
	virtual Student* Buy()const  //虚函数重写:函数名、返回值、参数列表完全相同(三同)
	{
		cout << "买票:半价" << endl;
		return 0;
	}
};
void func(const Person& p)
{
	p.Buy();
}
int main()
{
	Person p;
	func(p);
	Student s;
	func(s);
	return 0;
}

②析构函数的重写(基类与派生类析构函数的名字不同)

虽然函数名不相同,看起来违背了重写的规则,其实不是,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

没有重写析构函数,没有构成多态

cpp 复制代码
#include<iostream>

using namespace std;

class Person
{
public:
	~Person() { cout << "~Person()" << endl; }
};

class Student :public Person
{
public:
	~Student() 
	{ 
		cout << "~Student()" << endl; 
		delete[] ptr;
	}
private:
	int* ptr = new int[10];
};

int main()
{
	//切片
	Person* p = new Student;
	delete p;

	return 0;
}

我们可以看到如果没有重写析构函数,在使用new创建的对象,使用父类指针接受子类,发生切割后,他会去调用父类指针指向的父类的析构函数,如果子类中动态申请了内存,被没有通过析构函数去释放空间,就会造成内存泄露问题。

重写析构函数构成多态

cpp 复制代码
class Person
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student :public Person
{
public:
	virtual ~Student() 
	{ 
		cout << "~Student()" << endl; 
		delete[] ptr;
	}
private:
	int* ptr = new int[10];
};

int main()
{
	//切片
	Person* p = new Student;
	delete p;

	return 0;
}

重写析构函数构成多态时,父类指针也会去调用子类的析构函数达到释放空间的目的
为什么要将析构函数统一重命名为destructor?

因为需要去重写析构函数构成多态调用各自的析构函数

4.override 和 final(C++ 11)

使用override可以检测子类中是否重写父类中的某个方法

设计不想被继承的类

方法1:基类构造函数私有

方法2:基类析构函数私有

方法3:final

构造函数私有

使用final修饰

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

三、多态的原理

1.虚函数表

cpp 复制代码
// 这里常考一道笔试题:sizeof(Base)是多少?

class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }

private:
 int _b = 1;
};

为什么这里会是8?

我们可以看到b中还有一个虚函数指针表,表中还有一个指针,再加上_b,由此b所占内存大小为8

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析
虚函数表一般是存放在常量区(可以自行验证,虚继承表地址就是这个对象的前4个字节(32位)),并且同一个类型的变量使用同一份虚函数表。

虚函数表的生成

派生类的虚表生成 :a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生

类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己

新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

2.多态的原理

cpp 复制代码
#include<iostream>

using namespace std;

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

class Student:public Person
{
public:
	virtual void BuyTicket()const  //虚函数重写:函数名、返回值、参数列表完全相同(三同)
	{
		cout << "买票:半价" << endl;
	}
	int _a = 10;
};
void func(const Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	func(p);

	Student s;
	func(s);

	return 0;
}


实现多态时虚函数重写仅仅只是将函数体重写,还是用的原先的函数声明,如果函数传参时使用缺省类型,函数体在使用参数时默认使用父类的缺省值

cpp 复制代码
class A
   {
   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;
   }

3.动态绑定和静态绑定

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

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

四、单继承和多继承的虚函数表

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) (); //函数指针使用typedef也是这种定义方式 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;
}

int main()
{
	 Base b;
	 Derive d;
	 VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	 PrintVTable(vTableb);
	 VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	 PrintVTable(vTabled);
	 return 0;
}
 // 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
//指针的指针数组,这个数组最后面放了一个nullptr(不同编译器有所不同)
 // 1.先取b的地址,强转成一个int*的指针
 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
 // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
 // 4.虚表指针传递给PrintVTable进行打印虚表
 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再
//编译就好了。

2.多继承的虚函数表

cpp 复制代码
class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()" << endl;
	}
 
	virtual void fun2()
	{
		cout << "A::fun2()" << endl;
	}
};
 
class B 
{
public:
	virtual void fun3()
	{
		cout << "B::fun3()" << endl;
	}
 
	virtual void fun4()
	{
		cout << "b::fun4()" << endl;
	}
};
 
class C :public A, public B 
{
	virtual void fun1()
	{
		cout << "C::fun1()" << endl;
	}
 
	virtual void fun5()
	{
		cout << "C::fun5()" << endl;
	}
};
int main()
{
	C c;
	return 0;
}

结论:多继承的子类中存在两张虚表,且子类中未重写的虚函数放在第一张虚表中

五、抽象类

1.概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口

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

类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

cpp 复制代码
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();
}

抽象类不可实例化

继承了抽象类也不可实例化

继承了抽象类如果想实例化,可通过重写重虚函数

2.接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实

现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成

多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

相关推荐
未来可期LJ23 分钟前
【C++ 设计模式】单例模式的两种懒汉式和饿汉式
c++·单例模式·设计模式
Trouvaille ~1 小时前
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
c++·c++20·编译原理·编译器·类和对象·rvo·nrvo
little redcap1 小时前
第十九次CCF计算机软件能力认证-乔乔和牛牛逛超市
数据结构·c++·算法
AI原吾2 小时前
掌握Python-uinput:打造你的输入设备控制大师
开发语言·python·apython-uinput
机器视觉知识推荐、就业指导2 小时前
Qt/C++事件过滤器与控件响应重写的使用、场景的不同
开发语言·数据库·c++·qt
毕设木哥2 小时前
25届计算机专业毕设选题推荐-基于python的二手电子设备交易平台【源码+文档+讲解】
开发语言·python·计算机·django·毕业设计·课程设计·毕设
珞瑜·2 小时前
Matlab R2024B软件安装教程
开发语言·matlab
weixin_455446172 小时前
Python学习的主要知识框架
开发语言·python·学习
孤寂大仙v2 小时前
【C++】STL----list常见用法
开发语言·c++·list
她似晚风般温柔7893 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app