一个班级的VTT是什么?

39

最近我遇到了一个对我来说是新的 C++ 链接器错误。

libfoo.so: undefined reference to `VTT for Foo'
libfoo.so: undefined reference to `vtable for Foo'

我意识到了错误并解决了问题,但仍有一个困扰我的问题:VTT 究竟是什么?

另外: 对于那些感兴趣的人,当你忘记定义一个类中声明的第一个虚函数时,就会出现该问题。虚表将进入类的第一个虚函数的编译单元。如果你忘记定义该函数,你将得到一个链接器错误,它无法找到虚表,而不是更为开发者友好的“找不到函数”错误。


6
@AlokSave,这个问题不是关于重复的问题,而是关于缩写的问题,可能本身就很有用。 - unkulunkulu
对我来说也是同样的问题! - John
2个回答

55

这个页面"Notes on Multiple Inheritance in GCC C++ Compiler v4.0.1"现在已经下线,http://web.archive.org也没有存档。因此,我找到了一份文本的副本,位于tinydrblog,该文本已经被网络档案馆存档。

原始笔记全文以"博士编程语言研讨会:GCC内部"(2005年秋季)为一部分在线发布。毕业生摩根·德特斯(Morgan Deters)在华盛顿大学圣路易斯分校计算机科学系中的“分布式对象计算实验室”中发表了这些内容。
他的(存档的)个人主页

THIS IS THE TEXT by Morgan Deters and NOT CC-licensed.

Morgan Deters网页:

PART1:

基础知识:单继承

正如我们在课堂上讨论的那样,单继承会导致一个对象布局,在派生类数据之前放置了基类数据。因此,如果类AB定义如下:

class A {
public:
  int a;

};

class B : public A {
public:
  int b;
};

如果类型为B的对象如下排列(其中“b”是指向这种对象的指针):

b --> +-----------+
      |     a     |
      +-----------+
      |     b     |
      +-----------+

如果您有虚方法:

class A {
public:
  int a;
  virtual void v();
};

class B : public A {
public:
  int b;
};

那么您也将拥有一个虚函数表指针:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
b --> +----------+         | ptr to typeinfo for B |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+
      |     b    |
      +----------+

也就是说,top_offset和typeinfo指针位于vtable指针所指向的位置之上。

简单的多重继承

现在考虑多重继承:

class A {
public:
  int a;
  virtual void v();
};

class B {
public:
  int b;
  virtual void w();
};

class C : public A, public B {
public:
  int c;
};

在这种情况下,类型为C的对象被布局如下:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
c --> +----------+         | ptr to typeinfo for C |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+         |    -8 (top_offset)    |
      |  vtable  |---+     +-----------------------+
      +----------+   |     | ptr to typeinfo for C |
      |     b    |   +---> +-----------------------+
      +----------+         |         B::w()        |
      |     c    |         +-----------------------+
      +----------+

为什么一个类会有两个vtable呢?考虑类型替换。如果我有一个指向C的指针,我可以将它传递给期望指向A或B的指针的函数。如果一个函数期望一个指向A的指针,而我想把变量c(类型为指针到C)的值传递给它,那就没问题了。调用A::v()可以通过(第一个)vtable进行,被调用的函数可以通过我传递的指针以与任何指向A的指针相同的方式访问成员a。

然而,如果我将指针变量c的值传递给期望指向B的指针的函数,我们还需要一个B类型的子对象来引用它。这就是为什么我们有第二个vtable指针的原因。我们可以将指针值(c + 8字节)传递给期望指向B的指针的函数,然后一切都设置好了:它可以通过(第二个)vtable指针调用B::w(),并通过我们传递的指针以与任何指向B的指针相同的方式访问成员b。

请注意,这种“指针修正”也需要对被调用的方法进行。在这种情况下,类C继承了B的w()。当通过指向C的指针调用w()时,指针(在w()内部成为this指针)需要进行调整。这通常被称为this指针调整。

在某些情况下,编译器将生成一个thunk来修正地址。考虑与上面相同的代码,但这次C覆盖了B的成员函数w()

class A {
public:
  int a;
  virtual void v();
};

class B {
public:
  int b;
  virtual void w();
};

class C : public A, public B {
public:
  int c;
  void w();
};

C对象的布局和虚表现在如下所示:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
c --> +----------+         | ptr to typeinfo for C |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+         |         C::w()        |
      |  vtable  |---+     +-----------------------+
      +----------+   |     |    -8 (top_offset)    |
      |     b    |   |     +-----------------------+
      +----------+   |     | ptr to typeinfo for C |
      |     c    |   +---> +-----------------------+
      +----------+         |    thunk to C::w()    |
                           +-----------------------+

现在,当在B指针上调用C实例的w()函数时,将会调用thunk。thunk是做什么的呢?让我们通过gdb来反汇编它:

0x0804860c <_ZThn8_N1C1wEv+0>:  addl   $0xfffffff8,0x4(%esp)
0x08048611 <_ZThn8_N1C1wEv+5>:  jmp    0x804853c <_ZN1C1wEv>

因此,它仅调整了this指针并跳转到C::w()。一切都很好。

但是,上述是否意味着B的vtable始终指向C::w()?我的意思是,如果我们有一个指向B的指针,它是合法的B(而不是C),我们不想调用thunk,对吗?

对的。在B-in-C情况下,C中嵌入的B的vtable是特殊的。B的常规vtable是正常的,并直接指向B::w()

菱形问题:基类的多个副本(非虚拟继承)

好的。现在来解决真正困难的问题。回顾一下构成继承菱形时通常会遇到的多个基类副本的问题:

class A {
public:
  int a;
  virtual void v();
};

class B : public A {
public:
  int b;
  virtual void w();
};

class C : public A {
public:
  int c;
  virtual void x();
};

class D : public B, public C {
public:
  int d;
  virtual void y();
};

需要注意的是,D 继承自 BC,而 BC 都继承自 A。这意味着 D 中有两个 A 的副本。对象布局和虚表嵌入与前面的部分所述相同:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
d --> +----------+         | ptr to typeinfo for D |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+         |         B::w()        |
      |     b    |         +-----------------------+
      +----------+         |         D::y()        |
      |  vtable  |---+     +-----------------------+
      +----------+   |     |   -12 (top_offset)    |
      |     a    |   |     +-----------------------+
      +----------+   |     | ptr to typeinfo for D |
      |     c    |   +---> +-----------------------+
      +----------+         |         A::v()        |
      |     d    |         +-----------------------+
      +----------+         |         C::x()        |
                           +-----------------------+
当然,我们期望A的数据(成员a)在D对象布局中存在两次(确实如此),并且我们期望A的虚拟成员函数在vtable中表示两次(A::v()确实在那里)。好了,这里没有什么新东西。 菱形继承:虚拟基类的单一副本 但是如果我们应用虚拟继承呢?C++虚拟继承允许我们指定一个菱形继承,但只保证有一个虚拟继承基类的副本。因此,让我们按照以下方式编写代码:
class A {
public:
  int a;
  virtual void v();
};

class B : public virtual A {
public:
  int b;
  virtual void w();
};

class C : public virtual A {
public:
  int c;
  virtual void x();
};

class D : public B, public C {
public:
  int d;
  virtual void y();
};

突然间事情变得更加复杂了。如果我们在表示 D 的时候只能有一个 A 的副本,那么我们就不能再利用把 C 嵌入到 D 中(并且在 D 的虚函数表中嵌入 C 部分的虚函数表)的"技巧"。但是如果我们不能这样做,那么我们如何处理常规类型替换呢?

让我们尝试绘制布局图:

                                   +-----------------------+
                                   |   20 (vbase_offset)   |
                                   +-----------------------+
                                   |     0 (top_offset)    |
                                   +-----------------------+
                                   | ptr to typeinfo for D |
                      +----------> +-----------------------+
d --> +----------+    |            |         B::w()        |
      |  vtable  |----+            +-----------------------+
      +----------+                 |         D::y()        |
      |     b    |                 +-----------------------+
      +----------+                 |   12 (vbase_offset)   |
      |  vtable  |---------+       +-----------------------+
      +----------+         |       |    -8 (top_offset)    |
      |     c    |         |       +-----------------------+
      +----------+         |       | ptr to typeinfo for D |
      |     d    |         +-----> +-----------------------+
      +----------+                 |         C::x()        |
      |  vtable  |----+            +-----------------------+
      +----------+    |            |    0 (vbase_offset)   |
      |     a    |    |            +-----------------------+
      +----------+    |            |   -20 (top_offset)    |
                      |            +-----------------------+
                      |            | ptr to typeinfo for D |
                      +----------> +-----------------------+
                                   |         A::v()        |
                                   +-----------------------+

好的。所以你可以看到A现在与其他基类基本上以相同的方式被嵌入D中。但它被嵌入了D而不是其直接派生类中。


我认为vtable和VTT的这个基本描述也应该保存在stackoverflow上。 - osgx
这个答案正在meta上讨论 - Cerbrus
1
对于调查和出色处理死链问题的尊重。 - peterh
@osgx,你链接的问题昨晚我刚问过。能否请您为我提供一个答案?非常感谢! - choxsword
我看过的关于VTT和虚拟继承的最好解释! - Amine
显示剩余2条评论

17
THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. 

Morgan Deters的网页:

多重继承中的构造/析构:

在多重继承的情况下如何在内存中构造上述对象?我们如何确保部分构造的对象(以及其虚表)对构造函数是安全的?

幸运的是,这一切都已经被非常小心地处理了。假设我们正在构造一个新的类型为D的对象(例如通过new D)。首先,在堆中分配对象的内存并返回指针。D的构造函数被调用,但在进行任何D-特定的构造之前,它会在对象上调用A的构造函数(当然,在调整this指针后!)。A的构造函数填充了D对象的A部分,就好像它是A的实例一样。

d --> +----------+
      |          |
      +----------+
      |          |
      +----------+
      |          |
      +----------+
      |          |       +-----------------------+
      +----------+       |     0 (top_offset)    |
      |          |       +-----------------------+
      +----------+       | ptr to typeinfo for A |
      |  vtable  |-----> +-----------------------+
      +----------+       |         A::v()        |
      |    a     |       +-----------------------+
      +----------+

控制权返回给 D 的构造函数,该函数调用了 B 的构造函数。(这里不需要进行指针调整)当 B 的构造函数完成时,对象将如下所示:

 B-in-D
                          +-----------------------+
                          |   20 (vbase_offset)   |
                          +-----------------------+
                          |     0 (top_offset)    |
                          +-----------------------+
d --> +----------+        | ptr to typeinfo for B |
      |  vtable  |------> +-----------------------+
      +----------+        |         B::w()        |
      |    b     |        +-----------------------+
      +----------+        |    0 (vbase_offset)   |
      |          |        +-----------------------+
      +----------+        |   -20 (top_offset)    |
      |          |        +-----------------------+
      +----------+        | ptr to typeinfo for B |
      |          |   +--> +-----------------------+
      +----------+   |    |         A::v()        |
      |  vtable  |---+    +-----------------------+
      +----------+
      |    a     |
      +----------+

等等……B的构造函数通过更改对象的vtable指针来修改了对象的A部分!它怎么知道要区分这种B-in-D和B-in-something-else(或单独的B)呢?简单。 虚表表告诉它要这样做。这个结构,缩写为VTT,是用于构建的虚表的表格。在我们的例子中,D的VTT看起来像这样:

B-in-D
                                               +-----------------------+
                                               |   20 (vbase_offset)   |
            VTT for D                          +-----------------------+
+-------------------+                          |     0 (top_offset)    |
|    vtable for D   |-------------+            +-----------------------+
+-------------------+             |            | ptr to typeinfo for B |
| vtable for B-in-D |-------------|----------> +-----------------------+
+-------------------+             |            |         B::w()        |
| vtable for B-in-D |-------------|--------+   +-----------------------+
+-------------------+             |        |   |    0 (vbase_offset)   |
| vtable for C-in-D |-------------|-----+  |   +-----------------------+
+-------------------+             |     |  |   |   -20 (top_offset)    |
| vtable for C-in-D |-------------|--+  |  |   +-----------------------+
+-------------------+             |  |  |  |   | ptr to typeinfo for B |
|    vtable for D   |----------+  |  |  |  +-> +-----------------------+
+-------------------+          |  |  |  |      |         A::v()        |
|    vtable for D   |-------+  |  |  |  |      +-----------------------+
+-------------------+       |  |  |  |  |
                            |  |  |  |  |                         C-in-D
                            |  |  |  |  |      +-----------------------+
                            |  |  |  |  |      |   12 (vbase_offset)   |
                            |  |  |  |  |      +-----------------------+
                            |  |  |  |  |      |     0 (top_offset)    |
                            |  |  |  |  |      +-----------------------+
                            |  |  |  |  |      | ptr to typeinfo for C |
                            |  |  |  |  +----> +-----------------------+
                            |  |  |  |         |         C::x()        |
                            |  |  |  |         +-----------------------+
                            |  |  |  |         |    0 (vbase_offset)   |
                            |  |  |  |         +-----------------------+
                            |  |  |  |         |   -12 (top_offset)    |
                            |  |  |  |         +-----------------------+
                            |  |  |  |         | ptr to typeinfo for C |
                            |  |  |  +-------> +-----------------------+
                            |  |  |            |         A::v()        |
                            |  |  |            +-----------------------+
                            |  |  |
                            |  |  |                                    D
                            |  |  |            +-----------------------+
                            |  |  |            |   20 (vbase_offset)   |
                            |  |  |            +-----------------------+
                            |  |  |            |     0 (top_offset)    |
                            |  |  |            +-----------------------+
                            |  |  |            | ptr to typeinfo for D |
                            |  |  +----------> +-----------------------+
                            |  |               |         B::w()        |
                            |  |               +-----------------------+
                            |  |               |         D::y()        |
                            |  |               +-----------------------+
                            |  |               |   12 (vbase_offset)   |
                            |  |               +-----------------------+
                            |  |               |    -8 (top_offset)    |
                            |  |               +-----------------------+
                            |  |               | ptr to typeinfo for D |
                            +----------------> +-----------------------+
                               |               |         C::x()        |
                               |               +-----------------------+
                               |               |    0 (vbase_offset)   |
                               |               +-----------------------+
                               |               |   -20 (top_offset)    |
                               |               +-----------------------+
                               |               | ptr to typeinfo for D |
                               +-------------> +-----------------------+
                                               |         A::v()        |
                                               +-----------------------+

D的构造函数将一个指向D的VTT的指针传递给B的构造函数(在这种情况下,它传递了第一个B-in-D条目的地址)。事实上,上面用于对象布局的虚表是一个特殊的虚表,仅用于B-in-D的构造。

控制权返回到D的构造函数,并调用C的构造函数(带有一个指向“C-in-D + 12”条目的VTT地址参数)。当C的构造函数完成对象时,它看起来像这样:

 B-in-D
                                                        +-----------------------+
                                                        |   20 (vbase_offset)   |
                                                        +-----------------------+
                                                        |     0 (top_offset)    |
                                                        +-----------------------+
                                                        | ptr to typeinfo for B |
                    +---------------------------------> +-----------------------+
                    |                                   |         B::w()        |
                    |                                   +-----------------------+
                    |                          C-in-D   |    0 (vbase_offset)   |
                    |       +-----------------------+   +-----------------------+
d --> +----------+  |       |   12 (vbase_offset)   |   |   -20 (top_offset)    |
      |  vtable  |--+       +-----------------------+   +-----------------------+
      +----------+          |     0 (top_offset)    |   | ptr to typeinfo for B |
      |    b     |          +-----------------------+   +-----------------------+
      +----------+          | ptr to typeinfo for C |   |         A::v()        |
      |  vtable  |--------> +-----------------------+   +-----------------------+
      +----------+          |         C::x()        |
      |    c     |          +-----------------------+
      +----------+          |    0 (vbase_offset)   |
      |          |          +-----------------------+
      +----------+          |   -12 (top_offset)    |
      |  vtable  |--+       +-----------------------+
      +----------+  |       | ptr to typeinfo for C |
      |    a     |  +-----> +-----------------------+
      +----------+          |         A::v()        |
                            +-----------------------+

从上面您可以看到,C的构造函数再次修改了嵌入式A的虚表指针。现在,嵌入式的C和A对象正在使用特殊构造的C-in-D虚表,而嵌入式的B对象则使用特殊构造的B-in-D虚表。最后,D的构造函数完成了这项工作,我们得到了与之前相同的图示:

                                   +-----------------------+
                                   |   20 (vbase_offset)   |
                                   +-----------------------+
                                   |     0 (top_offset)    |
                                   +-----------------------+
                                   | ptr to typeinfo for D |
                      +----------> +-----------------------+
d --> +----------+    |            |         B::w()        |
      |  vtable  |----+            +-----------------------+
      +----------+                 |         D::y()        |
      |     b    |                 +-----------------------+
      +----------+                 |   12 (vbase_offset)   |
      |  vtable  |---------+       +-----------------------+
      +----------+         |       |    -8 (top_offset)    |
      |     c    |         |       +-----------------------+
      +----------+         |       | ptr to typeinfo for D |
      |     d    |         +-----> +-----------------------+
      +----------+                 |         C::x()        |
      |  vtable  |----+            +-----------------------+
      +----------+    |            |    0 (vbase_offset)   |
      |     a    |    |            +-----------------------+
      +----------+    |            |   -20 (top_offset)    |
                      |            +-----------------------+
                      |            | ptr to typeinfo for D |
                      +----------> +-----------------------+
                                   |         A::v()        |
                                   +-----------------------+

销毁发生的过程与构造相反。D 的析构函数被调用。在用户的销毁代码运行之后,析构函数调用 C 的析构函数,并指示其使用 D 的 VTT 的相关部分。C 的析构函数以与构造期间相同的方式操作 vtable 指针;也就是说,相关的 vtable 指针现在指向 C-in-D 构造期间的 vtable。然后它运行了 C 的用户销毁代码并返回控制权到 D 的析构函数,接下来 D 的析构函数通过 D 的 VTT 引用调用 B 的析构函数。B 的析构函数设置了对象的相关部分,使其引用 B-in-D 构造期间的 vtable。它运行了 B 的用户销毁代码并返回控制权到 D 的析构函数,最后 D 的析构函数调用 A 的析构函数。A 的析构函数将 A 部分对象的 vtable 更改为指向 A 的 vtable。最后,控制权返回到 D 的析构函数,对象的销毁完成。曾经由该对象使用的内存已经返回给系统。

实际上,这个故事有些更加复杂。你是否看到过 GCC 警告和错误信息中的 “in-charge” 和 “not-in-charge” 构造函数和析构函数规范,或者在 GCC 生成的二进制文件中看到过?事实上,可能会有两个构造函数实现和最多三个析构函数实现。

"in-charge"(或完整对象)构造函数是构造虚基类的构造函数,而 "not-in-charge"(或基类对象)构造函数则是不构造虚基类的构造函数。考虑上面的例子。如果构造一个 B,它的构造函数需要调用 A 的构造函数来构造它。同样,C 的构造函数需要构造 A。但是,如果 B 和 C 在构造 D 时构造,则它们的构造函数不应构造 A,因为 A 是虚基类,D 的构造函数将为 D 的实例精确地构造一次。考虑以下情况:

如果你执行 new A,A 的 "in-charge" 构造函数将被调用来构造 A。 当你执行 new B,B 的 "in-charge" 构造函数将被调用。它将调用 A 的 "not-in-charge" 构造函数。

new C 与 new B 类似。

一个新的 D 调用 D 的 "in-charge" 构造函数。我们通过这个例子进行了讲解。D 的 "in-charge" 构造函数调用了 A、B 和 C 的 "not-in-charge" 构造函数(按顺序)。

"in-charge" 析构函数是 "in-charge" 构造函数的类比——它负责销毁虚基类。同样,会生成一个 "not-in-charge" 析构函数。但是还有第三个。 "in-charge deleting" 析构函数是一种同时释放存储和销毁对象的析构函数。那么何时优先调用其中之一呢?

好吧,有两种可以被销毁的对象——在栈上分配的对象和在堆上分配的对象。考虑以下代码(给出我们之前的菱形继承层次结构,使用虚继承):

D d;            // allocates a D on the stack and constructs it
D *pd = new D;  // allocates a D in the heap and constructs it
/* ... */
delete pd;      // calls "in-charge deleting" destructor for D
return;         // calls "in-charge" destructor for stack-allocated D

我们可以看到,执行删除的代码并没有调用实际的delete操作符,而是由被删除对象的负责删除的析构函数来完成的。为什么要这样做?为什么不让调用者调用负责删除的析构函数,然后再删除对象呢?这样你只需要两个析构函数的实现而不是三个...

好吧,编译器确实可以这样做,但出于其他原因会更加复杂。考虑以下代码(假设有一个虚析构函数,您总是使用它,对吧?...对吧!?):

D *pd = new D;  // allocates a D in the heap and constructs it
    C *pc = d;      // we have a pointer-to-C that points to our heap-allocated D
    /* ... */
    delete pc;      // call destructor thunk through vtable, but what about delete?

如果您没有一个“负责删除”的D析构函数,那么delete操作将需要像析构函数的thunk一样调整指针。请记住,C对象嵌入在D中,因此我们上面的指向C的指针被调整为指向D对象的中间位置。我们不能只是删除这个指针,因为它不是在我们构造它时由malloc()返回的指针。

因此,如果我们没有一个负责删除的析构函数,我们就必须有一个到delete操作符的thunks(并在我们的vtable中表示它们),或者类似的其他东西。

Thunk, 虚拟和非虚拟

本部分尚未编写。

带有一侧虚拟方法的多重继承

好了。最后一个练习。如果我们有一个具有虚拟继承的菱形继承层次结构,如前所述,但仅沿着其中一侧具有虚拟方法怎么办?所以:

class A {
public:
  int a;
};

class B : public virtual A {
public:
  int b;
  virtual void w();
};

class C : public virtual A {
public:
  int c;
};

class D : public B, public C {
public:
  int d;
  virtual void y();
};

在这种情况下,对象的布局如下:

                                   +-----------------------+
                                   |   20 (vbase_offset)   |
                                   +-----------------------+
                                   |     0 (top_offset)    |
                                   +-----------------------+
                                   | ptr to typeinfo for D |
                      +----------> +-----------------------+
d --> +----------+    |            |         B::w()        |
      |  vtable  |----+            +-----------------------+
      +----------+                 |         D::y()        |
      |     b    |                 +-----------------------+
      +----------+                 |   12 (vbase_offset)   |
      |  vtable  |---------+       +-----------------------+
      +----------+         |       |    -8 (top_offset)    |
      |     c    |         |       +-----------------------+
      +----------+         |       | ptr to typeinfo for D |
      |     d    |         +-----> +-----------------------+
      +----------+
      |     a    |
      +----------+

因此,您可以看到C子对象没有虚拟方法,但仍具有vtable(尽管为空)。实际上,所有C的实例都有一个空的vtable。

感谢Morgan Deters!


2
抄袭和侵犯版权如果我曾经这么说过。 - v010dya
3
如果原作者被引用并且甚至受到感谢,那么这怎么算是剽窃呢? - jupp0r
Morgan Deters的网页:http://web.archive.org/web/20060908050947/http://www.cse.wustl.edu/~mdeters/,个人简介http://web.archive.org/web/20060910122623/http://www.cse.wustl.edu/~mdeters/bio/;更近期的页面https://cs.nyu.edu/~mdeters/;2015年1月23日讣告http://www.sent-trib.com/obituaries/dr-morgan-g-deters/article_70a9b22a-a307-11e4-ba08-476415c7fb1c.html“*Dr. Morgan G. Deters, PhD, 35岁,曾居住在Bowling Green,最近居住在纽约布鲁克林,于2015年1月17日在特立尼达和多巴哥去世。*”& http://cvc4.cs.stanford.edu/web/in-memoriam-morgan-deters/ - osgx
2
抱歉,你误读了我的评论,它是指第一条评论。这绝对不是剽窃,因为原作者已被引用并致谢。剽窃是将他人的作品拿来当做自己的。 - jupp0r
如果你没有一个“负责删除”的 D 类析构函数,那么 delete 操作将需要调整指针。不是的。析构函数只需返回需要传递给 operator delete 的指针即可。 - curiousguy
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接