Redian新闻
>
虚函数,C++开发者如何有效利用?

虚函数,C++开发者如何有效利用?

公众号新闻

  
本期博客,我们来介绍C++中的虚函数,并给出一些实际操作的建议。
文末有福利活动彩蛋!!

什么是虚函数?



虚函数是基类中声明的成员函数,且使用者期望在派生类中将其重新定义。那么,在 C++ 中,什么是虚函数呢? C++ 中,通常将虚函数用于实现运行时多态,该特性由 C++ 提供,适用于面向对象编程。我们将在下文更为详细地讨论运行时多态。不论函数调用所使用的指针或引用类型如何,虚函数最为重要的工作是确保函数调用正确


1虚函数的使用规则

C++ 虚函数必须遵循几个关键规则:

  • 在基类中使用 virtual 关键词来声明函数

  • 虚函数不能为静态函数

  • 为实现运行时多态,应使用指针或引用来访问虚拟函数

  • 对于基类和派生类而言,此类函数的原型应该相同(允许使用协变式返回类型,我们将在下文进行讨论)

  • 如果基类中含有虚函数,则应该使用虚拟析构函数,防止析构函数调用错误


2 用 C++ 运行虚函数的示例

虚函数在 C++ 中的运行情况:

class Pet {public:    virtual ~Pet() {}    virtual void make_sound() const = 0;};
class Dog: public Pet {public: virtual void make_sound() const override { std::cout << "raf raf\n"; }};
class Cat: public Pet {public: virtual void make_sound() const override { std::cout << "mewo\n"; }};
int main() { Cat mitzi; Dog roxy; Pet *pets[] = {&mitzi, &roxy}; for(auto pPet: pets) { pPet->make_sound(); }}
解释一下上述示例。
Pet 这是一个通用基类。但是我们仍然希望存在一个 make_sound 函数,这样,我们就能在不知道 pet 类型的情况下,在 pet 上调用 make_sound。仅在进行编译时,我们才能知道 pet 类型。因此,我们在基类中声明虚函数 make_sound,用 =0 来将其表示为由派生类实现的纯虚函数。
然后,再由 Dog 和 Cat 来真正实现该函数。实现函数期间,我们添加关键词 override,这样,编译器就能确保函数签名与基类中的签名相匹配。
在 main 中,我们可以在 Pet 指针上调用 make_sound,而无需在编译时知道该指针指向哪种 pet。我们会在运行时,根据实际存在的对象,实现所需函数。
我们必须要强调,这是一个非常简单的示例。我们也有其他解决方案应对这一简单示例(例如,为 pet’s sound 持有数据成员,并避免使用虚函数)。但我们想要展示虚函数的实现过程,因此不对其他解决方案进行额外展示。通常情况下,会使用虚函数为派生类中的不同行为建模,而相应行为不能用简单数据成员来建模。

3 协变式返回类型
我们提到过,若要实现虚函数,派生类函数的签名必须与基类中的签名相匹配。唯一允许的区别是在返回类型上,只要派生类的返回类型是基类返回的派生类型即可。让我们看看下面的示例:
class PetFactory {public:    virtual ~PetFactory() {}    virtual Pet* create() const = 0;} 
class DogFactory: public PetFactory {public: virtual Dog* create() const override { return new Dog(); }};
class CatFactory: public PetFactory {public: virtual Cat* create() const override { return new Cat(); }};
int main() { std::vector<Pet*> pets; DogFactory df; CatFactory cf; PetFactory* petFactory[] = {&df, &cf}; for(auto factory: petFactory) { pets.push_back(factory->create()); } for(auto pPet: pets) { pPet->make_sound(); } for(auto pPet: pets) { delete pPet; }}
在上述示例中,PetFactory 创建函数仅能知道它可以返回 Pet*,但使用协变式返回类型,DogFactory 和 CatFactory 则能知道更为具体的内容,这种虚函数的实现方式仍然行之有效。

2月16日,线上研讨会【C++开发效率提升的三大挑战与解决方案】
扫码文末二维码获取会议入口及C++资料包 

在 C++ 中使用虚函数的优点


现在,如果您已经花费时间研究过 C++,可能会注意到,不需要由虚函数来重新定义派生类中的基函数。但存在这样的巨大区别,使得虚函数不可或缺:虚函数覆写基类函数,从而实现运行时多态。从本质上讲,多态指一个函数或对象以不同方式执行的能力,具体情况视使用方式而定。这属于面向对象编程的关键特性——结合其他众多特性,使得 C++ 作为编程语言而有别于 C 语言。

1 代码更为灵活、更为通用
这是贯穿所有多态程序的主要优点:根据运行时已知的调用对象,通过允许以不同方式执行函数调用,能使程序更为灵活而通用。如此一来,运行时多态便能从真正意义上使您的代码反映现实——特别是各场景中的对象(或人、动物、形状)并不总是以相同方式执行。

2 代码可复用
通过使用虚函数,我们可以将只应实现一次的通用操作和不同子类中可能有所不同的具体细节区分开来。试想以下示例:如果我们希望实现 prism 类层次结构,则需要在各派生类中分别计算基面积,但可以使用派生类实现基面积计算,从而在基类中实现体积函数。实现代码如下:
class Prism {    double height;public:    virtual ~Prism() {}    virtual double baseArea() const = 0;    double volume() const {        return height * baseArea();    }    // ...};
class Cylinder: public Prism { double radius;public: double baseArea() const override { return radius * radius * std::numbers::pi } // ...};
3 契约式设计
术语“契约式设计”指如果代码设置有执行设计的契约,会比只通过文档来执行设计要好得多。虚函数,特别是纯虚函数,因其决定了在派生类中以不同方式重新实现特定操作的设计决策,可将其视为契约式设计工具。

虚函数的局限性



虚函数功能极为强大,但它们并非毫无缺点。开始使用虚函数前,您应该注意以下事项:


1 性能

无论是在运行时性能还是在内存方面,虚函数成本都要比普通函数高。

内存部分通常冗余,取决于实现方式,但最为常见的是每个对象都有一个额外内部指针。这并不是什么大问题,除非我们有数以百万计的小对象,这些小对象的额外指针可能会引起内存问题。

函数的运行时性能成本不是一次跳转而是两次跳转,或者如果可以内联函数,性能成本就是两次跳转而不是零次跳转。虚函数需要跳转到虚函数表,再跳转到函数本身。这种额外跳转增加了 CPU 指令缓存中指令未准备就绪的概率,因此,这两次跳转并非唯一成本。

最后,如果您需要实现多态,与其他替代方案相比,性能方面的额外成本通常也在情理之中。然而,若要将第一个虚函数添加到类中,通常需要考虑额外成本。


2设计问题

继承,特别是虚函数,会引起设计问题。继承层次结构设计糟糕可能会导致类膨胀和类之间关系异常。

从构造函数和析构函数调用虚函数的规则也会影响您的设计。从构造函数和析构函数调用的任何虚函数都不是多态函数,这样一来,有时需要将操作从构造函数转移到 init 虚拟函数。

为避免糟糕设计,应切记继承和多态并非是应对任何问题的最佳解决方案。请观看 Sean Parent 的演讲”Inheritance is the Base Class of Evil“,深入了解相关内容。


3 调试,容易出错

讽刺的是,虚函数面临的挑战之一是缺乏弹性。

由于需要遵循调用流程,调试虚函数调用可能会变得稍显混乱。一般来说,遵循函数调用并不十分困难,但根据对象类型,在遵循隐藏调度方面,仍然需要进行额外工作。调试器会自行纠正错误,但决定断点位置可能会变得更加困难。

至于更容易出错,在某些情况下,不应调用虚函数的基类实现;而在某些情况下,应在开始时调用,有时也在结束时调用。由于忘记调用基类实现,或是在错误的地方、不需要的时候调用,使用虚函数极其容易出错。

可将其视为契约式设计工具。
虚函数的替代方案



1 仅使用数据成员
第一种替代方案是尝试并对基于简单数据成员的不同行为进行建模。如果不同类型的唯一区别是 sound,那就将其转换为数据成员,在构造时进行初始化,这样就没有问题了。但在许多情况下,行为更加复杂,需要不同的实现方式。

2 变体
另一种方案是使用 std::variant 和 std::visit,特别是待支持的不同类型已知,且列表不会太长时,二者可能相关。您可以点击此处和此处阅读更多关于该方案的信息。

3 函数式编程
您可以传递待执行的操作,将其作为函数对象的 lambda,或者作为旧有 C 样式的函数指针,随后对其进行建模,而无需在类层次结构中对不同操作进行建模。通过该方法,您能将数据模型和可能想要执行的操作区分开来,这带来了极高灵活性。

4 静态多态
静态多态是一种基于模板的方法,用于获取多态动态,但基于编译时已知的实际想要使用的类型。例如,您可能希望代码同时支持 UDPConnection 和 TCPConnection,但在编译时,您可能想要知道使用 UDPConnection 或 TCPConnection 的具体流程。基于模板的静态多态可以实现更佳性能。
一些替代技术可能会导致项目编译时间变长。我们认为,特别是当您使用 Incredibuild 来加速构建时,这不会影响您的决策设计。首先选择合适的设计方案,然后使用正确工具来缩减编译时间即可。
用 Incredibuild 加速您的 C++ 虚函数



如果您想在不严重拖累编译速度和构建进程的情况下,从虚拟函数中受益,您就需要强大的计算能力作为后援。
Incredibuild 能够做到这一点。通过在虚拟机在本地网络上分配编译任务,Incredibuild 从根本上加快了 C++ 的编译速度。此外,Incredibuild 能与时下主流编译器和构建系统无缝集成,包括 Visual Studio、Qt Creator 和 Clang。
如此一来,虚函数便能具备极高的灵活性和效率,而无需花费时间来等待代码编译。
。首先选择合适的设计方案,然后使用正确工具来缩减编译时间即可。
总 结



1. 什么是 C++ 的虚函数?
虚拟函数是基类中声明的成员函数,将在派生类中重新定义。在 C++ 中,使用虚函数来实现运行时多态。
2. 虚函数存在哪些问题?
在运行时性能和内存使用方面,相比于普通函数,虚拟函数会造成更多影响。此外,虚拟函数会产生基于继承层次结构的设计问题,导致类膨胀和关系异常。最后,虚拟函数由于存在函数调用问题,往往难以进行调试。由于调用顺序的不可预测性,使用虚拟函数更容易引发错误。
3. 在 C++ 中,虚函数有何替代方案?
是的,为了实现更好的设计或者是获得更佳的性能,您可能要考虑一些替代方案。但鉴于 C++ 程序员普遍使用虚函数,您应将其视为工具包内的一项工具,必要时加以使用。
如果您选择了另一种替代方案,比如基于模板的静态多态,切勿让较长的编译时间影响您的设计方案。确保选用合适的工具来加速构建进程,如果您没有使用 Incredibuild,请了解我们的解决方案,看看 Incredibuild 在减少编译时间方面可实现的惊人效果。

2月16日,线上研讨会【C++开发效率提升的三大挑战与解决方案】
资深开发者探讨当下C++最热门话题


2月16日,线上研讨会【C++开发效率提升的三大挑战与解决方案】



转发活动海报至朋友圈,保留12h以上,
可联系Penny领取#development never stops#定制T恤1件 (数量有限先到先得)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
开发者反思:独立游戏开发,究竟是逐梦还是天坑?如何有效打造你的领英人设?“阳过”后,哪些症状需警惕心肌炎?如何有效应对?GDC年度开发者调查报告:元宇宙为时尚早,大部分开发者不加班这个Excel高手常用的随机函数,太牛X了!如何有效鸡娃?教你思维训练第一步!寒假弯道超车就靠他了!ChatGPT开启的「AGI OS」时代,创业者如何做应用开发 | 5Y View自监督为何有效?243页普林斯顿博士论文「理解自监督表征学习」,全面阐述对比学习、语言模型和自我预测三类方法如何有效运用德鲁克管理思想成为卓有成效的职场人?延安军民大规模群众性文艺活动除了作品集,还能如何有效提高申请材料的竞争力?【自学干货】拉丁语如何有效入门?适合初学者的书籍、播客和视频学习资源盘点!喉咙过不知烫,伤口愈忘痛痒!EA发行制作人:如何有创意地充分利用外包合作伙伴?Docker正在淘汰开源组织,CTO硬刚开发者,网友:想赚钱可以,但沟通方式烂透了壬寅热门汉字“战”当选苹果正在开发低价版头显;开发者认为未来几年内PSVR2都不太可能支持PC完美世界TA分享:AIGC如何助力游戏开发,从业者如何适应新环境?想要成功就业?在美留学生如何有效规划大学各阶段国产新冠口服药阿兹夫定片十问:谁适合服用?如何服用?政策调整后,如何有效防止医疗资源挤兑?华为开发者贡献 Linux 内核补丁,将核心内核函数速度提升 715 倍人山人海的迪斯尼乐园30秒教你学会Vlookup函数,简单到哭哭!业界首个适用于固体系统的神经网络波函数,登上Nature子刊孩子眼睛度数飞涨,该如何有效干预?节后返工,如何有效度过第一周?海外开发者热议:开发游戏让我不再玩游戏东胜物联发布AIoT开发者计划,将赋能和服务国内10000个网关开发者比Vlookup更逆天!这个强大的查询函数,看完我就跪了!早起有点宽裕的时间谈对美国将来面临问题的看法。职业“倦怠”期开发者如何转变心态如何有效地激发国有企业家精神开发者遭死亡威胁,项目停止开发职场年终总结提效利器!华为超级终端一拉即合,重构生产力
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。