Effective C++ 条款7 virtual析构函数

条款07 : 为多态基类声明virtual析构函数

说到多态, 我们应当非常了解其运行时绑定的机制, 简单来说就是我们可以在任何时候根据用户的需求将基类绑定为不同的派生类, 用相同的操作实现不同的效果, 这其中virtual函数起到了至关重要的作用, 每一个含有虚函数的类都会维护一个虚表, 以此实现基类到派生类的动态绑定.

这里书中提到了一个重要又经常发生的问题 : 如果drived class(派生类)对象经由一个base class(基类)指针被删除, 而该base class带着一个non-virtual析构函数, 那么结果是未定义的.

这里的结果未定义, 一般情况下是只会调用base class自己的析构函数, 销毁的是该对象的base class部分, 而drived class部分却没有被销毁, 就造成了诡异的”局部销毁”现象.

而解决这个问题的方法就是 : 给 base class 一个 virtual析构函数.

先回忆一下有关派生类析构函数的知识 : 派生类的析构函数会默认先调用上一层的析构函数. 也就是说是从当前派生类的析构函数开始, 递归式调用上一层的析构函数, 直到到达最深层的基类.

再思考上面的话, 当base class指针动态绑定drived class对象时, 如果我们想删除这个对象时, 正确的结果应该是调用动态绑定的drived class的析构函数, 这样才能正确地全部销毁, 而想这样调用就只能依赖于虚函数来实现, 也就是我们需要把base class的析构函数设置为virtual, 删除时就会根据虚表找到当前动态类型绑定的析构函数.

以下是书中的给出的样例 :

1
2
3
4
5
6
7
8
9
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // 现在, 行为正确

书中告诉我们 :

  • virtual 函数的目的是允许 derived class 的实现得以客制化.
  • 任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数.

当然, 无端地将所有的 classes 的析构函数声明为 virtual, 就像从未声明他们为 virtual 一样, 都是错误的.

如果 class 不含 virtual 函数, 通常表示它并不意图被用作一个 base class, 也就完全不需要声明virtual析构函数.

我们知道使用虚函数是要带来额外的花销的, 包括维护虚表, 虚指针表等一系列繁杂的动作, 会带来时间成本和空间成本, 而且书中还提到由于对象大小的增加会影响其可移植性.

所以请记住一个心得 : 只有当 class 内含至少一个 virtual 函数, 才为他声明 virtual析构函数.


请不要企图继承我们神圣的STL容器, 正因为考虑到上面虚函数有关时间, 空间, 可移植性的问题, STL容器并没有考虑被继承的情况, 它们的析构函数都是 non-virtual 的!


这里书中提到了一个构建抽象类的小窍门 :

我们一般不希望抽象类被实体化, 它只提供一些接口 :

  1. 被声明为纯虚函数的接口所有派生类必须重写.
  2. 被声明为虚函数的接口提供默认行为同时也允许派生类重写.
  3. 被声明为普通函数的接口可以被所有派生类继承, 使用.

我们在设计一个抽象类时, 也许并不希望有什么类是必须重写的, 就是不希望有纯虚函数, 只需要提供虚函数和普通函数即可, 然而一个类想要不被实体化, 必须存在一个纯虚函数, 那么哪里找一个纯虚函数呢?

如果你拿不定主意, 选析构函数就好了! 毕竟抽象类一定有多态的需求, 只要有多态的需求, 析构函数就必须是 virtual 的, 已经是虚函数了, 变成纯虚函数也没什么问题, 而且当我们的派生类没有新增动态资源时, 就算我们不手动重写析构函数也没关系! 因为编译器会自动帮我们生成! 这样既满足了必须有一个纯虚函数的要求, 又不需要我们顾及纯虚函数必须重写的问题, 多么完美 !

不过有一个小细节必须要注意, 所有派生类析构函数最后都会调用到最底层基类的析构函数, 所以我们需要对纯虚析构函数进行定义!

纯虚析构函数真的能定义吗? 答案是能的, 虽然大多数情况下纯虚函数都不用定义, 但是真要定义还是可以的.

以下是书中的示例 :

1
2
3
4
5
6
7
8
9
class AWOV {
public:
virtual ~AWOV() = 0; // 纯虚函数
// ...
};
AWOV::~AWOV() // 纯虚函数的定义
{
// delete ...
}

再次重申, 本章的主要观点 给 base class 一个 virtual析构函数 只适用于多态用途, 这种base class的设计目的就是为了用来通过 base class 接口处理 drived class 对象.

我们也应当知道很多 class 的设计就不是为了作为 base class 来使用, 也并非所有的 base class 的设计目的是为了多态用途, 就像我们上一个条款的Uncopyable类, 它被作为基类就不是为了多态, 而是为了赋予派生类不可拷贝的属性, 就不需要使用基类接口, 使用也只是使用派生类, 使用 virtual析构函数就没必要了.


请记住 :

  • 请为多态性质的base class设置 virtual析构函数, 反之请不要.
  • 不要试图继承STL容器.
  • 想设计抽象类又不知道纯虚函数选谁时, 选析构函数当纯虚函数.

by 天目中云


Effective C++ 条款7 virtual析构函数
http://example.com/2024/11/30/[Effective C++]条款7 virtual析构函数/
作者
天目中云
发布于
2024年11月30日
许可协议