什么是虚函数

虚函数是 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:
// 隐藏基类的show()方法
void show() {
std::cout << "Derived::show()" << std::endl;
}
};

int main() {
Base base;
Derived derived;
Base* ptr = &derived;

base.show(); // 输出: Base::show()
derived.show(); // 输出: Derived::show()
ptr->show(); // 输出: Base::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(); // 输出: Base::show()
derived.show(); // 输出: Derived::show()
ptr->show(); // 输出: Derived::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:
// 必须实现draw()方法
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};

class Rectangle : public Shape {
public:
// 必须实现draw()方法
void draw() override {
std::cout << "Drawing a rectangle" << std::endl;
}
};

int main() {
// Shape shape; // 错误:抽象类不能实例化

Shape* circle = new Circle();
Shape* rectangle = new Rectangle();

circle->draw(); // 输出: Drawing a circle
rectangle->draw(); // 输出: Drawing a rectangle

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 {
// 必须实现draw()方法
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}

class Rectangle extends Shape {
// 必须实现draw()方法
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}

public class Main {
public static void main(String[] args) {
// Shape shape = new Shape(); // 错误:抽象类不能实例化

Shape circle = new Circle();
Shape rectangle = new Rectangle();

circle.draw(); // 输出: Drawing a circle
rectangle.draw(); // 输出: Drawing a rectangle
}
}

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):

  1. 虚函数表(vtable):每个包含虚函数的类都有一个虚函数表,存储该类所有虚函数的地址
  2. 虚指针(vptr):每个对象都有一个虚指针,指向所属类的虚函数表
  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>

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;
}

// 重写print()方法
void print() override {
std::cout << "Derived::print()" << std::endl;
}
};

int main() {
Base* base = new Base();
Base* derived = new Derived();

base->show(); // 调用Base::show()
base->print(); // 调用Base::print()

derived->show(); // 调用Derived::show()
derived->print();// 调用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; // 正确调用Derived::~Derived()和Base::~Base()

return 0;
}

总结

  1. 虚函数

    • 允许在派生类中重写基类的方法
    • 实现动态绑定(运行时多态)
    • 基类可以提供默认实现
    • 包含虚函数的类可以实例化
  2. 纯虚函数

    • 是一种特殊的虚函数,不提供实现
    • 强制派生类必须实现
    • 包含纯虚函数的类是抽象类,不能实例化
    • 用于定义接口
  3. 非虚函数重写

    • 实际上是隐藏基类的方法
    • 实现静态绑定(编译时绑定)
    • 通过基类指针或引用调用时,调用基类的实现
  4. 与 Java 抽象函数的比较

    • C++ 的纯虚函数类似于 Java 的抽象函数
    • Java 使用 abstract 关键字定义抽象函数
    • Java 有专门的 interface 关键字定义接口,而 C++ 使用抽象类定义接口

虚函数和纯虚函数是 C++ 实现面向对象编程中多态的重要机制,它们使得代码更加灵活、可扩展,同时也促进了良好的代码设计。理解它们的工作原理和使用场景,对于编写高质量的 C++ 代码非常重要。