C++|虚函数

C++ 中的虚函数是实现 运行时多态(Runtime Polymorphism) 的关键机制,它允许你使用基类的指针或引用来调用派生类的函数实现。下面我用一些例子来说明它的使用方法、场景和注意事项。

🧠 1. 虚函数基础用法

虚函数通过在基类中使用 virtual关键字声明,在派生类中使用 override关键字(推荐)进行重写。

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
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() const {
        cout << "Some generic animal sound" << endl;
    }
    virtual ~Animal() {} // 虚析构函数确保正确释放资源
};

class Dog : public Animal {
public:
    void makeSound() const override {
        cout << "Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() const override {
        cout << "Meow!" << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound(); // 输出: Woof!
    animal2->makeSound(); // 输出: Meow!

    delete animal1;
    delete animal2;
    return 0;
}

输出

1
2
Woof!
Meow!

在这个例子中,尽管 animal1animal2都是 Animal*类型,但根据它们指向的实际对象类型调用相应的 makeSound版本。这就是运行时多态。

🎨 2. 实用场景:图形绘制

虚函数在定义接口和不同实现时非常有用,例如在图形库中:

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
#include <iostream>
#include <vector>
using namespace std;

class Shape {
public:
    virtual void draw() const = 0; // 纯虚函数,使Shape成为抽象类
    virtual ~Shape() = default;    // 虚析构函数
};

class Circle : public Shape {
public:
    void draw() const override {
        cout << "Drawing a circle." << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        cout << "Drawing a rectangle." << endl;
    }
};

int main() {
    vector<Shape*> shapes;
    shapes.push_back(new Circle());
    shapes.push_back(new Rectangle());

    for (const auto& shape : shapes) {
        shape->draw(); // 根据具体对象类型调用正确的draw函数
    }

    // 清理资源
    for (auto shape : shapes) delete shape;
    return 0;
}

输出

1
2
Drawing a circle.
Drawing a rectangle.

这里 Shape是一个抽象基类,它定义了接口,派生类提供具体实现。使用基类指针容器可以统一管理不同类型的派生类对象。

⚙️ 3. 实用场景:游戏开发

在游戏开发中,虚函数常用于实现不同游戏对象的更新逻辑:

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
#include <iostream>
#include <vector>
using namespace std;

class GameObject {
public:
    virtual void update() = 0; // 纯虚函数
    virtual ~GameObject() {}
};

class Player : public GameObject {
public:
    void update() override {
        cout << "Player is moving." << endl;
    }
};

class Enemy : public GameObject {
public:
    void update() override {
        cout << "Enemy is attacking." << endl;
    }
};

void updateAllObjects(vector<GameObject*>& objects) {
    for (auto obj : objects) {
        obj->update();
    }
}

int main() {
    vector<GameObject*> gameObjects;
    gameObjects.push_back(new Player());
    gameObjects.push_back(new Enemy());

    updateAllObjects(gameObjects);

    for (auto obj : gameObjects) delete obj;
    return 0;
}

输出

1
2
Player is moving.
Enemy is attacking.

通过统一的接口管理不同类型的对象,使代码更易于扩展和维护。

🔍 4. 进阶特性:多级继承中的虚函数

虚函数的特性会在继承链中保持,即使中间派生类没有重写虚函数。

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
#include <iostream>
using namespace std;

class Grandpa {
public:
    virtual void doSomething() {
        cout << "Grandpa's function" << endl;
    }
};

class Father : public Grandpa {
    // 不重写虚函数,隐式继承Grandpa的虚函数
};

class Grandson : public Father {
public:
    void doSomething() override {
        cout << "Grandson's implementation" << endl;
    }
};

int main() {
    Grandpa* ptr = new Grandson();
    ptr->doSomething(); // 输出: Grandson's implementation
    delete ptr;
    return 0;
}

输出

1
Grandson's implementation

这表明虚函数的动态绑定特性在多级继承中依然有效。

⚠️ 5. 重要注意事项

  • 虚析构函数:如果一个类可能被继承,并且会通过基类指针删除派生类对象,务必将基类的析构函数声明为虚函数。这样可以确保正确调用派生类的析构函数,避免资源泄漏。

    1
    2
    3
    4
    5
    6
    7
    8
    class Base {
    public:
        virtual ~Base() { /* 清理基类资源 */ }
    };
    class Derived : public Base {
    public:
        ~Derived() override { /* 清理派生类资源 */ }
    };
    
  • 纯虚函数与抽象类:在基类中声明纯虚函数(如 virtual void func() = 0;)可以使该类成为抽象类,不能实例化。派生类必须重写所有纯虚函数才能被实例化。这用于定义强制接口。

  • 性能考量:虚函数通过虚函数表(vtable)实现,调用时会有一次间接寻址的开销,通常非常小。在绝大多数应用场景中,这种开销相对于其带来的设计好处可以忽略不计。

  • 构造函数不能是虚函数:因为对象在构造时才能确定其确切类型,而虚函数机制在对象完全构建之前无法正常工作。

💎 总结

虚函数是C++实现运行时多态的核心机制,允许通过基类接口调用派生类特定实现。使用时要注意:

  • 使用 virtual关键字声明。
  • 派生类推荐使用 override关键字明确重写。
  • 基类析构函数通常应为虚函数
  • 纯虚函数用于定义接口规范。

希望这些例子能帮助你更好地理解和使用C++虚函数。