怎样在 C++ 中计算对象的实例化次数

C++只能实例化一个对象的类 # include <iostream> # include <string> using namespace std;1.创建类class Student { public: static Student* Limit(); //静态成员函数 void display(); private: static Student *total; //静态数据成员指针 阅读详情
                                                                         Objects Counting in C++

  在 C++ 中,对某个 class 所产生出来的 objects保持正确的计数,是办得到的,除非你面对一些疯狂份子。
译文保留原作时态,并未修改。以下是译文所采用的几个特别请读者注意的术语:
client :客端。
type:型别。为避免和其他近似术语混淆,本文译为「型别」而非「类型」。
instantiated:具现化。「针对一个 template,具体实现出一份实体」的意思。
instance:实体。「案例」是绝对错误的译法。
parameter:叁数。或称型式叁数,形叁。
argument:引数。或称实质叁数,实叁。

至於 class, object, data member, member function, constructor, destructor, template 等术语,皆保留不译。

有时候,容易的事情虽然容易,但它们还是隐藏着某种微妙。举个例子,假设你有个 class名为 Widget,你希望有某种办法找出程式执行期间究竟存在着多少个 Widget objects。方法之一(不但容易实作而且答案正确)就是为 Widget 准备一个 static 计数器,每当Widget constructor 被呼叫,就将该计数器加一,每当Widget destructor 被呼叫,就将该计数器减一。此外你还需要一个 static 成员函式 howMany( ),用来回报目前存在多少个 Widget objects。如果 Widget 什麽都没做,单单只是追踪其objects个数,那麽它看起来大约像这样:

class Widget {
public:
    Widget() { ++count; }
    Widget(const Widget&) { ++count; }
    ~Widget() { --count; }

    static size_t howMany()
    { return count; }

private:
    static size_t count;
};

// count 的定义。这应该放在一个实作档中。
size_t Widget::count = 0;


这可以有效运作。可别忘了实作出 copy constructor,因为编译器自动为 Widget 产生的那个 copy constructor 不会知道将 count 加一。

如果你只需要为 Widget 做计数工作,你已经完成了你的任务。但有时候你得为许多   classes 做相同的计数工作。一再进行重复的工作会令人沉闷而生厌,而沉闷与厌恶感会导致错误的发生。为了阻止这种局面,最好能够将上述的「物件计数」(object-counting)程式码包装起来,使它能够被任何 class重复运用。理想的包装应该符合以下条件:
容易使用 ─ 让那些需要此等服务的 class 设计者只需最小量的工作。最理想的情况是,他们不必做任何事情,只需宣称『我要对这种型别的 objects 进行计量』就好。
有效率 ─ 使用者不需被课徵任何非必要的空间税或时间税。
绝对安全 ─ 不可能突然导致一个错误计量。(我们不打算理会那些蓄意破坏的恶意使用者,他们会刻意试着混淆计量。在 C++ 中,诸如此类的使用者总是能找到办法来满足他们卑鄙而居心不良的行为)

暂停一下,想一想,你如何实作出一个可复用的「物件计数」套件,并满足以上所有目标。这或许比你所预想的还要困难。如果它真的如你所想像的那麽容易,你就不会在这本刊物上看到这篇文章了。
new, delete, 和 Exceptions

虽然你正全心全意地解决「物件计数」相关问题,请允许我把焦点暂时切换到一个似乎无关的主题上。这个主题就是「当 constructors 丢出异常,new 和 delete 之间的关系」。当你要求 C++ 动态配置一个 object,你会像这样地使用 new 运算式:

class ABCD { ... }; // ABCD = "A Big Complex Datatype"
ABCD *p = new ABCD; // 这是一个 new 运算式

这个 new 运算式的意义在语言层面已经确定,其行为无论如何无法被改变。它做两件事情。第一,它呼叫一个名为 operator new的记忆体配置函式。这个函式的责任是找出足够置放一个 ABCD object 的记忆体。如果配置成功,new 运算式接下来就唤起一个 ABCD constructor,在 operator new 找出的那块空间上建立一个 ABCD object。

但是,假设 operator new 丢出一个 std::bad_alloc 异常,会怎样?这种异常表示,动态配置记忆体的任务失败了。在上述的 new 运算式中,有两个函式可能发出这样的异常。第一个是 operator new,它企图找出足够的记忆体来放置一个 ABCD object。第二个是接下来执行的 ABCD constructor,它企图把生鲜记忆体转换为一个有效的 ABCD object。

如果异常来自 operator new,表示没有任何记忆体被配置出来。然而如果 operator new 成功而 ABCD constructor 发出异常,很重要的一件事就是将 operator new 所配置的记忆体释放掉。如果不这样,程式就会发生记忆体遗失(memory leak)。客端(也就是要求产生一个 ABCD object 的那段程式码)不可能知道到底哪一个函式发出异常。

多年以来,这是 C++ 标准草案中的一个漏洞,1995 年三月,C++ 标准委员会采纳了一个提议:如果在 new 运算式动作期间,operator new 配置记忆体成功而後继的 constructor 丢出异常,执行期系统(runtime system)就必须自动释放被 operator new 配置出来的记忆体。这个释放动作由 operator delete 执行,此函式类似 operator new。(详见本文最後方块栏目内的「placement new 和 placement delete」。)

这个「new 运算式和 operator delete 之间的关系」,会在我们企图将「物件计数机制」自动化的过程中,带来影响。
计算物件个数(Counting Objects)

有一种物件计数问题的解法,涉及发展出一个「物件计数专用」的 class。这个 class 看起来或许很像(甚至完全像)稍早展示的 Widget class:

// 稍後有一些讨论,告诉你为什麽这样的设计并不很正确

class Counter {  
public:          
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }
    static size_t howMany()
        { return count; }

private:
    static size_t count;
};

// 以下这行仍然应该放在一个实作档中。
size_t Counter::count = 0;

这里的想法是,任何 classes 如果需要记录当下存在的物件个数,只需使用 Counter来担任簿记工作即可。有两个明显的方法可以完成这项任务,其中之一是定义一个 Counter object,使它成为一个 class data member,像这样:

// 在需要计数的 class 中内嵌一个 Counter object。
class Widget {
public:
    .....  // Widget 该有的所有 public 成员,
           // 都放在这里。
    static size_t howMany()
    { return Counter::howMany(); }
private:
    .....  // Widget 该有的所有 private 成员,
           // 都放在这里。
    Counter c;
};

另一个方法是把 Counter 当做 base class,像这样:

// 让需要计数的 class 继承自 Counter
class Widget: public Counter {
    .....  // Widget 该有的所有 public 成员,
           // 都放在这里。
private:
    .....  // Widget 该有的所有 private 成员,
           // 都放在这里。
};

两种作法各有优劣。验证它们之前,我们必须注意,没有一个方法以其目前型式可以有效运作。问题在於 Counter 中的静态物件 count。这样的静态物件只有一个,但我们却需要为每一个使用Counter 的 class 准备一个。举个例子,如果我们打算对 Widgets 和 ABCDs 计数,我们需要两个 static size_t objects,而不是一个。让 Counter::count 成为 nonstatic,并不能解决这个问题,因为我们需要的是为每个 class 准备一个计数器,而不是为每个 object 准备一个计数器。

运用 C++ 中最广为人知但名称十分诡异的一个技俩,我们就可以取得我们想要的行为:我们可以把 Counter 放进一个 template 中,然後让每一个想要使用 Counter 的 class,「以自己为 template 引数」具现出这个 template。

让我再说一次。Counter 变成一个 template:

template<typename T>
class Counter {
public:
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }

    static size_t howMany()
    { return count; }

private:
    static size_t count;
};

template<typename T>
size_t
Counter<T>::count = 0; // 现在这一行可以放进表头档中了。

於是前述的第一种实作法改变为这样:

// 在需要计数的 class T 中内嵌一个 Counter<T> object。
class Widget {
public:
    .....
    static size_t howMany()
    {return Counter<Widget>::howMany();}
private:
    .....
    Counter<Widget> c;
};

第二种作法(继承法)则改变为这样:

// 让需要计数的 class T 继承自 Counter<T>
class Widget: public Counter<Widget> {    
    .....
};

注意在这两个情况中,我们是如何地将 Counter 取代为 Counter<Widget>。一如我稍早所说,每一个使用 Counter 的 class,都以它自己为引数,具现出那个 template。

「在一个 class 中,以自己为 template 引数,具现出一个 template,给自己使用」,这种策略最早系由 Jim Coplien 公开。他以多种语言(不只 C++)展示这种策略,并称此为一个「curiously recurring(诡异而循环的)template pattern」[注1]。我不认为 Jim 故意这麽命名,不过,他对此一  pattern(样式、模式) 的描述的确比他所取的名称好得多。那可真糟糕,因为 pattern 的名称很重要,而你现在所看到的这一个名称,无法涵盖「做了什麽,如何做出」等讯息。

patterns 的命名是一种艺术,我对此并不擅长。不过我或许会把这个 pattern 称为诸如「Do It For Me」这类名称。根本上,每一个「被 Counter 产生出来的 class」,都能够针对「将 Counter 具现化」的那个 class,提供一种服务,计算现有多少个 objects。所以,class Counter<Widget> 可以计算 Widget 的物件数量,class Counter<ABCD> 可以计算 ABCD 的物件数量。

现在,Counter 成了一个 template,不论内嵌式设计或继承式设计,都可以运作,所以我们接下来面临的是,评估其相对的强度和弱点。我们的一个设计标准是,对使用者而言,「物件计数」机能应该很容易获得。上述程式码很清楚地告诉我们,继承式设计比内嵌式设计容易,因为前者只要求提及「Counter 是一个 base class」,後者却要求必须定义一个 Counter data member,并要求使用者实作出一个 howMany( ) 以唤起 Counter 的 howMany( ) [注2]。虽然这样的工作也不是太多(客端的 howMany( ) 只需是个简单的 inline 函式),但只做一件事终究比做两件事容易些。所以让我们先把注意力放在继承式设计上。
使用 Public Inheritance(公开继承)

上述的继承式设计之所以能够运作,是因为 C++ 保证,每当一个 derived class object 被建构(或解构)时,其中的 base class 成份会先被建构(或後被解构)。让 Counter 成为一个 base class,便能确保:针对继承自 Counter 的 class,每当有一个 object 被产生或被摧毁,一定会有一个 Counter constructor 或 destructor 被唤起。

然而,任何时候只要牵涉 base classes 这个主题,就不要忘记 virtual destructors。Counter 应该有这样一个东西吗?良好的 C++ 物件导向设计规范说,是的,它应该有一个 virtual destructor。如果它没有,那麽当我们透过一个 base class pointer来删除一个 derived class object 时,会导致不可预期(未有定义)的结果,而且通常是不受欢迎的:

class Widget: public Counter<Widget>
{ ... };
Counter<Widget> *pw =
    new Widget;  // 获得一个 base class pointer,
                 // 指向一个 derived class object。    
......
delete pw; // 此将导致未知的结果 ─ 如果 base class
           // 缺乏一个 virtual destructor。

如此的行为将违反我们的需求条件:由於上述程式码没有任何不合理之处,我们的「物件计数」设计理应有绝对安全的表现。因此,这是 Counter 应该有个 virtual destructor 的强烈理由。

另一个需求条件是最佳效率(也就是不因「物件计数」而被课徵任何非必要的速度税和空间税),但我们在这里遇到一点麻烦。因为,virtual destructor(或任何虚拟函式)的出现,意味每一个 Counter(或其衍生类别)的 objects 都必须内含一个(隐藏的)虚拟指标,而这会增加物件的大小 ─ 如果它们原本并没有虚拟函式的话 [注3]。也就是说,如果 Widget 本身并无任何虚拟函式,型别为 Widget 的物件将会因为继承了 Counter<Widget> 而使大小扩张。我们不希望看到这种情况。

唯一的避免之道就是,找出一种方法,阻止客端「透过一个 base class pointer 删除一个 derived class object」。将 Counter 中的  operator delete 宣告为 private,似乎是一个合情合理的办法:

template<typename T>
class Counter {
public:
    .....
private:
    void operator delete(void*);
    .....
};

但如此一来,delete 运算式无法编译成功:

class Widget: public Counter<Widget> { ... };
Counter<Widget> *pw = new Widget;  ......
delete pw; // 错误。因为我们无法唤起 private operator delete

真是不幸。不过,真正有趣的是,new 运算式也不应该通过编译:

Counter<Widget> *pw =
    new Widget;  // 这一行应该无法通过编译,
                 // 因为 operator delete 是 private

请回忆一下稍早我对於 new, delete, exceptions(异常)的讨论,我说 C++ 的执行期系统(runtime system)有责任释放被 operator new 配置的记忆体 ─ 如果後继被呼叫的 constructor 失败的话。同时也请回忆一下,operator delete 是用来执行记忆体释放动作的函式。由於我们将 Counter 的 operator delete 宣告为 private,这会使得「藉由 new,将 objects 产生於 heap」的企图永远失败。

是的,这是违反直觉的,如果你的编译器不支援它,请不要惊讶。但是请注意,我所描述的行为是正确的。除此之外再无其他明显方法能够阻止「透过 Counter* pointer 删除 derived class objects」。由於我们已经拒绝「在 Counter 中设置一个 virtual destructor」的想法(它会引起非必要的空间税),所以我说,让我们放弃这个设计吧,让我们把注意力放在「使用一个 Counter data member」上面。
使用一个 Data Member

我们已经看过了「设置一个 Counter data member」这种设计所带来的缺点:客端必须同时定义一个 Counter data member 并撰写一个 inline 版的 howMany( ),用来呼叫 Counter 的 howMany( ) 函式。这些工作比我们希望加诸於客端程式身上的,多了一些,但它还不至於难以控制或管理。但是,除此之外还有另一个缺点:为某个 class增加一个 Counter data member,往往会扩张其 objects 的大小。

这几乎谈不上是什麽重大的启示。毕竟,增加一个 data member 而导致 objects 的大小增加,会令你惊讶吗?但是再看一眼,再想一想,请注意 Counter 的定义:

template<typename T>
class Counter {
public:
    Counter();
    Counter(const Counter&);
    ~Counter();

    static size_t howMany();
private:
    static size_t count;
};

请注意它并没有 nonstatic data members,意味每一个型别为  Counter 的 object 其实没有内含任何东西。也许我们会以为每一个型别为 Counter 的 object,大小为 0?也许吧,但那并不正确。在这一点上,C++ 的表现相当清楚。所有 objects 都有至少 1 byte 的大小,甚至即使这些 objects 没有任何 nonstatic data members。根据这样的定义,对於具现自 Counter template 的每一个 class,sizeof 会获得某个正值。所以每一个「内含一个 Counter object」的 class 将比「不内含 Counter object」者拥有更多资料。

(有趣的是,这并不意味一个「不含 Counter」的 class,其大小就一定比「内含一个 Counter」的兄弟有更大的体积。那是因为边界排列限制(alignment restrictions)可能会造成影响。举个例子,如果 class Widget 内含两个 bytes 的资料,但系统要求必须以 4-byte 来进行边界排列,所以每一个 Widget object 将内含两个 bytes 的补白,而 sizeof(Widget) 的结果为 4。如果,就像普遍的情况那样,编译器都满足「任何物件的大小不可能为 0」这一条件,於是将一个 char 安插到 Counter<Widget> 内,那麽 sizeof(Widget) 还是传回 4,纵使 Widget 内含一个 Counter<Widget> object 亦然。那个被含入的 Counter<Widget> object 仅仅取代了原本被补白的两个 bytes 中的一个。不过,这并不是常见情节,因此我们当然不能够在设计一个「物件计数」套件时,把它放进计划内。)

我在耶诞假期开始的时候,动笔写这篇文章(正确日期是感恩节当天,这也许能够让你了解,我是如何地庆祝这个重要的节日...),现在我的情绪已经很不好了。我要做的只不过是物件计数,我再也不想要东拉西扯什麽奇怪而额外的讨论了。
使用 Private Inheritance(私有继承)

再一次看看继承式设计的代码,那导致我们必须为 Counter 考虑一个 virtual destructor:

class Widget: public Counter<Widget>
{ ... };
Counter<Widget> *pw = new Widget;            
......
delete
pw;  // 导致未定义的(未知的)结果 ─
     // 如果 Counter 缺乏一个 virtual destructor。

稍早我们曾经试着藉由「阻止 delete 运算式顺利编译」而阻止这一系列动作,但是我们发现,那同时也阻止了 new 运算式的顺利编译。除此之外,还有其他某些东西也是我们可以禁止的。我们可以禁止一个 Widget* pointer(这是 new 的回传值)被隐式转型为一个 Counter<Widget>* pointer。换句话说,我们可以阻止继承体系中的指标型别转换。我们唯一需要做的就是将「public 继承」改为「private 继承」:

class Widget: private Counter<Widget>
{ ... };
Counter<Widget> *pw =
    new Widget;  // 错误! 没有隐式转换函式(implicit conversion)可以
                 // 将 Widget* 转为 Counter<Widget>*

此外,我们很开心地发现,以 Counter 做为 base class,并不会增加 Widget 的大小 ─ 如果和 Widget 独立个体的大小相比的话。是的,我知道我才刚刚告诉过你,没有任何 class 的大小为 0,但是 ─ 唔,那并不是我真正的意思。 我的真正意思是,没有任何一个 objects 的大小为 0。C++ 标准规格说得很清楚,一个 derived object 之中的「base-class 成份」的大小可以是 0。事实上,许多编译器都发展出所谓的「空白基础类别最佳化技术」(empty base optimization) [注4]。

因此,如果一个 Widget 内含一个 Counter,Widget 的大小一定会增加。因为 Counter 的 data member 完全属於自己,而不是别人的base-class 成份,因此它必须有非零大小。但如果 Widget 继承自 Counter,编译器便得以将 Widget 的大小保持在原先状态。这个事实为那些「记忆体使用状态非常紧绷而类别设计中涉及空白基础类别」的设计,提出了一个有趣的规则:当「private 继承」和「复合技术(containment, composition)」都能完成相同目的时,尽量选用「private 继承」。(译注:这一点乍见之下和 Scott Meyers 的《Effective C++ 2/e》条款42有所抵触。该条款最後一段建议大家,如果「private 继承」和「复合技术」都能完成相同目的,尽量选用复合技术。然而请你注意,本文所给的这个建议是有前提的。)

最後的设计几近完美。它实践了效率的要求,前提是你的编译器具有「空白基础类别最佳化」(empty base optimization)的能力,如此一来「继承自 Counter」这一事实才不会增加下层类别的物件大小。此外,所有的 Counter member functions 都必须是 inlin 函式。这样的设计也实践了安全需求,因为计数动作是由 Counter member functions 自动处理,那些函式会自动被 C++ 呼叫,而 private 继承机制的使用则阻止了隐式转型 ─ 「隐式转型」允许 derived-class objects 被当做 base-class objects 一样地处理。(好吧,我承认,它并非绝对安全:Widget 的作者可能荒谬地以一个 Widget 以外的类别来具现化 Counter,也就是说,他可能让 Widget 继承自 Counter<Gidget>。我对这种可能性所采取的态度是:不加理会。)

这样的设计对客端而言很容易使用,但是可能有人会咕哝说,还可以更简单。使用 private 继承机制,意味 howMany( ) 会在衍生类别中成为 private,所以衍生类别中必须含入一个 using declaration,使 howMany( ) 成为 public,才能被客端所用:

class Widget: private Counter<Widget> {
public:
    // 让 howMany 成为 public
    using Counter<Widget>::howMany;

    ..... // Widget 的剩馀部份没有改变。
};

class ABCD: private Counter<ABCD> {
public:
    // 让 howMany 成为 public
    using Counter<ABCD>::howMany;

    ..... // ABCD 的剩馀部份没有改变。
};

对那些并不支援 namespaces(命名空间)的编译器而言,以上目的也可以改用旧有的存取层级来完成(但并不被鼓励):

class Widget: private Counter<Widget> {
public:
    // 让 howMany 成为 public
    Counter<Widget>::howMany;

    ..... // Widget 的剩馀部份没有改变。
};

至此,有必要执行「物件计数」的那些客端程式,以及有必要让该计数器为其客户所用(亦即成为 class 介面的一份子)的classes,必须做两件事情:将 Counter 宣告为一个 base class 并让 howMany( ) 可被取用 [注5]。

然而,继承机制的使用,会导致两个值得注意的情况。第一件事是模棱两可(ambiguity)。假设我们打算对 Widgets 计数,而我们希望让这个计数值供一般运用。一如先前所展示,我们令 Widget 继承自 Counter<Widget>,并令 Widget::howMany( ) 成为 public。 现在假设我们有一个 class SpecialWidget,以 public 方式继承自 Widget,我们希望提供给 SpecialWidget 使用者一如 Widget 使用者所能享受的机能。没问题,只需令 SpecialWidget 继承自 Counter<SpecialWidget> 即可。

但这里出现了模棱两可(ambiguity)的问题。哪一个 howMany( ) 对 SpecialWidget 而言才是可用的呢?是继承自 Widget 的那个,或是继承自 Counter<SpecialWidget> 的那个?我们所希望的,当然是来自 Counter<SpecialWidget> 的那个,但是我们没办法在未明确写出 SpecialWidget::howMany( ) 的情况下说出我们的心愿。幸运的是,它只是一个简单的 inline 函式:

class SpecialWidget: public Widget,
                  private Counter<SpecialWidget> {
public:
    .....
    static size_t howMany()
    { return Counter<SpecialWidget>::howMany(); }
    .....
};

关於「使用继承机制来完成物件计数工作」的第二个意见是,Widget::howMany( ) 传回的值不只包括 Widget objects 的个数,也包括 Widget 衍生类别所产生的 objects。如果 Widget 的唯一衍生类别是 SpecialWidget,而一共有五个 Widget 独立物件和三个 SpecialWidgets独立物件,那麽 Widget::howMany( ) 将传回 8。毕竟,每一个 SpecialWidget 的建构,也同时会完成其基础类别(Widget 成份)的建构。
摘要

以下数点是你需要记住的:
物件计数工作的自动化并不困难,但也并非直观想像中的那麽简单。运用 "Do It For Me" pattern(Coplien 所谓的 "curiously recurring template pattern")便有可能产生正确数量的计数器。运用 private 继承机制,可以提供物件计数能力,而又不扩张物件的大小。
当客端有机会选择「继承自一个 empty class」或「内含某个 class object 做为 data member」时,继承是比较好的选择,因为它允许更紧密的物件。
由於 C++ 尽一切努力要在 heap objects 建构动作失败时避免发生记忆体漏洞(memory leaks),所以凡是需要用到 operator new 之程式码,通常也需要用到对应的 operator delete。
Counter class template 并不在乎你是否继承它,或内含它的一个 object。它看起来都一样。因此,客端可以自由选择使用「继承机制」或「复合(组合)技术」,甚至在同一个应用程式或程式库的不同地点使用不同的策略。
注解与叁考资料

[1] James O. Coplien. "The Column Without a Name: A Curiously Recurring Template Pattern," C++ Report, February 1995.

[2] 另一种方法是忽略 Widget::howMany( ),让客端直接呼叫 Counter<Widget>::howMany( )。然而,对本文目的而言,我们将假设我们希望 howMany( ) 是 Widget 介面的一部份。

[3] Scott Meyers. More Effective C++ (Addison-Wesley, 1996), pp. 113-122.

[4] Nathan Myers. "The Empty Member C++ Optimization," Dr. Dobb''s Journal, August 1997。可自以下网站获得:
http://www.cantrip.org/emptyopt.html.

[5] 只要对这个设计做一点简单的变化,就可以让 Widget 以 Counter<Widget> 计算物件个数,并且不让这个计数值被 Widget 的客户所用,甚至不允许 Counter<Widget>::howMany( ) 被直接呼叫。下面这个练习留给时间充裕的读者:继续讨论更多变化。
进一步的读物

如果想要学习更多关於 new 和 delete 的细节,请阅读 Dan Saks 在 CUJ 1997年一月至七月所主持的专栏,或是我的 More Effective C++ (Addison-Wesley, 1996) 条款8。如果想要更广泛地验证物件计数(object-counting)问题,包括如何限制某个 class 被具现化的次数,请看 More Effective C++ 条款 26。
致谢

Mark Rodgers, Damien Watkins, Marco Dalla Gasperina, 和 Bobby Schmidt 针对本文草稿提出了一些意见。他们的洞见和提议,使本文在许多方面有了更好的改善。
作者

Scott Meyers 是畅销书籍 Effective C++ 第二版和 More Effective C++ 的作者(两本书都由 Addison Wesley 出版)。你可以从 http://www.aristeia.com中找到更多有关於他、他的书、他的那只狗的讯息。译注:《Effective C++》第二版以 Meyers的狗为封面。
译者

陈崴,自由撰稿人,专长 C++/Java/OOP/GP/DP。惯以热情的文字表现冰冷的技术,以冷冽的文字表现深层的关怀。

sidebar : Placement new 与 placement delete

malloc( ) 在 C++ 中的对等物是 operator new,free( ) 在 C++ 中的对等物则是 operator delete。和 malloc( ) 及 free( ) 不同的是是,operator new 和 operator delete 都可以被重载,重载後的版本可接受与母版不同个数、不同型别的叁数。这对 operator new 来说一向正确,但直到最近,才对 operator delete 也成立。

operator new 的正常标记(signature)是:

void * operator new(size_t) throw (std::bad_alloc);

(从现在起,为了简化,我将刻意忽略 exception specifications(译注:就是上述的 throw (std::bad_alloc)),因为它们和我目前要说的重点没有什麽密切关系。)operator new 的重载版本只能增加新叁数,所以一个 operator new 重载版本可能长这个样子:

void * operator new(size_t, void *whereToPutObject)
{ return whereToPutObject; }

这个特殊版本的 operator new 接受一个额外的 void* 引数,指出此函式应该回传什麽指标。由於这个特殊形式在 C++ 标准程式库中是如此常见而有用(宣告於表头档 <new>),因而有了一个属於自己的名称:"placement new"。这个名称表现出其目的:允许程序员指出「一个 object 应该诞生於记忆体何处」。

随着时间过去,任何「要求额外引数」的 operator new 版本,也都渐渐采用 placement new 这个术语。事实上这个术语已经被铭记於 C++ 标准规格中。因此,当 C++ 程序员谈到所谓的 placement new 函式,他们所谈的可能是上述那个「需要额外一个 void* 叁数,用以指出物件置於何处」的版本,但也可能是指那些「所需引数比单一而必要之 size_t 引数更多」的任何 operator new 版本,包括上述函式,也包括其他「引数更多」的 operator new 函式。

换句话说,当我们把焦点集中在记忆体配置时,"placement new" 意味「operator new 的某个版本,接受额外引数」。这个术语在其他场合可能有其他意义,但我们不需继续深入,所以,到此为止。如果你需要更多细节,请叁考本文最後所列的叁考读物。

和 placement new 类似,术语 "placement delete" 意味「operator delete 的某个版本,接受额外引数」。operator delete 的「正常」标记如下:

void operator delete(void*);

所以,任何版本的 operator delete,只要接受的引数多於上述的 void*,就是一个 placement delete 函式。

现在让我们重回本文所讨论的一个主题。当 heap object 在建构期间丢出一个异常,会发生什麽事?再次考虑以下这个简单例子:

class ABCD { ... };
ABCD *p = new ABCD;

假设产生 ABCD object 时导致了一个异常。前列的主文内容指出,如果异常来自 ABCD 建构式,operator delete 会自动被唤起,释放 operator new 所配置的记忆体。但如果 operator new 被多载化,情况将如何?如果不同版本的 operator new 以不同的方式配置记忆体,情况又将如何?operator delete 如何知道该怎麽做才能正确释放记忆体?此外,如果 ABCD object 系以 placement new 产生出来(像下面这样),又该如何:

void *objectBuffer = getPointerToStaticBuffer();
ABCD *p = new (objectBuffer) ABCD; // 在一个静态缓冲区中产生一个 ABCD object

上述那个 placement new 并不配置任何记忆体。它只是传回一个指标,指向那个它所接受的静态缓冲区。也因此,不需要任何释放动作。

很显然,operator delete 所采取的行动(用以回复其对应之 operator new 的行为)必须视配置记忆体时所采用的 operator new 版本而定。

为了让程式员有机会指示「如何回复某个特殊版本之 operator new 的行为」,C++ 标准委员会扩展了 C++,允许 operator delete 也能够被多载化。当 heap object 的 constructor 丢出一个异常,整个游戏便改走另一条路,呼叫起特殊的 operator delete 版本,此一版本带有额外叁数型别,这些型别将对应於先前被唤起之 operator new 版本。

如果没有任何一个版本的 placement delete 的额外叁数能够对应於「被唤起之 placement new 的额外叁数」,那麽,就不会有任何 operator delete 被唤起。於是,operator new 的行为所带来的影响就无法被抹除。对於那些「placement 版」的 operator new,这没问题,因为它们并不真正配置记忆体。然而,一般而言,如果你产生一个自定的「placement 版」的 operator new,你也应该产生一个对应的自定的「placement 版」operator delete。

啊呀,大部份编译器都还没有支援 placement delete。这种编译器所产生出来的程式码,使你几乎总得蒙受一个记忆体漏洞(memory leak)。如果在 heap object 建构期间有一个异常被丢出,因为不会有任何人企图释放 constructor 被唤起之前被配置的记忆体。

译注:根据我的测试,GNU C++ 2.9, Borland C++Builder 40, Microsoft Visual C++ 6.0三家编译器都已经支援 placement 版本的operator delete。其中以 Visual C++ 最为体贴,当「没有任何一个版本的 placement delete 的额外叁数能够对应於被唤起之 placement new 的额外叁数」时,Visual C++ 会给你一个警告讯息:
no matching operator delete found; memory will not be freed if initialization throws an exception. 
C++只在栈或堆上实例化对象 C++如何让类对象只能在堆(栈)上分配空间一般情况下写一个类都是可以采用new在堆上分配空间,或直接采用 类名+对象名 的方式在栈上分配空间。但有时候,如果想让一个类只能在栈上或者堆上分配空间,又该怎么实现呢?下面分别来介绍如何定义只能在栈上或堆上实例化的类。注: 1.静态实例化对象 如 A a;的步骤为:先由编译器为对象在栈空间分配内存,移动栈顶指针,挪出适当的空间,然后在这个空间上调用构造函 阅读详情

相关推荐

C++对象实例化详解:普通对象与指针对象实战

普通对象是 C++ 中最基础的对象实例化形式,通常在栈上创建。这类对象的生命周期由编译器自动管理,其创建和销毁过程清晰可控,适用于大多数局部变量和临时对象的场景。理解普通对象的定义、创建方式及其生命周期管理机制,是掌握 C++ 面向对象编程的关键基础。在 C++ 中,类(class)是用户自定义的数据类型,它封装了数据(成员变量)和操作这些数据的方法(成员函数)。对象则是类的实例,是程序运行时具体的数据实体。在C++编程中,指针对象是实现动态内存管理的核心工具之一。与普通对象不同,指针对象通过。

weixin_42604188的博客 1199

C++ count函数的用法(可以用作统计个数)

algorithm头文件定义了一个count的函数,其功能类似于find。这个函数使用一对迭代器和一个值做参数,返回这个值出现次数的统计结果。 编写程序读取一系列int型数据,并将它们存储到vector对象中,然后统计某个指定的值出现了多少次(代码中向量的那种方式)。   #include &lt;iostream&gt; #include&lt;vector&gt; #include&l...

zhou的博客 7万+

C++对象实例化:普通对象与指针对象的区别及应用

在现代C++编程中,智能指针是管理动态内存的强大工具,它能够帮助开发者避免内存泄漏。智能指针的定义包括了类的模板,这些模板在超出作用域时会自动释放其管理的内存。, 和是 C++11 标准库中引入的三种智能指针类型。// 独占所有权智能指针// 共享所有权智能指针// ...return 0;

weixin_42613360的博客 901

Python练习题:统计总共实例化了多少个对象

需求:有一个计数器(属性),统计总共实例化了多少个对象 代码如下: class Student: school = 'Luffycity' count = 0 def __init__(self, name, age, sex): self.name = name self.age = age self.sex = sex ...

python学习者的博客 2920

磁编码器选型避坑指南:为什么MT6816比霍尔传感器更适合你的伺服系统?

本文深入对比了MT6816磁编码器与传统霍尔传感器在伺服系统中的应用。MT6816基于AMR技术,对磁场方向敏感而非强度,具备高精度、强抗干扰、高安装容差和极低延时等优势,尤其适合高转速、恶劣环境下的工业场景,能有效提升系统可靠性与长期稳定性。

rainy的博客 1102

浮点数加法器设计

一.设计内容及其要求 内容 根据浮点数的运算规则和IEEE754标准,实现浮点数的加法运算器。 要求: 浮点数格式:符号位1位,阶码8位,尾数23位 要求该实验能完整的体现浮点数的运算的五个步骤:对阶、尾数求和、规格化、舍入(要求使用对偶舍入)、溢出判断。 二.基本要素 浮点数 浮点数即实数,实数是所有有理数和无理数的集合。它之所以被称作浮点数,是因为小数点在数中的位置并不是固定的。一个浮点数值分为两部分存储:数值以及小数点在数值中的位置。 计算机中的浮点运算的计算结果一般是不确定的,一块芯片的浮点运算

Phoenix_ZengHao的博客 1万+

c++中统计类实例化的次数(static 的使用以及临时对象的理解)

目录 代码实现: 执行结果 结果分析 代码实现: // ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 // // test.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <iostream> #include <string> using namespa...

仗劍走天涯 1471

搞定汽车座椅电机EMC超标:一个PCB滤波板的实战设计与避坑心得

本文详细介绍了汽车座椅电机EMC超标问题的解决方案,通过PCB滤波板的实战设计,解决了传导发射测试超标问题。文章分享了在空间受限条件下的滤波电路架构设计、接地系统优化及迭代验证过程,为汽车直流电机的EMC整改提供了实用方法论。

weixin_30336061的博客 378

统计实例化对象的次数

为什么80%的码农都做不了架构师?>>> ...

weixin_33950035的博客 546

图片边界矢量化-地理配准

无地理和投影信息的JPG图片,通过ArcGIS进行地理配准(Georeferencing)完成数字化边界。

u012727861的博客 1904

centos7 python3.12.1 报错 No module named _ssl,坑我几个小时

网上的教程很多,但几乎是无效的,跟着我这教程,可以完美解决。

Amio_的博客 1万+

[python]统计类实例化的个数

要统计类实例化的个数,可以在类中定义一个类属性,每次创建实例时对该属性进行加一操作。

qq_42460624的博客 670

zemax显微镜设计

光学显微镜(Optical Microscope,简写OM)是利用光学原理,把人眼所不能分辨的微小物体放大成像,以供人们提取微细结构信息的。我这里是插入进来之后才反转,如果之前已经在目镜的单独文件夹反转过了就不用了。将目镜的焦距设定为25mm,通过缩放,然后将镜头调转过来。镜头数据中的曲率半径、物距像距都设置为变量。将镜头的焦距整体变为16.25mm。将孔径类型设置为按照光阑尺寸浮动。打开物镜的文件,选择插入镜头。由英国物理学家虎克制作。2、像空间孔径0.25。3、像面大小-0.9。

fendoudecheng的博客 8544

C++类与对象:从定义到实例化

本文介绍了C++中类的定义、访问限定符、类域、类的实例化以及this指针等核心概念。

2501_90654211的博客 1725

29、Power BI 中行级安全(RLS)的实现与应用

本文详细介绍了在Power BI中实现行级安全(RLS)的方法,包括静态RLS和动态RLS的实现流程、适用场景及对比分析。通过具体业务场景,展示了如何创建安全角色、定义规则、验证角色、发布模型以及分配成员。此外,还探讨了更复杂的动态RLS场景,如基于父子层次结构的数据访问控制和从外部源获取用户信息的实现方式。文章最后总结了最佳实践建议及未来发展趋势,为企业在Power BI中高效、安全地实施数据访问控制提供了全面指导。

terraform7cloud的博客 195

【openwrt-21.02】T750 openwrt 双系统说明及切换方法

高度集成的 T750 平台大幅减少组件数量,为产品设计者提供更小的尺寸和更低的功耗。T750 采用先进的 7nm 制程,高度集成 5G 调制解调器和四核 Arm CPU,提供较强的功能和配置,设备制造商得以打造精巧的高性能 CPE 产品,如固定无线接入(FWA)路由器和移动热点。3. preloader 加载并启动 A 系统,若 A 系统的任一启动阶段加载失败,preloader 将重新加载并启动 A 系统 3 次,若 3 次均失败,则自动切换至 B 系统,加载并启动 B 系统。A/B 系统切换流程如下。

wgl307293845的博客 1413

C++类模板实例化

C++类模板实例化 文章目录C++类模板实例化1. 简介2. 类模板实例化2.1 隐式实例化(Implicit instantiation)2.2 显示实例化(Explicit instantiation)2.3 类模板中的静态成员 1. 简介 该文章主要总结C++类模板实例化的方法。类模板的实例化包含两种: 隐式实例化(Implicit instantiation) 显示实例化(Explicit instantiation) 2. 类模板实例化 类模板本身不是类型、对象或任何其他实体。仅包含模板定义的

jinzhu1911的专栏 1万+

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

C++的三种实例化对象方式

C++的三种实例化对象方式 2019-05-04 19:15:43d89538239阅读数 385 一、隐式创建 class CEmployee { private: int age; int id; public: CEmployee() { cout << "这是构造函数" << endl; } CEmployee(int id) { this->id...

cxu123321的博客 3065

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

C++类的定义和实例化对象和this指针

struct也可以像class差不多定义类,格式我不多说了直接看代码。

Visual_progress的博客 2145

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

C++ 语法:在类的定义中实例化类的对象

这种方式常用于实现一些复杂的数据结构,如链表中的节点类,节点类中可以有一个指针指向下一个节点(如果下一个节点也是同类型的),在某些情况下,这个指针也可能指向自身来形成特殊的结构,如循环链表。如上述 Car 类中实例化 Engine 对象的方式,将其声明为私有成员变量(private),外部代码不能直接访问这个 Engine 对象,只能通过 Car 类提供的公有方法(如 drive 和 park)来间接操作它,这样可以对内部的 Engine 对象进行良好的封装,符合面向对象编程中数据隐藏的原则。

wgf350357114的博客 1739

C++中计算类对象的大小

计算类对象的大小 计算类对象的大小只需要将类中的成员变量加起来,同时注意内存对齐。 例 class A { public: void SetAInfo(const char* name, char* gender ,char age) { } private: char _name[20]; char _gender[3]; int _age; }; int main() { cout << sizeof(A) << endl; return 0; } 根据结

fiskerzero的博客 753

C++ 实例化对象

实例化对象: 意味着一定有调用构造函数 实例化就是给,数据成员分配内存,构造对象。对象的成员函数和普通函数的区别就是,成员函数有个指向当前对象的this指针,可以访问对象的成员变量,其依赖于对象。静态函数就更像一个全局函数,没有this指针,可以通过类名直接访问,可以被对象的成员函数访问,静态函数只能访问类的静态成员。这些可访问属性是由编译器来实现的,编译之后就没有对象、成员这些东西了,都是数据和方法。 一.在栈区实例化对象 1.调用无参构造函数实例化对象 类型名 --- class/struct

weixin_53272394的博客 2900

C++从入门到实战(六)类和对象(第二部分)C++成员对象及其实例化,对象大小与this详解

在上一篇博客中,我们初步认识了 C++ 类的核心概念:类是数据与操作的封装体,通过访问限定符(public/private/protected)实现数据保护,并利用类域(::)明确成员归属我的个人主页,欢迎来阅读我的其他文章我的C++知识文章专栏欢迎来阅读指出不足本篇我们将深入探索类的实例化过程、对象内存布局以及this指针的奥秘类的成员变量和成员函数在内存中如何存储?为什么空类的对象大小是 1 字节?成员函数如何区分不同对象的数据?实例化概念。

2402_83322742的博客 1528

【C++】类和对象--类,实例化,this指针

前面的几篇文章我们介绍了命名空间,inline,nullptr等C++中常见的的基础概念。今天的文章我们来介绍一些C++中类与对象的内容。在现实世界中,经常有属于同一类的对象。例如,你的自行车只是世界上很多自行车中的一辆。在面向对象软件中,也有很多共享相同特征的不同的对象:矩形、雇用记录、视频剪辑等。可以利用这些对象的相同特征为它们建立一个集合。而这个集合就称为类。类是定义同一类所有对象的变量和方法的蓝图或原型。

tidal_wave的博客 1317

【Cpp】类和对象

类的认识,类和对象(一)

石墨博客 1174
上一篇: 利用C++模板技术支持多种计算策略
下一篇: 虚拟机会让程序更慢吗?
undefined_behavior
博客等级 码龄19年 1粉丝 20原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值