C++ 中计算对象个数 Objects Counting in C++ (C++ User's Journal, 1998/04)

作者:Scott Meyers 译者:陈崴

C++ 中,对某个 class 所产生出来的 objects保持正确的计数,是办得到的,除非你面对一些疯狂份子。

侯捷注:本文系北京《程序员》杂志 2001/08 的文章。译笔顺畅,技术饱满。 承译者陈崴先生与《程序员》杂志负责人蒋涛先生答允, 转载于此,以飨台湾读者,非常感谢。 未得陈崴先生与蒋涛先生二人之同意,任何人请勿将此文再做转载。


译注:本文发表日期比 C++ Standard 发表日期早,但文中内容皆符合 C++ Standard。译文保留原作时态,并未修改。以下是译文所采用的几个特别请读者注意的术语: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; }
???staticsize_t howMany()
???????{ return count; }
?
private:
???staticsize_t count;
};
?
//以下这行仍然应该放在一个实作档中。
size_t Counter::count = 0;

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

//在需要计数的class中内嵌一个Counter object
class Widget {
public:
???.....?// Widget该有的所有public成员,
??????????//都放在这里。
???staticsize_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
class Counter {
public:
???Counter() { ++count; }
???Counter(const Counter&) { ++count; }
???~Counter() { --count; }
?
???static size_t howMany()
???{ return count; }
?
private:
???static size_t count;
};
?
template
size_t
Counter::count = 0; //现在这一行可以放进表头档中了。

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

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

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

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

注意在这两个情况中,我们是如何地将 Counter 取代为 Counter。一如我稍早所说,每一个使用 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 的对象数量,class Counter 可以计算 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 destructorsCounter 应该有这样一个东西吗?良好的 C++ 对象导向设计规范说,是的,它应该有一个 virtual destructor。如果它没有,那么当我们透过一个 base class pointer来删除一个 derived class object 时,会导致不可预期(未有定义)的结果,而且通常是不受欢迎的:

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

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

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

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

template
class Counter {
public:
???.....
private:
???void operator delete(void*);
???.....
};

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

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

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

Counter *pw =
???new Widget;?//这一行应该无法通过编译,
????????????????//因为operator deleteprivate

请回忆一下稍早我对于 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
class Counter {
public:
???Counter();
???Counter(const Counter&);
???~Counter();
?
???static size_t howMany();
private:
???staticsize_t count;
};

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

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

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

使用 Private Inheritance(私有继承)

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

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

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

class Widget:privateCounter
{ ... };
Counter*pw =
???newWidget;?//错误!没有隐式转换函式(implicit conversion)可以
?????????????????//Widget*转为Counter*

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

因此,如果一个 Widget 内含一个 CounterWidget 的大小一定会增加。因为 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:privateCounter {
public:
???//howMany成为public
???using Counter::howMany;
?
???..... // Widget的剩馀部份没有改变。
};
?
class ABCD:privateCounter {
public:
???//howMany成为public
???using Counter::howMany;
?
???..... // ABCD的剩馀部份没有改变。
};

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

class Widget:privateCounter {
public:
???//howMany成为public
???Counter::howMany;
?
???..... // Widget的剩馀部份没有改变。
};

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

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

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

class SpecialWidget:public Widget,
?????????????????private Counter{
public:
???.....
???static size_t howMany()
???{ returnCounter::howMany(); }
???.....
};

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

摘要

以下数点是你需要记住的:

·???????? 对象计数工作的自动化并不困难,但也并非直观想象中的那么简单。运用 "Do It For Me" patternCoplien 所谓的 "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::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 的客户所用,甚至不允许 Counter::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 newfree( ) 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++ 标准程序库中是如此常见而有用(宣告于表头档 ),因而有了一个属于自己的名称:"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.


?附英文原文:http://www.cuj.com/documents/s=8066/cuj9804meyers/

Counting Objects in C++

Scott Meyers

It isn't hard to keep a count of all the objects allocated for a given class in C++, unless you have to deal with distractions.


Sometimes easy things are easy, but they're still subtle. For example, suppose you have a class Widget, and you'd like to have a way to find out at run time how many Widget objects exist. An approach that's both easy to implement and that gives the right answer is to create a static counter in Widget, increment the counter each time a Widget constructor is called, and decrement it whenever the Widget destructor is called. You also need a static member function howMany to report how many Widgets currently exist. If Widget did nothing but track how many of its type exist, it would look more or less like this:

class Widget {
public:
Widget() { ++count; }
Widget(const Widget&) { ++count; }
~Widget() { --count; }
static size_t howMany()
{ return count; }
private:
static size_t count;
};
// obligatory definition of count. This
// goes in an implementation file
size_t Widget::count = 0;

This works fine. The only mildly tricky thing is to remember to implement the copy constructor, because a compiler-generated copy constructor for Widget wouldn't know to increment count.

If you had to do this only for Widget, you'd be done, but counting objects is something you might want to implement for several classes. Doing the same thing over and over gets tedious, and tedium leads to errors. To forestall such tedium, it would be best to somehow package the above object-counting code so it could be reused in any class that wanted it. The ideal package would:

  • be easy to use — require minimal work on the part of class authors who want to use it. Ideally, they shouldn't have to do more than one thing, that is, more than basically say "I want to count the objects of this type."
  • be efficient — impose no unnecessary space or time penalties on client classes employing the package.
  • be foolproof — be next to impossible to accidently yield a count that is incorrect. (We're not going to worry about malicious clients, ones who deliberately try to mess up the count. In C++, such clients can always find a way to do their dirty deeds.)

Stop for a moment and think about how you'd implement a reusable object-counting package that satisfies the goals above. It's probably harder than you expect. If it were as easy as it seems like it should be, you wouldn't be reading an article about it in this magazine.

new, delete, and Exceptions

While you're mulling over your solution to the object-counting problem, allow me to switch to what seems like an unrelated topic. That topic is the relationship between new and delete when constructors throw exceptions. When you ask C++ to dynamically allocate an object, you use a new expression, as in:

class ABCD { ... }; // ABCD = "A Big Complex Datatype"
ABCD *p = new ABCD; // a new expression

The new expression — whose meaning is built into the language and whose behavior you cannot change — does two things. First, it calls a memory allocation function called operator new. That function is responsible for finding enough memory to hold an ABCD object. If the call to operator new succeeds, the new expression then invokes an ABCD constructor on the memory that operator new found.

But suppose operator new throws a std::bad_alloc exception. Exceptions of this type indicate that an attempt to dynamically allocate memory has failed. In the new expression above, there are two functions that might give rise to that exception. The first is the invocation of operator new that is supposed to find enough memory to hold an ABCD object. The second is the subsequent invocation of the ABCD constructor that is supposed to turn the raw memory into a valid ABCD object.

If the exception came from the call to operator new, no memory was allocated. However, if the call to operator new succeeded and the invocation of the ABCD constructor led to the exception, it is important that the memory allocated by operator new be deallocated. If it's not, the program has a memory leak. It's not possible for the client — the code requesting creation of the ABCD object — to determine which function gave rise to the exception.

For many years this was a hole in the draft C++ language specification, but in March 1995 the C++ Standards committee adopted the rule that if, during a new expression, the invocation of operator new succeeds and the subsequent constructor call throws an exception, the runtime system must automatically deallocate the memory that operator new allocated. This deallocation is performed by operator delete, the deallocation analogue of operator new. (For details, see the sidebar on placement new and placement delete.)

It is this relationship between new expressions and operator delete affects us in our attempt to automate the counting of object instantiations.

Counting Objects

In all likelihood, your solution to the object-counting problem involved the development of an object-counting class. Your class probably looks remarkably like, perhaps even exactly like, the Widget class I showed earlier:

// see below for a discussion of why
// this isn't quite right
class Counter {
public:
Counter() { ++count; }
Counter(const Counter&) { ++count; }
~Counter() { --count; }
static size_t howMany()
{ return count; }
private:
static size_t count;
};
// This still goes in an
// implementation file
size_t Counter::count = 0;

The idea here is that authors of classes that need to count the number of objects in existence simply use Counter to take care of the bookkeeping. There are two obvious ways to do this. One way is to define a Counter object as a class data member, as in:

// embed a Counter to count objects
class Widget {
public:
.....  // all the usual public
// Widget stuff
static size_t howMany()
{ return Counter::howMany(); }
private:
.....  // all the usual private
// Widget stuff
Counter c;
};

The other way is to declare Counter as a base class, as in:

// inherit from Counter to count objects
class Widget: public Counter {
.....  // all the usual public
// Widget stuff
private:
.....  // all the usual private
// Widget stuff
};

Both approaches have advantages and disadvantages. But before we examine them, we need to observe that neither approach will work in its current form. The problem has to do with the static object count inside Counter. There's only one such object, but we need one for each class using Counter. For example, if we want to count both Widgets and ABCDs, we need two static size_t objects, not one. Making Counter::count nonstatic doesn't solve the problem, because we need one counter per class, not one counter per object.

We can get the behavior we want by employing one of the best-known but oddest-named tricks in all of C++: we turn Counter into a template, and each class using Counter instantiates the template with itself as the template argument.

Let me say that again. Counter becomes a template:

template
class Counter {
public:
Counter() { ++count; }
Counter(const Counter&) { ++count; }
~Counter() { --count; }
static size_t howMany()
{ return count; }
private:
static size_t count;
};
template
size_t
Counter::count = 0; // this now can go in header

The first Widget implementation choice now looks like:

// embed a Counter to count objects
class Widget {
public:
.....
static size_t howMany()
{return Counter::howMany();}
private:
.....
Counter c;
};

And the second choice now looks like:

// inherit from Counter to count objects
class Widget: public Counter {
.....
};

Notice how in both cases we replace Counter with Counter. As I said earlier, each class using Counter instantiates the template with itself as the argument.

The tactic of a class instantiating a template for its own use by passing itself as the template argument was first publicized by Jim Coplien. He showed that it's used in many languages (not just C++) and he called it "a curiously recurring template pattern" [1]. I don't think Jim intended it, but his description of the pattern has pretty much become its name. That's too bad, because pattern names are important, and this one fails to convey information about what it does or how it's used.

The naming of patterns is as much art as anything else, and I'm not very good at it, but I'd probably call this pattern something like "Do It For Me." Basically, each class generated from Counter provides a service (it counts how many objects exist) for the class requesting the Counter instantiation. So the class Counter counts Widgets, and the class Counter counts ABCDs.

Now that Counter is a template, both the embedding design and the inheritance design will work, so we're in a position to evaluate their comparative strengths and weaknesses. One of our design criteria was that object-counting functionality should be easy for clients to obtain, and the code above makes clear that the inheritance-based design is easier than the embedding-based design. That's because the former requires only the mentioning of Counter as a base class, whereas the latter requires that a Counter data member be defined and that howMany be reimplemented by clients to invoke Counter's howMany [2]. That's not a lot of additional work (client howManys are simple inline functions), but having to do one thing is easier than having to do two. So let's first turn our attention to the design employing inheritance.

Using Public Inheritance

The design based on inheritance works because C++ guarantees that each time a derived class object is constructed or destroyed, its base class part will also be constructed first and destroyed last. Making Counter a base class thus ensures that a Counter constructor or destructor will be called each time a class inheriting from it has an object created or destroyed.

Any time the subject of base classes comes up, however, so does the subject of virtual destructors. Should Counter have one? Well-established principles of object-oriented design for C++ dictate that it should. If it has no virtual destructor, deletion of a derived class object via a base class pointer yields undefined (and typically undesirable) results:

class Widget: public Counter
{ ... };
Counter *pw =
new Widget;  // get base class ptr
// to derived class object
......
delete pw; // yields undefined results
// if the base class lacks
// a virtual destructor

Such behavior would violate our criterion that our object-counting design be essentially foolproof, because there's nothing unreasonable about the code above. That's a powerful argument for giving Counter a virtual destructor.

Another criterion, however, was maximal efficiency (imposition of no unnecessary speed or space penalty for counting objects), and now we're in trouble. We're in trouble because the presence of a virtual destructor (or any virtual function) in Counter means each object of type Counter (or a class derived from Counter) will contain a (hidden) virtual pointer, and this will increase the size of such objects if they don't already support virtual functions [3]. That is, if Widget itself contains no virtual functions, objects of type Widget would increase in size if Widget started inheriting from Counter. We don't want that.

The only way to avoid it is to find a way to prevent clients from deleting derived class objects via base class pointers. It seems that a reasonable way to achieve this is to declare operator delete private in Counter:

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

Now the delete expression won't compile:

class Widget: public Counter { ... };
Counter *pw = new Widget;  ......
delete pw; // Error. Can't call private
// operator delete

Unfortunately — and this is the really interesting part — the new expression shouldn't compile either!

Counter *pw =
new Widget;  // this should not
// compile because
// operator delete is
// private

Remember from my earlier discussion of new, delete, and exceptions that C++'s runtime system is responsible for deallocating memory allocated by operator new if the subsequent constructor invocation fails. Recall also that operator delete is the function called to perform the deallocation. But we've declared operator delete private in Counter, which makes it invalid to create objects on the heap via new!

Yes, this is counterintuitive, and don't be surprised if your compilers don't yet support this rule, but the behavior I've described is correct. Furthermore, there's no other obvious way to prevent deletion of derived class objects via Counter* pointers, and we've already rejected the notion of a virtual destructor in Counter. So I say we abandon this design and turn our attention to using a Counter data member instead.

Using a Data Member

We've already seen that the design based on a Counter data member has one drawback: clients must both define a Counter data member and write an inline version of howMany that calls the Counter's howMany function. That's marginally more work than we'd like to impose on clients, but it's hardly unmanageable. There is another drawback, however. The addition of a Counter data member to a class will often increase the size of objects of that class type.

At first blush, this is hardly a revelation. After all, how surprising is it that adding a data member to a class makes objects of that type bigger? But blush again. Look at the definition of Counter:

template
class Counter {
public:
Counter();
Counter(const Counter&);
~Counter();
static size_t howMany();
private:
static size_t count;
};

Notice how it has no nonstatic data members. That means each object of type Counter contains nothing. Might we hope that objects of type Counter have size zero? We might, but it would do us no good. C++ is quite clear on this point. All objects have a size of at least one byte, even objects with no nonstatic data members. By definition, sizeof will yield some positive number for each class instantiated from the Counter template. So each client class containing a Counter object will contain more data than it would if it didn't contain the Counter.

(Interestingly, this does not imply that the size of a class without a Counter will necessarily be bigger than the size of the same class containing a Counter. That's because alignment restrictions can enter into the matter. For example, if Widget is a class containing two bytes of data but that's required to be four-byte aligned, each object of type Widget will contain two bytes of padding, and sizeof(Widget) will return 4. If, as is common, compilers satisfy the requirement that no objects have zero size by inserting a char into Counter, it's likely that sizeof(Widget) will still yield 4 even if Widget contains a Counter object. That object will simply take the place of one of the bytes of padding that Widget already contained. This is not a terribly common scenario, however, and we certainly can't plan on it when designing a way to package object-counting capabilities.)

I'm writing this at the very beginning of the Christmas season. (It is in fact Thanksgiving Day, which gives you some idea of how I celebrate major holidays...) Already I'm in a Bah Humbug mood. All I want to do is count objects, and I don't want to haul along any extra baggage to do it. There has got to be a way.

Using Private Inheritance

Look again at the inheritance-based code that led to the need to consider a virtual destructor in Counter:

class Widget: public Counter
{ ... };
Counter *pw = new Widget;
......
delete
pw;  // yields undefined results
// if Counter lacks a virtual
// destructor

Earlier we tried to prevent this sequence of operations by preventing the delete expression from compiling, but we discovered that that also prohibited the new expression from compiling. But there is something else we can prohibit. We can prohibit the implicit conversion from a Widget* pointer (which is what new returns) to a Counter* pointer. In other words, we can prevent inheritance-based pointer conversions. All we have to do is replace the use of public inheritance with private inheritance:

class Widget: private Counter
{ ... };
Counter *pw =
new Widget;  // error! no implicit
// conversion from
// Widget* to
// Counter*

Furthermore, we're likely to find that the use of Counter as a base class does not increase the size of Widget compared to Widget's stand-alone size. Yes, I know I just finished telling you that no class has zero size, but — well, that's not really what I said. What I said was that no objects have zero size. The C++ Standard makes clear that the base-class part of a more derived object may have zero size. In fact many compilers implement what has come to be known as the empty base optimization [4].

Thus, if a Widget contains a Counter, the size of the Widget must increase. The Counter data member is an object in its own right, hence it must have nonzero size. But if Widget inherits from Counter, compilers are allowed to keep Widget the same size it was before. This suggests an interesting rule of thumb for designs where space is tight and empty classes are involved: prefer private inheritance to containment when both will do.

This last design is nearly perfect. It fulfills the efficiency criterion, provided your compilers implement the empty base optimization, because inheriting from Counter adds no per-object data to the inheriting class, and all Counter member functions are inline. It fulfills the foolproof criterion, because count manipulations are handled automatically by Counter member functions, those functions are automatically called by C++, and the use of private inheritance prevents implicit conversions that would allow derived-class objects to be manipulated as if they were base-class objects. (Okay, it's not totally foolproof: Widget's author might foolishly instantiate Counter with a type other than Widget, i.e., Widget could be made to inherit from Counter. I choose to ignore this possibility.)

The design is certainly easy for clients to use, but some may grumble that it could be easier. The use of private inheritance means that howMany will become private in inheriting classes, so such classes must include a using declaration to make howMany public to their clients:

class Widget: private Counter {
public:
// make howMany public
using Counter::howMany;
..... // rest of Widget is unchanged
};
class ABCD: private Counter {
public:
// make howMany public
using Counter::howMany;
..... // rest of ABCD is unchanged
};

For compilers not supporting namespaces, the same thing is accomplished by replacing the using declaration with the older (now deprecated) access declaration:

class Widget: private Counter {
public:
// make howMany public
Counter::howMany;
.....  // rest of Widget is unchanged
};

Hence, clients who want to count objects and who want to make that count available (as part of their class's interface) to their clients must do two things: declare Counter as a base class and make howMany accessible [5].

The use of inheritance does, however, lead to two conditions that are worth noting. The first is ambiguity. Suppose we want to count Widgets, and we want to make the count available for general use. As shown above, we have Widget inherit from Counter and we make howMany public in Widget. Now suppose we have a class SpecialWidget publicly inherit from Widget and we want to offer SpecialWidget clients the same functionality Widget clients enjoy. No problem, we just have SpecialWidget inherit from Counter.

But here is the ambiguity problem. Which howMany should be made available by SpecialWidget, the one it inherits from Widget or the one it inherits from Counter? The one we want, naturally, is the one from Counter, but there's no way to say that without actually writing SpecialWidget::howMany. Fortunately, it's a simple inline function:

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

The second observation about our use of inheritance to count objects is that the value returned from Widget::howMany includes not just the number of Widget objects, it includes also objects of classes derived from Widget. If the only class derived from Widget is SpecialWidget and there are five stand-alone Widget objects and three stand-alone SpecialWidgets, Widget::howMany will return eight. After all, construction of each SpecialWidget also entails construction of the base Widget part.

Summary

The following points are really all you need to remember:

  • Automating the counting of objects isn't hard, but it's not completely straightforward, either. Use of the "Do It For Me" pattern (Coplien's "curiously recurring template pattern") makes it possible to generate the correct number of counters. The use of private inheritance makes it possible to offer object-counting capabilities without increasing object sizes.
  • When clients have a choice between inheriting from an empty class or containing an object of such a class as a data member, inheritance is preferable, because it allows for more compact objects.
  • Because C++ endeavors to avoid memory leaks when construction fails for heap objects, code that requires access to operator new generally requires access to the corresponding operator delete too.
  • The Counter class template doesn't care whether you inherit from it or you contain an object of its type. It looks the same regardless. Hence, clients can freely choose inheritance or containment, even using different strategies in different parts of a single application or library. o

Notes and References

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

[2] An alternative is to omit Widget::howMany and make clients call Counter::howMany directly. For the purposes of this article, however, we'll assume we want howMany to be part of the Widget interface.

[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. Also available at http://www.cantrip.org/emptyopt.html.

[5] Simple variations on this design make it possible for Widget to use Counter to count objects without making the count available to Widget clients, not even by calling Counter::howMany directly. Exercise for the reader with too much free time: come up with one or more such variations.

Further Reading

To learn more about the details of new and delete, read the columns by Dan Saks on the topic (CUJ January - July 1997), or Item 8 in my More Effective C++ (Addison-Wesley, 1996). For a broader examination of the object-counting problem, including how to limit the number of instantiations of a class, consult Item 26 of More Effective C++.

Acknowledgments

Mark Rodgers, Damien Watkins, Marco Dalla Gasperina, and Bobby Schmidt provided comments on drafts of this article. Their insights and suggestions improved it in several ways.

Scott Meyers authored the best-selling Effective C++, Second Edition and More Effective C++ (both published by Addison Wesley). Find out more about him, his books, his services, and his dog at http://www.aristeia.com.

Counting Objects in C++相关推荐

  1. git clone项目文件时报错解决remote: Enumerating objects: 19, done. remote: Counting objects: 100% (19/19), don

    操作: git clone https://github.com/... 1 报错: remote: Enumerating objects: 19, done. remote: Counting o ...

  2. 人车密度估计--Towards perspective-free object counting with deep learning

    Towards perspective-free object counting with deep learning ECCV2016 https://github.com/gramuah/ccnn ...

  3. Git 常用操作(6)- 推送到远程仓库(git push)删除远程分支(git push origin --delete)

    1. git remote add--添加远程仓库 在GitHub 上创建的仓库路径为 "git@github.com:用户名/git-tutorial.git".现在我们用git ...

  4. Git 常用操作(5)- git clone/git checkout -b/git diff/git push/git pull

    1. git clone--获取远程仓库 当 git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容.它只会获取数据然后 让你自己合并. 然而,有一个命令叫作 git p ...

  5. Git 常用操作(2)- 创建标签

    1. 列出标签 在 Git 中列出已有的标签是非常简单直观的.只需要输入 git tag: $ git tag v0.1 v1.3 2. 创建标签 Git 使用两种主要类型的标签:轻量标签(light ...

  6. Gitea——私有git服务器搭建详细教程

    本文将从源代码和docker安装两种方式带大家从0-1通过Gitea搭建一个私有git服务器 Gitea--私有git服务器搭建教程 什么是Gitea 一.源代码安装方式 1. 前置环境要求 2. 下 ...

  7. git ssh创建分支_Git(2):在gitlab中创建开发用户,以及master分支的安全管理

    一.创建用户 1.创建管理gitlab的开发人员的用户 2.配置用户信息 3.将用户添加到java-daem组中 4.用户登录成功后,在用户界面为用户添加ssh认证 5.在linux主机中将maste ...

  8. Git详解之九 Git内部原理

    以下内容转载自:http://www.open-open.com/lib/view/open1328070620202.html Git 内部原理 不管你是从前面的章节直接跳到了本章,还是读完了其余各 ...

  9. 【go】sdk + idea-plugin 开发工具安装

    http://golang.org/doc/install/source 第一步:windows 安装 git第二步$ git clone https://go.googlesource.com/go ...

最新文章

  1. JavaScript之面向对象学习三原型语法升级
  2. 【PAT】A1028 List Sorting
  3. 在Windows2012R2中如何安装IIS8.5
  4. python实现条件匹配_python3 re如何匹配满足条件的选项?
  5. lintcode-137-克隆图
  6. 费诺编码的gui页面设计_GUI设计和UI设计有什么区别?
  7. nginx html目录 404 盘符_nginx配置场景分析 location /
  8. Golang sort 包使用
  9. 拓端tecdat|sas神经网络:构建人工神经网络模型来识别垃圾邮件
  10. 转:requirejs2.0新特性介绍
  11. “仿QQ局域网聊天软件”项目-常用编程技巧总结
  12. 12款开源或免费的3D建模软件
  13. 基于asp.net075丰田汽车4S店销售管理系统
  14. 华硕aura完全卸载_这把键盘,或是顶级光轴键盘!华硕TUF GAMING K7光轴机械键盘...
  15. CAN总线协议以及概念
  16. Oracle查数据库某字段的本年数,上年同期数,同比
  17. 常见对称加密、解密、破解
  18. lsdyna如何设置set中的node_list_如何监视 DOM 树的变动?
  19. 听课记录(09/22)
  20. 如何避免用户“漫天要价”和“就地还钱”

热门文章

  1. flutter仿微信ui
  2. java swing awt绘制一个图片查看器 图片显示 图片控件
  3. 河北画报杂志河北画报杂志社河北画报编辑部2023年第8期目录
  4. “项目冲刺”博客——第二篇
  5. Java Swing学习
  6. 哈希函数的特征_哈希函数及其特征
  7. flash打造视频照相系列教程
  8. 如何使您的网站可访问:WCAG 2.1 W3C建议
  9. python制作聊天机器人_Python实现聊天机器人
  10. Verilog HDL 基本要素(2)