当前位置: 首页 > news >正文

C++进阶(2)——多态

目录

相关概念

多态的实现和实现

多态的构成条件

虚函数

虚函数的重写、覆盖

虚函数重写的其他问题

协变(了解即可)

析构函数的重写

override和final关键字

override关键字

final关键字

重载、重写和隐藏的对比

纯虚函数和抽象类

实现多态的原理

虚函数表指针

多态到底如何实现呢?

动态绑定和静态绑定

虚函数表

单继承中的虚函数表

多继承中的虚函数表

菱形虚拟继承


相关概念

我们的多态通俗得讲就是多种形态,多态被分为编译时的多态(静态多态)和运行时的多态(动态多态),这里我们重点放在了运行时的多态,因为我们其实已经说了编译时多态(就是我们前面说过的函数重载和函数模板,他们之所以被称之为编译时多态是因为他们是通过我们传入的参数来实现我们的多态的)。

我们的运行时多态就是去完成某种行为(函数),传入的对象不同完成的行为也就是会不同,比如我们动物的叫声,不同的动物叫声不同,传入猫对象时就是“ (>^ω^) 喵 ”,传入狗对象的时候就是“汪汪”。

多态的实现和实现

多态的构成条件

我们的多态是一个继承关系下的类对象去调用同一个函数产生的不同行为(Java中的接口和这个比较像),多态还有两个必须要有的重要条件:

1、必须是基类指针或是引用调用虚函数。

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

虚函数

类的成员函数前面使用了关键字virtual修饰,那么这个成员函数就被称之为虚函数

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

敲黑板:
1、我们这里不能在非成员函数前面加上virtual关键字修饰。

2、这里的关键字virtual和前面的虚拟继承使用的关键字virtual并不是同一个,这里不要混淆了。

虚函数的重写、覆盖

派生类中有一个和基类完全相同的虚函数(这里的相同指的是派生类虚函数和基类虚函数的返回值类型、函数名和参数列表相同),我们称派生类的虚函数重写(覆盖)了基类的虚函数。

我们这里举个栗子:

代码如下:

#include <iostream>
using namespace std;
class Animal {public:virtual void talk() const {}
};
class Dog : public Animal {public:virtual void talk() const {cout << "汪汪" << endl;}
};
class Cat : public Animal {public: virtual void talk() const {cout << "(>^ω^) 喵" << endl;}
};
void Listen(const Animal& animal) {animal.talk();
}
void Listen(const Animal* animal){animal->talk();
}
int main() {Cat cat;Dog dog;Listen(cat);Listen(&cat);Listen(dog);Listen(&dog);return 0;
}

测试效果如图:

敲黑板:
我们这里在重写我们的基类虚函数的时候,派生类的虚函数不加我们的关键字virtual也是可以的(结果不变),这个时候虽然我们还是可以构成重写(因为继承机制中我们的基函数被继承下来了在派生类中依旧保持了虚函数的属性),但是我们这样的写法不是规范写法,不建议这么做,但是我在做一些笔试题的时候经常是这么写的,有的题还会故意埋个坑让你判断是不是构成多态(是构成的)。

我们这里来个题放松一下:

以下程序输出结果是什么?

A:A->0 B:B->1 C:A->1 D: B->0 E:编译出错 F: 以上都不正确

代码:

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

答案:

B解析:我们这里的B实际调用的是B类中的func函数,但是我们的默认参数是静态绑定的(原来的值),所以还是我们的A中的1。

虚函数重写的其他问题

协变(了解即可)

派生类重写我们的基类函数的时候,与我们的基类虚函数的返回值类型不同,也就是说我们的基类虚函数返回基类对象的指针或是引用,派生类虚函数返回派生类对象的指针或是引用的时候,就称之为协变。这样的情况下我们还是能构成我们的重写的,但是实际意义并不大,只做了解即可。

示例代码如下:

#include <cstddef>
#include <iostream>
using namespace std;
class A{};
class B : public A{};
class Animal {public:virtual A* talk() const {cout << "不知道" << endl;return nullptr;}
};
class Dog : public Animal {public:virtual B* talk() const {cout << "汪汪" << endl;return nullptr;}
};
void Listen(const Animal* animal){animal->talk();
}
int main() {Dog dog;Listen(&dog);return 0;
}

测试效果如图:

析构函数的重写

我们的基类的析构函数是虚函数,这个时候我们的派生类析构函数只要定义了,不论我们是不是加上关键字virtual,都构成对于基类的析构函数的重写,虽然我们看上去基类和派生类的析构函数的名字不符合我们的重写的规则,但是实际上我们的编译器对于析构函数是有特殊处理的,我们在统一编译之后会处理成destructor,所以基类的析构函数加上了virtual关键字,那么派生类的析构函数就会构成重写。

代码示例如下:

#include <iostream>
using namespace std;
class A {public:virtual ~A() {cout << "~A()" << endl;}
};
class B : public A {public:~B() {cout << "~B()" << _p << endl;}protected:int* _p = new int[10];
};
int main() {A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

我们这里分了两种情况:

1、如果我们的~A()不加上我们的virtual,那么delete p2的时候就会只是调用我们的A的析构函数,并不会调用我们B的析构函数,这样做就会导致我们的内存泄露问题,因为这个时候我们的B没有释放资源。

测试效果如图:

2、使用了virtual关键字修饰我们的~A()函数。

测试效果如图:

敲黑板:

这个问题经常在面试的过程中考察,我们一定要结合类似上面的栗子跟面试官讲清楚。

override和final关键字

override关键字

从上面的栗子中我们可以知道我们的C++对于函数的重写还是比较严格的,但是有的时候我们会疏忽一些情况,比如函数名写错了等导致无法构成重载,而这种错误在我们的编译器编译的时候是不会报错的,只有运行之后我们发现了结果不对才会发现,所以我们的C++11提供了关键字override(类似于Java中的注解(@Override)),可以帮助我们检测是否重写。

示例代码:

#include <iostream>
using namespace std;
class Car {public:virtual void Drive() {}
};
class BWM : public Car {public:virtual void Drive(int x) override {cout << "BWM好开" << endl;}
};
int main() {return 0;
}

测试效果如图:

我们这里直接就会报错

final关键字

我们的final关键字可以修饰我们的虚函数,表示我们不想让派生类来重写这个虚函数。

示例代码如下:

#include <iostream>
using namespace std;
class Car {public:virtual void Drive() final {}
};
class BWM : public Car {public:virtual void Drive() {cout << "BWM好开" << endl;}
};
int main() {return 0;
}

测试效果如图:

这里就会报错了

重载、重写和隐藏的对比

这里要对比地理解,画个图方便理解,图示:

纯虚函数和抽象类

在虚函数的后面加上一个=0,那么这个函数就是纯虚函数了,纯虚函数不需要定义实现(这里的不需要说的是我们的实现没有什么意义,因为最后还是要被派生类重写,但是语法上是可以的实现的)。我们这里把包含了纯虚函数的类叫做是抽象类,抽象类不能实例化出对象的如果我们的派生类继承了之后不重写我们的纯虚函数,那么派生类也是一个抽象类(这实际上是一种强制实现,不实现就不可能实例化对象)

下面给一个示例代码:

#include <iostream>
using namespace std;
class Car {public:virtual void Drive() = 0;
};
class BWM : public Car {public:virtual void Drive() {cout << "宝马好开" << endl;}
};
class BENZ : public Car {public:virtual void Drive() {cout << "奔驰好开" << endl;}
};int main() {Car car; // 报错Car* car_bwm = new BWM;car_bwm->Drive();Car* car_benz = new BENZ;car_benz->Drive();return 0;
}

这里实例化car会报错:

测试效果:

实现多态的原理

虚函数表指针

我们这里还是来一个常考的笔试题来引入:

下面编译为32位程序的运行结果是什么()

A.编译报错   B.运行报错   C.8   D.12

代码如下:

#include <iostream>
using namespace std;class Car {
public:virtual void Drive() {cout << "Drive()" << endl;}
private:int x = 1;char _c = 'A';
};int main() {Car b;cout << sizeof(b) << endl;return 0;
}

答案:

D

测试效果如图:

这是为什么呢?这是因为我们的b对象除了x和_c成员外还有一个_vfptr在对象的前面(有一些平台可能在对象的后面),这个指针就是我们的虚函数表指针(v表示我们的virtual,f表示我们的function)。一个含有虚函数的类至少有一虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,所以虚函数表也被叫做是虚表。

多态到底如何实现呢?

比如下面的代码,我们从底层的角度上面看,我们的Func函数中的ptr->BuyTicket()是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?

代码如下:

#include <iostream>
using namespace std;
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;}
};
void Func(Person* ptr) {ptr->BuyTicket();
}
int main() {Person ps;Student st;Soldier so;Func(&ps);Func(&st);Func(&so);return 0;
}

通过下面的图可以看到,我们满足了多态条件之后,底层满足了多态条件之后,底层不再是编译时通过调用对象来确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址。

对于我们的Person:

对于我们的Sudent:

这里我们可以看到虽然都是Person指针ptr在调用BuyTicker,但是本质上和ptr没什么关系,而是由我们的ptr指向的对象来决定的,这里也就体现了我们的多态行为(不同对象去完成同一种行为的时候,展现出了不同的形态)。

我们这里再思考一个问题:为什么我们要使用父类的指针或是引用来调用我们的虚函数,而我们的父类对象调用就不行呢?

当我们使用父类指针或是引用的时候,实际上是一种切片的行为,切片操作会让我们的父类指针或是引用得到我们的父类对象或是子类对象切出来的一部分,如图:

但是当我们是用的是父类对象的就会不一样了,我这里会调用我们的父类拷贝构造函数对那部分成员变量进行我们的拷贝构造,构造出来的父类对象p1和p2的虚函数表指针都是指向的父类对象的虚函数表,这样也就没有了多态的行为。

动态绑定和静态绑定

动态绑定:满足多态条件的函数调用是在运行的时候进行绑定的,也就是说在运行的时候才到指向对象的虚函数表中找到我们调用的函数的地址,这就叫动态绑定。

静态绑定:对不满足多态条件(指针或是引用+调用虚函数)的函数调用是在编译的时候就绑定了的,也就是编译的时候就确定调用函数的地址,这就叫做静态绑定。

我这里可以反汇编看一下:
动态绑定:

这里我们可以看到我们我们是在运行时到ptr指向对象的虚函数表中确定我们调用函数地址。

静态绑定:

这类我们可以看到我们的编译器是直接调用了我们的函数地址(这里的BuyTicket不是虚函数了)。

虚函数表

我们这里关于虚函数表的理解其实是很多样的,因为我们有很多中情况会有虚函数表且具体情况也有一些差异存在,主要是分为单继承、多继承以及之前讨论过的菱形继承。

单继承中的虚函数表

代码示例:

#include <iostream>
using namespace std;
class A {
public:	virtual void func1() {}
private:int _a;
};
class B : public A {
public:virtual void func1() {}virtual void func2() {}
private:int _b;
};
int main() {A a;B b;return 0;
}

在我们的单继承关系中,派生类虚函数表的生成规则:

1、继承基类的虚函数表。

2、派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

3、虚函数表中会有我们新增的虚函数地址。

测试效果图如下:

这里我们在监视的时候只看到了父类的虚函数表,我们这里需要借助我们的内存窗口才可以看到。

多继承中的虚函数表

代码示例:

#include <iostream>
using namespace std;
class A {
public:	virtual void func1() {}
private:int _a;
};
class B {
public:virtual void func1() {}virtual void func2() {}
private:int _b;
};
class C : public A, public B {
public:virtual void func1() {}virtual void func3() {}
private:int _c;
};
int main() {A a;B b;C c;return 0;
}

在多继承中,我们的派生类的虚函数表指针的生成规则:

1、分别继承了各个基类的虚表的内容到我们的派生类的各个虚表。

2、派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

3、在派生类的第一个继承基类的虚函数表中添加上派生类的虚函数表。

测试效果如图:

菱形虚拟继承

代码示例:

#include <iostream>
using namespace std;
class A {
public:	virtual void func1() {}
private:int _a;
};
class B : virtual public A{
public:virtual void func1() {}virtual void func2() {}
private:int _b;
};
class C : virtual public A {
public:virtual void func1() {}virtual void func3() {}
private:int _c;
};
class D : public B, public C {
public:virtual void func1() {}virtual void func4() {}
private:int _d;
};
int main() {A a;B b;C c;D d;return 0;
}

在A类对象中的成员及其分布:

在B类对象中的成员及其分布:

我们的B中其实还有一个虚基表指针,这里没有显示出来,可以看之前的博客有介绍的。

在C类对象中的成员及其分布:

这里和B中的情况差不多。

在D类对象中的成员及其分布:

这里面还有B类和C类对象的虚基表指针,而且这里的func4()没有显示。

虚函数表到底在哪里呢?

这个问题其实是没有标准答案的,因为C++标准并没有做出规定,所以我可以自己写个代码来验证一下:

#include <iostream>
using namespace std;
class Base {public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }protected:int a = 1;
};class Derive : public Base {public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main() {int i = 0;static int j = 1;int *p1 = new int;const char *p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base *p3 = &b;Derive *p4 = &d;printf("Person虚表地址:%p\n", *(int *)p3);printf("Student虚表地址:%p\n", *(int *)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}

测试效果如图:

这里我们发现我们的虚函数表地址和我们的常量区(代码段)很类似,所以我们在vs下是在代码段。

http://www.dtcms.com/a/424718.html

相关文章:

  • 营销网站建站开发什么是交换链接
  • 校园风险管理网站建设方案wordpress使用php版本号
  • html头部
  • 韩国网站域名分类常州seo第一人
  • 建设部网站官网办事厅音乐网站素材
  • 人工智能-机器学习day4
  • 网站建设和维护要点重庆cms建站模板
  • 做外汇需要了解的网站网页制作有什么软件
  • 做棋牌网站建设云南网站开发公司找哪家
  • 文案网站策划书织梦网站系统删除
  • Linux开发工具(一)
  • 虚拟资源站码支付wordpress合江县住房建设规划局网站
  • 国企网站建设标准房地产市场发展趋势
  • 网站 用户粘度无人区高清免费看完整版
  • 做数据可视化的网站汕头网站开发找哪里
  • 【MySQL】深分页的性能优化,游标方案和覆盖索引+延迟回表方案
  • 进入深圳市住房和建设局网站胶州市城乡建设局网站
  • 进口倾角传感器代理与水平监测传感器厂家的选择指南
  • 自定义手机网站建设图片分类展示网站源码
  • 基于python数据挖据的教学监控系统的设计与应用
  • 网络舆情监测系统:洞察网络舆论的利器
  • AI 超级智能体全栈项目阶段三:自定义 Advisor 与结构化输出实现以及对话记忆持久化开发
  • 网站后台模板夜间正能量网站入口网址不用下载
  • 主机屋建网站源码房山建站公司
  • xtuoj 整数分类
  • 精品网站设计欣赏网站站内关键词优化
  • 免费分类信息网站大全全网搜索指数查询
  • 科技网站建设做seo是要先有网站吗
  • Xilinx FPGA上电和配置
  • 网站制作百度cc在线代理