探索多态的本质【C++】

文章目录

  • 多态的构成条件
    • 虚函数
    • 虚函数的重写(覆盖)
  • 虚函数重写的两个例外
  • C++11 override和final
  • 区分重载、覆盖(重写)、隐藏(重定义)
  • 抽象类
  • 接口继承和实现继承
  • 多态的原理
    • 虚函数表
  • 动态绑定和静态绑定
    • 动态绑定
    • 静态绑定
  • 单继承中的虚函数表
  • 多继承中的虚函数表
  • 菱形继承、菱形虚拟继承

多态的构成条件

在继承中要构成多态还有两个条件:
1、必须通过基类的指针或者引用调用虚函数。

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

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

//子类
class Student : public Person
{
public:
	//派生类的虚函数重写了父类的虚函数
	virtual void BuyTicket() const
	{
		cout << "买票-半价" << endl;
	}
};

引用
//void func(const Person &  p)
//{
//	p.BuyTicket();
//}

//指针
void func(const Person *  p)
{
	p->BuyTicket();
}



int main()
{    
	//多态条件
	// 1、调用函数必须是重写的虚函数
	//基类必须是指针或者引用


	//多态,不同对象传递过去,调用不同参数
	//多态调用看指向的对象
	//普通对象,看当前类型

	//引用
	/*func(Person());
	func(Student());*/


	//指针
	Person pp;
	func(&pp);
	Student st;
	func(&st);

	return 0;
}

虚函数

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

class A
{
public:
	virtual void func()
	{
		cout << "virtual void func() " << endl;
	}
};

注意:
只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual

虚函数这里的virtual和虚继承中的virtual虽然是同一个关键字,但是它们之间没有任何关系。

虚函数的virtual是为了实现多态
虚继承的virtual是为了解决菱形继承的数据冗余和二义性。

虚函数的重写(覆盖)

如果派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同,这里所说的参数列表是指参数类型要相同)
此时我们称该派生类的虚函数重写了基类的虚函数。

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	//派生类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//子类
class Soldier : public Person
{
public:
	//派生类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "优先-买票" << endl;
	}
};

通过父类Person的指针或者引用调用虚函数BuyTicket,此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

void Func(Person& p)
{
	//通过父类的引用调用虚函数
	p.BuyTicket();
}
void Func(Person* p)
{
	//通过父类的指针调用虚函数
	p->BuyTicket();
}
int main()
{
	Person p;   //普通人
	Student st; //学生
	Soldier sd; //军人

	Func(p);  //买票-全价
	Func(st); //买票-半价
	Func(sd); //优先买票

	Func(&p);  //买票-全价
	Func(&st); //买票-半价
	Func(&sd); //优先买票
	return 0;
}

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用)

虚函数重写的两个例外

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

派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

//基类
class A
{

};
//派生类
class B : public A
{

};

//基类
class Person
{
public:
	//虚函数
	virtual  A* fun()
	{
		cout << "A* Person::f()" << endl;
		return new A;
	}
};
//派生类
class Student : public Person
{
public:
	// 虚函数
		virtual  B * fun()
	{
		cout << "B* Person::f()" << endl;
		return new B;
	}

};
int main()
{
	Person p;
	Student st;
	//基类指针指向基类对象
	Person* ptr1 = &p;
	//基类指针指向子类对象
	Person* ptr2 = &st; //切片
	
	//父类指针ptr1指向的p是父类对象,调用父类的虚函数
	ptr1->fun();

	//父类指针ptr2指向的st是子类对象,调用子类的虚函数
	ptr2->fun();
	return 0; 
}

析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

//析构函数加上virtual ,是虚函数重写,为什么?
// 因为析构函数都被处理成了destructor这个统一的名字,为什么统一处理成destructor?
//因为统一处理成destructor,是要将派生类和基类的析构函数构成重写,而重写是构成多态的一个重要条件
class Person
{
public:
	virtual  ~Person()
	{
		cout << "~Person()" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual  ~Student()
	{
		cout << "~Student()" << endl;
		delete[]ptr;
	}
protected :	
	int* ptr = new int[10];
};

int main()
{
	//Person p;
	//Student s;//析构顺序:先子后父

	Person* p = new Person;
	delete p;

	p = new Student;
	delete p;//p->destuctor() +  operator delete (p)
	   //这里我们希望p->destuctor()是一个多态调用 ,而不是普通调用

	return 0;
}

在继承当中,子类和的析构函数和父类的析构函数构成隐藏的原因就在这里,这里表面上看子类的析构函数和父类的析构函数的函数名不同,但是为了构成重写,编译后析构函数的名字会被统一处理成destructor();

C++11 override和final

C++11提供了override和final两个关键字,可以帮
助用户检测是否重写。

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

//基类
class Person
{
public:
	//虚函数
	//final:修饰虚函数,表示该虚函数不能再被重写
	 virtual void BuyTicket() final
	{
		cout << "买票-全价" << endl;
	}
};

//派生类
class Student : public Person
{
public:
	
	virtual void BuyTicket()//err
	{
		cout << "买票-半价" << endl;
	}
};

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

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

//派生类
class Student : public Person
{
public:
	//override,派生类完成基类的重写,就不报错
	virtual  void BuyTicket() override
	{
		cout << "买票-半价" << endl;
	}
};

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

探索多态的本质【C++】_第1张图片

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
int main()
{
	Car c; //err
	return 0; 
}

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

//抽象类

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

//派生类继承抽象类

class Benz : public Car
{
public:
	//重写纯虚函数
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	//派生类重写了纯虚函数,可以实例化对象
	Benz b1;
	Car* p1 = &b1;
	p1->Drive();
	return 0;
}

抽象类不能实例化出对象,那抽象类存在的意义是什么?

抽象类体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。

接口继承和实现继承

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

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

建议: 所以如果不实现多态,就不要把函数定义成虚函数。

多态的原理

虚函数表

看下面的代码

class Base
{
public:

	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:

	int _b = 1;
};

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

b对象当中除了_b成员外,实际上还有一个_vfptr(虚函数表指针简称虚表指针)放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表
探索多态的本质【C++】_第2张图片

#include 
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:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过观察,我们发现:
基类对象b和基类对象d当中除了自己的成员变量之外,基类和派生类对象都有一个虚表指针,分别指向属于自己的虚表。
探索多态的本质【C++】_第3张图片
实际上虚表当中存储的就是虚函数的地址

派生类虽然继承了基类的虚函数Func1和Func2,但是派生类对基类的虚函数Func1进行了重写。所以,派生类对象d的虚表当中存储的是基类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。

注意:Func2是虚函数,所以继承下来后放进了子类的虚表,而Func3是普通成员函数,继承下来后不会放进子类的虚表。并且,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

总结:派生类的虚表生成步骤如下
1、先将基类中的虚表内容拷贝一份到派生类的虚表

2、如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址

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

虚表是什么阶段初始化的?虚函数存在哪里?
虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。

那虚表是存在哪里的?

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
	virtual void Func1() { }
	virtual void Func2() { }
//protected:
	int _a = 0;
};
class Student : public Person
{
public:
	//派生类重写基类虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
	virtual void Func3() { }
protected:
	int _b = 1;
};
int main()
{
	Person ps;
	Student st; 
	//栈
	int a = 0;
	printf("栈:%p\n", &a);

	//静态区
	static  int b = 0;
	printf("静态区:%p\n", &b);

	//堆
	int* p = new int;
	printf("堆:%p\n", p);

	const char* str = "hello world";
	printf("常量区(代码段):%p\n", str);

	printf("虚表1:%p\n",*(   (int*)&ps ) );
	printf("虚表2:%p\n", *( (int*) &st ) );

	return  0; 
}

从上述代码可以发现虚表地址与代码段的地址非常接近,由此可以得出虚表是存在代码段的

详细分析下面的代码
为什么当父类Person指针指向的是父类对象Mike时,调用的就是父类的BuyTicket?
当父类Person指针指向的是子类对象Johnson时,调用的就是子类的BuyTicket?

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

	int _a = 1;
};

class Student : public Person 
{
public:
	//派生类重写基类虚函数
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl;
	}
	int _b = 1;
};

int main()
{
	Person Mike;
	Student Johnson;
	
	Johnson._b = 3; //以便观察是否完成切片
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

对象Mike中包含一个成员变量_a和一个虚表指针,对象Johnson中包含两个成员变量_a和_b以及一个虚表指针,这两个对象当中的虚表指针分别指向自己的虚表。

探索多态的本质【C++】_第4张图片

通过上图可分析:
1、父类指针p1指向Mike对象,p1->BuyTicket在Mike的虚表中找到的虚函数就是Person::BuyTicket。

2、父类指针p2指向Johnson对象,p2>BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。

这样就实现出了不同对象去完成同一行为时,展现出不同的形态,即多态

多态构成的两个条件,
1、完成虚函数的重写,
2、必须使用父类的指针或者引用去调用虚函数。
完成虚函数的重写是因为需要完成子类虚表当中虚函数地址的覆盖,这样才能做到指针指向父类,调用父类对象,指针指向子类,调用子类对象

为什么多态的设计必须使用父类的指针或者引用,不使用父类的对象?
指针和引用的切片不存在拷贝问题 ,但是对象的切片需要拷贝
子类赋值给父类对象切片,不会拷贝虚表,如果拷贝虚表,那么父类对象虚表中是父类虚函数还是子类虚函数就不确定了

探索多态的本质【C++】_第5张图片

使用父类的指针或者引用时,实际上是一种切片行为,切片时只会让父类指针或者引用得到父类对象或子类对象中切出来的那一部分。

探索多态的本质【C++】_第6张图片
用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是不一样的,最终调用的函数也是不一样的。

Person p1 = Mike;
Person p2 = Johnson;

使用父类对象时,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

总结:

对象的调用:
如果是普通对象的调用(不符合多态),看调用者的类型,普通对象的调用在编译时就确定好了地址
如果调用符合多态,看指向的对象,在运行时到指向对象的虚函数表中找调用函数的地址从而完成调用

动态绑定和静态绑定

动态绑定

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

静态绑定

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

普通对象的调用

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

//派生类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

int main()
{
	Student st;
	Person p = st;
	//不构成多态,函数的调用是在编译时就确定的

	p.BuyTicket();
	return 0;
}

探索多态的本质【C++】_第7张图片
将调用函数的那句代码翻译成汇编就只有以上两条汇编指令,也就是直接调用的函数。

使用多态调用

//基类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
protected:
	int _a = 0;
};

//派生类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
protected:
	int _b = 1;
};

int main()
{
	Student st;
	Person & p = st;
	//构成多态,看指向的对象

	p.BuyTicket();
	return 0;
}

探索多态的本质【C++】_第8张图片
构成多态时调用函数的那句代码翻译成汇编后就变成了八条汇编指令,原因就是我们需要在运行时,先到指定对象的虚表中找到要调用的虚函数,然后才能进行函数的调用。

体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的。

单继承中的虚函数表

//基类
class Base
{
public:
	virtual void func1() 
	{ 
		cout << "Base::func1()" << endl;
	}
	virtual void func2() 
	{
		cout << "Base::func2()" << endl; 
	}
private:
	int _a = 0 ;
};
//派生类
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 =1 ;
};

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

派生类和基类的内存分布

探索多态的本质【C++】_第9张图片
单继承关系当中,派生类的虚表生成过程如下:

1、继承基类的虚表内容到派生类的虚表。
2、对派生类重写了的虚函数地址进行覆盖,比如func1。
3、虚表当中新增派生类当中新的虚函数地址,比如func3和func4。

在调试过程中,某些编译器的监视窗口当中看不到虚表当中的func3和func4,可能是编译器的监视窗口故意隐藏了这两个函数,也可以认为这是一个小bug,此时想要看到派生类对象完整的虚表有两个方法。

使用内存监视窗口
探索多态的本质【C++】_第10张图片
使用代码打印虚表内容

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base b;
	PrintVFT((VFPTR*)(*(int*)&b)); //打印基类对象b的虚表地址及其内容
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的虚表地址及其内容
	return 0;
}

多继承中的虚函数表

//基类1
class Base1
{
public:
	virtual void func1() 
	{ 
		cout << "Base1::func1()" << endl; 
	}
	virtual void func2() 
	{
		cout << "Base1::func2()" << endl;
	}
private:
	int _b1;
};

//基类2
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;
	return 0;
}

探索多态的本质【C++】_第11张图片
多继承中,派生类的虚表生成过程如下:

1、分别继承各个基类的虚表内容到派生类的各个虚表当中。
2、对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1。
3、在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如func3。

看到派生类对象完整的虚表有两种方法。
一、使用内存监视窗口

探索多态的本质【C++】_第12张图片

二、使用代码打印虚表内容
在派生类第一个虚表地址的基础上,向后移sizeof(Base1)个字节即可得到第二个虚表的地址。

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base1 b1;
	Base2 b2;
	PrintVFT((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容
	PrintVFT((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的第一个虚表地址及其内容
	PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容
	return 0;
}

菱形继承、菱形虚拟继承

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