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

C++虚函数(详解)

一、什么是虚函数?

在基类中使用 virtual 关键字修饰的成员函数称为虚函数。

用法格式:

virtual 函数返回值类型 函数名 (参数表){

    函数体;

}

虚函数是 C++ 中实现多态性的重要机制,它允许在派生类中重写基类中的函数。虚函数的核心特性在于,它使得通过基类指针或引用调用函数时,可以根据实际对象的类型选择相应的函数实现。

二、虚函数的作用

虚函数的作用,用专业术语来解释就是实现多态性(Polymorohism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。

具体而言,就像买火车票这件事。同样是买火车票,不同的人去买有不同的结果,学生买火车票半价,而军人优先购票等。

下面来看这样一段代码:

#include<iostream>
using namespace std;
class A
{
    public:
        void print()
        {
            cout<<"This is A"<<endl;
        }
};
 
class B : public A
{
    public:
        void print()
        {
            cout<<"This is B"<<endl;
        }
};
 
class C : public A
{
    public:
        void print()
        {
            cout<<"This is C"<<endl;
        }
};

int main()
{
    //为了在以后便于区分,我这段main()代码叫做main1
    A a;
    B b;
    C c;
    a.print();
    b.print();
    c.print()
    return 0;
}

运行结果:

This is A
This is B
This is C

通过class A、class B和class C的print()这个接口,可以看出这三个class因个体的差异而采用了不同的策略,但这并不是多态性行为。这里,a.print()b.print()c.print() 都是静态绑定,因此编译器直接根据对象的类型选择调用相应的 print() 方法,而不管是否存在派生类。

现在把main()处的代码改一改。

#include<iostream>
using namespace std;
class A
{
public:
    void print()
    {
        cout<<"This is A"<<endl;
    }
};

class B : public A
{
public:
    void print()
    {
        cout<<"This is B"<<endl;
    }
};

class C : public A
{
public:
    void print()
    {
        cout<<"This is C"<<endl;
    }
};

int main()
{
    //为了在以后便于区分,我这段main()代码叫做main2
    A a;
    B b;
    C c;
    A *p1 = &a;
    A *p2 = &b;
    A *p3 = &c;

    p1->print();
    p2->print();
    p3->print();
    return 0;
}

运行结果:

This is A
This is A
This is A

问题来了,p2、p3明明指向的是class B、class C的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数。

#include<iostream>
using namespace std;
class A
{
public:
    virtual void print()
    {
        cout<<"This is A"<<endl;
    }
};

class B : public A
{
public:
    void print()
    {
        cout<<"This is B"<<endl;
    }
};

class C : public A
{
public:
    void print()
    {
        cout<<"This is C"<<endl;
    }
};

int main()
{
    //为了在以后便于区分,我这段main()代码叫做main2
    A a;
    B b;
    C c;
    A *p1 = &a;
    A *p2 = &b;
    A *p3 = &c;

    p1->print();
    p2->print();
    p3->print();
    return 0;
}

运行结果:

This is A
This is B
This is C

毫无疑问,class A的成员函数print()已经成了虚函数,那么class B的print()成了虚函数了吗?回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了(语法上可加可不加,不加的话编译器会自动加上,但为了阅读方便和规范性,建议加上)。
现在重新运行main2的代码,这样输出的结果就是This is A、This is B和This is B了。
现在来消化一下,我作个简单的总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数(动态绑定)。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

三、虚函数的底层实现

虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数。

实现原理:虚函数表+虚表指针

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。

四、使用虚函数后的变化

(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表。
(3) 对每个函数调用都需要增加在表中查找地址的操作。

五、虚函数的注意事项

在使用虚函数时,有一些重要的注意事项,以确保代码的正确性、可维护性和性能。以下是一些关键点:

1. 基类析构函数应为虚函数

如果类包含虚函数,其析构函数也应声明为虚函数。这确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免资源泄漏。

2. 尽量避免在构造和析构函数中使用虚函数

在对象的构造和析构阶段,虚指针尚未设置(构造时)或已经清除(析构时),因此调用虚函数可能导致未定义行为。应避免在构造函数和析构函数中调用虚函数。

3. 虚函数的重写

当派生类重写基类的虚函数时,使用 override 关键字可以增加可读性,并在编译时进行检查。如果派生类中的函数没有正确重写基类的虚函数,编译器会给出警告。

4. 性能开销

虚函数调用比普通函数调用的性能开销稍高,因为它涉及查找虚函数表。对于频繁调用的性能敏感函数,尽量使用非虚函数,或者考虑其他设计方案。

5. 多重继承中的虚函数

在多重继承中,虚函数可能会导致复杂性。应仔细设计类的继承结构,以避免潜在的二义性问题。使用虚基类可以帮助解决此类问题。

6. 指向虚函数的指针

在一些情况下,使用基类指针指向派生类对象时,应确保适当地处理对象的生命周期,避免内存泄漏或访问无效指针。


http://www.kler.cn/a/373221.html

相关文章:

  • Linux文件系统学习(未完)
  • nmcli、ip、ifcfg配置网络区分方法
  • 【Python单元测试】pytest框架单元测试 配置 命令行操作 测试报告 覆盖率
  • 大数据-191 Elasticsearch - ES 集群模式 配置启动 规划调优
  • 省域经济高质量发展水平测算及数据2000-2021年
  • 使用Docker Compose简化微服务部署
  • 无人机避障——路径规划篇(一) JPS跳点搜索算法A*算法对比
  • React四官方文档总结一UI与交互
  • 4.2-7 运行MR应用:词频统计
  • flutter VideoPlayer适配:保持视频的原始宽高比,缩放视频使它完全覆盖父容器
  • Vue生成名片二维码带logo并支持下载
  • 《人工智能炒股:变革与挑战》
  • 《YOLO 目标检测》—— YOLO v3详细介绍
  • Linux rabbitmq客户端 SimpleAmqpClient 源码编译
  • docker 数据目录迁移
  • 正确认识HTTP和HTTPS协议及其在Java Web项目中的应用!
  • 1_信息化项目实施方案
  • 数据结构:(OJ387)字符串中的第一个唯一字符
  • 恋爱脑学Rust之闭包三Traits:Fn,FnOnce,FnMut
  • [Mysql] 介绍一下PROCEDURE、TRIGGERS和EVENTS
  • AdaBoost与前向分步算法
  • 使用openssl生成自签名证书(多域名)用于https的ssl验证
  • 【Java SE】变量与常量
  • JVM机制
  • 视频美颜平台的搭建指南:基于直播美颜SDK的完整解决方案
  • 可视化应急指挥平台在应急通信中的优势