不用服务器做网站,百度账户托管公司,网站建设全部流程包括备案,seo视频教程百度云文章目录 前言一、多态的概念二、多太的定义和实现2.1 多太的构造条件2.2 虚函数2.3 重写(覆盖)2.4 C11 override 和 final2.5 重载#xff0c;隐藏#xff0c;重写 三、多态的原理3. 1虚函数表3.2 虚函数表如何完成多态的功能3.3 虚函数表存储在内存空间的那个区域#xff… 文章目录 前言一、多态的概念二、多太的定义和实现2.1 多太的构造条件2.2 虚函数2.3 重写(覆盖)2.4 C11 override 和 final2.5 重载隐藏重写 三、多态的原理3. 1虚函数表3.2 虚函数表如何完成多态的功能3.3 虚函数表存储在内存空间的那个区域 前言 一、多态的概念 多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态。 举例对于买票的这个行为来说成年人买票全价儿童买票半价学生买票打折军人优先买票……不同的人虽然都是进行买票的行为但买票过程的细节不完全相同。而为了让不同的对象进行同一行为产生不同的状态。我们则需要采用面向对象的三大特性之一多态。
下面看一段简单的多态代码后文进行解释
class Person
{
public:virtual void BuyTicket() { cout Person::BuyTicket() endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout Student::BuyTicket() endl; }
};
int main()
{Person pn;Student st;Person* ppn pn;ppn-BuyTicket(); //普通人买票ppn st;ppn-BuyTicket(); //学生买票return 0;
}打印结果
二、多太的定义和实现
2.1 多太的构造条件
多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。就像上面的代码Student继承了Person。Person对象买票全价Student对象买票半价。
而在继承中构成多态有两个条件牢记
必须通过基类的指针或者引用调用虚函数。此时基类的指针或引用已经被赋值为了派生类的对象的地址且被调用的函数必须是虚函数且派生类必须对基类的虚函数进行了重写。
这中间出现了两个陌生的名词虚函数和重写因此我们首先要了解这两个词的意思是什么
2.2 虚函数 虚函数即被virtual修饰的类成员函数称为虚函数 比如BuyTicket()函数
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl;}
};2.3 重写(覆盖) 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同称子类的虚函数重写了基类的虚函数。(重写也可叫作覆盖 class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout 买票-半价 endl; }
};注意: 派生类进行重写时可以不加virtual只要派生类里的函数和基类的虚函数的返回值类型、函数名字、参数列表完全相同编译器会自动识别为基类虚函数的重写。但一般为了可读性还是加上返回值类型、函数名字、参数列表完全相同。其中参数列表完全相同指的是参数类型形参的名字完全相同对于缺省值不作要求. virtual void func(int val 3) {} virtual void func(int val 4) {} 它们依然构造重写虚函数的重写是对函数体进行重写 基类里的虚函数virtual void func(int val 3) {cout 基类 val;} 派生类的虚函数virtual void func(int val 4) {cout 派生类 val;} 当你调用派生类的虚函数func()时你会发现打印的结果是派生类3,即它会使用基类的虚函数头 派生类的虚函数体。后文有个面试题考察了这个知识。 虚函数重写的两个特例 协变(基类与派生类虚函数返回值类型不同) 上面说了虚函数重写需要返回值的类型相同但是给了一个特例基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时也可以是虚函数的重写这种情况被称为协变 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;}
};析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor,因此所有的析构函数都满足函数名相同。 class Person {
public:virtual ~Person() {cout ~Person() endl;}
};
class Student : public Person {
public:virtual ~Student() { cout ~Student() endl; }
};问题为什么要对析构函数的名称进行处理博客析构函数的名称为什么统一处理为destructor 知晓了这两个条件我们来看上面的那段代码。
class Person
{
public:virtual void BuyTicket() { cout Person::BuyTicket() endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout Student::BuyTicket() endl; }
};
int main()
{Person pn;Student st;Person* ppn pn;ppn-BuyTicket(); //普通人买票ppn st;ppn-BuyTicket(); //学生买票return 0;
}解释:为什么ppn能调用到派生类的BuyTicket() ppn的类型是基类person的指针满足多态的第一个条件ppn已经被赋值为派生类对象st的地址且BuyTicket()对基类的BuyTicket进行了重写。满足第二个条件 因此:ppn-BuyTicket会调用子类的虚函数。 2.4 C11 override 和 final final修饰虚函数表示该虚函数不能再被重写 使用场景当你不想某个虚函数被重写时可以加上final override:帮助派生类检查是否完成重写 2.5 重载隐藏重写 三、多态的原理
看完上面的内容相信你会有以下的困惑
为什么基类的对象或指针能调用到派生类的函数为什么限定为基类的指针或引用基类的对象不行吗
要解答这些问题我们必须要了解定义虚函数时产生的虚函数表。
3. 1虚函数表
class A
{
public:virtual void func1() {};virtual void func2() {};char a;
};
int main()
{ A A1;cout sizeof(A1);return 0;
}在32位的机器下请问上面的打印结果是什么 如果func的前面没有加virtual结果很明显是1但加上virtual后,结果变成了8. 那多出的内存放了些什么东西呢 我们此时打开监视窗口:
发现A1中出现了一个指针_vfptr我们猜测它代表的什么意思v即virtualf即functionptr即指针猜测它是虚函数指针。但它下面有【0】【1】这又表明它可能是个数组。结合以下即_vfptr是虚函数指针数组。 事实上它还真是一个虚函数指针数组只不过我们将这个数组叫做虚函数表简称虚表。 这时我们可以确定2个事实
多出来的内存存储了一个指针32位下的指针是4字节加上char a的1字节最后在进行内存对齐结果就是8字节。这个指针指向的空间并不存储在对象里面如果存储在对象里那么对象的大小应该大于8字节。
此时我们可以画一个简图 知晓了虚函数表的存在随之而来的就有2个问题
虚函数表是如何完成多态的功能虚函数表并没有存储在对象里那它存储在什么地方
同时加上前文提到的问题为什么多态的构成条件要求是基类的指针或引用
3.2 虚函数表如何完成多态的功能 通过监视窗口我们可以看到基类对象ps的虚表存储的值_vfptr[0] 0x00bc15a5,而派生类对象st的基类的那一部分存储的虚表里的值_vfptr[0] 0x00bc1596二者值不相同说明二者存储了不同的虚函数地址一个存储的地址是person::BuyTicket, 另一个存储的是student::BuyTicket;除此之外_vfptr都是存储在基类的那一部分。 据此我们基本确定调用虚函数的过程如下
基类对象和派生类对象都会创建虚函数表基类对象的虚函数表存储基类的虚函数地址派生类对象的虚函数表会存储派生类的虚函数地址。当我们使用基类的对象的指针或引用去调用时分别取指向对象的虚表去寻找。这就解释了为什么不能使用基类的对象因为基类的对象里的虚表存储的是基类虚函数的地址无法找到派生类的虚函数。 3.3 虚函数表存储在内存空间的那个区域
A:栈 B:堆 C:代码段常量区 D:数据段静态区 先说答案 代码段常量区
验证如下 思路比较虚函数表内存储的地址与其他存储区域的地址进行对比看谁更接近。 通过上面的结果可以看出虚表的地址 与 常量区的地址最为接近。 如何提取虚表的地址首先对象第一个存储的便是虚函数表指针因此前4个字节32位存储便是虚函数表的地址(int*)ps 即是 _vfptr的地址 再解引用便是 _vfptr存储的地址即虚函数表的地址