Menu

2025-05-12-虚函数

post on 12 May 2025 about 5843words require 20min
CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,可以请我喝一杯咖啡~

2025-05-12-虚函数

参考资源

对 C++ 虚函数不了解?看完这篇文章掌握虚函数的原理和作用

4-虚函数_哔哩哔哩_bilibili

C++ 虚函数和纯虚函数的区别

C++ 多态–虚函数 virtual 及 override_虚函数 override-CSDN 博客

C++ 虚函数

虚函数是在父类中定义的一种特殊类型的函数,允许子类重写该函数以适应其自身需求。虚函数的调用取决于对象的实际类型,而不是指针或引用类型。通过将函数声明为虚函数,可以使继承层次结构中的每个子类都能够使用其自己的实现,从而提高代码的可扩展性和灵活性。在 C++ 中,使用关键字”virtual”来定义虚函数。

  • 虚函数不代表函数为不被实现的函数。
  • 为了允许用基类的指针来调用子类的这个函数。
  • 定义一个函数为纯虚函数,才代表函数没有被实现。
  • 纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

虚函数虚在所谓”推迟联编”或者”动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为”虚”函数。

Motivation

虚函数可以让子类对象在运行时动态地继承和修改父类的成员函数,使得代码更加灵活、可重用,并且可以实现多态性和抽象类等高级特性。

  1. 通过虚函数,可以实现多态性(Polymorphism),即同一个函数名可以在不同的子类中表现出不同的行为,这样可以提高代码的可重用性和灵活性。
  2. 避免静态绑定,在使用父类指针或引用调用子类对象的成员函数时,如果没有使用虚函数,则会进行静态绑定(Static Binding),即只能调用父类的成员函数,无法调用子类特有的成员函数。
  3. 虚函数的调用是动态绑定(Dynamic Binding)的,即在运行时根据指针或引用所指向的对象类型来选择调用哪个函数,从而实现动态多态性。
  4. 抽象类是一种不能直接实例化的类,只能被其他类继承并实现其虚函数。通过定义纯虚函数(Pure Virtual Function),可以使得一个类成为抽象类,强制其子类必须实现该函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 形状基类,定义了计算面积和周长的虚函数
class Shape {
public:
   // 计算面积的虚函数,提供默认实
   // 使用virtual关键字,允许派生类重写
   virtual double calculateArea() {
       return 0.0; // 基类提供默认实现
   }

   // 计算周长的虚函数,提供默认实现
   // 使用virtual关键字,允许派生类重写
   virtual double calculatePerimeter() {
       return 0.0; // 基类提供默认实现
   }

   // 虚析构函数,确保派生类对象正确释放
   virtual ~Shape() {} // 虚析构函数
};

// 矩形类,继承自Shape基类
class Rectangle : public Shape {
private:
   // 矩形的私有成员:宽和高
   double width;
   double height;

public:
   // 构造函数,初始化矩形的宽和高
   Rectangle(double w, double h) : width(w), height(h) {}

   // 重写基类的calculateArea虚函数
   // override关键字确保正确重写基类虚函数
   double calculateArea() override {
       return width * height; // 矩形面积计算
   }

   // 重写基类的calculatePerimeter虚函数
   double calculatePerimeter() override {
       return 2 * (width + height); // 矩形周长计算
   }
};

// 圆形类,继承自Shape基类
class Circle : public Shape {
private:
   // 圆形的私有成员:半径
   double radius;
   // 圆周率常量
   const double PI = 3.14159;

public:
   // 构造函数,初始化圆形的半径
   Circle(double r) : radius(r) {}

   // 重写基类的calculateArea虚函数
   // override关键字确保正确重写基类虚函数
   double calculateArea() override {
       return PI * radius * radius; // 圆形面积计算
   }

   // 重写基类的calculatePerimeter虚函数
   double calculatePerimeter() override {
       return 2 * PI * radius; // 圆形周长计算
   }
};

Concept

虚函数只能借助于指针或者引用来达到多态的效果。

多态的本质是“同一个函数调用,能够根据不同对象表现出不同的行为

Method

1747056760477QGSmb9FB5obI4CxbD3RcFRIynCe.png

知识点

虚函数声明

从简单的例子开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal
{
public:
    virtual void shout()
    {
        cout << "动物叫了一声\n";
    }
};

class Cat : public Animal
{
public:
    void shout() override
    {
        cout << "喵喵喵\n";
    }
};

解释:

  • 代码通过 Animal 基类定义了一个虚函数 shout(),为派生类提供了一个默认实现。
  • Cat 类继承自 Animal,使用 override 关键字重写了 shout() 方法,展示了虚函数允许派生类提供自己的特定实现。
  • 这个示例体现了 C++ 多态性的基本原理:基类定义一个通用接口,派生类可以根据自身特性灵活地实现具体行为。

需要注意的是,在子类中重写虚函数时,其访问权限不能更严格(即不能由 public 变为 private 或 protected),否则编译器会报错。

纯虚函数与抽象类

纯虚函数

纯虚函数是指在基类中定义的没有实现的虚函数。使用纯虚函数可以使该函数只有函数原型,而没有具体的实现。注:这里的“=0”表示该函数为纯虚函数。

纯虚函数的作用是让子类必须实现该函数,并且不能直接创建该类对象(即该类为抽象类)。

1
virtual void func() = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Shape {
public:
   // 纯虚函数,没有默认实现
   virtual double calculateArea() = 0;
   
   // 包含纯虚函数的类成为抽象类
   virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
   // 必须实现基类的纯虚函数
   double calculateArea() override {
       return width * height;
   }
   
   void draw() override {
       // 绘制矩形的具体实现
   }

private:
   double width;
   double height;
};

解释:

  • 代码通过 Shape 基类定义了两个纯虚函数 calculateArea()draw(),使 Shape 成为一个抽象类。
  • 纯虚函数(= 0)没有默认实现,强制派生类必须提供具体实现。
  • Rectangle 类继承自 Shape,必须实现所有纯虚函数,否则仍将是抽象类。
  • 这个示例展示了纯虚函数作为接口的设计模式,确保派生类提供必要的具体行为。

抽象类

抽象类是包含纯虚函数的类,它们不能被实例化,只能被继承。抽象类只能用作其他类的基类。如果一个类继承了抽象类,则必须实现所有的纯虚函数,否则该类也会成为抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Shape
{
public:
_    // 纯虚函数_
    virtual double getArea() = 0;
};

_// 继承自抽象类Shape_
class Rectangle : public Shape
{
public:
    double **width**;
    double **height**;
    double getArea() { return **width** * **height**; }
};

_// 继承自抽象类Shape_
class Circle : public Shape
{
public:
    double **radius**;
    double getArea() { return 3.14 * **radius** * **radius**; }
};

Shape 为抽象类,其中包含纯虚函数 getArea(),Rectangle 和 Circle 均继承自 Shape,并且实现了 getArea()函数的具体内容。

多重继承中的虚函数

在多重继承中,如果一个类同时继承了多个基类,而这些基类中都有同名的虚函数,那么子类必须对这些虚函数进行重写并实现。此时,需要使用作用域限定符来指明重写的是哪个基类的虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base1
{
public:
    virtual void func() { cout << "Base1::func()" << endl; }
};

class Base2
{
public:
    virtual void func() { cout << "Base2::func()" << endl; }
};

class Derived : public Base1, public Base2
{
public:
    virtual void func()
    {
        Base1::func();
        Base2::func();
    }
};

派生类 Derived 同时继承了 Base1 和 Base2,这两个基类中都有名为 func 的虚函数。在 Derived 中,我们通过使用作用域限定符 Base1::和 Base2::,分别调用了两个基类中的虚函数。

总结

  • 优点:

    • 实现多态性:通过虚函数,可以在不知道对象具体类型的情况下,调用特定对象的方法。
    • 代码灵活性:虚函数允许子类覆盖父类的方法,并且不需要改变基类的代码。
    • 代码可维护性:虚函数使得代码易于维护和扩展,因为子类可以通过重载虚函数来实现自己的行为。
  • 缺点:

    • 额外的开销:虚函数需要额外的开销来支持运行时的动态绑定和查找虚表。这可能会影响程序的性能。
    • 可能会引起混淆:由于虚函数的存在,同名的函数可能会被不同的类定义。如果没有正确的使用虚函数,可能会导致混淆和错误的结果。
    • 不适合于小型应用:虚函数对于小型应用来说可能过于复杂和冗余。在这种情况下,使用虚函数可能会导致更多的开销而不是提高效率。

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <string>

// 基类Entity,定义了一个虚函数GetName()
class Entity {
public:
   // 虚函数,返回一个默认的实体名称
   // 使用virtual关键字,允许派生类重写
   virtual std::string GetName() { return "Entity"; }
};

// 派生类Player,继承自Entity
class Player : public Entity {
private:
   // 私有成员,存储玩家名称
   std::string m_Name;

public:
   // 构造函数,使用传入的名称初始化玩家
   Player(const std::string& name) :m_Name(name) {};

   // 重写基类的GetName()虚函数
   // override关键字确保正确重写基类虚函数
   std::string GetName() override { return m_Name; };

   // 设置玩家名称的成员函数
   void SetName(std::string name) { m_Name = name; };
};

// 打印实体名称的函数
// 接受一个Entity指针作为参数,体现了多态性
void PrintName(Entity* entity) {
   // 调用虚函数GetName(),实际执行的是对象的具体实现
   std::cout << entity->GetName() << std::endl;
};

int main() {
   // 创建一个基类Entity对象的指针
   Entity* e = new Entity();
   // 调用PrintName(),将打印"Entity"
   PrintName(e);

   // 创建一个Player对象,名称为"tanke"
   Player* p = new Player("tanke");
   // 调用PrintName(),将打印"tanke"
   PrintName(p);

   // 修改Player对象的名称为"wangjie"
   p->SetName("wangjie");

   // 将Player指针赋值给基类指针,体现了多态性
   Entity* e1 = p;
   // 调用PrintName(),将打印"wangjie"
   PrintName(p);

   // 等待用户输入,保持窗口打开
   std::cin.get();

   return 0;
}

解释:

  • 代码通过 Entity 基类定义了一个虚函数 GetName(),为派生类提供了一个默认实现。
  • Player 类继承自 Entity,并使用 override 关键字重写了 GetName() 方法。
  • PrintName() 函数接受一个 Entity 指针,展示了 C++ 多态性的关键特征 - 可以通过基类指针调用派生类的具体实现。

1747056796676Ue1zbj8hco94t9xuRBRcd0jWnXg.png

思考

  1. 虚函数应用于继承层次结构中的多态性,即通过基类指针或引用调用派生类对象的成员函数。
  2. 可以将虚函数作为接口定义,让不同的派生类实现自己的版本,以满足各自的需求。
  3. 避免在构造函数和析构函数中调用虚函数,因为此时对象还未完全构造或已经被销毁。
  4. 虚函数的声明应该在公共部分(例如基类),而不是在私有部分(例如派生类)中声明。
  5. 将虚函数的实现定义为 inline 可以提高程序的执行效率。
  6. 在使用纯虚函数时,需要给出其具体实现。可以在派生类中实现,也可以在基类中实现。
  7. 避免过度使用虚函数,因为虚函数会增加程序的开销。在没有必要的情况下,可以使用普通成员函数代替虚函数。
Loading comments...