我们都知道C++中什么是虚函数,但是深层次上它们是如何实现的呢?

vtable可以在运行时修改甚至直接访问吗?

vtable 是针对所有类都存在,还是只针对那些至少具有一个虚函数的类?

抽象类是否至少有一个条目的函数指针为 NULL?

使用单个虚函数是否会减慢整个类的速度?或者只调用虚拟函数?如果虚拟函数实际上被覆盖或不被覆盖,速度是否会受到影响,或者只要它是虚拟的就没有影响。

有帮助吗?

解决方案

深层次的虚函数是如何实现的?

《C++ 中的虚拟函数》:

每当程序声明了虚函数时,就会为该类构造一个 v 表。v 表由包含一个或多个虚函数的类的虚函数地址组成。包含虚函数的类的对象包含一个虚拟指针,该指针指向内存中虚拟表的基地址。每当有虚函数调用时,都会使用 v 表来解析函数地址。包含一个或多个虚函数的类的对象在内存中该对象的最开始处包含一个称为 vptr 的虚拟指针。因此,在这种情况下,对象的大小会随着指针的大小而增加。这个vptr包含了内存中虚拟表的基址。请注意,虚拟表是特定于类的,即,无论类包含多少个虚拟函数,一个类都只有一个虚拟表。该虚拟表又包含该类的一个或多个虚拟函数的基地址。当对对象调用虚拟函数时,该对象的 vptr 提供内存中该类的虚拟表的基地址。该表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是虚拟函数调用期间动态绑定的解析方式。

vtable可以在运行时修改甚至直接访问吗?

总的来说,我相信答案是“不”。您可以进行一些内存修改来查找 vtable,但您仍然不知道调用它的函数签名是什么样的。您希望使用此功能(该语言支持的)实现的任何目标都应该可以实现,而无需直接访问 vtable 或在运行时修改它。另请注意,C++ 语言规范 才不是 指定需要 vtable - 但这是大多数编译器实现虚拟函数的方式。

vtable 是针对所有对象都存在,还是只针对那些至少具有一个虚函数的对象?

相信 这里的答案是“这取决于实现”,因为规范首先不需要 vtable。然而,在实践中,我相信所有现代编译器只会在类至少有 1 个虚拟函数时创建 vtable。存在与 vtable 相关的空间开销以及与调用虚拟函数与非虚拟函数相关的时间开销。

抽象类是否至少有一个条目的函数指针为 NULL?

答案是语言规范未指定,因此取决于实现。如果未定义(通常没有定义),则调用纯虚函数会导致未定义的行为 (ISO/IEC 14882:2003 10.4-2)。实际上,它确实在 vtable 中为该函数分配了一个槽,但没有为其分配地址。这使得 vtable 不完整,需要派生类来实现该功能并完成 vtable。有些实现只是在 vtable 条目中放置一个 NULL 指针;其他实现放置一个指向虚拟方法的指针,该方法执行类似于断言的操作。

请注意,抽象类可以定义纯虚函数的实现,但只能使用限定 ID 语法来调用该函数(即,在方法名称中完全指定类,类似于从派生类)。这样做是为了提供易于使用的默认实现,同时仍然要求派生类提供重写。

使用单个虚拟函数是否会减慢整个类的速度,或者仅减慢对虚拟函数的调用?

这已经超出了我的知识范围,所以如果我错了,请有人帮助我!

相信 只有类中的虚拟函数才会经历与调用虚拟函数和调用虚拟函数相关的时间性能损失。非虚函数。无论哪种方式,类的空间开销都是存在的。请注意,如果有 vtable,则每个 vtable 仅有 1 个 班级, 每一个都没有 目的.

如果虚拟函数实际上被重写或不被重写,速度是否会受到影响,或者只要它是虚拟的就没有影响?

我不认为与调用基本虚拟函数相比,被重写的虚拟函数的执行时间不会减少。但是,与为派生类和基类定义另一个 vtable 相关的类会产生额外的空间开销。

其他资源:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (通过回程机)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

其他提示

  • vtable可以在运行时修改甚至直接访问吗?

不可移植,但如果您不介意肮脏的把戏,当然可以!

警告:不建议 1 岁以下的儿童、成人使用此技术 969, ,或来自半人马座阿尔法星的毛茸茸的小生物。副作用可能包括 从你鼻子里飞出的恶魔, ,突然出现 犹格·索托斯 作为所有后续代码审查的必需批准者,或追溯添加 IHuman::PlayPiano() 到所有现有实例]

在我见过的大多数编译器中,vtbl * 是对象的前 4 个字节,而 vtbl 内容只是其中的成员指针数组(通常按照它们声明的顺序,基类的第一个)。当然还有其他可能的布局,但这就是我通常观察到的。

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

现在来搞点恶作剧吧……

在运行时更改类:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

替换所有实例的方法(monkeypatching 类)

这个有点棘手,因为 vtbl 本身可能位于只读存储器中。

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

由于 mprotect 操作,后者很可能会让病毒检查程序和链接唤醒并引起注意。在使用 NX 位的过程中,它很可能会失败。

使用单个虚函数是否会减慢整个类的速度?

或者只调用虚拟函数?如果虚拟函数实际上被覆盖或不被覆盖,速度是否会受到影响,或者只要它是虚拟的就没有影响。

拥有虚函数会减慢整个类的速度,因为在处理此类的对象时,必须初始化、复制另一项数据……。对于一个有六名左右成员的班级来说,差异应该可以忽略不计。对于只包含一个的类 char 成员,或者根本没有成员,差异可能会很明显。

除此之外,需要注意的是,并非每次对虚函数的调用都是虚函数调用。如果您有一个已知类型的对象,编译器可以发出正常函数调用的代码,甚至可以内联所述函数(如果需要)。只有当您通过可能指向基类的对象或某个派生类的对象的指针或引用进行多态调用时,您才需要 vtable 间接寻址并在性能方面付出代价。

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

无论函数是否被覆盖,硬件必须执行的步骤本质上是相同的。从对象中读取vtable的地址,从适当的槽中检索函数指针,并通过指针调用函数。就实际性能而言,分支预测可能会产生一些影响。例如,如果大多数对象引用给定虚拟函数的相同实现,那么即使在检索指针之前,分支预测器也有可能正确预测要调用哪个函数。但哪个函数是通用函数并不重要:它可能是大多数对象委托给未覆盖的基本情况,或者大多数对象属于同一子类,因此委托给相同的覆盖情况。

它们是如何深层次实施的?

我喜欢 jheriko 使用模拟实现来演示这一点的想法。但我会使用 C 来实现类似于上面代码的东西,以便更容易看到底层。

父类Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

派生类 Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

函数 f 执行虚函数调用

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

所以你可以看到,vtable只是内存中的一个静态块,主要包含函数指针。多态类的每个对象都会指向与其动态类型对应的vtable。这也使得RTTI和虚拟功能之间的联系更加清晰:你可以通过查看类指向的虚函数表来检查它的类型。上面的内容在很多方面都得到了简化,例如多重继承,但总体概念是合理的。

如果 arg 属于类型 Foo* 你拿 arg->vtable, ,但实际上是一个类型的对象 Bar, ,那么你仍然得到正确的地址 vtable. 。那是因为 vtable 始终是对象地址的第一个元素,无论是否被调用 vtable 或者 base.vtable 在正确键入的表达式中。

通常使用 VTable,即指向函数的指针数组。

此答案已纳入 社区维基解答

  • 抽象类是否至少有一个条目的函数指针为 NULL?

答案是它是未指定的 - 如果未定义(通常没有),调用纯虚函数会导致未定义的行为(ISO/IEC 14882:2003 10.4-2)。有些实现只是在 vtable 条目中放置一个 NULL 指针;其他实现放置一个指向虚拟方法的指针,该方法执行类似于断言的操作。

请注意,抽象类可以定义纯虚函数的实现,但只能使用限定 ID 语法来调用该函数(即,在方法名称中完全指定类,类似于从派生类)。这样做是为了提供易于使用的默认实现,同时仍然要求派生类提供重写。

您可以使用函数指针作为类成员和静态函数作为实现,或者使用指向成员函数和成员函数的实现的指针来重新创建 C++ 中的虚函数的功能。这两种方法之间只有符号上的优势......事实上,虚函数调用本身只是一种符号上的便利。事实上继承只是一种符号上的方便......这一切都可以在不使用语言特性进行继承的情况下实现。:)

下面是未经测试的垃圾,可能有错误的代码,但希望能演示这个想法。

例如

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

我会尽力让它变得简单:)

我们都知道C++中什么是虚函数,但是深层次上它们是如何实现的呢?

这是一个带有函数指针的数组,函数是特定虚函数的实现。该数组中的索引表示为类定义的虚拟函数的特定索引。这包括纯虚函数。

当一个多态类派生于另一个多态类时,我们可能会出现以下情况:

  • 派生类不会添加新的虚函数,也不会覆盖任何虚函数。在这种情况下,此类与基类共享 vtable。
  • 派生类添加并重写虚方法。在这种情况下,它有自己的虚函数表,其中添加的虚函数的索引从最后一个派生函数开始。
  • 继承中的多个多态类。在这种情况下,我们在第二个和下一个碱基之间有一个索引移位,以及它在派生类中的索引

vtable可以在运行时修改甚至直接访问吗?

不是标准方式 - 没有 API 可以访问它们。编译器可能有一些扩展或私有 API 来访问它们,但这可能只是一个扩展。

vtable 是针对所有类都存在,还是只针对那些至少具有一个虚函数的类?

仅那些至少具有一个虚函数(甚至是析构函数)或派生至少一个具有其 vtable(“多态”)的类的类。

抽象类是否至少有一个条目的函数指针为 NULL?

这是一个可能的实现,但没有实践。相反,通常有一个函数打印诸如“纯虚函数调用”之类的内容,并且执行以下操作: abort(). 。如果您尝试在构造函数或析构函数中调用抽象方法,则可能会发生对它的调用。

使用单个虚函数是否会减慢整个类的速度?或者只调用虚拟函数?如果虚拟函数实际上被覆盖或不被覆盖,速度是否会受到影响,或者只要它是虚拟的就没有影响。

速度减慢仅取决于呼叫是解析为直接呼叫还是虚拟呼叫。其他都不重要。:)

如果通过指针或对象引用来调用虚函数,那么它将始终被实现为虚调用——因为编译器永远无法知道运行时将分配给该指针的对象类型,以及它是否属于是否覆盖该方法的类。只有在两种情况下,编译器才能将对虚拟函数的调用解析为直接调用:

  • 如果您通过值(变量或返回值的函数的结果)调用该方法 - 在这种情况下,编译器不会怀疑对象的实际类是什么,并且可以在编译时“硬解析”它。
  • 如果声明了虚方法 final 在您拥有调用它的指针或引用的类中(仅在 C++11 中)。在这种情况下,编译器知道该方法不能进行任何进一步的重写,并且它只能是此类中的方法。

请注意,虚拟调用仅具有取消引用两个指针的开销。使用 RTTI(尽管仅适用于多态类)比调用虚拟方法慢,如果您找到用两种这样的方式实现相同事物的情况。例如,定义 virtual bool HasHoof() { return false; } 然后仅覆盖为 bool Horse::HasHoof() { return true; } 将为您提供打电话的能力 if (anim->HasHoof()) 这会比尝试更快 if(dynamic_cast<Horse*>(anim)). 。这是因为 dynamic_cast 在某些情况下,甚至必须递归地遍历类层次结构,以查看是否可以从实际的指针类型和所需的类类型构建路径。虽然虚拟调用始终相同 - 取消引用两个指针。

这里有一个 可运行 现代 C++ 中虚拟表的手动实现。它具有明确定义的语义,没有黑客攻击,也没有 void*.

笔记: .*->* 是不同的运算符 *->. 。成员函数指针的工作方式不同。

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

每个对象都有一个指向成员函数数组的 vtable 指针。

所有这些答案中都没有提到的是,在多重继承的情况下,基类都有虚拟方法。继承类有多个指向 vmt 的指针。结果是此类对象的每个实例的大小都更大。大家都知道,具有虚方法的类有 4 个字节的额外空间用于 vmt,但在多重继承的情况下,每个具有虚方法的基类的 vmt 都是 4 倍。4是指针的大小。

Burly 的答案在这里是正确的,除了这个问题:

抽象类是否至少有一个条目的函数指针为 NULL?

答案是根本不为抽象类创建虚拟表。没有必要,因为无法创建这些类的对象!

换句话说,如果我们有:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

通过pB访问的vtbl指针将是D类的vtbl。这正是多态性的实现方式。也就是如何通过pB访问D方法。B 类不需要 vtbl。

回应迈克下面的评论......

如果我描述的B类有一个虚方法 富() 不被 D 和虚方法覆盖 酒吧() 被覆盖,那么 D 的 vtbl 将有一个指向 B 的指针 富() 以及对自己的 酒吧(). 。仍然没有为 B 创建 vtbl。

我之前做了一个非常可爱的概念证明(看看继承顺序是否重要);让我知道你的 C++ 实现是否真的拒绝它(我的 gcc 版本只给出分配匿名结构的警告,但这是一个错误),我很好奇。

CCPolite.h:

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h:

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

主程序:

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

输出:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

请注意,由于我从不分配我的假对象,因此无需进行任何破坏;析构函数会自动放置在动态分配对象的作用域末尾,以回收对象文字本身和 vtable 指针的内存。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top