C++基础概念的详细介绍
C++基础概念的详细介绍
pointers(指针)、references(引用)、casts(类型转换)、arrays(数组)、constructors(构造)- 再沒有比这些更基础的议题了。几乎最简单的C++ 程序也会用到其中大部份特性,而许多程序会用到上述所有特性。
尽管你可能已经十分熟悉语言的这一部份,有时候它们还是会令你吃惊。特别是对那些从C转到C++ 的程序员,因为references, dynamic casts, default constructors 及其它non-C 性质背后的观念,往往带有一股幽暗阴郁的色彩。
這一章描述pointers 和references 的差异,並告诉你它们的适当使用时机。本章介绍新的C++ 类型转换(casts)语法,並解释为什么新式类型转换法比旧式的C类型转换法优越。本章也检验C 的数组概念以及C++ 的多态(polymorphism)概念,並说明为什么將这两者混合运用是不智之举。最后,本章讨论default constructors(默认构造函数)的正方和反方意见,並提出一些建议作法,让你回避语言的束缚(因为在你不需default
constructors 的情況下,C++ 也会给你一个。
只要留心下面各条款的各项忠告,你将向著一個很好的目标迈进:你所生产的软件可以清楚而正确地表現出你的设计意图。
指针与引用的区别
指针与引用看上去完全不同(指针用操作符“*”和“->”,引用使用操作符“. ”),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?
首先,要认识到在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。
“但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?”
char *pc = 0; // 设置指针为空值 char& rc = *pc; // 让引用指向空值
这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生)。应该躲开写出这样代码的人,除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。
因为引用肯定会指向一个对象,在C++里,引用应被初始化。
string& rs; // 错误,引用必须被初始化
string s("xyzzy");
string& rs = s; // 正确,rs指向s
指针没有这样的限制。
string *ps; // 未初始化的指针
// 合法但危险
不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。
void printDouble(const double& rd)
{
cout << rd; // 不需要测试rd,它
} // 肯定指向一个double值
相反,指针则应该总是被测试,防止其为空:
void printDouble(const double *pd)
{
if (pd) { // 检查是否为NULL
cout << *pd;
}
}
指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。
string s1("Nancy");
string s2("Clancy");
string& rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍旧引用s1,
// 但是 s1的值现在是
// "Clancy"
ps = &s2; // ps 现在指向 s2;
// s1 没有改变
总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
还有一种情况,就是当你重载某个操作符时,你应该使用引用。最普通的例子是操作符。这个操作符典型的用法是返回一个目标对象,其能被赋值。
vector<int> v(10); // 建立整形向量(vector),大小为10;
// 向量是一个在标准C库中的一个模板(见条款M35)
v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值
如果操作符[]返回一个指针,那么后一个语句就得这样写:
*v[5] = 10;
但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。
(这有一个有趣的例外,参见条款M30)
当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针。
尽量使用C++风格的类型转换
仔细想想地位卑贱的类型转换功能(cast),其在程序设计中的地位就象goto语句一样令人鄙视。但是它还不是无法令人忍受,因为当在某些紧要的关头,类型转换还是必需的,这时它是一个必需品。
不过C风格的类型转换并不代表所有的类型转换功能。
一来它们过于粗鲁,能允许你在任何类型之间进行转换。不过如果要进行更精确的类型转换,这会是一个优点。在这些类型转换中存在着巨大的不同,例如把一个指向const对象的指针(pointer-to-const-object)转换成指向非const对象的指针(pointer-to-non-const-object)(即一个仅仅去除const的类型转换),把一个指向基类的指针转换成指向子类的指针(即完全改变对象类型)。传统的C风格的类型转换不对上述两种转换进行区分。(这一点也不令人惊讶,因为C风格的类型转换是为C语言设计的,而不是为C++语言设计的)。
二来C风格的类型转换在程序语句中难以识别。在语法上,类型转换由圆括号和标识符组成,而这些可以用在C++中的任何地方。这使得回答象这样一个最基本的有关类型转换的问题变得很困难:“在这个程序中是否使用了类型转换?”。这是因为人工阅读很可能忽略了类型转换的语句,而利用象grep的工具程序也不能从语句构成上区分出它们来。
C++通过引进四个新的类型转换操作符克服了C风格类型转换的缺点,这四个操作符是, static_cast, const_cast, dynamic_cast, 和reinterpret_cast。在大多数情况下,对于这些操作符你只需要知道原来你习惯于这样写,
(type) expression
而现在你总应该这样写:
static_cast<type>(expression)
例如,假设你想把一个int转换成double,以便让包含int类型变量的表达式产生出浮点数值的结果。如果用C风格的类型转换,你能这样写:
int firstNumber, secondNumber;
...
double result = ((double)firstNumber)/secondNumber;
如果用上述新的类型转换方法,你应该这样写:
double result = static_cast<double>(firstNumber)/secondNumber;
这样的类型转换不论是对人工还是对程序都很容易识别。
static_cast在功能上基本上与C风格的类型转换一样强大,含义也一样。它也有功能上限制。例如,你不能用static_cast象用C风格的类型转换一样把struct转换成int类型或者把double类型转换成指针类型,另外,static_cast不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。
其它新的C++类型转换操作符被用在需要更多限制的地方。const_cast用于类型转换掉表达式的const或volatileness属性。通过使用const_cast,你向人们和编译器强调你通过类型转换想做的只是改变一些东西的constness或者 volatileness属性。这个含义被编译器所约束。如果你试图使用const_cast来完成修改constness 或者volatileness属性之外的事情,你的类型转换将被拒绝。下面是一些例子:
class Widget { ... };
class SpecialWidget: public Widget { ... };
void update(SpecialWidget *psw);
SpecialWidget sw; // sw 是一个非const 对象。
const SpecialWidget& csw = sw; // csw 是sw的一个引用
// 它是一个const 对象
update(&csw); // 错误!不能传递一个const SpecialWidget* 变量
// 给一个处理SpecialWidget*类型变量的函数
update(const_cast<SpecialWidget*>(&csw));
// 正确,csw的const被显示地转换掉(
// csw和sw两个变量值在update
//函数中能被更新)
update((SpecialWidget*)&csw);
// 同上,但用了一个更难识别
//的C风格的类型转换
Widget *pw = new SpecialWidget;
update(pw); // 错误!pw的类型是Widget*,但是
// update函数处理的是SpecialWidget*类型
update(const_cast<SpecialWidget*>(pw));
// 错误!const_cast仅能被用在影响
// constness or volatileness的地方上。,
// 不能用在向继承子类进行类型转换。
到目前为止,const_cast最普通的用途就是转换掉对象的const属性。
第二种特殊的类型转换符是dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时):
Widget *pw;
...
update(dynamic_cast<SpecialWidget*>(pw));
// 正确,传递给update函数一个指针
// 是指向变量类型为SpecialWidget的pw的指针
// 如果pw确实指向一个对象,
// 否则传递过去的将使空指针。
void updateViaRef(SpecialWidget& rsw);
updateViaRef(dynamic_cast<SpecialWidget&>(*pw));
//正确。 传递给updateViaRef函数
// SpecialWidget pw 指针,如果pw
// 确实指向了某个对象
// 否则将抛出异常
dynamic_casts在帮助你浏览继承层次上是有限制的。它不能被用于缺乏虚函数的类型上(参见条款M24),也不能用它来转换掉constness:
int firstNumber, secondNumber;
...
double result = dynamic_cast<double>(firstNumber)/secondNumber;
// 错误!没有继承关系
const SpecialWidget sw;
...
update(dynamic_cast<SpecialWidget*>(&sw));
// 错误! dynamic_cast不能转换
// 掉const。
如你想在没有继承关系的类型中进行转换,你可能想到static_cast。如果是为了去除const,你总得用const_cast。
这四个类型转换符中的最后一个是reinterpret_cast。使用这个操作符的类型转换,其的转换结果几乎都是执行期定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。
reinterpret_casts的最普通的用途就是在函数指针类型之间进行转换。例如,假设你有一个函数指针数组:
typedef void (*FuncPtr)(); // FuncPtr is 一个指向函数
// 的指针,该函数没有参数
// 返回值类型为void
FuncPtr funcPtrArray[10]; // funcPtrArray 是一个能容纳
// 10个FuncPtrs指针的数组
让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:
int doSomething();
你不能不经过类型转换而直接去做,因为doSomething函数对于funcPtrArray数组来说有一个错误的类型。在FuncPtrArray数组里的函数返回值是void类型,而doSomething函数返回值是int类型。
funcPtrArray[0] = &doSomething; // 错误!类型不匹配
reinterpret_cast可以让你迫使编译器以你的方法去看待它们:
funcPtrArray[0] = // this compiles
reinterpret_cast<FuncPtr>(&doSomething);
转换函数指针的代码是不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果(参见条款M31),所以你应该避免转换函数指针类型,除非你处于着背水一战和尖刀架喉的危急时刻。一把锋利的刀。一把非常锋利的刀。
如果你使用的编译器缺乏对新的类型转换方式的支持,你可以用传统的类型转换方法代替static_cast, const_cast, 以及reinterpret_cast。也可以用下面的宏替换来模拟新的
类型转换语法:
#define static_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define const_cast(TYPE,EXPR) ((TYPE)(EXPR))
#define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR))
你可以象这样使用使用:
double result = static_cast(double, firstNumber)/secondNumber;
update(const_cast(SpecialWidget*, &sw));
funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);
这些模拟不会象真实的操作符一样安全,但是当你的编译器可以支持新的的类型转换时,它们可以简化你把代码升级的过程。
没有一个容易的方法来模拟dynamic_cast的操作,但是很多函数库提供了函数,安全地在派生类与基类之间进行类型转换。如果你没有这些函数而你有必须进行这样的类型转换,你也可以回到C风格的类型转换方法上,但是这样的话你将不能获知类型转换是否失败。
当然,你也可以定义一个宏来模拟dynamic_cast的功能,就象模拟其它的类型转换一样:
#define dynamic_cast(TYPE,EXPR) (TYPE)(EXPR)
请记住,这个模拟并不能完全实现dynamic_cast的功能,它没有办法知道转换是否失败。
我知道,是的,我知道,新的类型转换操作符不是很美观而且用键盘键入也很麻烦。如果你发现它们看上去实在令人讨厌,C风格的类型转换还可以继续使用并且合法。然而,正是因为新的类型转换符缺乏美感才能使它弥补了在含义精确性和可辨认性上的缺点。并且,使用新类型转换符的程序更容易被解析(不论是对人工还是对于工具程序),它们允许编译器检测出原来不能发现的错误。这些都是放弃C风格类型转换方法的强有力的理由。还有第
三个理由:也许让类型转换符不美观和键入麻烦是一件好事。
不要对数组使用多态
类继承的最重要的特性是你可以通过基类指针或引用来操作派生类。这样的指针或引用具有行为的多态性,就好像它们同时具有多种形态。C++允许你通过基类指针和引用来操作派生类数组。不过这根本就不是一个特性,因为这样的代码几乎从不如你所愿地那样运行。
假设你有一个类BST(比如是搜索树对象)和继承自BST类的派生类BalancedBST:
class BST { ... };
class BalancedBST: public BST { ... };
在一个真实的程序里,这样的类应该是模板类,但是在这个例子里并不重要,加上模板只会使得代码更难阅读。为了便于讨论,我们假设BST和BalancedBST只包含int类型数据。
有这样一个函数,它能打印出BST类数组中每一个BST对象的内容:
void printBSTArray(ostream& s,
const BST array[],
int numElements)
{
for (int i = 0; i < numElements; ) {
s << array[i]; //假设BST类
} //重载了操作符<<
}
当你传递给该函数一个含有BST对象的数组变量时,它能够正常运行:
BST BSTArray[10];
...
printBSTArray(cout, BSTArray, 10); // 运行正常
然而,请考虑一下,当你把含有BalancedBST对象的数组变量传递给printBSTArray函数时,会产生什么样的后果:
BalancedBST bBSTArray[10];
...
printBSTArray(cout, bBSTArray, 10); // 还会运行正常么?
你的编译器将会毫无警告地编译这个函数,但是再看一下这个函数的循环代码:
for (int i = 0; i < numElements; ) {
s << array[i];
}
这里的array[I]只是一个指针算法的缩写:它所代表的是*(array)。我们知道array是一个指向数组起始地址的指针,但是array中各元素内存地址与数组的起始地址的间隔究竟有多大呢?它们的间隔是i*sizeof(一个在数组里的对象),因为在array数组[0]到[I]间有I个对象。编译器为了建立正确遍历数组的执行代码,它必须能够确定数组中对象的大
小,这对编译器来说是很容易做到的。参数array被声明为BST类型,所以array数组中每一个元素都是BST类型,因此每个元素与数组起始地址的间隔是i*sizeof(BST)。
至少你的编译器是这么认为的。但是如果你把一个含有BalancedBST对象的数组变量传递给printBSTArray函数,你的编译器就会犯错误。在这种情况下,编译器原先已经假设数组中元素与BST对象的大小一致,但是现在数组中每一个对象大小却与BalancedBST一致。派生类的长度通常都比基类要长。我们料想BalancedBST对象长度的比BST长。如果如此的话,printBSTArray函数生成的指针算法将是错误的,没有人知道如果用BalancedBST数组
来执行printBSTArray函数将会发生什么样的后果。不论是什么后果都是令人不愉快的。
如果你试图删除一个含有派生类对象的数组,将会发生各种各样的问题。以下是一种你可能采用的但不正确的做法。
//删除一个数组, 但是首先记录一个删除信息
void deleteArray(ostream& logStream, BST array[])
{
logStream << "Deleting array at address "
<< static_cast<void*>(array) << '/n';
delete [] array;
}
BalancedBST *balTreeArray = // 建立一个BalancedBST对象数组
new BalancedBST[50];
...
deleteArray(cout, balTreeArray); // 记录这个删除操作
这里面也掩藏着你看不到的指针算法。当一个数组被删除时,每一个数组元素的析构函数也会被调用。当编译器遇到这样的代码:
delete [] array;
它肯定象这样生成代码:
// 以与构造顺序相反的顺序来
// 解构array数组里的对象
for ( int i = 数组元素的个数 1; i >= 0;--i)
{
array[i].BST::~BST(); // 调用 array[i]的
} // 析构函数
因为你所编写的循环语句根本不能正确运行,所以当编译成可执行代码后,也不可能正常运行。语言规范中说通过一个基类指针来删除一个含有派生类对象的数组,结果将是不确定的。这实际意味着执行这样的代码肯定不会有什么好结果。多态和指针算法不能混合在一起来用,所以数组与多态也不能用在一起。
值得注意的是如果你不从一个具体类(concrete classes)(例如BST)派生出另一个具体类(例如BalancedBST),那么你就不太可能犯这种使用多态性数组的错误。正如条款M33所解释的,不从具体类派生出具体类有很多好处。我希望你阅读一下条款M33的内容。
(WQ加注:VC++中,能够有正确的结果,因为它根本没有数组new/delete函数。)
:避免无用的缺省构造函数
缺省构造函数(指没有参数的构造函数)在C++语言中是一种让你无中生有的方法。构造函数能初始化对象,而缺省构造函数则可以不利用任何在建立对象时的外部数据就能初始化对象。有时这样的方法是不错的。例如一些行为特性与数字相仿的对象被初始化为空值或不确定的值也是合理的,还有比如链表、哈希表、图等等数据结构也可以被初始化为空容器。
但不是所有的对象都属于上述类型,对于很多对象来说,不利用外部数据进行完全的初始化是不合理的。比如一个没有输入姓名的地址簿对象,就没有任何意义。在一些公司里,所有的设备都必须标有一个公司ID号码,所以在建立对象以模型化一个设备时,不提供一个合适的ID号码,所建立的对象就根本没有意义。
在一个完美的世界里,无需任何数据即可建立对象的类可以包含缺省构造函数,而需要数据来建立对象的类则不能包含缺省构造函数。唉!可是我们的现实世界不是完美的,所以我们必须考虑更多的因素。特别是如果一个类没有缺省构造函数,就会存在一些使用上的限制。
请考虑一下有这样一个类,它表示公司的设备,这个类包含一个公司的ID代码,这个
ID代码被强制做为构造函数的参数:
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber);
...
};
因为EquipmentPiece类没有一个缺省构造函数,所以在三种情况下使用它,就会遇到问题。第一中情况是建立数组时。一般来说,没有一种办法能在建立对象数组时给构造函数传递参数。所以在通常情况下,不可能建立EquipmentPiece对象数组:
EquipmentPiece bestPieces[10]; // 错误!没有正确调用
// EquipmentPiece 构造函数
EquipmentPiece *bestPieces =
new EquipmentPiece[10]; // 错误!与上面的问题一样
不过还是有三种方法能回避开这个限制。对于使用非堆数组(non-heap arrays)(即不在堆中给数组分配内存。译者注)的一种解决方法是在数组定义时提供必要的参数:
int ID1, ID2, ID3, ..., ID10; // 存储设备ID号的
// 变量
...
EquipmentPiece bestPieces[] = { // 正确, 提供了构造
EquipmentPiece(ID1), // 函数的参数
EquipmentPiece(ID2),
EquipmentPiece(ID3),
...,
EquipmentPiece(ID10)
};
不过很遗憾,这种方法不能用在堆数组(heap arrays)的定义上。
一个更通用的解决方法是利用指针数组来代替一个对象数组:
typedef EquipmentPiece* PEP; // PEP 指针指向
//一个EquipmentPiece对象
PEP bestPieces[10]; // 正确, 没有调用构造函数
PEP *bestPieces = new PEP[10]; // 也正确
在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象:
for (int i = 0; i < 10; ++i)
bestPieces[i] = new EquipmentPiece( ID Number );
不过这中方法有两个缺点,第一你必须删除数组里每个指针所指向的对象。如果你忘了,就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针。
如果你为数组分配raw memory,你就可以避免浪费内存。使用placement new方法在内存中构造quipmentPiece对象:
// 为大小为10的数组 分配足够的内存
// EquipmentPiece 对象; 详细情况请参见条款M8
// operator new[] 函数
void *rawMemory =
operator new[](10*sizeof(EquipmentPiece));
// make bestPieces point to it so it can be treated as an
// EquipmentPiece array
EquipmentPiece *bestPieces =
static_cast<EquipmentPiece*>(rawMemory);
// construct the EquipmentPiece objects in the memory
// 使用"placement new" (参见条款M8)
for (int i = 0; i < 10; ++i)
new (&bestPieces[i]) EquipmentPiece( ID Number );
注意你仍旧得为每一个EquipmentPiece对象提供构造函数参数。这个技术(和指针数组的主意一样)允许你在没有缺省构造函数的情况下建立一个对象数组。它没有绕过对构造函数参数的需求,实际上也做不到。如果能做到的话,就不能保证对象被正确初始化。
使用placement new的缺点除了是大多数程序员对它不熟悉外(能使用它就更难了),还有就是当你不想让它继续存在使用时,必须手动调用数组对象的析构函数,然后调用操作符delete[]来释放raw memory:(WQ加注,已经有placement delete/delete []操作符了,它会自动调用析构函数。)
// 以与构造bestPieces对象相反的顺序
// 解构它。
for (int i = 9; i >= 0; --i)
bestPieces[i].~EquipmentPiece();
// deallocate the raw memory
operator delete[](rawMemory);
如果你忘记了这个要求而使用了普通的数组删除方法,那么你程序的运行将是不可预测的。这是因为:直接删除一个不是用new操作符来分配的内存指针,其结果没有被定义。
delete [] bestPieces; // 没有定义! bestPieces
//不是用new操作符分配的。
有关new、placement new和它们如何与构造函数、析构函数一起使用的更多信息.
对于类里没有定义缺省构造函数所造成的第二个问题是它们无法在许多基于模板(template-based)的容器类里使用。因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。这个要求总是来自于模板内部,被建立的模板参数类型数组里。例如一个数组模板类:
template<class T>
class Array {
public:
Array(int size);
...
private:
T *data;
};
template<class T>
Array<T>::Array(int size)
{
data = new T[size]; // 为每个数组元素
... //依次调用 T::T()
}
在多数情况下,通过仔细设计模板可以杜绝对缺省构造函数的需求。例如标准的vector模板(生成一个类似于可扩展数组的类)对它的类型参数没有必须有缺省构造函数的要求。不幸的是,很多模板类没有以仔细的态度去设计。这样,没有缺省构造函数的类就不能与许多模板兼容。当C++程序员深入领会了模板设计以后,这样的问题应该不再那么突出了。这会花多长时间,完全在于个人的造化。
最后讲一下在设计虚基类时所面临的要提供缺省构造函数还是不提供缺省构造函数的两难决策。不提供缺省构造函数的虚基类,很难与其进行合作。因为几乎所有的派生类在实例化时都必须给虚基类构造函数提供参数。这就要求所有由没有缺省构造函数的虚基类继承下来的派生类(无论有多远)都必须知道并理解提供给虚基类构造函数的参数的含义。派生类的作者是不会企盼和喜欢这种规定的。
因为这些强加于没有缺省构造函数的类上的种种限制,一些人认为所有的类都应该有缺省构造函数,即使缺省构造函数没有足够的数据来完整初始化一个对象。比如这个原则的拥护者会这样修改EquipmentPiece类:
class EquipmentPiece {
public:
EquipmentPiece( int IDNumber = UNSPECIFIED);
...
private:
static const int UNSPECIFIED; // 其值代表ID值不确定。
};
这允许这样建立EquipmentPiece对象
EquipmentPiece e; //这样合法
这样的修改使得其他成员函数变得复杂,因为不再能确保EquipmentPiece对象进行了有意义的初始化。假设它建立一个因没有ID而没有意义的EquipmentPiece对象,那么大多数成员函数必须检测ID是否存在。如果不存在ID,它们将必须指出怎么犯的错误。不过通常不明确应该怎么去做,很多代码的实现什么也没有提供:只是抛出一个异常或调用一个函数终止程序。当这种情形发生时,很难说提供缺省构造函数而放弃了一种保证机制的做法是
否能提高软件的总体质量。
提供无意义的缺省构造函数也会影响类的工作效率。如果成员函数必须测试所有的部分是否都被正确地初始化,那么这些函数的调用者就得为此付出更多的时间。而且还得付出更多的代码,因为这使得可执行文件或库变得更大。它们也得在测试失败的地方放置代码来处理错误。如果一个类的构造函数能够确保所有的部分被正确初始化,所有这些弊病都能够避免。缺省构造函数一般不会提供这种保证,所以在它们可能使类变得没有意义时,尽量去避
免使用它们。使用这种(没有缺省构造函数的)类的确有一些限制,但是当你使用它时,它也给你提供了一种保证:你能相信这个类被正确地建立和高效地实现。
. 运算符
运算符重载--你不得不喜欢它们!它们允许给予你的自定义类型有着和C++内建类型完全相样的语法,更有甚者,它们允许你将强大的能量注入到运算符背后的函数体中,而这是在内建类型上从未听说过的。当然,你能够使得如同“+”和“==”这样的符号做任何你想做的事,这个事实意味着使用运算符重载你可能写出的程序完全无法理解。C++的老手知道如何驾驭运算符重载的威力而不至于滑落到“不可理解”的深渊。
遗憾的是,很容易导致滑落。单参数的构造函数和隐式类型转换操作符尤其棘手,因为它们会被调用在没有任何的源代码显示了这样的调用的地方。这会导致程序的行为难于理解。一个不同的问题发生在重载“&&”和“||”这样的运算符时,因为从内建类型到自定义类型的类型转换函数在语法上产生了一个微妙的变化,而这一点非常容易被忽视。最后,许多操作符通过标准的方式彼此的联系在一起,但重载操作符使得改变这种公认的联系成为可
能。
在下面的条款中,我集中解释重载的运算符何时并且如何被调用,它们如何运作,它们应该如何彼此联系,以及如何获得这些方面的控制权。有了这一章的信息,你可以和专家一样重载(或不重载)一个运算符。
谨慎定义类型转换函数
C++编译器能够在两种数据类型之间进行隐式转换(implicit conversions),它继承了C语言的转换方法,例如允许把char隐式转换为int和从short隐式转换为double。因此当你把一个short值传递给准备接受double参数值的函数时,依然可以成功运行。C中许多这种可怕的转换可能会导致数据的丢失,它们在C++中依然存在,包括int到short的转换和double到char的转换。
你对这些类型转换是无能为力的,因为它们是语言本身的特性。不过当你增加自己的类型时,你就可以有更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换。
有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值。以下有两个例子:
class Name { // for names of things
public:
Name(const string& s); // 转换 string 到
// Name
...
};
class Rational { // 有理数类
public:
Rational(int numerator = 0, // 转换int到
int denominator = 1); // 有理数类
...
};
隐式类型转换运算符只是一个样子奇怪的成员函数:operator 关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。例如为了允许Rational(有理数)类隐式地转换为double类型(在用有理数进行混合类型运算时,可能有用),你可以如此声明Rational类:
class Rational {
public:
...
operator double() const; // 转换Rational类成
}; // double类型
在下面这种情况下,这个函数会被自动调用:
Rational r(1, 2); // r 的值是1/2
double d = 0.5 * r; // 转换 r 到double,
// 然后做乘法
以上这些说明只是一个复习,我真正想说的是为什么你不需要定义各种类型转换函数。根本问题是当你在不需要使用转换函数时,这些的函数缺却会被调用运行。结果,这些不正确的程序会做出一些令人恼火的事情,而你又很难判断出原因。
让我们首先分析一下隐式类型转换运算符,它们是最容易处理的。假设你有一个如上所述的Rational类,你想让该类拥有打印有理数对象的功能,就好像它是一个内置类型。因
此,你可能会这么写:
Rational r(1, 2);
cout << r; // 应该打印出"1/2"
再假设你忘了为Rational对象定义operator<<。你可能想打印操作将失败,因为没有合适的的operator<<被调用。但是你错了。当编译器调用operator<<时,会发现没有这样的函数存在,但是它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。类型转换顺序的规则定义是复杂的,但是在现在这种情况下,编译器会发现它们能调用Rational::operator double函数来把r转换为double类型。所以上述代码打印的结果是一个浮点数,而不是一个有理数。这简直是一个灾难,但是它表明了隐式类型转换的缺点:它们的存在将导致错误的发生。
解决方法是用不使用语法关键字的等同的函数来替代转换运算符。例如为了把
Rational对象转换为double,用asDouble函数代替operator double函数:
class Rational {
public:
...
double asDouble() const; //转变 Rational
}; // 成double
这个成员函数能被显式调用:
Rational r(1, 2);
cout << r; // 错误! Rationa对象没有
// operator<<
cout << r.asDouble(); // 正确, 用double类型
//打印r
在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再会发生,这点损失是值得的。一般来说,越有经验的C++程序员就越喜欢避开类型转换运算符。例如在C++标准库(参见Effective C++条款49和M35)委员会工作的人员是在此领域最有经验的,他们加在库函数中的string类型没有包括隐式地从string转换成C风格的char*的功能,而是定义了一个成员函数c_str用来完成这个转换,这是巧合么?我看不是。
通过单参数构造函数进行隐式类型转换更难消除。而且在很多情况下这些函数所导致的问题要甚于隐式类型转换运算符。
举一个例子,一个array类模板,这些数组需要调用者确定边界的上限与下限:
template<class T>
class Array {
public:
Array(int lowBound, int highBound);
Array(int size);
T& operator[](int index);
...
};
第一个构造函数允许调用者确定数组索引的范围,例如从10到20。它是一个两参数构造函数,所以不能做为类型转换函数。第二个构造函数让调用者仅仅定义数组元素的个数(使用方法与内置数组的使用相似),不过不同的是它能做为类型转换函数使用,能导致无穷的痛苦。
例如比较Array<int>对象,部分代码如下:
bool operator==( const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
if (a == b[i]) { // 哎呦! "a" 应该是 "a[i]"
do something for when
a[i] and b[i] are equal;
}
else {
do something for when they're not;
}
我们想用a的每个元素与b的每个元素相比较,但是当录入a时,我们偶然忘记了数组下标。当然我们希望编译器能报出各种各样的警告信息,但是它根本没有。因为它把这个调用看成用Array<int>参数(对于a)和int(对于b[i])参数调用operator==函数,然而没有operator==函数是这样的参数类型,我们的编译器注意到它能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数。然后编译器如此去编译,生成的代码就象这样:
for (int i = 0; i < 10; ++i)
if (a == static_cast< Array<int> >(b[i])) ...
每一次循环都把a的内容与一个大小为b[i]的临时数组(内容是未定义的)比较。这不仅不可能以正确的方法运行,而且还是效率低下的。因为每一次循环我们都必须建立和释放Array<int>对象。
通过不声明运算符(operator)的方法,可以克服隐式类型转换运算符的缺点,但是单参数构造函数没有那么简单。毕竟,你确实想给调用者提供一个单参数构造函数。同时你也希望防止编译器不加鉴别地调用这个构造函数。幸运的是,有一个方法可以让你鱼肉与熊掌兼得。事实上是两个方法:一是容易的方法,二是当你的编译器不支持容易的方法时所必须使用的方法。
容易的方法是利用一个最新编译器的特性,explicit关键字。为了解决隐式类型转换而特别引入的这个特性,它的使用方法很好理解。构造函数用explicit声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法:
template<class T>
class Array {
public:
...
explicit Array(int size); // 注意使用"explicit"
...
};
Array<int> a(10); // 正确, explicit 构造函数
// 在建立对象时能正常使用
Array<int> b(10); // 也正确
if (a == b[i]) ... // 错误! 没有办法
// 隐式转换
// int 到 Array<int>
if (a == Array<int>(b[i])) ... // 正确,显式从int到
// Array<int>转换
// (但是代码的逻辑
// 不合理)
if (a == static_cast< Array<int> >(b[i])) ...
// 同样正确,同样
// 不合理
if (a == (Array<int>)b[i]) ... //C风格的转换也正确,
// 但是逻辑
// 依旧不合理
在例子里使用了static_cast(参见条款M2),两个“>”字符间的空格不能漏掉,如果
这样写语句:
if (a == static_cast<Array<int>>(b[i])) ...
这是一个不同的含义的语句。因为C++编译器把“>>”做为一个符号来解释。在两个“>”间没有空格,语句会产生语法错误。
如果你的编译器不支持explicit,你不得不回到不使用成为隐式类型转换函数的单参数构造函数。
我前面说过复杂的规则决定哪一个隐式类型转换是合法的,哪一个是不合法的。这些规则中没有一个转换能够包含用户自定义类型(调用单参数构造函数或隐式类型转换运算符)。你能利用这个规则来正确构造你的类,使得对象能够正常构造,同时去掉你不想要的隐式类型转换。
再来想一下数组模板,你需要用整形变量做为构造函数参数来确定数组大小,但是同时又必须防止从整数类型到临时数组对象的隐式类型转换。你要达到这个目的,先要建立一个新类ArraySize。这个对象只有一个目的就是表示将要建立数组的大校你必须修改Array的单参数构造函数,用一个ArraySize对象来代替int。代码如下:
template<class T>
class Array {
public:
class ArraySize { // 这个类是新的
public:
ArraySize(int numElements): theSize(numElements) {}
int size() const { return theSize; }
private:
int theSize;
};
Array(int lowBound, int highBound);
Array(ArraySize size); // 注意新的声明
...
};
这里把ArraySize嵌套入Array中,为了强调它总是与Array一起使用。你也必须声明ArraySize为公有,为了让任何人都能使用它。
想一下,当通过单参数构造函数定义Array对象,会发生什么样的事情:
Array<int> a(10);
你的编译器要求用int参数调用Array<int>里的构造函数,但是没有这样的构造函数。编译器意识到它能从int参数转换成一个临时ArraySize对象,ArraySize对象只是Array<int>构造函数所需要的,这样编译器进行了转换。函数调用(及其后的对象建立)也就成功了。
事实上你仍旧能够安心地构造Array对象,不过这样做能够使你避免类型转换。考虑一下以下代码:
bool operator==( const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
if (a == b[i]) ... // 哎呦! "a" 应该是 "a[i]";
// 现在是一个错误。
为了调用operator==函数,编译器要求Array<int>对象在”==”右侧,但是不存在一个参数为int的单参数构造函数。而且编译器无法把int转换成一个临时ArraySize对象然后通过这个临时对象建立必须的Array<int>对象,因为这将调用两个用户定义(user-defined)的类型转换,一个从int到ArraySize,一个从ArraySize到Array<int>。
这种转换顺序被禁止的,所以当试图进行比较时编译器肯定会产生错误。
ArraySize类的使用有些象一个有目的的帮手,这是一个更通用技术的应用实例。类似于ArraySize的类经常被称为proxy classes,因为这样类的每一个对象都为了支持其他对象的工作。ArraySize对象实际是一个整数类型的替代者,用来在建立Array对象时确定数组大校Proxy对象能帮你更好地控制软件的在某些方面的行为,否则你就不能控制这些行为,比如在上面的情况里,这种行为是指隐式类型转换,所以它值得你去学习和使用。你可能会问你如何去学习它呢?一种方法是转向条款M30;它专门讨论proxy classes。
在你跳到条款M30之前,再仔细考虑一下本条款的内容。让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。
自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别 很久以前(八十年代),没有办法区分++和--操作符的前缀与后缀调用。这个问题遭到程序员的报怨,于是C++语言得到了扩展,允许重载increment 和 decrement操作符的两种形式。
然而有一个句法上的问题,重载函数间的区别决定于它们的参数类型上的差异,但是不论是increment或decrement的前缀还是后缀都只有一个参数。为了解决这个语言问题,C++规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0做为int参数的值给该函数:
class UPInt { // "unlimited precision int"
public:
UPInt& operator++(); // ++ 前缀
const UPInt operator++(int); // ++ 后缀
UPInt& operator--(); // -- 前缀
const UPInt operator--(int); // -- 后缀
UPInt& operator+=(int); // += 操作符,UPInts
// 与ints 相运算
...
};
UPInt i;
++i; // 调用 i.operator++();
i++; // 调用 i.operator++(0);
--i; // 调用 i.operator--();
i--; // 调用 i.operator--(0);
这个规范有一些古怪,不过你会习惯的。而尤其要注意的是:这些操作符前缀与后缀形式返回值类型是不同的。前缀形式返回一个引用,后缀形式返回一个const类型。下面我们将讨论++操作符的前缀与后缀形式,这些说明也同样适用于--操作符。从你开始做C程序员那天开始,你就记住increment的前缀形式有时叫做“增加然后取
回”,后缀形式叫做“取回然后增加”。这两句话非常重要,因为它们是increment前缀与后缀的形式上的规范。
// 前缀形式:增加然后取回值
UPInt& UPInt::operator++()
{
*this += 1; // 增加
return *this; // 取回值
}
// postfix form: fetch and increment
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this; // 取回值
++(*this); // 增加
return oldValue; // 返回被取回的值
}
后缀操作符函数没有使用它的参数。它的参数只是用来区分前缀与后缀函数调用。如果你没有在函数里使用参数,许多编译器会显示警告信息,很令人讨厌。为了避免这些警告信息,一种经常使用的方法时省略掉你不想使用的参数名称;如上所示。
很明显一个后缀increment必须返回一个对象(它返回的是增加前的值),但是为什么
是const对象呢?假设不是const对象,下面的代码就是正确的:
UPInt i;
i++++; // 两次increment后缀
这组代码与下面的代码相同:
i.operator++(0).operator++(0);
很明显,第一个调用的operator++函数返回的对象调用了第二个operator++函数。
有两个理由导致我们应该厌恶上述这种做法,第一是与内置类型行为不一致。当设计一个类遇到问题时,一个好的准则是使该类的行为与int类型一致。而int类型不允许连续进行两次后缀increment:
int i;
i++++; // 错误!
第二个原因是使用两次后缀increment所产生的结果与调用者期望的不一致。如上所示,第二次调用operator++改变的值是第一次调用返回对象的值,而不是原始对象的值。
因此如果:
i++++;
是合法的,i将仅仅增加了一次。这与人的直觉相违背,使人迷惑(对于int类型和UPInt都是一样),所以最好禁止这么做。
C++禁止int类型这么做,同时你也必须禁止你自己写的类有这样的行为。最容易的方法是让后缀increment 返回const对象。当编译器遇到这样的代码:
i++++; // same as
i.operator++(0).operator++(0);
它发现从第一个operator++函数返回的const对象又调用operator++函数,然而这个函数是一个non-const成员函数,所以const对象不能调用这个函数。如果你原来想过让一个函数返回const对象没有任何意义,现在你就知道有时还是有用的,后缀increment和decrement就是例子.
如果你很关心效率问题,当你第一次看到后缀increment函数时,你可能觉得有些问题。这个函数必须建立一个临时对象以做为它的返回值,(参见条款M19),上述实现代码建立了一个显示的临时对象(oldValue),这个临时对象必须被构造并在最后被析构。前缀increment函数没有这样的临时对象。由此得出一个令人惊讶的结论,如果仅为了提高代码效率,UPInt的调用者应该尽量使用前缀increment,少用后缀increment,除非确实需要使用后缀increment。让我们明确一下,当处理用户定义的类型时,尽可能地使用前缀increment,因为它的效率较高。
我们再观察一下后缀与前缀increment操作符。它们除了返回值不同外,所完成的功能是一样的,即值加一。简而言之,它们被认为功能一样。那么你如何确保后缀increment和前缀increment的行为一致呢?当不同的程序员去维护和升级代码时,有什么能保证它们不会产生差异?除非你遵守上述代码里的原则,这才能得到确保。这个原则是后缀increment和decrement应该根据它们的前缀形式来实现。你仅仅需要维护前缀版本,因为后缀形式自动与前缀形式的行为一致。
正如你所看到的,掌握前缀和后缀increment和decrement是容易的。一旦了解了他们正确的返回值类型以及后缀操作符应该以前缀操作符为基础来实现的规则,就足够了。
不要重载“&&”,“||”, 或“,”
与C一样,C++使用布尔表达式短路求值法(short-circuit evaluation)。这表示一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。例如:
char *p;
...
if ((p != 0) && (strlen(p) > 10)) ...
这里不用担心当p为空时strlen无法正确运行,因为如果p不等于0的测试失败,strlen不会被调用。同样:
int rangeCheck(int index)
{
if ((index < lowerBound) || (index > upperBound)) ...
...
}
如果index小于lowerBound,它不会与upperBound进行比较。
很早以前,上述行为特性就被反复灌输给C和C++的程序员,所以他们都知道该特性。而且他们也依赖于短路求值法来写程序。例如在上述第一个代码中,当p为空指针时确保strlen不会被调用是很重要的,因为C++标准说(正如C标准所说)用空指针调用strlen,结果不确定。
C++允许根据用户定义的类型,来定制&&和||操作符。方法是重载函数operator&& 和operator||,你能在全局重载或每个类里重载。然而如果你想使用这种方法,你必须知道你正在极大地改变游戏规则。因为你以函数调用法替代了短路求值法。也就是说如果你重载了操作符&&,对于你来说代码是这样的:
if (expression1 && expression2) ...
对于编译器来说,等同于下面代码之一:
if (expression1.operator&&(expression2)) ...
// when operator&& is a
// member function
if (operator&&(expression1, expression2)) ...
// when operator&& is a
// global function
这好像没有什么不同,但是函数调用法与短路求值法是绝对不同的。首先当函数被调用时,需要运算其所有参数,所以调用函数functions operator&& 和 operator||时,两个参数都需要计算,换言之,没有采用短路计算法。第二是C++语言规范没有定义函数参数的计算顺序,所以没有办法知道表达式1与表达式2哪一个先计算。完全可能与具有从左参数到右参数计算顺序的短路计算法相反。
因此如果你重载&&或||,就没有办法提供给程序员他们所期望和使用的行为特性,所以不要重载&&和||。
同样的理由也适用于逗号操作符,但是在我们深入研究它之前,我还是暂停一下,让你不要太惊讶,“逗号操作符?哪有逗号操作符?”确实存在。逗号操作符用于组成表达式,你经常在for循环的更新部分(update part)里遇见它。例如下面来源于Kernighan's and Ritchie's 经典书籍The C Programming Language 第二版(Prentice-Hall, 1988)的函数:
// reverse string s in place
void reverse(char s[])
{
for (int i = 0, j = strlen(s)-1;
i < j;
++i, --j) // 啊! 逗号操作符!
{
int c = s[i];
s[i] = s[j];
s[j] = c;
}
}
在for循环的最后一个部分里,i被增加同时j被减少。在这里使用逗号很方便,因为在最后一个部分里只能使用一个表达式,分开表达式来改变i和j的值是不合法的。
对于内建类型&&和||,C++有一些规则来定义它们如何运算。与此相同,也有规则来定义逗号操作符的计算方法。一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。所以在上述循环的最后部分里,编译器首先计算++i,然后是—j,逗号表达式的结果是--j。
也许你想为什么你需要知道这些内容呢?因为你需要模仿这个行为特性,如果你想大胆地写自己的逗号操作符函数。不幸的是你无法模仿。
如果你写一个非成员函数operator,你不能保证左边的表达式先于右边的表达式计算,因为函数(operator)调用时两个表达式做为参数被传递出去。但是你不能控制函数参数的计算顺序。所以非成员函数的方法绝对不行。
剩下的只有写成员函数operator的可能性了。即使这里你也不能依靠于逗号左边表达式先被计算的行为特性,因为编译器不一定必须按此方法去计算。因此你不能重载逗号操作符,保证它的行为特性与其被料想的一样。重载它是完全轻率的行为。
你可能正在想这个重载恶梦究竟有没有完。毕竟如果你能重载逗号操作符,你还有什么不能重载的呢?正如显示的,存在一些限制,你不能重载下面的操作符:
. .* :: ?:
new delete sizeof typeid
static_cast dynamic_cast const_cast reinterpret_cast
你能重载:
operator new operator delete
operator new[] operator delete[]
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
() []
(有关new和delete还有operator new, operator delete, operator new[], and
operator delete[]的信息参见条款M8)
当然能重载这些操作符不是去重载的理由。操作符重载的目的是使程序更容易阅读,书写和理解,而不是用你的知识去迷惑其他人。如果你没有一个好理由重载操作符,就不要重载。在遇到&&, ||, 和 ,时,找到一个好理由是困难的,因为无论你怎么努力,也不能让它们的行为特性与所期望的一样。
:理解各种不同含义的new和delete
人们有时好像喜欢故意使C++语言的术语难以理解。比如说new操作符(new operator)和new操作(operator new)的区别。
当你写这样的代码:
string *ps = new string("Memory Management");
你使用的new是new操作符。这个操作符就象sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。
你所能改变的是如何为对象分配内存。new操作符调用一个函数来完成必需的内存分配,你能够重写或重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字是operator new。
函数operator new 通常这样声明:
void * operator new(size_t size);
返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。(如果你喜欢,你能写一种operator new函数,在返回一个指针之前能够初始化内存以存储一些数值,但是一般不这么做。)参数size_t确定分配多少内存。你能增加额外的参数重载函数operator new,但是第一个参数类型必须是size_t。
你一般不会直接调用operator new,但是一旦这么做,你可以象调用其它函数一样调用它:
void *rawMemory = operator new(sizeof(string));
操作符operator new将返回一个指针,指向一块足够容纳一个string类型对象的内存。
就象malloc一样,operator new的职责只是分配内存。它对构造函数一无所知。operator new所了解的是内存分配。把operator new 返回的未经处理的指针传递给一个对象是new操作符的工作。当你的编译器遇见这样的语句:
string *ps = new string("Memory Management");
它生成的代码或多或少与下面的代码相似(更多的细节见Effective C++条款8和条款10,还有我的文章Counting object里的注释。):
void *memory = // 得到未经处理的内存
operator new(sizeof(string)); // 为String对象
call string::string("Memory Management") //初始化
on *memory; // 内存中
// 的对象
string *ps = // 是ps指针指向
static_cast<string*>(memory); // 新的对象
注意第二步包含了构造函数的调用,你做为一个程序员被禁止这样去做。你的编译器则没有这个约束,它可以做它想做的一切。因此如果你想建立一个堆对象就必须用new操作符,不能直接调用构造函数来初始化对象。
.. placement new
有时你确实想直接调用构造函数。在一个已存在的对象上调用构造函数是没有意义的,因为构造函数用来初始化对象,而一个对象仅仅能在给它初值时被初始化一次。但是有时你有一些已经被分配但是尚未处理的(raw)内存,你需要在这些内存中构造一个对象。你可以使用一个特殊的operator new ,它被称为placement new。
下面的例子是placement new如何使用,考虑一下:
class Widget {
public:
Widget(int widgetSize);
...
};
Widget * constructWidgetInBuffer(void *buffer,
int widgetSize)
{
return new (buffer) Widget(widgetSize);
}
这个函数返回一个指针,指向一个Widget对象,对象在转递给函数的buffer里分配。当程序使用共享内存或memory-mapped I/O时这个函数可能有用,因为在这样程序里对象必须被放置在一个确定地址上或一块被例程分配的内存里。(参见条款M4,一个如何使用placement new的一个不同例子。)
在constructWidgetInBuffer里面,返回的表达式是:
new (buffer) Widget(widgetSize)
这初看上去有些陌生,但是它是new操作符的一个用法,需要使用一个额外的变量(buffer),当new操作符隐含调用operator new函数时,把这个变量传递给它。被调用的operator new函数除了待有强制的参数size_t外,还必须接受void*指针参数,指向构造对象占用的内存空间。这个operator new就是placement new,它看上去象这样:
void * operator new(size_t, void *location)
{
return location;
}
这可能比你期望的要简单,但是这就是placement new需要做的事情。毕竟operator new的目的是为对象分配内存然后返回指向该内存的指针。在使用placement new的情况下,调用者已经获得了指向内存的指针,因为调用者知道对象应该放在哪里。placement new必须做的就是返回转递给它的指针。(没有用的(但是强制的)参数size_t没有名字,以防止编译器发出警告说它没有被使用;见条款M6。) placement new是标准C++库的一部分(见
Effective C++ 条款49)。为了使用placement new,你必须使用语句#include <new>(或者如果你的编译器还不支持这新风格的头文件名<new.h>)。
让我们从placement new回来片刻,看看new操作符(new operator)与operator new的关系,你想在堆上建立一个对象,应该用new操作符。它既分配内存又为对象调用构造函数。如果你仅仅想分配内存,就应该调用operator new函数;它不会调用构造函数。如果你想定制自己的在堆对象被建立时的内存分配过程,你应该写你自己的operator new函数,然后使用new操作符,new操作符会调用你定制的operator new。如果你想在一块已经获得
指针的内存里建立一个对象,应该用placement new。
.. Deletion and Memory Deallocation
为了避免内存泄漏,每个动态内存分配必须与一个等同相反的deallocation对应。函数operator delete与delete操作符的关系与operator new与new操作符的关系一样。当你看到这些代码:
string *ps;
...
delete ps; // 使用delete 操作符
你的编译