65.9K
CodeProject 正在变化。 阅读更多。
Home

不可继承类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.08/5 (29投票s)

2003 年 7 月 1 日

CPOL

6分钟阅读

viewsIcon

128480

一个不能被继承的类

引言

我们为什么不应该从 STL 类继承,这是一个常见的问题。这个问题的答案很简单。三大规则 [CLI95]。换句话说,如果你想让一个类成为基类,那么它应该有一个虚析构函数。然而,STL 中没有一个类定义了虚析构函数。即使你尝试从 STL 类继承,编译器也不会报错,但程序中存在资源泄露的高风险。如果编译器能在编译期间捕获尽可能多的错误,就能让程序员的生活更轻松。因此,将一个类设计成无法被继承(即,一个 Final 类)是一个好主意。

背景

有一天,我和一位主要从事 Java 工作的朋友讨论时,比较了 Java 和 C++ 的面向对象特性。他向我介绍了一个 Java 中称为 Final 类的有趣概念,即一个类不能再被进一步继承。当时对我来说这是一个全新的概念,我非常喜欢它,因为我经常希望能在 C++ 中创建一个别人无法继承的类。他让我用 C++ 实现这一点,于是我开始思考并进行了一些实验。在进一步讨论如何实现之前,讨论一下我们为什么需要它是有用的。

我们为什么可能需要 Final 类?

一个可能的场景,我想到的“Final 类”可能有用的是一个没有虚析构函数的类。假设你创建了一个没有虚析构函数的类,它可能包含一些动态分配的对象。现在,如果有人从中继承一个类,并动态创建派生类对象,那么就会明显地产生资源泄露,尽管编译器不会对此提出警告。

class B { 
. . .
};

class D : public B {
. . .
};

B* pB = new D;
delete pB; // resource leak

实现 Final 类的细节

为了使一个类成为 final,一个可能想到的第一个解决方案是将其构造函数设为私有。然后使用静态函数来创建对象。但这种方法有一个问题,即类中可能有多个构造函数,你必须将它们全部设为私有。仍然有可能有人创建另一个公共构造函数。解决这个问题的快速方法是将析构函数设为私有而不是构造函数,因为类中只能有一个析构函数。

class FinalClass
{
private:
  ~FinalClass() { }
public:
  static FinalClass* CreateInstance()
  {
    return new FinalClass;
  }
. . .
};
这种方法的一个问题是对象不能在栈上创建;对象必须在堆上创建。现在,由该类的用户负责销毁该对象。在这种情况下,如果用户忘记在不再需要对象时从堆中删除该类的对象,则存在资源泄露的风险。

让我们来看看另一种实现 Final 类的方法,同时仍然允许该类的客户端在栈上创建对象,而不仅仅是在堆上。我们都知道,当我们使一个类成为另一个类的友元时,它可以创建该类的对象,即使其析构函数是私有的。因此,我们可以创建一个临时类并将其构造函数设为私有。然后继承一个类,并使派生类成为其基类的友元,因为如果我们不这样做,我们就无法继承一个构造函数或析构函数为私有的类的类。

class Temp
{
private:
  ~Temp() { };
  friend class FinalClass;
};

class FinalClass : public Temp
{
. . .
};
但是当你从 FinalClass 继承任何类时,这都可以正常工作。假设你从 FinalClass 派生了一个派生类。现在当你创建派生类对象时,它的构造函数会被调用,它会调用 FinalClass 的构造函数,而 FinalClass 的构造函数会调用 Temp 的构造函数,因为 FinalClassTemp 的友元。所以一切都很正常,我们的 Final 类仍然可以被继承,换句话说,它目前不是 final 的。

在更详细地讨论 Final 类之前,让我们讨论著名的多重继承问题,或者说是菱形问题。在这种情况下,你通过将基类设为虚基类并虚继承中间类来解决该问题。但是虚基类的目的是什么?事实上,大多数派生类(创建了其对象的类)的对象的构造函数会直接调用虚基类的构造函数。所以我们解决了菱形问题,因为现在最派生的类(在菱形问题中是底部)直接调用最基类(在菱形问题中是顶部)的构造函数,现在顶部类只有一个副本。

我们可以在这里应用相同的技术。我们让 FinalClass 虚继承自 Temp 类,并将 Temp 设为虚基类。现在,每当有人尝试从 FinalClass 继承类并创建其对象时,其构造函数会尝试调用 Temp 的构造函数。但 Temp 的构造函数是私有的,所以编译器会报错,并在编译期间给出错误,因为你的派生类不是 Temp 的友元。请记住,友元关系不会在派生类中继承。毕竟,你父母的朋友不是你的朋友,你的朋友也不是你孩子的朋。但当你创建 FinalClass 对象时,它的构造函数可以调用 Temp 的构造函数,因为 FinalClassTemp 的友元。所以这是我们 Final 类的最终版本,它也可以在栈上创建,而不仅仅是在堆上。

class Temp
{
private:
  ~Temp() { };
  friend class FinalClass;
};

class FinalClass : virtual public Temp
{
. . .
};
但是,如果你需要创建多个 Final 类,情况又如何呢?你必须编写两倍的类,即,如果你需要 N 个 Final 类,那么你必须编写 2N 个类,因为在这个方法中,我们有一个带有私有构造函数的临时类和你的 Final 类。所以你必须一遍又一遍地编写大量相同的代码。为什么不尝试利用模板,并尝试将一个类设计成这样,如果你从该类继承任何类,你的类将自动成为 Final 类。这种类的代码非常简单。

template <typename T>
class MakeFinal
{
private:
  ~MakeFinal() { };
  friend T;
};
不要忘记虚继承你的 Final 类,以确保它成为虚基类。并将你的 Final 类名作为模板参数传递,这样你的类就成了它的友元,并可以调用它的构造函数。

class FinalClass : virtual public MakeFinal<FinalClass>
{
};

结论

当然,任何事物都不是没有代价的。你必须支付额外的字节来存储关于虚基类的信息。大多数编译器实现使用一个指针来存储虚继承情况下虚基类的信息。因此,对象的大小不仅仅是所有成员变量存储分配的总和,而比你预期的要大。请记住,这个指针,即指向虚基类的指针,与虚函数情况下引入的虚指针不同 [LIP96]。这完全是一个实现问题;C++ 标准没有关于调用虚函数、虚指针和虚表机制的规定 [ISO98]。

只添加一个小类并更改几行代码,就可以使你的程序更可靠,并减少如果你不使用 Final 类而可能产生的资源泄露问题。

参考

  1. [CLI95] 1995 C++ FAQs Marshall P Cline, Greg A Lomow
  2. [ISO98] 1998 International Standard Programming Language C++
  3. [LIP96] 1996 Inside the C++ Object Model Stanley B Lippman
© . All rights reserved.