简体中文版翻译:申旻,nicrosoft@sunistudio.com(东日制作室,东日文档)
有,是基于标准库有一个 std::vector 模板可以提供这种行为的认识。
没有,是基于内建数组类型需要在编译期指定其长度的认识。
有,是基于即使对于内建数组类型也可以在运行期指定第一维索引边界的认识。例如,看一下前一个FAQ,如果你只需要数组的第一维的维数具有灵活性,你可以申请一个新的数组的数组,而不是一个指向多个数组的指针数组:
const unsigned ncols = 100; // ncols = 数组的列数 class Fred { /*...*/ }; void manipulateArray(unsigned nrows) // nrows = 数组的行数 { Fred (*matrix)[ncols] = new Fred[nrows][ncols]; // ... delete[] matrix; }
如果你所需要的不是在运行期改变数组的第一维维数,则不能这么做。
但非万不得已,不要用数组。因为数组是会带来麻烦的。如果可以的话,使用某些类的对象。万不得已才用数组。
[ Top | Bottom | Previous section | Next section ]
使用命名的构造函数用法。
就如命名的构造函数用法的通常做法,所有构造函数是private: 或protected:,且有一个或多个public static create()方法(因此称为“命名的构造函数,named constructors”),每个构造函数对应一个。此时, create() 方法通过 new 来分配对象。由于构造函数本身都不是public,因此没有其他方法来创建该类的对象。
class Fred { public: // create() 方法就是 "命名的构造函数,named constructors": static Fred* create() { return new Fred(); } static Fred* create(int i) { return new Fred(i); } static Fred* create(const Fred& fred) { return new Fred(fred); } // ... private: // 构造函数本身是 private 或 protected: Fred(); Fred(int i); Fred(const Fred& fred); // ... };
这样,创建 Fred 对象的唯一方法就是通过 Fred::create():
int main() { Fred* p = Fred::create(5); // ... delete p; }
如果你希望 Fred有派生类,则须确认构造函数在 protected: 节中。
注意,如果你想允许Fred类的对象成为Wilma类的成员,可以把Wilma 作为 Fred 的友元。当然,这样会软化最初的目标,也就是强迫 Fred 对象总是通过 new 来分配。
[ Top | Bottom | Previous section | Next section ]
如果你所需要的只是分发指向同一个对象的多个指针,并且当最后一个指针消失的时候能自动释放该对象的能力的话,你可以使用类似如下的“只能指针(smart pointer)”类:
// Fred.h class FredPtr; class Fred { public: Fred() : count_(0) /*...*/ { } // 所有的构造函数都要设置 count_ to 0 ! // ... private: friend FredPtr; // 友元类 unsigned count_; // count_ 必须被所有构造函数初始化 // count_ 就是指向 this的对FredPtr象数目 }; class FredPtr { public: Fred* operator-> () { return p_; } Fred& operator* () { return *p_; } FredPtr(Fred* p) : p_(p) { ++p_->count_; } // p 不能为 NULL ~FredPtr() { if (--p_->count_ == 0) delete p_; } FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; } FredPtr& operator= (const FredPtr& p) { // 不要改变这些语句的顺序! // (如此的顺序适当的处理了自赋值) ++p.p_->count_; if (--p_->count_ == 0) delete p_; p_ = p.p_; return *this; } private: Fred* p_; // p_ 永远不为 NULL };
自然,你可以使用嵌套类,将FredPtr改名为Fred::Ptr。
注意,在构造函数,拷贝构造函数,赋值算符和析构函数中增加一点检查,就可以软化上面的“不远不为 NULL”的规则。如果你这样做的话,可能倒不如在“*”和“->”算符中放入一个p_ != NULL检查(至少是一个 assert())。我不推荐operator Fred*() ,因为它可能让人们意外地取得Fred*。
FredPtr的隐含约束之一是它可能指向通过 new分配的Fred对象。如果要真正的安全,可以使所有的Fred构造函数成为private,为每个构造函数加一个用new来分配Fred 对象且返回一个FredPtr (不是Fred*)的public (static) create() 方法来加强这个约束。这种办法是创建Fred对象而得到一个FredPtr的唯一办法(“Fred* p = new Fred()”会被“FredPtr p = Fred::create()”取代)。这样就没人会意外破坏引用计数的机制了。
例如,如果Fred有一个Fred::Fred() 和一个Fred::Fred(int i, int j),class Fred 会变成:
class Fred { public: static FredPtr create(); // 定义如下的 class FredPtr {...} static FredPtr create(int i, int j); // 定义如下的 class FredPtr {...} // ... private: Fred(); Fred(int i, int j); // ... }; class FredPtr { /* ... */ }; inline FredPtr Fred::create() { return new Fred(); } inline FredPtr Fred::create(int i, int j) { return new Fred(i,j); }
最终结果是你现在有了一种办法来使用简单的引用计数为给出的对象提供“指针语义(pointer semantics)”。Fred类的用户明确地使用FredPtr 对象,它或多或少的类似Fred*指针。这样做的好处是用户可以建立多个FredPtr“智能指针”对象的拷贝,当最后一个FredPtr对象消失时,它所指向的 Fred 对象会被自动释放。
如果你希望给用户以“引用语义”而不是“指针语义”的话,可以使用引用计数提供“写时拷贝(copy on write)”<!--rawtext:[16.22]:rawtext-->。
[ Top | Bottom | Previous section | Next section ]
引用计数可以由指针语义或引用语义完成。前一个FAQ显示了如何使用指针语义进行引用计数。本FAQ将显示如何使用引用语义进行引用计数。
基本思想是允许用户认为他们在复制Fred对象,但实际上真正的实现并不进行复制,直到一些用户试图修改隐含的Fred 对象才进行真正的复制。
Fred::Data类装载了Fred 类所有的数据。 Fred::Data也有一个额外的成员count_,来管理引用计数。Fred 类最后成了一个指向Fred::Data的“智能指针”(内部的)。
class Fred { public: Fred(); // 默认构造函数 Fred(int i, int j); // 普通的构在函数 Fred(const Fred& f); Fred& operator= (const Fred& f); ~Fred(); void sampleInspectorMethod() const; // this 对象不会变 void sampleMutatorMethod(); // 会改变 this o对象 // ... private: class Data { public: Data(); Data(int i, int j); Data(const Data& d); // 由于只有 Fred 能访问 Fred::Data 对象, // 只要你愿意,你可以使得 Fred::Data的数据为 public, // 但如果那样使你不爽,就把数据作为 private // 还要用friend Fred;使 Fred 成为友元类 // ... unsigned count_; // count_ 是指向的this的Fred 对象的数目 // count_ m必须被所有的构造函数初始化为 1 // (从 1 开始是因为它被创建它的Fred 对象所指) }; Data* data_; }; Fred::Data::Data() : count_(1) /*初始化其他数据*/ { } Fred::Data::Data(int i, int j) : count_(1) /*初始化其他数据*/ { } Fred::Data::Data(const Data& d) : count_(1) /*初始化其他数据*/ { } Fred::Fred() : data_(new Data()) { } Fred::Fred(int i, int j) : data_(new Data(i, j)) { } Fred::Fred(const Fred& f) : data_(f.data_) { ++ data_->count_; } Fred& Fred::operator= (const Fred& f) { // 不要更该这些语句的顺序! // (如此的顺序适当地处理了自赋值) ++ f.data_->count_; if (--data_->count_ == 0) delete data_; data_ = f.data_; return *this; } Fred::~Fred() { if (--data_->count_ == 0) delete data_; } void Fred::sampleInspectorMethod() const { // 该方法承诺 (“const”) 不改变 *data_中的任何东西 // 除此以外,任何数据访问将简单地使用“data_->...” } void Fred::sampleMutatorMethod() { // 该方法可能需要改变 *data_中的数据 // 因此首先检查this是否唯一的指向 *data_ if (data_->count_ > 1) { Data* d = new Data(*data_); // 调用 Fred::Data的拷贝构造函数 -- data_->count_; data_ = d; } assert(data_->count_ == 1); // 现在该方法如常进行“data_->...”的访问 }
如果非常经常地调用 Fred 的默认构造函数,你可以为所有通过Fred::Fred()构造的Fred 共享一个公共的Fred::Data 对象来消除那些 new调用。为避免静态初始化顺序问题,该共享的 Fred::Data 对象在一个函数内“首次使用”时才创建。如下就是对以上的代码做的改变(注意,该共享的Fred::Data对象的析构函数永远不会被调用;如果这成问题的话,要么解决静态初始化顺序的问题,要么索性返回到如上描述的方法):
class Fred { public: // ... private: // ... static Data* defaultData(); }; Fred::Fred() : data_(defaultData()) { ++ data_->count_; } Fred::Data* Fred::defaultData() { static Data* p = NULL; if (p == NULL) { p = new Data(); ++ p->count_; // 确保它不会成为 0 } return p; }
注意:如果 Fred 通常作为基类的话,也可以为类层次提供引用计数。
[ Top | Bottom | Previous section | Next section ]
前一个FAQ给出了引用语义的引用计数策略,但迄今为止都针对单个类而不是分层次的类。本FAQ扩展之前的技术以允许为类层次提供引用计数。基本不同之处在于现在Fred::Data是类层次的根,着可能使得它有一些虚函数。注意 Fred 类本身仍然没有任何的虚函数。
虚构造函数用法用来建立 Fred::Data 对象的拷贝。要选择创建哪个派生类,如下的示例代码使用了命名构造函数用法,但还有其它技术(构造函数中加一个switch语句等)。示例代码假设了两个派生类:Der1和Der2。派生类的方法并不查觉引用计数。
class Fred { public: static Fred create1(const std::string& s, int i); static Fred create2(float x, float y); Fred(const Fred& f); Fred& operator= (const Fred& f); ~Fred(); void sampleInspectorMethod() const; // this 对象不会被改变 void sampleMutatorMethod(); // 会改变 this 对象 // ... private: class Data { public: Data() : count_(1) { } Data(const Data& d) : count_(1) { } // 不要拷贝 'count_' 成员! Data& operator= (const Data&) { return *this; } // 不要拷贝 'count_' 成员! virtual ~Data() { assert(count_ == 0); } // 虚析构函数 virtual Data* clone() const = 0; // 虚构造函数 virtual void sampleInspectorMethod() const = 0; // 纯虚函数 virtual void sampleMutatorMethod() = 0; private: unsigned count_; // count_ 不需要是 protected 的 friend Fred; // 允许Fred 访问 count_ }; class Der1 : public Data { public: Der1(const std::string& s, int i); virtual void sampleInspectorMethod() const; virtual void sampleMutatorMethod(); virtual Data* clone() const; // ... }; class Der2 : public Data { public: Der2(float x, float y); virtual void sampleInspectorMethod() const; virtual void sampleMutatorMethod(); virtual Data* clone() const; // ... }; Fred(Data* data); // 创建一个拥有 *data 的 Fred 智能引用 // 它是 private 的以迫使用户使用 createXXX() 方法 // 要求:data 必能为 NULL Data* data_; // Invariant: data_ is never NULL }; Fred::Fred(Data* data) : data_(data) { assert(data != NULL); } Fred Fred::create1(const std::string& s, int i) { return Fred(new Der1(s, i)); } Fred Fred::create2(float x, float y) { return Fred(new Der2(x, y)); } Fred::Data* Fred::Der1::clone() const { return new Der1(*this); } Fred::Data* Fred::Der2::clone() const { return new Der2(*this); } Fred::Fred(const Fred& f) : data_(f.data_) { ++ data_->count_; } Fred& Fred::operator= (const Fred& f) { // 不要更该这些语句的顺序! // (如此的顺序适当地处理了自赋值) ++ f.data_->count_; if (--data_->count_ == 0) delete data_; data_ = f.data_; return *this; } Fred::~Fred() { if (--data_->count_ == 0) delete data_; } void Fred::sampleInspectorMethod() const { // 该方法承诺 ("const") 不改变*data_中的任何东西 // 因此我们只要“直接把方法传递”给 *data_: data_->sampleInspectorMethod(); } void Fred::sampleMutatorMethod() { // 该方法可能需要更该 *data_中的数据 // 因此首先检查this 是否唯一的指向*data_ if (data_->count_ > 1) { Data* d = data_->clone(); // 虚构造函数用法 -- data_->count_; data_ = d; } assert(data_->count_ == 1); // 现在“直接把方法传递给” *data_: data_->sampleInspectorMethod(); }
自然,Fred::Der1 和Fred::Der2 的构造函数和sampleXXX方法将需要被以某种途径适当的实现。
[ Top | Bottom | Previous section | Next section ]
不能,(通常)不会。
有两个基本的办法破坏引用计数机制:
如果某人获得了Fred* (而不是别强制使用的FredPtr),该策略就会被破坏。如果FredPtr类有返回一个 Fred&的operator*()的话,就可能得到Fred*:FredPtr p = Fred::create(); Fred* p2 = &*p;。是的,那是奇异的、不被预期的,但它可能发生。该漏洞有两个方法弥补:重载Fred::operator&()使它返回一个FredPtr,或改变FredPtr::operator*()的返回类型,使它返回一个FredRef(FredRef是一个模拟引用的类;它需要拥有Fred所拥有的所有方法,并且需要将这些方法的调用转送给隐含的Fred对象;第二种选择可能成为性能瓶颈,这取决于编译器在内联方法中的表现)。另一个方法是消除 FredPtr::operator*() ——相应的会失去取得和使用 Fred& 的能力。但即使你这样做了,某些人仍然可以通过显式的调用 operator->(): FredPtr p = Fred::create(); Fred* p2 = p.operator->();来取得一个Fred* 。 如果某人有一个泄漏的和/或悬空的FredPtr指针的话,该策略会被破坏。基本上我们说Fred是安全的,但我们无法阻止别人对FredPtr 对象做傻事。(并且如果我们可以通过FredPtrPtr对象来解决的话,则对于FredPtrPtr仍然有相同的问题)。这里的一个漏洞是如果某人使用 new 创建了一个FredPtr ,然后FredPtr就可能有泄漏(这里最糟的情况是有泄漏,但通常还是比悬空指针要好一点点)。该漏洞可以通过将FredPtr::operator new() 声明为private来弥补,从而防止 new FredPtr()。此处另一个漏洞是如果某人创建了一个局部的FredPtr对象,则可取得FredPtr的地址并传递给FredPtr*。如果FredPtr*生存期比FredPtr更长,就可能成为悬空指针——颤抖的指针。该漏洞可以通过防止取得 FredPtr的地址来弥补(重载FredPtr::operator&()为private),相应的会损失一些功能。但即使你这样做了,他们只要这样做:FredPtr p; ... FredPtr& q = p;(或者将FredPtr&传递其它什么),仍然可以创建 FredPtr*与一样危险的FredPtr&。并且,即使我们弥补了所有那些漏洞,C++ 还有奇妙的称为指针转换(pointer cast)的语法。使用一两个指针转换,一个有意的程序员可以创造一个大得足以穿过一辆卡车的漏洞。
此处的教训是:(a) 无论你多么的智者千虑,也不可能防止间谍,(b) 你可以简单的防止错误。
因此我建议:用易建易用的机制来防止错误,不要操心试图去防止间谍。即使你殚精竭力做了,也不会成功,得不偿失。
如果不能使用C++语言本身来防止间谍,还有其它办法吗?有。我为它亲自用旧式风格的代码检视。由于间谍技巧通常包括一些奇异的语法和/或指针转换的使用和联合(union),你可以使用工具来指出大多数的“是非之地”。
[ Top | Bottom | Previous section | Next section ]
能。
相比于前面所述的“智能指针”技术,垃圾收集技术:
更轻便 通常更有效 (尤其当平均的对象尺寸较小时或多线程环境中) 能处理数据中的“循环(cycles)”(如果数据结构能形成循环,引用计数技术通常会有“泄漏”) 有时会泄漏其它对象(由于垃圾收集器必要的保守性,有时会进入一个看上去象是指针的随机位模式的分配单元,尤其是如果分配单元较大时,可能导致该分配单元有泄漏)。 与现存的库工作得更好(由于智能指针需要显式使用,可能很难集成到现存的库中)[ Top | Bottom | Previous section | Next section ]
通常,好像有两种风味的C++垃圾收集器:
保守的垃圾收集器。这些垃圾收集器对于栈和C++对象的分布知之甚少或一无所知,只是寻找看上去象指针的位模式。实践中与 C 以及 C++ 代码共同工作,尤其是平均的对象尺寸较小时,这里有一些例子,按字母顺序:
Boehm-Demers-Weiser collector Geodesic Systems collector混合的垃圾收集器。这些垃圾收集器通常适当地扫描栈,但需要程序员提供堆对象的布局信息。这需要程序员方面做更多工作,但结果是提高性能。这里有一些例子,按字母顺序:
Bartlett's mostly copying collector Attardi and Flagella's CMM (如果谁有 URL,请发给我)。由于C++垃圾收集器通常是保守的,如果一个位模式“看上去”象是有可能是指向另外一个未使用块的指针,就会有泄漏。当指向某块的指针实际超出了块(这是非法的,但一些程序员会越过该限制;唉)以及(很少)当一个指针被编译器的优化所隐藏,也会使它困惑。在实践中,这些问题通常不严重,然而倘若收集器有一些关于对象布局的提示的话,可能会改善这些情况。
[ Top | Bottom | Previous section | Next section ]
更多信息,详见垃圾收集 FAQ。
[ Top | Bottom | Previous section | Next section ]
E-mail the author[ C++ FAQ Lite | Table of contents | Subject index | About the author | ? | Download your own copy ]Revised Apr 8, 2001