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

C++虚函数完全指南:从内存布局到动态多态的实现奥秘

目录

虚函数

虚函数的实现原理

虚函数指针

虚函数的覆盖

动态多态(虚函数机制)被激活的条件(重点*)

虚函数表*

虚函数的限制

虚函数的各种访问情况


1.什么叫多态?

多态( polymorphism )是面向对象设计语言的基本特征之一。仅仅是将数据和函数捆绑在一起,进行类的封装,使用一些简单的继承,还不能算是真正应用了面向对象的设计思想。多态是面向对象的精髓。多态可以简单地概括为“一个接口,多种方法”。比如说:警车鸣笛,普通人反应一般,但逃犯听见会大惊失色,拔腿就跑。

通常是指对于同一个消息、同一种调用,在不同的场合,不同的情况下,执行不同的行为 。

  1. 为什么需要多态性?

我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是为了代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性。

如果项目耦合度很高的情况下,维护代码时修改一个地方会牵连到很多地方,会无休止的增加开发成本。而降低耦合度,可以保证程序的扩展性。而多态对代码具有很好的可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。

C++支持两种多态性:编译时多态和运行时多态。

编译时多态:也称为静态多态,我们之前学习过的函数重载运算符重载就是采用的静态多态,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,又称为静态联编。

运行时多态:在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫动态多态。C++通过虚函数来实现动态联编。接下来,我们提到的多态,不做特殊说明,指的就是动态多态

虚函数

虚函数的定义在一个成员函数的前面加上virtual关键字,该函数就成为虚函数

看这样一个例子:

基类和派生类中定义了同名的display函数

class Base{
public:Base(long x): _base(x){}void display() const{cout << "Base::display()" << endl;}
private:long _base;
};class Derived
: public Base
{
public:Derived(long base,long derived): Base(base), _derived(derived){}void display() const{cout << "Derived::display()" << endl;}
private:long _derived;
};void print(Base * pbase){pbase->display();
}void test0(){Base base(10);Derived dd(1,2);print(&base);cout << endl;//用一个基类指针指向派生类对象//能够操纵的只有基类部分print(&dd);cout << "sizeof(Base):" << sizeof(Base) << endl;cout << "sizeof(Derived):" << sizeof(Derived) << endl;
}

得到的结果

——给Base中的display函数加上virtual关键字修饰,得到的结果

从运行结果中我们发现,virtual关键字加入后,发生了一件“奇怪”的事情 —— 用基类指针指向派生类对象后,通过这个基类对象竟然可以调用派生类的成员函数。

而且,基类和派生类对象所占空间的大小都改变了,说明其内存结构发生了变化。

内存结构如下所示:

虚函数的实现原理

虚函数指针

当Base的display函数加上了virtual关键字,变成了一个虚函数,Base对象的存储布局就改变了。在存储的开始位置会多加一个虚函数指针,该虚函数指针指向一张虚函数表(简称虚表),其中存放的是虚函数的入口地址

Derived继承了Base类,那么创建一个Derived对象,依然会创建出一个Base类的基类子对象

在Derived类中又定义了display函数,发生了覆盖的机制(override),覆盖的是虚函数表中虚函数的入口地址

Base* p 去指向Derived对象,依然只能访问到基类的部分。用指针p去调用display函数,发现是一个虚函数,那么会通过vfptr找到虚表,此时虚表中存放的是Derived::display的入口地址,所以调用到Derived的display函数。

虚函数的覆盖

如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。虚函数一般用于灵活拓展,所以需要派生类中对此虚函数进行覆盖。覆盖的格式有一定的要求:

  • 与基类的虚函数有相同的函数名;

  • 与基类的虚函数有相同的参数个数;

  • 与基类的虚函数有相同的参数类型;

  • 与基类的虚函数有相同的返回类型。

我们在派生类中对虚函数进行覆盖时,很有可能写错函数的形式(函数名、返回类型、参数个数),等到要使用时才发现没有完成覆盖。这种错误很难发现,所以C++提供了关键字override来解决这一问题。

关键字override的作用:

在虚函数的函数参数列表之后,函数体的大括号之前,加上override关键字,告诉编译器此处定义的函数是要对基类的虚函数进行覆盖。

class Base{
public:virtual void display() const{cout << "Base::display()" << endl;}
private:long _base;
};class Derived
: public Base
{
public://想要在派生类中定义虚函数覆盖基类的虚函数//很容易打错函数名字,同时又不会报错//没有完成有效的覆盖/* void dispaly() const{   //不会报错     *//* void dispaly() const override   //编译器会报错   */void display() const override{cout << "Derived::display()" << endl;}
private:long _derived;};

覆盖 总结:

(1)覆盖是在虚函数之间的概念,需要派生类中定义的虚函数与基类中定义的虚函数的形式完全相同

(2)当基类中定义了虚函数时,派生类去进行覆盖,即使在派生类的同名的成员函数前不加virtual,依然是虚函数;

(3)发生在基类派生类之间,基类与派生类中同时定义形式相同的虚函数。覆盖的是虚函数表中的入口地址,并不是覆盖函数本身。

动态多态(虚函数机制)被激活的条件(重点*)

虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?其实激活条件还是比较严格的,需要满足以下全部要求:

  1. 基类定义虚函数

  2. 派生类中要覆盖虚函数 (覆盖的是虚函数表中的地址信息)

  3. 创建派生类对象

  4. 基类的指针指向派生类对象(或基类引用绑定派生类对象)

  5. 通过基类指针(引用)调用虚函数

最终的效果:基类指针调用到了派生类实现的虚函数。(如果没有虚函数机制,基类指针只能调用到基类的成员函数)

虚函数表*

在虚函数机制中virtual关键字的含义

1、虚函数是存在的;(存在)

2、通过间接的方式去访问;(间接)

3、通过基类的指针访问到派生类的函数,基类的指针共享了派生类的方法(共享)

如果没有虚函数,当通过pbase指针去调用一个普通的成员函数,那么就不会通过虚函数指针和虚表,直接到程序代码区中找到该函数;

有了虚函数,去找这个虚函数的方式就成了间接的方式。

对虚函数和虚函数表有了基本认知后,我们可以思考这样几个问题(面试常考题)

1、虚表存放在哪里?

编译完成时,虚表应该已经存在;在使用的过程中,虚函数表不应该被修改掉(如果能修改,将会找不到对应的虚函数)——应该存在只读段——具体位置不同厂家有不同实现。

2、一个类中虚函数表有几张?

虚函数表(虚表)可以理解为是一个数组,存放的是一个个虚函数的地址

一个类可以没有虚函数表(没有虚函数就没有虚函数表);

可以将虚函数表理解为一个存在类型为函数指针类型的数组,虚函数指针就是指向这个存放类型为函数指针类型的数组的第一个元素的一个指针。

可以有一张虚函数表(即使这个类有多个虚函数,将这些虚函数的地址都存在虚函数表中);

也可以有多张虚函数表(继承多个有虚函数的基类)

3、虚函数机制的底层实现是怎样的?

虚函数机制的底层是通过虚函数表实现的。当类中定义了虚函数之后,就会在对象的存储开始位置,多一个虚函数指针,该虚函数指针指向一张虚函数表,虚函数表中存储的是虚函数入口地址。

  1. 三个概念的区分

重载 (overload) : 发生在同一作用域中, 当函数名称相同时 ,函数参数类型、顺序 、个数任一不同;

隐藏 (oversee) : 发生在基类派生类之间 ,函数名称相同时,就构成隐藏(参数不同也能构成隐藏);

覆盖(override): 发生在基类派生类之间,基类与派生类中同时定义返回类型、参数信息、名字都相同的虚函数,覆盖的是虚函数表中的入口地址,并不是覆盖函数本身

虚函数的限制

虚函数机制给C++提供了灵活的用法,但仍然受到了一些约束,以下几种函数不能设为虚函数:

1.构造函数不能设为虚函数

构造函数的作用是创建对象,完成数据的初始化,而虚函数机制被激活的条件之一就是要先创建对象,有了对象才能表现出动态多态。如果将构造函数设为虚函数,那此时构造未执行完,对象还没创建出来,存在矛盾。

2.静态成员函数不能设为虚函数

虚函数的实际调用:  this -> vfptr -> vtable -> virtual function,但是静态成员函数没有this指针,所以无法访问到vfptr

vfptr是属于一个特定对象的部分,虚函数机制起作用必然需要通过vfptr去间接调用虚函数。静态成员函数找不到这样特定的对象。

3.Inline函数不能设为虚函数

因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,所以起作用的时机是冲突的。如果同时存在,inline失效。

4.普通函数不能设为虚函数

虚函数要解决的是对象多态的问题,与普通函数无关

虚函数的各种访问情况

虚函数机制的触发条件中规定了要使用基类指针(或引用)来调用虚函数,那么其他的调用方式会是什么情况呢?

  1. 通过派生类对象直接调用虚函数

并没有满足动态多态触发机制的条件,此时只是Derived中定义display函数对Base中的display函数形成了隐藏。

  1. 在构造函数和析构函数中访问虚函数

class Grandpa
{
public:Grandpa(){ cout << "Grandpa()" << endl; }~Grandpa(){ cout << "~Grandpa()" << endl; }virtual void func1() {cout << "Grandpa::func1()" << endl;}virtual void func2(){cout << "Grandpa::func2()" << endl;}
};class Parent
: public Grandpa
{
public:Parent(){cout << "Parent()" << endl;//func1();//构造函数中调用虚函数}~Parent(){cout << "~Parent()" << endl;//func2();//析构函数中调用虚函数}
};class Son
: public Parent
{
public:Son() { cout << "Son()" << endl; }~Son() { cout << "~Son()" << endl; }virtual void func1() override {cout << "Son::func1()" << endl;}virtual void func2() override{cout << "Son::func2()" << endl;}
};void test0(){Son ss;Grandpa * p = &ss;p->func1();p->func2();
}

用Grandpa类指针p指向Son类对象,用这个指针p调用func1/func2.结果是指针p调用到的是Son类的func1和func2函数。

说明即使Parent中没有对func1和fucn2覆盖,在Son中也可以对func1和func2覆盖。

—— 如果在Parent类的构造和析构函数中调用虚函数

创建一个Son对象

在parent的构造函数执行时,并不知道是在构造Son的对象(因为先创建基类子对象然后才会创建自己的对象),在此过程中,只能看到本层及以上的部分(因为Grandpa类的基类子对象已经创建完毕,虚表中记录了Grandpa::func1和func2的地址)

在Parent的析构函数执行时,此时Son的析构函数已经执行完了,可以理解为Son需要进行的回收工作都已经结束了。所以Parent的析构函数也只能看到本层及以上的部分

(表现的是静态联编)

——如果Parent类中也覆盖了func1和func2,那么会调用Parent本层的虚函数。

总结:

C++标准规定,在构造函数和析构函数中,通过this指针(隐式或显式)调用的虚函数会被解析为当前类(即构造函数或析构函数所属的类)的版本,而不是通过虚函数表来动态解析。

下面为测试代码,可自行测试

#include <iostream>
using namespace std;class Grandpa
{
public:Grandpa() { cout << "Grandpa()" << endl; }~Grandpa() { cout << "~Grandpa()" << endl; }virtual void func1() {cout << "Grandpa::func1()" << endl;}virtual void func2() {cout << "Grandpa::func2()" << endl;}
};class Parent: public Grandpa
{
public:Parent() {cout << "Parent()" << endl;func1();//构造函数中调用虚函数}~Parent() {cout << "~Parent()" << endl;func2();//析构函数中调用虚函数}virtual void func1() {cout << "Parent::func1()" << endl;}virtual void func2() {cout << "Parent::func2()" << endl;}
};class Son: public Parent
{
public:Son() { cout << "Son()" << endl; }~Son() { cout << "~Son()" << endl; }virtual void func1() override {cout << "Son::func1()" << endl;}virtual void func2() override {cout << "Son::func2()" << endl;}
};void test0() {//没有数据成员只有一个虚函数指针cout << sizeof(Grandpa) << endl;//只包含Grandpa成员子对象会带有的虚函数指针cout << sizeof(Parent) << endl;//还是一个虚函数指针cout << sizeof(Son) << endl;Son ss;//使用Son对象在parent的构造函数中调用func1 在析构函数中调用func2//Grandpa* p = &ss;对grandpa中的func1 和 func2 进行覆盖//p->func1();//p->func2();}
int main()
{test0();return 0;
}

3.在普通成员函数中调用虚函数

class Base{
public:Base(long x): _base(x){}virtual void display() const{cout << "Base::display()" << endl;}void func1(){display();cout << _base << endl;}void func2(){Base::display();}
private:long _base = 10;
};class Derived
: public Base
{
public:Derived(long base,long derived): Base(base), _derived(derived){}void display() const override{cout << "Derived::display()" << endl;}
private:long _derived;
};void test0(){Base base(10);Derived derived(1,2);base.func1();base.func2();derived.func1();derived.func2();
}

第1/2/4次调用,显然调用Base的display函数。

第3次调用的情况比较特殊:

derived对象调用func1函数,因为Derived类中没有重新定义自己的func1函数,所以会去调用基类子对象的func1函数(通过基类子对象调用func1函数)。

可以理解为this指针此时发生了向上转型,成为了Base*类型。此时this指针还是指向的derived对象,就符合基类指针指向派生类对象的条件,在func1中调用虚函数display,触发动态多态机制。

相关文章:

  • AVIOContext 再学习
  • Linux之基础开发工具(yum,vim,gcc,g++)
  • C与指针5——字符串合集
  • 第二章:一致性基础 A Primer on Memory Consistency and Cache Coherence - 2nd Edition
  • 共铸价值:RWA 联合曲线价值模型,撬动现实资产生态
  • 【算法应用】基于灰狼算法优化深度信念网络回归预测(GWO-DBN)
  • 快速掌握--cursor
  • C# 编程核心:控制流与方法调用详解
  • word论文排版常见问题汇总
  • 【时间之外】官网视频风波
  • 鼠标交互初体验:点击屏幕生成彩色气泡(EGE 库基础)
  • LeetCode —— 102. 二叉树的层序遍历
  • 【项目实践】boost 搜索引擎
  • CSS布局
  • React18组件通信与插槽
  • 图论之幻想迷宫
  • dstack 是 Kubernetes 和 Slurm 的开源替代方案,旨在简化 ML 团队跨顶级云、本地集群和加速器的 GPU 分配和 AI 工作负载编排
  • Linux常用命令29——delgroup删除组
  • AI Agent开发第48课-DIFY中利用AI动态判断下一步流程-DIFY调用API、REDIS、LLM
  • 卷积神经网络进化史:从LeNet-5到现代架构的完整发展脉络
  • 又一日军“慰安妇”制度受害者去世,大陆登记在册幸存者仅剩7人
  • “80后”海南琼海市长傅晟,去向公布
  • 有人悬赏十万寻找“全国仅剩1只”的斑鳖,发帖者回应并证实
  • 上海畅通“外转内”,外贸优品成“香饽饽”
  • 申活观察|演出场次破纪录、入境游导游档期忙,上海文旅商“热力”拉满
  • 五四青年节|青春韵脚