C++ 多态

文章目录

  • 多态的概念
  • 虚函数
  • 抽象类
  • 多态的原理
    • 虚函数指针与虚函数表
    • 多继承与虚函数表
    • 菱形虚拟继承
  • 动态绑定与静态绑定

多态的概念

多种状态 —— 不同对象去完成某个行为,会有不同的状态

虚函数

virtual void Func(){;} //虚函数,和之前的虚继承没什么关联,只是都用了 virtual 关键字
//关于买票的多态
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//子类父类中函数名、参数、返回值相同,带有 virtual,此时两函数不构成隐藏,而是重写/覆盖
void Func(Person& p)//父类的指针或引用去调用
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);//输出 "买票-全价" 
	Func(st);//输出 "买票-半价" 
	return 0;
}

多态的条件(达到多态的效果所需要的条件)

  1. 虚函数重写
  2. 父类的指针或引用去调用

★ 有下面这样一段代码:

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

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

int main()
{
	Person* ptr1 = new Person;
	Person* ptr2 = new Student;

	delete ptr1;
	delete ptr2;

	return 0;
}

输出结果(发现子类析构有问题):
在这里插入图片描述
原因:
首先,我们之前学过 delete 的原理:调用析构函数,调用 operator delete 释放空间

在不考虑多态的情况下:相应类型的指针调用相应的函数(即如图所示调用了两个Person的析构)
对此,设计者用多态解决了这个问题:析构函数函数名会被统一处理成destructor(以此重写函数)(但这也使得不加 virtual 时 destructor 函数构成隐藏,这也就是前面学习继承时遇到的问题的来源),然后在两析构函数前加上 virtual 就构成了多态,问题解决

关于 “相应类型的指针调用相应的函数(访问相应的成员)”:
C++ 多态_第1张图片
不满足多态时,函数构成隐藏,但子类对象调用时隐藏才会被体现,父类调用时没这回事

补充:
不满足多态:看调用者的类型,调用这个类型的成员函数(指针/引用变量本身的类型是谁就调用谁的成员函数)
满足多态: 看指向的对象的类型,调用这个类型的成员函数(指针/引用变量指向/引用的是谁就调用谁的成员函数)

● 多态(虚函数重写)的特殊情况:

  1. 子类中可以不加 virtual:虚函数重写的原理是子类继承父类的接口(函数声明),重写实现
  2. 协变:虚函数重写时,返回值可以不同,但必须是父子关系的指针或引用
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;}
};

相关习题 1:
C++ 多态_第2张图片

解析:
C++ 多态_第3张图片
相关习题 2:C++ 多态_第4张图片
解析:
C++ 多态_第5张图片

重载、覆盖(重写)、隐藏(重定义)
C++ 多态_第6张图片

抽象类

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

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

抽象类的子类会继承纯虚函数,子类可以通过重写覆盖纯虚函数

多态的原理

虚函数指针与虚函数表

普通的成员函数:

class Person
{
public:
	void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
	void BuyTicket() { cout << "买票-半价" << endl; }//重写
};
void Func(Person& p) {//父类引用调用,构成多态
	p.BuyTicket();
}
int main() {
	Person p;  Func(p);
	Student s; Func(s);
	return 0;
}

调试时发现对象里什么都没有:
在这里插入图片描述
构成多态时:

class Person
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func1() { ; }
};
class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }//重写
	virtual void func1() { ; }
};
void Func(Person& p) {//父类引用调用,构成多态
	p.BuyTicket();
}
int main() {
	Person p;  Func(p);
	Student s; Func(s);
	return 0;
}

调试时发现对象中都有一个叫做_vfptr的指针(virtual function (table) pointer – 虚函数表指针)
C++ 多态_第7张图片

虚函数表存储着父/子类中用 virtual 修饰的函数的地址

我们写的调用函数的代码在编译期间被处理成相应的指令:

BuyTicket(地址)

普通函数调用,编译期间直接将相应的函数地址填入
构成多态时,编译期间通过传入函数的参数(即父类的指针或引用)中的虚函数指针找到相应的虚函数表里存储的虚函数地址并将其填入

● 编译器根据是否构成多态,编译时将对应的函数调用处理成不同的指令

通过调试我们可以看到父类的指针/引用 指向/表示子类对象时能使用子类的虚函数表指针:
C++ 多态_第8张图片
因为这样的设计,如果传入父类,编译器可以直接访问父类的虚函数表指针;如果传入子类,编译器也照样能从父类的指针/引用里获取子类的虚函数表指针,从而实现传入什么调用什么

● 为什么要规定形参必须是父类的指针或引用?直接传对象给形参不行吗(指针、引用是直接访问虚函数表指针,那直接传参构建一个新的对象的内部就有该指针,照样可以访问不是吗)?
如果允许这么做,你就有机会得到一个父类对象,但其内部存储的却是子类的虚函数表指针 —— 全乱了

● 为什么要设计成用虚函数指针指向虚函数表而不是直接把虚函数表写在对象里?
类的成员共用一份虚函数表,节省空间

● 虚函数表什么时候初始化?
构造函数的初始化列表

● 虚函数表存在哪里?
常量区

补充:只要有虚函数就有虚表(虚表的有无与是否构成多态无关),通过虚函数表指针去寻找虚函数地址的行为称为 多态调用;

多继承与虚函数表

● 打印虚函数表(测试用,了解即可)

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
};
//用程序打印虚表
typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR table[])//VF_PTR* table
{
	for (int i = 0; table[i] != nullptr; ++i)//VS中的虚函数表最后以nullptr结尾,所以我们可以这么写
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	//打印子类中Base1的虚函数表
	PrintVFTable((VF_PTR*)(*(int*)&d));
  //打印子类中Base2的虚函数表
	//PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));
	Base2* ptr2 = &d;
	PrintVFTable((VF_PTR*)(*(int*)(ptr2)));
	return 0;
}

多继承时存在的问题:

//这部分就是摘自上面的代码
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
};
int main() {
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func1();
	return 0;
}

子类中func1被重写,但调试时我们发现ptr1与ptr2中的虚函数指针指向的虚函数表中func1的函数地址却不同(如下图),这是为什么呢?(忘记从x64切到x86的环境了,不过过程大差不差)
C++ 多态_第9张图片

值得(给自己)一提的是,Derive多继承了Base1和Base2,也就有了2张虚表(如上图)
还有就是如果你去打印Derive里的两个虚表,你会发现func3只被记录在Base1的虚表里(了解就行,毕竟编译器就是这么写的):
C++ 多态_第10张图片

我们可以查看反汇编的指令:

C++ 多态_第11张图片

分析:Base2* ptr 表示的地址经过了封装。可以看到Base1*ptr2 在调用函数过程指令较多,其中有一步为“ sub ”,这一步是在修正this指针
C++ 多态_第12张图片

func1构成了多态,而ptr2理应指向子类对象d,但Base2*指针会发生偏移(如图),因此需要修正其指向的位置

菱形虚拟继承

这部分可以略过,说起来太麻烦,而且用的少考不到的,索性就不具体说了

class A
{
public:
	virtual void func1()
	{}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{}

	virtual void func3()
	{}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()//此处必须重写,不然不知道D对象的虚函数表里的func
	{

	}
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

C++ 多态_第13张图片

C++ 多态_第14张图片

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
    比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
    行为,调用具体的函数,也称为动态多态

动态多态(也就是这讲了半天的多态)的虚函数表在编译期间也就确定了,运行时确定的是虚函数表指针

你可能感兴趣的:(c++)