樊梓慕:个人主页
个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》
每一个不曾起舞的日子,都是对生命的辜负
目录
前言
1.多态的概念
2.多态的定义及细节
2.1虚函数
2.2虚函数的重写
2.2.1虚函数重写的两个例外
2.3普通调用和多态调用的区别
2.4『 final』和『 override』关键字(C++11)
2.4.1『 final』
2.4.2『 override』
2.5重载、重写(覆盖)、隐藏(重定义)的对比
3.抽象类
3.1概念
3.2意义
3.3『 接口继承』和实现继承
4.多态的原理
4.1虚函数表
4.2多态的原理
4.3动态绑定和静态绑定
4.3.1静态绑定(不构成多态)
4.3.2动态绑定(构成多态)
4.4单继承和多继承关系的虚函数表
4.4.1单继承关系的虚函数表
4.4.2多继承关系的虚函数表
4.4.3利用代码打印出虚函数表
本篇文章博主将与大家共同学习多态的相关内容,并且会对之前继承的学习作补充。
欢迎大家收藏以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。
=========================================================================
GITEE相关代码:樊飞 (fanfei_c) - Gitee.com
=========================================================================
通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 。
举个例子:比如买票这个行为 ,当普通人买票时,是全价买票;学生买票时,是半价买票;
如:
在继承中要构成多态需要两个条件:
对于上面新出现的两个概念做解释:
注意:在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后,父类的虚函数被继承下来了,所以在子类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
(1)协变(父类与子类函数返回值类型不同)
协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。
如:
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; }
};
所以如果有人问:有关虚函数的重写,两个虚函数的返回值是必须相同的么?
你就知道这是个坑了,因为有『 协变』这一特殊情况,返回值类型不相同也可能满足虚函数重写。
(2)析构函数的重写(父类与子类析构函数名字不同)
我们知道,编译器对析构函数的名称会做特殊处理,编译后析构函数的名称统一处理成destructor(),这样就变相的满足了函数名相同了。
所以我们一般都给析构函数前加上virtual关键字,这样如果有子类继承,析构重写正好,没有子类继承也不影响。
有的同学可能会有疑问:为什么一定要重写父类的析构函数呢?
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的组成如下:
由于ptr指针的类型都是父类Person,所以当执行delete时,prt1和ptr2调用的都是父类的析构。
所以我们需要『 多态』,来让不同的对象调用它们对应的析构。
还记得构成多态的两个条件么?
所以我们需要将父子类的析构函数设为虚函数,从而满足构成多态的条件。
通过上面析构函数的例子,相信大家已经体会到了多态的妙用,不同的对象调用不同的函数。
这里我们就来总结一下普通调用和多态调用的区别。
普通调用:根据指针、引用、对象的类型调用对应的函数;
多态调用:根据指针、引用指向的对象调用对应的函数。
(1)修饰虚函数,表示该虚函数不能再被重写;
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; } //err
};
(2)修饰类,该类不能被继承
override的作用是让编译器帮助用户检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错,override作用发生在编译时。
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car
{
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
在虚函数的后面写上=0,则这个函数为纯虚函数;
包含纯虚函数的类叫做抽象类(也叫接口类)。
(1)抽象类不能实例化出对象:
#include
using namespace std;
//抽象类(接口类)
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
int main()
{
Car c; //抽象类不能实例化出对象,error
return 0;
}
(2)子类继承抽象类必须重写纯虚函数,否则不能实例化出对象:
#include
using namespace std;
//抽象类(接口类)
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
//派生类
class Benz : public Car
{
public:
//重写纯虚函数
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
//派生类
class BMV : public Car
{
public:
//重写纯虚函数
virtual void Drive()
{
cout << "BMV-操控" << endl;
}
};
int main()
{
//派生类重写了纯虚函数,可以实例化出对象
Benz b1;
BMV b2;
//不同对象用基类指针调用Drive函数,完成不同的行为
Car* p1 = &b1;
Car* p2 = &b2;
p1->Drive(); //Benz-舒适
p2->Drive(); //BMV-操控
return 0;
}
下面是一道常考的笔试题:Base类实例化出对象的大小是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
也就是说除了成员变量_b还有另外要存储的内容,这部分内容也就是实现多态的核心:『 虚函数表指针』。
b对象当中除了_b成员外,实际上还有一个『 _vfptr』放在对象的前面(不同平台会有不同设计)。
『 _vfptr』叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,『 每一个』含有虚函数的类中『 都至少有一个虚表指针』。
那放在继承的框架下虚表指针会有什么样的设计呢?我们继续往下看。
针对上面的代码我们做出以下改造:
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;
}
通过监视窗口观察:
当然我这样画可能会有歧义,注意对象里存储的不是函数指针数组。而是指向该函数指针数组的指针,本质是指针,大概模型应为下图所示:
观察得出以下结论:
(1)父类b对象和子类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1。
所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数地址的覆盖。
重写是语法的叫法,覆盖是原理层的叫法。
(2)Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。
(3)此外,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。(如果你在调试时发现结尾不是nullptr,需要重新生成解决方案即可)。
总结一下,子类的虚表生成步骤如下
注意
为什么父类指针指向不同的对象就能实现多态呢?
#include
using namespace std;
//父类
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
private:
int _b;
};
//子类
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
private:
int _d;
};
int main()
{
Person Mike;
Student Johnson;
Person* p1 = &Mike;
Person* p2 = &Johnson;
p1->BuyTicket(); //买票-全价
p2->BuyTicket(); //买票-半价
return 0;
}
两个父类指针分别指向对应的Mike与Johnson对象,找到对应的虚表,调用对应的函数,即:
我们可以通过以下代码进一步理解静态绑定和动态绑定:
//父类
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
int main()
{
Student Johnson;
Person p = Johnson; //不构成多态
p.BuyTicket();
return 0;
}
不构成多态,函数的调用是在编译时确定的。
查看汇编代码,发现直接调用函数:
int main()
{
Student Johnson;
Person& p = Johnson; //构成多态
p.BuyTicket();
return 0;
}
构成多态,函数的调用是在运行时确定的。
查看汇编代码,发现需要经历一系列操作访问虚表:
构建单继承模型方便研究:
//父类
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
private:
int _b;
};
//子类
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 _d;
};
通过监视窗口观察:
注意:这里有一个非常奇怪的现象,为什么监视窗口中d对象中没有func3和func4??
这其实可以认为是vs编译器的一个bug,实际上是有的,只不过监视窗口并没有显示出来,我们可以通过虚表指针在内存窗口中找到该虚表:
所以我们可以得到如下关系:
结论:在单继承关系当中,子类的虚表生成过程如下
构建多继承模型方便研究:
//父类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;
};
通过监视窗口观察:
同样的疑问:监视窗口中d对象中的func3去哪了?
根据单继承关系给我们的启示,是vs编译器的一个bug,可是问题又来了,d对象有两个父类,对应着两张虚函数表,那么d中的虚函数放在了哪张表中呢?
找到了!红框内存储的函数指针无人认领,那必然是剩下的func3咯。
所以我们可以得到如下关系:
结论:在多继承关系当中,子类的虚表生成过程如下
如何在终端输出虚函数表呢?
以单继承关系模型的场景为例:
前提:使用VS编译器,因为在VS平台中虚表结尾设置为nullptr,我们就可以利用该空指针作边界检测,然后输出对应的虚函数表。
typedef void(*VFPTR)(); //tepedef虚函数指针类型
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;
}
思路:取出b、d对象的头字节,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
有关『 菱形继承』模型这里就不讨论了,因为菱形继承本来就是一种非常危险的行为,不建议大家设计出菱形继承,实际中也很少会使用,所以大家只需要掌握单继承和多继承模型即可。
=========================================================================
如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容
博主很需要大家的支持,你的支持是我创作的不竭动力
~ 点赞收藏+关注 ~
=========================================================================