什么是虚函数
虚函数是 C++ 中实现多态的核心机制,它允许在基类中声明一个函数,然后在派生类中重写(override)这个函数,使得通过基类指针或引用调用该函数时,会根据实际指向的对象类型来调用相应的派生类实现。
虚函数的定义
在 C++ 中,通过在函数声明前添加 virtual 关键字来定义虚函数:
1 2 3 4 5 6 7
| class Base { public: virtual void show() { std::cout << "Base::show()" << std::endl; } };
|
什么是纯虚函数
纯虚函数是一种特殊的虚函数,它在基类中只声明函数签名,不提供实现,而是要求派生类必须提供实现。包含纯虚函数的类被称为抽象类,不能直接实例化。
纯虚函数的定义
在 C++ 中,通过在虚函数声明的末尾添加 = 0 来定义纯虚函数:
1 2 3 4 5
| class Base { public: virtual void show() = 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 25 26 27 28 29
| #include <iostream>
class Base { public: void show() { std::cout << "Base::show()" << std::endl; } };
class Derived : public Base { public: void show() { std::cout << "Derived::show()" << std::endl; } };
int main() { Base base; Derived derived; Base* ptr = &derived; base.show(); derived.show(); ptr->show(); return 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 25 26 27 28 29
| #include <iostream>
class Base { public: virtual void show() { std::cout << "Base::show()" << std::endl; } };
class Derived : public Base { public: void show() override { std::cout << "Derived::show()" << std::endl; } };
int main() { Base base; Derived derived; Base* ptr = &derived; base.show(); derived.show(); ptr->show(); return 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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <iostream>
class Shape { public: virtual void draw() = 0; virtual ~Shape() {} };
class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle" << std::endl; } };
class Rectangle : public Shape { public: void draw() override { std::cout << "Drawing a rectangle" << std::endl; } };
int main() { Shape* circle = new Circle(); Shape* rectangle = new Rectangle(); circle->draw(); rectangle->draw(); delete circle; delete rectangle; return 0; }
|
C++ 虚函数、纯虚函数与 Java 抽象函数的比较
Java 中的抽象函数
在 Java 中,抽象函数是通过 abstract 关键字定义的,它与 C++ 的纯虚函数类似,只声明函数签名,不提供实现。包含抽象函数的类必须声明为抽象类,不能直接实例化。
区别与联系
| 特性 |
C++ 虚函数 |
C++ 纯虚函数 |
Java 抽象函数 |
| 关键字 |
virtual |
virtual + = 0 |
abstract |
| 实现 |
基类可以提供实现 |
基类不提供实现 |
基类不提供实现 |
| 类类型 |
普通类 |
抽象类 |
抽象类 |
| 继承要求 |
派生类可选择重写 |
派生类必须重写 |
派生类必须重写 |
| 多态实现 |
动态绑定 |
动态绑定 |
动态绑定 |
| 接口定义 |
不适合定义接口 |
适合定义接口 |
适合定义接口 |
Java 抽象函数示例
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
| abstract class Shape { public abstract void draw(); }
class Circle extends Shape { @Override public void draw() { System.out.println("Drawing a circle"); } }
class Rectangle extends Shape { @Override public void draw() { System.out.println("Drawing a rectangle"); } }
public class Main { public static void main(String[] args) { Shape circle = new Circle(); Shape rectangle = new Rectangle(); circle.draw(); rectangle.draw(); } }
|
C++ 与 Java 的接口对比
- C++:使用包含纯虚函数的抽象类来定义接口
- Java:使用
interface 关键字定义接口,所有方法默认是抽象的
Java 接口示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| interface Shape { void draw(); }
class Circle implements Shape { @Override public void draw() { System.out.println("Drawing a circle"); } }
class Rectangle implements Shape { @Override public void draw() { System.out.println("Drawing a rectangle"); } }
|
虚函数的工作原理
虚函数的实现依赖于虚函数表(vtable)和虚指针(vptr):
- 虚函数表(vtable):每个包含虚函数的类都有一个虚函数表,存储该类所有虚函数的地址
- 虚指针(vptr):每个对象都有一个虚指针,指向所属类的虚函数表
- 动态绑定:当通过基类指针或引用调用虚函数时,会通过虚指针找到相应的虚函数表,然后调用对应的函数
虚函数表示例
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>
class Base { public: virtual void show() { std::cout << "Base::show()" << std::endl; } virtual void print() { std::cout << "Base::print()" << std::endl; } };
class Derived : public Base { public: void show() override { std::cout << "Derived::show()" << std::endl; } void print() override { std::cout << "Derived::print()" << std::endl; } };
int main() { Base* base = new Base(); Base* derived = new Derived(); base->show(); base->print(); derived->show(); derived->print(); delete base; delete derived; return 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 25 26 27 28 29 30 31 32
| #include <iostream>
class Base { public: virtual ~Base() { std::cout << "Base::~Base()" << std::endl; } };
class Derived : public Base { private: int* data;
public: Derived() { data = new int[10]; std::cout << "Derived::Derived()" << std::endl; } ~Derived() override { delete[] data; std::cout << "Derived::~Derived()" << std::endl; } };
int main() { Base* base = new Derived(); delete base; return 0; }
|
总结
虚函数:
- 允许在派生类中重写基类的方法
- 实现动态绑定(运行时多态)
- 基类可以提供默认实现
- 包含虚函数的类可以实例化
纯虚函数:
- 是一种特殊的虚函数,不提供实现
- 强制派生类必须实现
- 包含纯虚函数的类是抽象类,不能实例化
- 用于定义接口
非虚函数重写:
- 实际上是隐藏基类的方法
- 实现静态绑定(编译时绑定)
- 通过基类指针或引用调用时,调用基类的实现
与 Java 抽象函数的比较:
- C++ 的纯虚函数类似于 Java 的抽象函数
- Java 使用
abstract 关键字定义抽象函数
- Java 有专门的
interface 关键字定义接口,而 C++ 使用抽象类定义接口
虚函数和纯虚函数是 C++ 实现面向对象编程中多态的重要机制,它们使得代码更加灵活、可扩展,同时也促进了良好的代码设计。理解它们的工作原理和使用场景,对于编写高质量的 C++ 代码非常重要。