C++编程规范(五)类设计和继承

这一章最有价值的条款:33
32.要清楚自己所编写的类
不同的类有不同的目的,因此有不同的规则。对于值类(value class,如std::pair,std::vector):
1.有一个公共析构函数,拷贝构造函数和带有值语义的赋值
2.没有虚函数(包括析构函数)
3.作为具体类使用而不是基类
4.常在栈上实例化,或者作为另一个类的成员

基类是类层次的基础:
1.有一个public且virtual或者protected且nonvirtual的析构函数,有一个non public的拷贝构造和赋值操作符
2.通过虚函数建立接口
3.通常作为具体派生类的一部分在堆上动态实例化,并通过(智能)指针使用

特征类(trait class)是带有类型信息的模板:
1.只包含typedef和静态函数,没有可更改的状态或虚成员。
2.通常不实例化(禁止构造)

策略类代表一类可替换的行为(pluggable behavior):
1.包含或不包含状态和虚函数
2.通常不单独实例化,只作为基类或成员

异常类较少见地把值和引用语义混合在一起,通过值抛出但通过引用捕获:
1.有一个公共析构函数和无错的构造函数(特别是无错的拷贝构造;拷贝构造抛出异常会终止程序)
2.有虚函数,常实现克隆和检查
3.优先从std::exception虚拟继承

辅助类(ancillary class)特别地支持一些特殊理念,如RAII。他们应该易于正确使用,而且难于误用。

 

33.应使用小类而不是大类(monolithic class)
小类易于编写,正确,测试和使用。尽量使用表现简单概念的小类,而不是想要实现多且复杂的概念的乱七八糟的类。
1.小类在合适的粒度上表现一个概念。巨类表现多个不相关的概念,使用其中一个概念会带来其他概念的脑力开销。
2.小类易于理解,使用和重用。
3.小类易于部署。通常一个巨类作为一个笨重的不可分的单元进行部署。例如,一个巨类Matrix也许实现一些异常的功能,如计算特征值,即使大部分的用户只需要简单的线性代数。更好的一种方式应该是把多个功能实现为对一个小类Matrix进行操作的多个非成员函数。、
4.巨类破坏封装。一个类有很多不必作为成员的成员函数时,类的私有实现具有不必要的可见性。
5.巨类通常是期望为一个问题提供一个完整解决方案的结果。实际上这种做法从来没有成功。
6.巨类由于承担太多职责,难于保证其正确性。

 

34.优先使用组合而不是继承
继承是C++中仅次于friend的紧密耦合关系。可能的话尽量避免紧耦合,因此,除非真的知道继承有益于设计,应优先使用组合。即使是有经验的开发者也常滥用继承。软件工程中一条正确的规则是将耦合最小化:如果可以用另一种方式表达一种关系,就用最弱的关系。
只有在找不到可替代的更弱的关系,否则不用继承。如果能用组合表达类关系,就用组合。这里组合指的是一个类型中嵌入另一个类型的成员变量。组合和继承相比有以下优势:
1.更大的灵活性同时不影响调用代码。私有数据成员是在你的掌控之下,在不影响客户端代码的情况下,可以采用值或指针或Pimpl方式持有数据成员;只需要修改使用到它类成员函数。如果需要不同的功能,可以轻易地修改成员的类型或者持有方式同时保持类公共接口一致。
相反,如果是从公共继承关系开始,有可能客户端会依赖这种继承关系,这会给之后的修改带来困难。
2.更多的编译时隔离,更短的编译时间。通过指针而不是基类或直接成员来持有一个对象可以减少对头文件的依赖,因为声明一个对象的指针不需要对象的完全类定义。相反,继承总是要求基类的完全定义要可见。一种常见的技术就是把所有私有成员都隐藏在一个单一指针之后,称为Pimpl。
3.less weirdness:从一个类型的继承会将与该类型处于同一名字空间的函数和函数模板引入到名字查找中,这很微妙,而且难于调试。
4.更广的应用性:有些类从一开始就不是设计成为基类,更多的类可以承担一个成员的角色。
5.良好的健壮性和安全性:继承的紧耦合使得编写安全代码更难。
6.降低复杂度和脆弱性:继承会带来额外的并发症,如名字隐藏以及由于之后基类修改所带来的其他并发问题。

不能说继承就没有优点,使用公共继承可实现可互换性。即使不为所有调用者提供可互换性,以下从最常见情况(前两项)到极少见(其余)情况,都应提供非公共继承:
1.如果需要覆盖虚函数
2.如果需要访问受保护成员
3.If you need to construct the used object before, or destroy it after, a base class.
4.如果需要关注虚基类
5.如果知道可以从空基类优化中受益,
6.如果需要受控的多态。也就是需要可互换关系,但是这种关系只能对某些经过选择的代码可见(通过友元关系)。

 

35.避免从不设计为基类的类继承
把一个独立的类作为基类是一个严重的设计错误。要增加类的行为,应优先增加非成员函数而不是成员函数,要增加状态,使用组合而不是继承。不要从具体基类中继承。
初学者有时从值类继承,如类string,以增加更多的功能。但是定义自由(非成员)函数比建立一个super_string更好,理由如下:
1.非成员函数可以和已有的操作string的代码很好一起工作。如果使用了super_string,将会在整个代码中进行修改类型和函数签名。
2.如果使用了super_string,使用string参数的接口函数有几种选择:1)远离super_string增加的功能(无用); 2)将参数拷贝到super_string(浪费); 3)将string引用转换为super_string引用(不灵活且可能非法)
3.由于string可能没有protected成员,super_string成员函数没有比非成员函数具有更多的对string内部成员的访问权。
4.如果super_string隐藏了string的函数,会在代码中引起大范围的混乱。

有人可能不喜欢非成员函数,因为其调用句法是Fun(str)而不是str.Fun(),这仅仅是句法习惯和熟悉程度。

从一个带有公共非虚析构函数的类继承冒有风险:即删除一个实际上指向super_string对象的string指针。

例1,组合取代公共或私有继承。如果确实需要一个localized_string,类似于string但需要更多的状态和功能,而且很多string的函数实现不需要修改。这可以通过包含string而不是继承来实现(防止切片和为定义的多态删除),并增加转移(passthrough)函数来访问那些不需修改的函数
class localized_string{
public:
//为不需修改的string函数提供passthrough函数
  void clear();                //重定义clear
  bool is_in_klington() const; //增加功能

private:
  std::string impl_;
  //增加其他状态
}

例2,std::unary_function,尽管没有虚函数,实际上它是作为一个基类来设计的,并不违反本条款。

 

36.尽量提供抽象接口
抽象接口有助于准确了解抽象而无需关心具体实现细节。抽象接口是完全由(纯)虚函数构成的抽象类,没有数据成员,通常也没有成员函数的实现。抽象接口中避免状态简化了整个类层次设计。。
应遵循DIP(Dependency Inversion Principle):
1.高层次模块不依赖于低层次模块,而都依赖于抽象。
2.抽象不依赖于细节,相反,细节依赖于抽象。

类层次应以抽象类作为基类。抽象基类必须关注于功能而不是具体实现。
DIP有三个重要的设计好处:
1.提高健壮性。系统的实现(即不稳定部分)依赖于系统的抽象(稳定部分)。对于一个健壮的设计,修改只会产生局部影响。而对于一个脆弱的系统设计,微小的修改甚至会影响到意想不到的地方。采用具体基类的设计就有这个问题。
2.更灵活。对抽象正确的建模,针对新的需求设计新的实现是很容易的。
3.良好的模块化。依赖于抽象的设计其依赖相当简单:高度可变部分依赖于稳定部分,反之不然。另一种极端,带有夹杂着实现细节接口的设计可能有错综复杂的依赖关系,难于作为一个单元重新应用在其他系统中。

Sutter的第二次机会条律(Law of second Chances):正确理解接口是最重要的事情。其他的问题可以稍后再改正。接口错了,或许不能再改正。
使用多继承的设计表达性强,但是难于正确使用。特别是,多继承的状态很难管理。

例,备份系统。在一些设计中,高层次组件依赖于低层次细节。将这样一个程序应用于新文件系统和备份硬件会引起系统的重新设计。如果备份系统的逻辑围绕着文件系统和硬件设备良好设计的抽象进行设计,就不需要重新设计,只需要增加啊抽象接口的新实现以及插入到系统中。

空基类优化是一个例外,继承(非公共)只用于优化目的。
基于策略的设计中,高层次组件依赖于实现细节(策略)。然而,这只是一种静态多态的使用。

 

37.公共继承即可互换性。继承不是为了重用,而是为了被重用。
公共继承允许基类指针或引用实际指向某个派生类对象,不破坏代码正确性,对现有代码不需要修改。
公共继承不是为了重用(现有的存在于基类的)代码,而是为了被(现有的多态使用基类对象的代码)重用。
根据Liskov的替换原则,公共继承必须是is-a关系。所有基类契约必须满足,所有对虚成员函数的覆盖和基类版本相比不能增加或减少基类所定义的契约。继承的误用破坏了正确性。错误实现的继承大部分是由于没有遵守基类建立的隐式或显式契约。

一个常被引用的例子:两个类Square和Retangle,每个类都有设置宽和高的虚函数。Square不能从Retangle继承,因为使用可变Retangle的代码假设SetWidth不会修改高度,根据这种假设Square::SetWidth不能维持正方形的契约。
当人们用公共继承来进行真实世界的类比,公共继承所描述的is-a被误用了:一个正方形确实是矩形,但是一个Square不是一个Retangle。因此,我们应该用"Work like a"(或usable as a)来描述公共继承以避免误用。

公共继承和重用有关,但是不是通常人们所想的那种重用。公共继承不是为了派生类重用基类代码来实现,这种implemented in terms of关系没错,但是应该通过组合来实现,或者少数特殊情况下通过非公共继承来实现。
一个新的派生类是现有通用抽象的新特例。现有通过调用Base虚函数来使用Base*或Base&的多态代码应能无缝地使用继承自Base的MyNewDerivedType对象。新的派生类型为现有代码增加新功能,但是现有代码不必修改。

策略类和混合类型是例外,他们通过公共继承增加行为,但是这不是滥用公共继承来实现implemented in terms of关系。

 

38.实践安全的覆盖(改写)
当覆盖虚函数时,保持可互换性。不要改变虚函数的缺省参数。应显式重新声明覆盖为virtual,小心造成基类函数重载的隐藏。
尽管派生类增加了状态(数据成员),他们描述的是基类的子集而不是超集。考虑到包含关系意味着可互换性-可应用于整个集合的操作应能应用于他们的子集。基类保障了一个操作的前提和后置条件,任何派生类也要符合此类保障。
定义一个可失败的(指可以抛出异常)派生覆盖,只有在基类没有声明该操作总是成功的情况下才是正确的。例如,有一个类Employee,提供了一个虚函数GetBuilding,如果需要写一个派生类RemoteContractor,覆盖GetBuilding操作,而且此操作有时会抛出异常或返回null?只有当Employee文档说明了GetBuilding可能会失败且RemoteContractor按照Employee描述的方式报告错误,此RemoteContrator才是正确的。

覆盖虚函数不要修改默认参数,这是由于多态调用虚函数时默认参数的取值容易引起混乱。看以下代码:
  
小心隐藏了基类中的函数重载。例如:

如果基类的重载是可见的,应采用using在派生类重新声明:

 

39.考虑声明虚函数为非公共的,公共函数为非虚的
在修改代价较大的基类中(特别是类库和框架中的基类),应声明公共函数为非虚的。考虑考虑声明虚函数为私有的,或者如果派生类需要能调用基类版本,声明为protected.(此建议不适用于析构函数)这是非虚拟接口(Nonvirtual Interface)模式,类似于模板方法。
一个公共虚函数本质上有两个完全不同且相互竞争的职责,因而有两种完全不同的用途:
1.定义接口:作为是公共函数,它对外界展示了其接口。
2.说明实现细节:作为虚函数,它为派生类提供了一种取代基类实现的方式,可以说它是一个定制点。
这两种职责可能是冲突的,因此通常一个函数很难很好地同时实现这两种职责。一个公共虚函数有两种完全不同的职责和用途意味着它没有很好地将分离职责,违反了条款5和11,应考虑另一种方式。

将公共函数和虚函数分离有以下好处:
1.还接口其原来的面貌:公共接口和定制接口分离,两者都能很好地以自然的形式出现,而不是找到一种折中的方式迫使两者看起来一样。通常这两种接口需要不同的函数和不同的参数。例如,外部的调用者可能需要调用一个公共函数Process执行一个完整的逻辑工作单元;而定制器只需要覆盖处理的一部分,可以只覆盖其中若干虚函数(如DoProcessPhase1, DoProcessPhase2等)而不是覆盖所有处理函数。
2.基类在控制之下:基类在其接口和策略的完全控制之下,可以在一个便利的可重用的位置实施其前提和后置条件,加入新功能。
3.基类面对变化仍然很健壮:在不影响使用或从此基类继承的代码的同时,我们可以随意进行修改:增加前提和后置条件检查,将处理分离为多个步骤,重构,使用Pimpl方式实现接口/实现分离,或者为基类的可定制性做其他修改。如果使用公共虚函数,之后对基类的修改将会影响到使用或继承此基类的代码。

 

例外,由于析构函数的特殊执行顺序,NVI不能用于析构函数。

NVI不直接支持协变返回类型。如果需要不使用dynamic_cast转换就对调用代码可见的协变,将虚函数定义为公共的更容易。

 

40.避免提供隐式转换
在提供隐式转换前请三思。应优先考虑显式转换(explicit构造函数和命名转换函数)。
隐式转换有两个问题:
1.会在意外的地方触发
2.不能和语言的其他特性很好地配合

隐式转换构造函数(通过一个参数进行调用,没有声明为explicit)和重载配合不佳,会产生不可见的临时对象。通过operator T成员函数定义的转换也不好-他们和隐式构造函数配合不佳,且使得各类没有意义的代码可以编译。
在C++中,一个转换序列最多包含一次用户自定义的转换。当转换序列中包含内置的转换和用户自定义的转换时,其结果会非常混乱,解决方法很简单:
1.默认情况下,对单参数构造函数使用explict
2.使用命名函数来提供转换,而不是使用转换操作符。

例1,重载。假设有一个Widget::Widget(usigned int),它能隐式调用;有一个Display重载函数,参数为double和Widget,考虑以下代码:
void Display(double);
void Display(const Widget&);
Display(5); //创建并显示一个Widget

例2,可以工作的错误。假设String类提供了operator const char*:
class String{
//...
public:
 operator const char*();
}
则以下可笑的代码可以编译,s1和s2是String:
int x=s1-s2;    //行为未定义
const char* p=s1-5; //行为未定义
p=s1+'0';      //结果和期望不一样
if(s1 == "0"){...} //结果和期望不一样

这就是为什么标准string避免提供operator const char*的原因。

例外,少数情况下,隐式转换时的调用代码简短且直观。标准string提供了对const char*的隐式构造函数,由于设计者采取了以下预防措施,因此标准string可以很好地工作:
1.没有到const char*的自动转换,而是通过两个命名函数c_str和data提供转换
2.所有std::string的比较操作符被重载,可以按照任意顺序比较const char和std::string,这避免了创建隐藏的临时变量。
对于重载函数,仍然存在一些奇怪的行为:
void Display(int);
void Display(std::string);
Display(NULL);   //调用Display(int)
此代码的调用结果很令人惊讶。(顺便说一句,如果它调用了Display(std::string),代码的行为将未定义,因为从一个空指针构造一个std::string对象是非法的,但是它的构造函数没有要求对是否为空进行检查)。

 

41.将数据成员声明为私有,除了无行为的聚集(C风格的struct)
应将数据成员保持为私有成员。只有C风格的struct类型,只是组合了一组数据,并不封装或提供行为,才会声明所有数据为公有数据。

信息隐藏是优良软件工程的关键,私有数据是一个类保持其不变量和应对将来可能发生变化的最好方式。类如果对抽象进行建模,则必须维护其不变量。而一个类拥有公有数据意味着他的某些状态的变化是不受控的,不可预期的,和其他状态的变化不能同步。这也意味着抽象必须和使用抽象的代码共同承担维护不变量的职责,很明显,从根本上说这是无可辩驳的缺陷。应该直接拒绝此类设计。

保护成员同样有这个问题。拥有保护成员意味着要和当前或将来派生于此类的代码一起承担维护不变量的职责。

一个类同时拥有公有和非公有数据的设计令人困惑且不一致。私有数据说明存在不变量且有保护他们的意图;掺杂着公有变量也就意味着无法清晰地确定此类是否是作为抽象。

非私有数据总是次于get/set函数,他可以提供健壮的版本控制。可以考虑用Pimpl模式隐藏类私有成员。

例1.合适的封装。大部分类应该拥有全部私有数据,并暴露足够的接口。List类常用的Node通常包含一些数据和两个指针next_和prev_,Node成员通常不需要对List隐藏。
例2.TreeNode.考虑一个根据TreeNode<T>实现的Tree<T>,TreeNode<T>拥有previous/next/parent指针和一个T对象。TreeNode成员可以全是公有,因为Tree可以直接对他们操作,但是Tree必须应该将TreeNode隐藏,这是Tree的内部细节,调用者不能直接操作。Tree不隐藏包含的T对象,这是调用者的职责。容器使用迭代器抽象暴露包含的对象,同时隐藏内部结构。
例3.Getter和Setter。如果没有合适的领域抽象,非私有数据可以通过get和set函数隐藏并作为私有数据,这提供了最小的抽象和健壮的版本控制。

使用函数引起对"color"的讨论,从具体的状态到可以随心所欲的按照自己要求实现的抽象状态:可以在不影响调用代码的前提下将color改为一个不是int的内部编码,增加代码以便在改变颜色时更新显示,增加设备,以及其他改变。最差情况下,调用者需要重新编译(也就是我们保留了源码级兼容性),最好就是不需要重新编译或链接(如果更改保留了二进制兼容性)。如果一开始的设计color就是一个公有成员,源码级或二进制级兼容性也无法适应上述更改。

例外,Get/set函数有用,但是一个主要包含get/set的类可能是一个设计不良的类,应考虑此类是一个抽象或者应该是struct。例如std::pair<T,U>,只是用于标准容器将两个不相关的类型T和U聚集起来,pair本身没有增加其他的行为或不变量。

 

42.不要泄露内部数据
避免返回你的类管理的内部数据的句柄,如此客户端就不会不受控制地修改对象的内部状态。

 

考虑一个类Socket

class Socket {

public:

  // … constructor that opens handle_, destructor that closes handle_, etc. …

  int GetHandle() const {return handle_;}     // avoid this

private:

  int handle_;                                   // perhaps an OS resource handle

};

 

数据隐藏是强有力的抽象和单元化工具,但是隐藏数据和暴露数据句柄是自相矛盾的,就像锁好你的房子但是把钥匙遗留在锁上一样。这是因为:

1.       客户端现在有两种方式实现功能。可以使用类的抽象(如类Socket)或者直接操作类内部所依赖的实现(如handle_)。后者方式中,对象对于它认为它所拥有的资源的变化一无所知。这种情况下类无法增加或修改功能,因为客户现在可以绕过任何增加的,被修改过的实现以及类的任何不变式,因此正确的错误处理也变得不可能。

2.       由于客户端依赖于类的内部实现,类无法对其进行任何修改。如果Sockets随后进行升级,以支持使用不同低级原语集合的协议,获取handle_和对其进行操作的调用代码将会悄无声息地受到影响。

3.       调用代码可以修改类未知的的状态,因此类不能实施其不变式。

4.       客户端代码可以保存类所返回的 句柄,并在类的代码使他们失效后尝试去使用。

 

一种常见的错误就是忘记const不能通过指针传递,Sockets::GetHandle()是一个const成员,一些使用handle_值的系统函数的调用将会修改handle_间接引用的数据。

以下指针的例子类似,尽管我们可以看到情况稍微好些,至少返回const类型可以减少意外的误用。

class String {
  char* buffer_;
public:
  char* GetBuffer() const {return buffer_;}  // bad: should return const char*
  // …
};
此代码从技术上说是有效合法的。显然,客户端可以使用GetBuffer,通过不带显式转换的方式修改一个String对象,例如strcpy( s.GetBuffer(), "Very Long String…" );事实上编译器对此代码的编译不会产生任何警告。如果此成员函数返回const char*,对上述的代码至少会在编译时产生编译时错误,以上代码将需要显式转换。

 

即使返回const指针也不会消除所有的意外误用,因为泄漏内部数据相关的另一个问题涉及到内部数据的有效性。上述String例子中,调用代码可能会存储GetBuffer返回的指针,执行某个操作,导致String增长(和移动)缓冲区,最后可能会使用这个指向已经不再存在的缓冲区的指针。因此,如果你认为你有很好的理由这么做,你必须在文档中描述返回值的有效值和使它失效的操作。

 

例外,

有时出于兼容性的原因,类必须提供对内部数据的访问,例如和遗留代码以及其他系统的接口。例如,std::basic_string通过data和c_str成员函数访问其内部数据,是为了和使用C风格指针的函数兼容,这些函数应该不存储或写指针。这些后门访问函数是罪恶之源,应该少用,谨慎地用。

 

43.明智地使用PImpl惯用手法

C++的私有成员是不可访问,但是不是不可见,考虑使用PImpl惯用手法使C++的私有成员真正不可见,实现编译器防火墙,增加信息隐藏。

 

当建立一个编译器防火墙将调用代码和类的私有部分隔离有意义时,使用PImpl惯用手法:将类隐藏在一个不透明的指针之后,指针指向一个声明但未定义的类。

class Map {

 // …

private:

 struct  Impl;

 shared_ptr<Impl> pimpl_;

};

 

PImpl应隐藏所有私有成员,包括数据和函数。这样,对类私有部分实现的任意修改不需要调用代码的重新编译,这种独立性和自由正是PImple的特点。

至少有三种使用PImpl的理由,他们都源于C++可访问性和可见性的区分。特别是类的私有成员在成员函数和友元之外不能访问,但是对于所有能看见此类定义的代码是可见的。

 

PImpl的第一个结果就是由于处理不必要的类型信息而需要更长的编译时间。值类型的私有数据成员,或者值类型的私有成员函数参数,或者在可见的函数实现中使用的参数,即使在此编译单元中从来就不需要的参数类型也需要预先定义,这就会导致更长的编译时间。

 

class C {

 // …

private:

 AComplicatedType act_;

};

 

包含C类定义的头文件也需要#include AComplicatedType定义的头文件。

 

第二个结果就是会对调用函数的代码产生二义性和名字隐藏。即使私有成员函数从来不会在类外和友元之外调用,他们参与名字查找和重载解析,会导致调用失效或二义性。C++在可访问性检查之前进行名字查找和重载解析。

   

A处可以写成::Twice( 21 ),迫使查找一个全局函数,B处可以选择c.Twice( string("Hello") )强制重载解析选择相应的函数。类似此类调用问题可以采用非PImpl的方式解决,但是不是所有

PImpl问题都有其他解决方法。

 

第三个结果是对错误处理和安全性的影响。

class Widget {// …

public:

 Widget& operator=( const Widget& );

 

private:

 T1 t1_;

 T2 t2_;

};

 

如果T1和T2的操作可能失败且不可恢复,则我们无法编写operator=提供条款71的强保证甚至基本保证也不行。通过以下简单的转换,至少错误安全的赋值可以提供基本保证,一旦T1和T2的操作没有副作用,则可以提供强保证:因此应该通过指针而不是值来持有数据对象,所有都应隐藏在一个PImpl指针之后。

   

 

44.优先编写非成员非友元函数

非成员非友元函数将依赖最小化,提高封装性:函数体不能依赖于类的非公有成员。非成员非友元函数将巨类分解成多个独立的功能,减少耦合。一个模板不知道某个操作是否是给定类型的成员,是很难编写的,因此非成员非友元函数也有利于提高通用性。

 

使用以下算法来决定一个函数是否应为类成员或友元函数:

//如果你没有其他选择,那就没有选择;如果必须是成员,则该函数为成员

如果函数是必须作为成员的操作符=, ->, [], 或(), 则函数必须是成员;

//如果可以是非成员非友元,或者可从非成员友元中获益,则为非成员

否则:a)如果函数需要其他类型作为左参数(如操作符<<和>>),b)最左边的参数需要类型转换,c)可以使用类的公共接口实现,则函数为非成员(a和b情况可以作为友元);如果需要虚拟行为,增加一个虚拟成员函数提供虚拟行为, 根据此函数实现非成员函数。

否则,函数为成员函数。

 

例:basic_string。这是一个具有103个成员函数的巨类,其中有71个可以写成非成员非友元函数,而且保持同样的效率。这些函数中,有很多是在重复algorithm中已经实现的功能,或者有些函数如果不作为basic_string的成员将会得到更广泛的使用。

 

45.总是一起提供new和delete

类专门的重载void* operator new(parms)总是伴随着相应的重载void operator delete(void*, parms),params是一系列额外的参数类型,其中第一个总是std::size_t。new[]和delete[]也一样。

 

即使有相当的理由,但是有一个更隐晦的原因是:编译器可能会期望对T::operator delete的重载,即使你实际上从来没有进行调用。这就是为什么需要成对提供operator new 和operator dlete(operator new[]和operator delete[])的原因。

 

假设有一个带定制分配内存的类:

class T {

  // …

 

  static void* operator new(std::size_t);

  static void* operator new(std::size_t, CustomAllocator&);

 

  static void operator delete(void*, std::size_t);

};

 

调用者可以分配使用缺省的分配器(new T)或定制分配器(new(alloc) T,alloc是CustomAllocator对象)分配T对象。

调用者唯一可以调用的operator delete是缺省的operator delete(size_t).

 

但是,编译器仍然需要悄悄调用delete的另一个重载T::operator delete(size_t, CustomAllocator&),这是因为

T* p = new(alloc) T;

被编译器扩展成如下代码:

void* __compilerTemp = T::operator new(sizeof(T), alloc);

T* p;

try {

  p = new (__compilerTemp) T;      // 在地址 __compilerTemp处构造一个T对象

}

 

catch(...) {                        // 构造失败

 T::operator delete(__compilerTemp, sizeof(T), alloc);

 throw;

}

因此,从逻辑上讲,如果分配成功但是构造失败,编译器自动插入代码调用T::operator new相应的T::operator delete。

然而C++标准规定了只有在operator delete的相应重载存在的情况下才会产生上述代码,这也就是说如果构造失败,会产生内存泄漏。

编写此书时对六种流行的编译器进行测试,只有两种对这种情况产生告警。

 

例外,对于以下operator new的本地形式

void* T::operator new(size_t, void* p) { return p; }

不需要相应的operator delete,因为没有真正的分配内存发生。所测试的所有编译器不发出任何缺少void T::operator delete(void*, size_t, void*)的欺骗性告警。

 

46.如果要提高类专门的new,应提供所有的标准形式(普通,in-place和不抛出)

如果类定义了任意一种对operator new的重载,则应提供所有三种operator new的重载-普通,in-place和不抛出。否则,他们将被隐藏,类的用户将无法使用。

 

C++中,如果在一个作用域中定义了一个名字,它将会隐藏所有包含作用域的相同名字,重载不会发生于跨作用域。

假设定义了一个类的operator new:

class C {

 // …

 static void* operator new(size_t, MemoryPool&);//隐藏三种标准形式

};

 

如果有人要写如原先的new C表达式,编译器将会拒绝,因为它无法找到合适的operator new,类中定义的operator new将隐藏其他重载,包括我们熟悉的内置的全局版本:

void* operator new(std::size_t);                               // plain new

void* operator new(std::size_t, std::nothrow_t) throw();       // nothrow new

void* operator new(std::size_t, void*);                        // in-place new

或者类定义了其中一种标准形式,会隐藏另外两种。

 

通常,这三种new重载应该具有相同的可见性(即使可见仍然可以声明为私有成员,例如你需要显式禁止普通或者不抛出的operator new)。

应该避免隐藏in-place的new,因为STL普遍使用。

 

两种情况下,有两种不同的方式来暴露被隐藏的operator new。

如果类的基类也定义了operator new,暴露基类的operator new 的方式是:

class C : public B {// …

public:

 using B::operator new;

};

 

否则,如果没有基类版本,或者基类不定义operator new,需要通过简短的转发函数(不能使用using来引入全局命名空间的名字):

  

以上建议同样也适用于new[]和delete[]。

避免在客户端代码调用new (nothrow),但是仍应提供以防万一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值