惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
Webroot Blog
Webroot Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News | PayPal Newsroom
S
Security Affairs
T
Tor Project blog
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
H
Heimdal Security Blog
Hacker News: Ask HN
Hacker News: Ask HN
Help Net Security
Help Net Security
U
Unit 42
云风的 BLOG
云风的 BLOG
The Hacker News
The Hacker News
Cisco Talos Blog
Cisco Talos Blog
量子位
F
Full Disclosure
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
有赞技术团队
有赞技术团队
T
Troy Hunt's Blog
P
Privacy & Cybersecurity Law Blog
Forbes - Security
Forbes - Security
人人都是产品经理
人人都是产品经理
L
Lohrmann on Cybersecurity
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
博客园 - Franky
腾讯CDC
AI
AI
Last Week in AI
Last Week in AI
Latest news
Latest news
Google Online Security Blog
Google Online Security Blog
N
Netflix TechBlog - Medium
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
IT之家
IT之家
Martin Fowler
Martin Fowler
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
酷 壳 – CoolShell
酷 壳 – CoolShell

博客园 - 鹅要长大

【c++编程基础】理解lambda表达式 【c++编程基础】字节对齐pack的理解 【linux基础知识】理解nohup 【linux基础】如何理解python train_dtld.py 2>&1 | tee my_error_log.txt 【工具使用】docker的使用 【基础算法】银行家舍入法 【ubuntu工具】ubuntu好用工具总结 【ubuntu基础工具使用】ubuntu中screen的安装与使用 【linux操作基础】多目录文件复制且同名不覆盖 【linux基础操作】windows系统生成的txt文件在ubuntu打开乱码 【opencv编程基础】fillpoly和polylines函数的理解 【c++编程基础】std::unique的理解 【编程基础】计算三个顶点之间的夹角 【计算几何算法】道格拉斯普克(Douglas-Peuker)算法 [c++]c++ 工程代码中的debug时条件编译隐去的代码会影响程序运行的性能和耗时吗 【CV基础】语义分割任务计算类别权重 【opencv基础】resize使用的问题 git stash的用法 gflags的使用 opencv 判断某个坐标点是否在多边形内cv::pointPolygonTest 判断多边形的顶点内外角点以及对approxPolyDP函数的理解 用Python从零实现贝叶斯分类器 【CV数据集】智慧城市之CCPD车牌数据集
【c++编程基础】虚函数virtual的理解与使用
鹅要长大 · 2026-07-14 · via 博客园 - 鹅要长大

前言

本文主要记录一下对c++编程中虚函数virtual的理解。

虚函数virtual理解

virtual允许子类重新实现,并通过基类指针调用时能够动态选择正确函数,即运行时多态。虚函数或虚方法可以被子类继承和覆盖,编译期无法确定目标函数的具体指向。

如果基类的函数是“虚”的,则调用到的都是最终派生类(英语:most-derived class)中的函数实现,与指针或引用的类型无关。反之,如果函数非“虚”,调用到的函数就在编译期根据指针或者引用所指向的类型决定。

在C++中,在基类的成员函数声明前加上关键字virtual即可让该函数成为虚函数,派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本,即虚函数调用时,不看“调用代码写在哪个类”,而看“当前对象实际属于哪个类”。。

内部机制

c++实现虚函数通常依靠虚函数表vtable,vptr指针指向虚函数表vtable,每个类都有自己的vtable,vtable就是保存自己类中虚函数的地址。如果明确指定调用基类版本,则不会动态绑定。构造函数与析构函数不会向下调用,接口决定调用方式,对象决定执行版本。

定义虚函数的限制

(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
(3)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数。
(4)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。

性能影响

虚函数的性能开销主要源于其实现动态多态的机制,包括间接函数调用、内存开销、编译器优化受限、影响分支预测。调用虚函数需要通过虚表指针间接查表跳转,这带来了额外的指令开销和内存占用(每个包含虚函数的类需要维护虚函数表,每个对象需要存储虚表指针)。然而,更主要的性能影响在于,由于编译器在编译时通常无法确定具体调用哪个函数,导致许多优化(如函数内联)无法进行,并且可能影响处理器的分支预测效率。

纯虚函数/纯虚方法

纯虚函数和抽象类在接口设计中大量使用。如果一个类包含至少一个纯虚函数,则这个类为抽象类。抽象类不能实例化,可以定义指针,因为指针只是一个地址。子类必须实现纯虚函数。因为软件设计中,经常需要定义规范,故需要抽象类。

纯虚函数 = 父类只规定“必须有什么功能”,但不告诉你“具体怎么实现”。
抽象类 = 包含纯虚函数的“接口模板”,不能直接创建对象,只能被继承。

纯虚函数

class Animal
{
public:
    virtual void Speak() = 0;  // 接口,表示这个函数没有实现,必须由子类实现。
};

纯虚函数通常只有声明没有定义,但是虚析构函数必须有实现,因为派生类析构时一定会调用析构函数。

image

 接口基类必须有virtual析构函数,

class A
{
public:
    ~A() = default; // c++11引入,使用编译器自动生成的默认实现。
};

抽象接口类只是接口,本身没有资源,故其西沟函数使用default足够。

代码示例

virtual_1.cc      Show() 不是虚函数,因此调用基类版本。

#include <iostream>
using namespace std;

class Base
{
public:
    void Func()
    {
        cout<<"Base::Func"<<endl;
        Show();
    }

    void Show()
    {
        cout<<"Base::Show"<<endl;
    }
};

class Derived:public Base
{
public:
    void Show()
    {
        cout<<"Derived::Show"<<endl;
    }
};

int main()
{
    Derived d;
    d.Func();
    return 0;
}

View Code

output:

virtual_2.cc   Show() 是虚函数,发生动态绑定,调用派生类版本。

#include <iostream>
using namespace std;

class Base
{
public:
    void Func()
    {
        cout<<"Base::Func"<<endl;
        Show();
    }

    virtual void Show()
    {
        cout<<"Base::Show"<<endl;
    }
};

class Derived:public Base
{
public:
    void Show() override
    {
        cout<<"Derived::Show"<<endl;
    }
};

int main()
{
    Derived d;
    d.Func();
    return 0;
}

View Code

output:

virtual_3.cc    代码通过 Base::Show() 明确指定调用基类版本,绕过虚函数动态绑定。

如果明确指定Base::Show();,即强制调用基类版本,则不会动态绑定。

#include <iostream>
using namespace std;

class Base
{
public:
    void Func()
    {
        cout<<"Base::Func"<<endl;
        Base::Show(); // 强制调用基类版本。
    }

    virtual void Show()
    {
        cout<<"Base::Show"<<endl;
    }
};

class Derived:public Base
{
public:
    void Show() override
    {
        cout<<"Derived::Show"<<endl;
    }
};

int main()
{
    Derived d;
    d.Func();
    return 0;
}

View Code

output:

virtual_4.cc    基类构造期间调用虚函数时,不会分派到尚未构造完成的派生类,因此调用 Base::Init()。

构造函数/析构函数,构造期间,对象还没有构造完成,故虚函数不会向下调用。

#include <iostream>
using namespace std;

class Base
{
public:
    Base() 
    {     
        Init(); 
    }  
    virtual void Init() 
    {     
        cout<<"Base::Init"; 
    }
    void Func()
    {
        cout<<"Base::Func"<<endl;
        Show();
    }

    virtual void Show()
    {
        cout<<"Base::Show"<<endl;
    }
};

class Derived:public Base
{
public:
    void Init() override 
    {
         cout<<"Derived::Init"; 
    }
    void Show() override
    {
        cout<<"Derived::Show"<<endl;
    }
};

int main()
{
    Derived d;
    return 0;
}

View Code

output:

参考

1. virtual百科