C++的虚函数和纯虚函数
在C++中,虚函数(Virtual Function)和纯虚函数(Pure Virtual Function)是实现面向对象多态性(Polymorphism)的核心机制。特别是“动态多态”(运行时多态),完全依赖于这两个概念。
下面我将为你详细、通俗地解析这两个概念及其区别、底层原理和使用注意事项。
一、 虚函数(Virtual Function)
1. 什么是虚函数?
虚函数是在基类中使用 virtual 关键字声明的成员函数。它的主要作用是允许在派生类中重写(Override)该函数,从而实现通过基类指针或引用调用派生类的同名函数。
2. 核心特性:动态绑定
普通的函数调用在编译时就已经决定了(静态绑定)。而虚函数是在运行时根据指针(或引用)实际指向的对象类型来决定调用哪个类的方法(动态绑定)。
3. 代码示例:
#include <iostream>
class Animal {
public:
// 声明虚函数
virtual void speak() {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Dog : public Animal {
public:
// 重写基类的虚函数
void speak() override { // override是C++11引入的关键字,用于显式标明重写,防止拼写错误
std::cout << "Dog barks: Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Cat meows: Meow!" << std::endl;
}
};
int main() {
Animal* ptr1 = new Dog();
Animal* ptr2 = new Cat();
Animal* ptr3 = new Animal();
ptr1->speak(); // 输出: Dog barks: Woof! (根据实际对象调用Dog的speak)
ptr2->speak(); // 输出: Cat meows: Meow! (根据实际对象调用Cat的speak)
ptr3->speak(); // 输出: Animal makes a sound.
delete ptr1; delete ptr2; delete ptr3;
return 0;
}
二、 纯虚函数(Pure Virtual Function)
1. 什么是纯虚函数?
纯虚函数是一种特殊的虚函数,它在基类中没有具体实现(虽然语法上允许有,但极少这么用),并要求任何非抽象的派生类必须提供该函数的实现。
声明方式是在虚函数声明的末尾加上 = 0。
2. 核心特性:抽象类(Abstract Class)
- 包含至少一个纯虚函数的类被称为“抽象类”。
- 抽象类不能被实例化(不能直接
new一个抽象类对象)。 - 抽象类的主要作用是定义接口(规范派生类必须具备哪些功能)。
- 如果派生类没有重写基类的纯虚函数,那么该派生类也会变成抽象类,同样无法实例化。
3. 代码示例:
#include <iostream>
// Shape是一个抽象类,因为它包含纯虚函数
class Shape {
public:
// 纯虚函数:只定义接口,不提供具体实现
virtual double getArea() = 0;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 必须实现纯虚函数,否则Circle也是抽象类
double getArea() override {
return 3.14159 * radius * radius;
}
};
int main() {
// Shape s; // 错误!不能实例化抽象类
Shape* myShape = new Circle(5.0);
std::cout << "Area: " << myShape->getArea() << std::endl; // 输出: Area: 78.5397
delete myShape;
return 0;
}
三、 虚函数 vs 纯虚函数 核心区别总结
| 特性 | 虚函数 (virtual) |
纯虚函数 (virtual ... = 0) |
|---|---|---|
| 声明语法 | virtual void func(); |
virtual void func() = 0; |
| 是否有实现 | 必须有具体的函数实现。 | 可以没有实现(只定义接口)。 |
| 派生类重写 | 可选的。派生类可以重写,也可以直接继承基类的实现。 | 强制的。派生类必须重写,否则派生类也无法实例化。 |
| 对类实例化的影响 | 包含虚函数的类可以被实例化。 | 包含纯虚函数的类是抽象类,绝对不能被实例化。 |
| 设计目的 | 提供默认功能,允许子类自定义修改。 | 定义统一接口规范,强制子类去实现。 |
四、 底层原理解析:虚函数表(vtable)与虚表指针(vptr)
面试中常问:C++是怎么实现动态绑定的?
答案是:虚函数表(Virtual Table, vtable) 和 虚表指针(Virtual Pointer, vptr)。
- 虚函数表(vtable):
当一个类包含虚函数时,编译器会为该类生成一个虚函数表。这是一个隐藏的数组,里面存储了该类所有虚函数的函数指针。 - 虚表指针(vptr):
编译器还会为该类的每一个对象实例隐式地添加一个指针(通常在对象的内存起始位置),这个指针指向该对象所属类的虚函数表。 - 调用过程:
当通过基类指针调用虚函数时,程序在运行时:- 通过指针找到实际对象。
- 通过对象内部的
vptr找到该类的vtable。 - 在
vtable中查找对应的函数指针,然后调用真正的函数。
(这也是为什么含有虚函数的对象体积通常会大一个指针的大小,比如64位系统下大8字节)。
五、 极度重要的注意事项:虚析构函数
在使用多态时,基类的析构函数必须声明为虚函数!
如果你通过基类指针去删除一个派生类对象,且基类的析构函数不是虚函数,那么只会调用基类的析构函数,派生类的析构函数不会被执行,从而导致严重的内存泄漏。
class Base {
public:
Base() { std::cout << "Base Constructor" << std::endl; }
// 必须加上 virtual
virtual ~Base() { std::cout << "Base Destructor" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived Constructor" << std::endl; }
~Derived() override { std::cout << "Derived Destructor" << std::endl; }
};
int main() {
Base* ptr = new Derived();
delete ptr;
// 如果基类析构是虚函数,输出:Derived Destructor -> Base Destructor (正确)
// 如果基类析构不是虚函数,只会输出:Base Destructor (内存泄漏!)
return 0;
}
补充知识:构造函数可以是虚函数吗?
绝对不能。
因为虚函数的调用依赖于对象的 vptr(虚表指针),而 vptr 是在构造函数执行期间才被初始化的。如果构造函数是虚函数,此时 vptr 还没有构建完毕,根本无法实现多态。