【学习笔记】virtual 关键字,虚函数与多态

本文详细介绍了C++中的运行时多态,重点讨论了虚函数和`virtual`关键字的作用。通过实例展示了如何使用虚函数实现动态绑定,以及虚函数的使用规则和建议。同时提到了纯虚函数和抽象类的概念,强调了在实现多态时,析构函数通常是虚拟的。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先决条件

  1. 了解 C++ 中的多态 这个概念

本文中父类和基类同义,子类和派生类同义

多态,运行时多态与 virtual 关键字

在这里插入图片描述
在 C++ 中,要实现多态有两种大的方式,一种是编译时多态或者静态绑定,这是一种静态多态的实现方式,它主要包括了重载和模板两种方式;另一种是运行时多态或者叫做动态绑定和后期绑定,也称之为动态多态,它的实现方案就是利用虚函数来达到运行时多态的效果。

而虚函数在 C++ 中,通过 virtual 关键字,让其标注在函数上来告诉编译器这是一个虚函数。
在这里插入图片描述

多态,运行时多态与虚函数

那么什么是多态呢? 就是一个函数在不同的场景下,表现出不同的状态(行为,特性等)。

下面举两个例子,第一个例子比较偏生活。比如你是一家公司的 CEO,你手下有销售部,运营部,技术部三个部门,你对三个部门下达一个指令 干活了三个部门虽然接到的是相同的指令,但是实际上三个部分真正执行的是不同的任务,销售部的人去外面跑销售,运营部的人负责产品的运行与营销,技术部的人负责开发新产品和维护旧产品。
在这里插入图片描述
第二个例子则贴近编程语言本身。我们知道很多语言都有 + 操作符,当 + 操作符的两边是数字时,则完成数字的数学加法操作,而当 + 操作符的两边是字符串时,则有时(具体看该语言是否支持用 + 连接两个字符串)可以完成字符串的连接操作,如下是一个示例。

// 数字的加法操作
int main()
{
    int a = 3;
    int b = 5;
    int c = a + b;
    
    cout << c << endl; // 8
    
    return 0;
}

// 字符串的连接操作
int main()
{
    string firstName = "Yu ";
    string lastName = "Zhang";

    string name = firstName + lastName;
    
    cout << name << endl; // Yu Zhang
    
    return 0;
}

简单介绍了多态的概念,但本篇主要介绍的是运行时多态,所以现在再来说一下什么是运行时多态。

我们应该知道,一个 C++ 程序的执行分为预处理编译汇编链接然后运行这样五个步骤,而程序真正运行(跑起来)是发生在运行这个步骤中,根据我们的需要,有时我们需要在这个阶段实现多态效果,就称之为运行时多态,在 C++ 中,我们通过虚函数来完成这个目的。

虚函数与 virtual 关键字

接下来,我们通过一个代码示例来演示虚函数如何实现运行时多态的。

在下面的例子中,我创造了一个类 人类,有两个派生类(亚洲人欧洲人)继承自人类,每个类中都有一个同名,同参数列表的成员函数,叫做 YourRace() 用来打印调用的对象的种族。

此外,我们还创建了一个 SayYourRace(Human &h) 的非成员函数,它有一个用来接收 Human 类的参数,这样我们传入任何派生自 Human 的类对象就都可以打印了。

#include <iostream>
using namespace std;

#include <iostream>
using namespace std;

class Human
{
public:
    void YourRace()
    {
        cout << "我们都是人类" << endl;
    }
};

class Aisan : public Human
{
public:
    void YourRace()
    {
        cout << "我是亚洲人" << endl;
    }
};

class European : public Human
{
public:
    void YourRace()
    {
        cout << "我是欧洲人" << endl;
    }
};
// void SayYourRace(Human *h) 也是一样的效果
void SayYourRace(Human &h){
	h.YourRace();
}

int main()
{
    Aisan a;
    European e;
    
	SayYourRace(a);	// 我们都是人类
	SayYourRace(e); // 我们都是人类
}

上面的代码中,我们创建了两个对象 a,e 分别代表亚洲人和欧洲人,然后把它们分别传入 SayYourRace(Human &h) 函数中,我们显然期望它能够分别打印我是亚洲人我是欧洲人这两句话,然而,事实胜于雄辩,最终控制台输出的两条语句都是我们都是人类

为什么会这样?明明我们传入的都是 AisanEuropean 的对象啊!!!

事实上,因为在 C++ 中,基类指针指向派生类的对象,默认情况下,C++ 采用静态绑定(编译时绑定,前期绑定),在这种情况下传入的 a 和 e 仍然被编译器视为 Human 的对象。

那么如果要解决这个问题,即想要调用派生类的 YourRace 函数,为了达到这个目的,我们需要在运行时再查看使用的对象,在代码上的做法是,在基类的 YourRace 函数前面增加 virtual 关键字即可,它会告诉编译器,这里执行动态绑定。

它的具体语法是这样的:virtual void YourRace(){...};

#include <iostream>
using namespace std;

class Human
{
public:
	// 在基类的同名函数中加上 virtual 关键字
    virtual void YourRace()
    {
        cout << "我们都是人类" << endl;
    }
};

class Aisan : public Human
{
public:
    void YourRace()
    {
        cout << "我是亚洲人" << endl;
    }
};

class European : public Human
{
public:
    void YourRace()
    {
        cout << "我是欧洲人" << endl;
    }
};

void SayYourRace(Human &h){
	h.YourRace();
}

int main()
{
    Aisan a;
    European e;
    
	SayYourRace(a);	// 我是亚洲人
	SayYourRace(e); // 我是欧洲人
}

可以看到,本段代码仅仅是新增了一个 virtual 关键字,就实现了调用派生类成员函数的功能。

综上,这里我们总结一下动态多态实现的必要条件

  1. 一定是继承的场景下
  2. 子类方法覆盖父类的虚函数
  3. 在创建对象时,是父类指针指向子类对象

单独谈谈 virtual 关键字的作用

可以看到,增加了 virtual 关键字后,实际上,是在运行时才确定对象的类型,执行动态链接,然后绑定函数调用,所以这又叫动态绑定。

比如在上面的第二段代码中,ha 实际指向的对象是 aa 的类型是 Aisan,所以最终调用的是 Aisan 类的 YourRace 函数。

在基类中使用 virtual 关键字,表明基类要求派生类重写这个函数。当使用指针或者应用调用虚函数时,调用被动态绑定到运行时才发生,也就是根据被绑定的对象的类型不同,执行具体的版本,就像上面所展示的那样。

【注】非虚函数的解析发生在编译过程。虚函数的解析发生在运行时。

其他内容

虚函数的使用规则

好的,上面我们已经谈过了虚函数,virtual 关键字的作用,下面来罗列一下它们使用时的注意事项,当然对于每个条款背后的细节,我不会在这里展示,有需要的朋友可以自行了解。

  1. 重载(overload),重写(override)与隐藏(hide),当我罗列出这三个词的时候,你能不能真正区别它们的含义和用法?

    在本文中,与虚函数相关的是重写(override)。重写的本质是,函数参数列表和函数名完全一样,修改函数体的内容。

    但是有时,不可避免派生类的同名函数中,我们会误写(比如新增一个参数),编译器无法检测出这个问题,只有运行时才会发现有问题,于是我们可以通过添加关键字 override,来告诉编译器帮我们检查重写的函数有没有问题。

  2. virtual 不能修饰静态成员

  3. virtual 不能修饰构造函数,但可以修饰析构函数,并且用于多态目的的基类通常都应该定义一个 virtual 的析构函数。(关于这一点,你可以查看《Effective C++》07 条款,获得更详细的内容。

  4. 派生类继承的基类里面的虚函数,在派生类中隐式的,也是虚函数,也就是说派生类不需要显式的手写一个 virtual,但在**《C++ 面向对象高效编程》**一书中,作者还是推荐大家显式标注

  5. 虚函数不能使用模板,但普通的成员函数可以使用模板

class A
{
public:
	// 这是错误的
	template<typename T>
	virtual void exampleWithVirtualFunc(T a) = 0;
	// 这是正确的
	template<typename T>
	void exampleWithMemberFunc(T a)
	{
		// ...
	}
}

虚函数使用建议

  1. 如果基类存在的目的是为了多态,那么最好将析构函数声明为 virtual。这里的原因在于,我们实现多态的时候,需要用基类指针指向派生类对象,若不对析构函数进行 virtual,那么在删除(delete)基类指针的时候,实际调用的是基类的析构函数(静态绑定),因而未删除实际的派生类的对象,造成内存泄漏
  2. 如果一个多态目的的类不使用 virtual 函数,表示它不应该被当做基类
    【注】基类也可以有其它的目的,比如很多容器类的基类就不存在 virtual
  3. 我们知道在多态场景下,析构函数最好是声明为虚函数,那么为什么构造函数不能声明为析构函数呢?实际上,有这样的逻辑,在构造函数阶段,构造函数会生成指向虚函数表的指针 vptr,然后 vptr 指向虚函数表,如果构造函数也是虚函数,那么就成了先有鸡还是先有蛋的问题了

虚函数与纯虚函数

  1. 描述:C++ 中的纯虚函数(或抽象函数)是我们没有实现的虚函数(即我们不想给它写点什么代码,因为它反正会被子类对象的同名函数重写)
  2. 注意点
    1. 只要类存在一个纯虚函数,那么这个类就被称为抽象类,抽象类是不能被实例化的,它的目的就是让子类继承,子类实现。其实和现实也一样,比如我定义了一个抽象的标准,这个抽象的标准并不能产生具体的产品,只有实际让工厂根据自身的情况生产了产品才叫做实例化
    2. 为什么抽象类不能实例化:因为抽象类包含纯虚函数,而纯虚函数没有函数体,并非一个完整的函数,无法调用,也无法分配内存空间
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      // 基类中的纯虚函数,它必须被派生类实现
      virtual int area() = 0;
};

参考与拓展

  1. C++ virtual function【javatpoint】
  2. Virtual Function in C++【geeksforgeeks】
  3. 为什么静态static成员函数不能成为virtual虚函数
  4. 析构函数可以为virtual,构造函数则不能。原因?
  5. Why Destructors in C++ can be virtual but constructors cannot be virtual?
  6. 为什么要用基类指针指向派生类对象?
  7. 《Primer C++》
  8. 区别:编译时与运行时(Combile-time Vs Runtime)
  9. Virtual Destructor:为什么析构函数可以是,也最好是 virutal 的。
  10. override Keyword in C++ :virtual 与 override 搭配使用。
  11. 《Effective C++》
  12. 父类指针指向子类对象
  13. 《C++ 面向对象高效编程》
  14. 对象切割
  15. C++ Polymorphism with Example

深入拓展

  1. C++虚函数的实现基本原理
  2. C++ 虚函数表剖析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值