virtual
关键字在C++中主要用于实现多态性,特别是动态多态性。它可以与成员函数、析构函数、基类等进行组合使用。以下是virtual
关键字的不同用法及其意义:
-
虚函数 (
virtual
with member functions):class Base { virtual void func(); };
使用
virtual
关键字声明的成员函数被称为虚函数。在派生类中,虚函数可以被重写。这允许我们通过基类的指针或引用调用派生类的函数实现,实现动态绑定。- 注意:多个虚函数只占用一个虚指针(4字节),甚至继承的父类中有虚函数的话,子类中的虚函数不占用空间。
-
虚析构函数 (
virtual
with destructor):class Base { virtual ~Base(); };
如果基类的析构函数是虚的,那么通过基类指针删除派生类对象时,派生类的析构函数也会被正确地调用。这避免了资源泄漏和未定义的行为。
-
纯虚函数 (
virtual
with= 0
):class AbstractBase { virtual void pureVirtualFunc() = 0; };
纯虚函数是一个在基类中没有定义实现的虚函数。包含纯虚函数的类称为抽象类,不能直接实例化。派生类必须提供纯虚函数的实现,除非它们也是抽象类。
-
虚基类 (
virtual
with inheritance):class Derived : virtual public Base { };
虚继承用于解决多继承中的菱形问题。当两个或更多的类从同一个基类虚继承时,只有一个基类实例会存在于最终的派生类对象中,从而避免了重复和二义性。
-
虚指针(其不能被拿来使用,它只是实现虚功能的核心)
虚指针(vptr
)是C++的运行时多态机制的核心组件。当一个类有虚函数时,每个对象的内部都包含一个指向虚函数表(通常称为vtable
)的指针。这个指针就是虚指针。
下面是一个简单的例子来说明vptr
和vtable
的工作原理:
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base constructor" << endl; }
virtual void func() { cout << "Base func" << endl; }
virtual ~Base() { cout << "Base destructor" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived constructor" << endl; }
void func() override { cout << "Derived func" << endl; }
~Derived() { cout << "Derived destructor" << endl; }
};
int main() {
Base* obj = new Derived(); // 从输出我们可以看到构造函数的调用顺序
obj->func(); // 这里会调用 Derived 的 func,而不是 Base 的 func
delete obj; // 注意析构函数的调用顺序,这也是由于虚函数
return 0;
}
当创建Derived
对象时:
Base
类的构造函数被调用。Derived
类的构造函数被调用。
在内部,每个Derived
对象都有一个vptr
,它指向Derived
类的vtable
。vtable
是一个函数指针数组,其中存放了该类重写的虚函数的地址。在上述例子中,Derived
的vtable
将包含指向Derived::func
和Derived::~Derived
的指针。
当我们通过基类指针调用虚函数时,程序会:
- 查找对象的
vptr
。 - 使用
vptr
找到对应的vtable
。 - 调用存储在
vtable
中的相应虚函数的地址。
为什么要有virtual
这样的关键字:
-
多态性支持:
virtual
允许我们实现动态多态性,这是面向对象编程的核心概念之一。通过virtual
,我们可以在运行时根据对象的实际类型动态地决定调用哪个函数版本,而不是在编译时静态决定。 -
灵活性和扩展性:
virtual
允许基类为其函数提供默认实现,同时允许派生类提供特定实现。这为设计灵活的和可扩展的类库和API提供了基础。 -
强制派生类实现:通过纯虚函数,基类可以定义接口,而不提供实现,从而强制派生类提供特定的实现。这有助于确保派生类遵循特定的设计约定或模式。
重写(override)
override
是C++11引入的一个关键字,用于明确表示派生类中的函数意图覆盖(重写)基类中的虚函数。
使用override
的好处:
- 编译时检查:如果标记了
override
的函数并没有在基类中找到一个匹配的虚函数进行覆盖,编译器会报错。这为程序员提供了一个强制手段来确保函数确实是覆盖了基类的某个虚函数,而不是不小心定义了一个新函数。 - 提高代码清晰度:当查看代码时,
override
关键字明确告诉读者该函数是重写了基类中的虚函数。
例子:
class Base {
public:
virtual void foo();
};
class Derived : public Base {
public:
void foo() override; // 正确使用:Derived 中的 foo 重写了 Base 中的 foo
// void bar() override; // 错误使用:Base 中没有虚函数 bar,这会导致编译时错误
};
在上述代码中,Derived
类中的foo
函数被标记为override
,这意味着它应该重写Base
类中的一个虚函数。如果没有在Base
类中找到一个可以被Derived::foo
重写的函数,编译器就会报错。
使用override
关键字是个好习惯,因为它可以避免由于函数签名的微小变化导致的潜在错误,并提高代码的可读性。
纯虚函数和虚函数
纯虚函数和虚函数都是为了支持多态,但它们之间有以下主要区别:
-
定义:
- 虚函数:在基类中有一个默认的实现(但也可以没有实现,只有声明),派生类可以重写(override)该函数。
- 纯虚函数:在基类中没有实现,只有函数声明,并且声明后面加上
= 0
来表示这是一个纯虚函数。
-
目的:
- 虚函数:允许派生类选择性地重写基类中的实现。
- 纯虚函数:强制派生类提供一个该函数的实现,确保每个派生类都为这个函数提供自己的版本。
-
抽象类:
- 如果一个类包含至少一个纯虚函数,那么这个类被称为抽象类。你不能直接实例化一个抽象类。但是,一个类可以有虚函数而不是抽象类。
-
使用场景:
- 虚函数:当你希望为函数提供一个默认的实现,但同时允许派生类提供其特定的实现时。
- 纯虚函数:当你希望确保每个派生类都提供自己的函数实现,并且没有合适或没有意义的默认实现时。
例子:
class Base {
public:
virtual void foo() {
// 默认实现
}
virtual void bar() = 0; // 纯虚函数
};
class Derived : public Base {
public:
// 虚函数 foo 的重写是可选的
void foo() override {
// 重写基类中的 foo 函数
}
// 纯虚函数 bar 的重写是必须的
void bar() override {
// 必须为 bar 提供实现
}
};
简而言之,虚函数为派生类提供了一个选择:重写或使用基类版本。纯虚函数则强制派生类提供一个实现。