C++虚函数(详解)

一、什么是虚函数?

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

用法格式:

cpp 复制代码
virtual 函数返回值类型 函数名 (参数表){

    函数体;

}

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

二、虚函数的作用

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

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

下面来看这样一段代码:

cpp 复制代码
#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;
}

运行结果:

cpp 复制代码
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()处的代码改一改。

cpp 复制代码
#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;
}

运行结果:

cpp 复制代码
This is A
This is A
This is A

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

cpp 复制代码
#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;
}

运行结果:

cpp 复制代码
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. 指向虚函数的指针

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

相关推荐
zongzi_49443 分钟前
二次封装的天气时间日历选择组件
开发语言·javascript·ecmascript
kikyo哎哟喂1 小时前
Java 代理模式详解
java·开发语言·代理模式
duration~1 小时前
SpringAOP模拟实现
java·开发语言
一条晒干的咸魚1 小时前
【Web前端】实现基于 Promise 的 API:alarm API
开发语言·前端·javascript·api·promise
就爱六点起1 小时前
C/C++ 中的类型转换方式
c语言·开发语言·c++
我明天再来学Web渗透1 小时前
【SQL50】day 2
开发语言·数据结构·leetcode·面试
猫猫的小茶馆1 小时前
【C语言】指针常量和常量指针
linux·c语言·开发语言·嵌入式软件
DanielYQ2 小时前
LCR 001 两数相除
开发语言·python·算法
召木2 小时前
C++小白实习日记——Day 2 TSCNS怎么读取当前时间
c++·职场和发展
yngsqq2 小时前
037集——JoinEntities连接多段线polyline和圆弧arc(CAD—C#二次开发入门)
开发语言·c#·swift