【C++】多态

文章目录

  • 一、什么是多态
  • 二、多态的定义
    • 1.多态的构成条件
    • 2.虚函数
    • 3.虚函数的重写
      • ①协变(基类与派生类虚函数返回值类型不同)
      • ②析构函数的重写(基类与派生类析构函数的名字不同)
    • 4.override 和 final(C++ 11)
    • 5.重载、重定义(隐藏)、重写(覆盖)对比
  • 三、多态的原理
    • 1.虚函数表
    • 2.多态的原理
    • 3.动态绑定和静态绑定
  • 四、单继承和多继承的虚函数表
    • 1.单继承的虚函数表
    • 2.多继承的虚函数表
  • 五、抽象类
    • 1.概念
    • 2.接口继承和实现继承


一、什么是多态

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

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

二、多态的定义

1.多态的构成条件

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

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

2.虚函数

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

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

3.虚函数的重写

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

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

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

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

虚函数重写的特例:

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

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

#include

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

【C++】多态_第1张图片

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

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

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

#include

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

【C++】多态_第2张图片

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

重写析构函数构成多态

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

【C++】多态_第3张图片

重写析构函数构成多态时,父类指针也会去调用子类的析构函数达到释放空间的目的

为什么要将析构函数统一重命名为destructor?
因为需要去重写析构函数构成多态调用各自的析构函数

4.override 和 final(C++ 11)

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

【C++】多态_第4张图片

设计不想被继承的类
方法1:基类构造函数私有
方法2:基类析构函数私有
方法3:final

构造函数私有
【C++】多态_第5张图片

使用final修饰
【C++】多态_第6张图片

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

【C++】多态_第7张图片

三、多态的原理

1.虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?

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

private:
 int _b = 1;
};

【C++】多态_第8张图片

为什么这里会是8?
【C++】多态_第9张图片
我们可以看到b中还有一个虚函数指针表,表中还有一个指针,再加上_b,由此b所占内存大小为8
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析

虚函数表一般是存放在常量区(可以自行验证,虚继承表地址就是这个对象的前4个字节(32位)),并且同一个类型的变量使用同一份虚函数表。
【C++】多态_第10张图片

虚函数表的生成

派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

2.多态的原理

#include

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

【C++】多态_第11张图片
【C++】多态_第12张图片

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

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

【C++】多态_第13张图片

3.动态绑定和静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
【C++】多态_第14张图片

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

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

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。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。
【C++】多态_第15张图片

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,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再
//编译就好了。

【C++】多态_第16张图片

2.多继承的虚函数表

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

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

【C++】多态_第17张图片

五、抽象类

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

抽象类不可实例化
【C++】多态_第18张图片

继承了抽象类也不可实例化
【C++】多态_第19张图片

继承了抽象类如果想实例化,可通过重写重虚函数
【C++】多态_第20张图片

2.接口继承和实现继承

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

你可能感兴趣的:(C++,c++,开发语言)