05:谨慎定义类型转换函数
有两种函数允许编译器进行隐式类型转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构造函数。该函数可以是只定义了一个参数,也可以是定义了多个参数但第一个参数以后的所有参数都有缺省值。
隐式类型转换运算符的形式是:operator type()。不用定义函数的返回类型,因为返回类型就是type。例如为了允许Rational(有理数)类隐式地转换为double类型,可以如此声明Rational类:
class Rational { public: operator double() const; // 转换Rational类成double类型 }; Rational 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 r(1, 2); cout << r; // 错误! Rationa对象没有 operator<< cout << r.asDouble(); // 正确, 用double类型打印r
在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再会发生,这点损失是值得的。一般来说,越有经验的C++程序员就越喜欢避开类型转换运算符。例如在C++标准库中的string类型没有包括隐式地从string转换成C风格的char*的功能,而是定义了一个成员函数c_str用来完成这个转换。
通过单参数构造函数进行隐式类型转换更难消除,而且在很多情况下这些函数所导致的问题要甚于隐式类型转换运算符。举一个例子,一个array类模板,这些数组需要调用者确定边界的上限与下限:
template<class T> class Array { public: Array(int size); T& operator[](int index); ... }; 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; } }
上面的for循环本意是想用a的每个元素与b的每个元素相比较,但当录入a时,却忘记了数组下标。这种情况下我们希望编译器找出这种错误,但是它根本没有。因为它把这个调用看成用Array<int>参数(参数a)和int参数(参数b[i])调用operator==函数,虽然没有operator==函数是这样的参数类型,编译器注意到它能通过调用Array<int>构造函数转换int类型到Array<int>类型。编译器如此去编译,生成的代码就像这样:
for (int i = 0; i < 10; ++i) if (a == static_cast< Array<int> >(b[i])) ...
可以使用explicit关键字解决这个问题:只要将constructors声明为explicit,编译器便不能因隐式类型转换的需要而调用它们。不过显式类型转换仍然是允许的:
template<class T> class Array { public: ... explicit Array(int size); ... }; Array<int> a(10); // 没问题,explicit ctors可以像往常一样作为对象构造之用 Array<int> b(10); // 也没问题 if (a == b[i]) ... // 错误! 无法将int隐式转换为Array<int> if (a == Array<int>(b[i])) ... // 没问题,显式转换(但是代码逻辑上存疑) if (a == static_cast< Array<int> >(b[i])) ... //同样没问题 if (a == (Array<int>)b[i]) ... // C旧式转换也没问题
还有一种不使用explicit的方法,它利用这样的规则:没有任何一个转换过程(sequence of conversions)可以内含一个以上的“用户定制转换行为”(调用单参数构造函数或隐式类型转换运算符)。考虑Array template,现在需要一种方法,不但允许以一个整数作为构造函数的参数来指定数组大小,又能阻止一个整数被隐式转换为一个临时性Array对象:
template<class T> class Array { public: class ArraySize { // this class is new public: ArraySize(int numElements): theSize(numElements) {} int size() const { return theSize; } private: int theSize; }; Array(ArraySize size); // note new declaration ... };
现在,当调用”Array<int> a(10);”时,你的编译器寻找拥有单一int类型参数的构造函数,但是这种构造函数不存在,不过编译器知道它能将int转换为一个ArraySize对象,而该对象正是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]) ... // 错误
这种情况下,编译器不能考虑将int转换为一个临时性的ArraySize对象,然后再根据这个临时对象产生Array<int>对象,因为那将调用两个用户定制转换行为,这样的转换是禁止的。
类似ArraySize这样的类,往往被称为proxy classes,因为它的每一个对象都是为了其他对象而存在的,好像其他对象的代理人一样。
06:区别increment/decrement操作符的前置和后置形式
重载++或--操作符时,因为++或--的前置式和后置式都是没有参数的,因此无法以参数来区分前置还是后置,所以规定重载时后置式有一个int参数:
class UPInt { public: UPInt& operator++(); // 前置++ const UPInt operator++(int); // 后置++ UPInt& operator--(); // 前置-- const UPInt operator--(int); // 后置-- ... }; // prefix form: increment and fetch UPInt& UPInt::operator++() { *this += 1; return *this; } // postfix form: fetch and increment const UPInt UPInt::operator++(int) { const UPInt oldValue = *this; ++(*this); return oldValue; } UPInt i; ++i; // 调用i.operator++(); i++; // 调用i.operator++(0); --i; // 调用i.operator--(); i--; // 调用i.operator--(0);
需要注意的是,后置操作符函数没有使用它的参数,但如果没有在函数里使用参数,许多编译器会报警。为了避免编译器报警,惯常的做法是省略掉不想使用的参数名称。
注意,这些操作符前置与后置形式返回值类型是不同的。前置形式返回一个引用,后置形式返回一个 const 类型。如果后置形式返回的不是一个const对象的话,则像i++++(等价于i.operator++(0).operator++(0))这样的动作就合法了,这就和内建类型的行为不一致了;而且即便能够两次实施increment操作符,第二个operator++所改变的对象也是第一个operator++返回的对象,而不是原对象。
单以效率而言,UPInt 的调用者应该尽量使用前置 increment,少用后置 increment,因为后置实现必须产生一个临时对象,而前置式就没有如此的临时对象。因此,当处理用户定义的类型时,尽可能地使用前置increment,因为它的效率更高。
上面的代码还有一个问题,后置与前置 increment 操作符,它们除了返回值不同外,所完成的功能是一样的,那么如何确保后置 increment和前置 increment 的行为一致呢?为了确保行为一致,必须遵循一个原则:后置increment 和 decrement 应该根据它们的前置形式来实现。这样仅仅需要维护前置版本。
07:千万不要重载&&, ||和, 操作符
&&和||操作符采用“短路式”计算左右表达式的值,但是一旦要对这俩操作符进行重载,也就是说:
if (expression1 && expression2) ...
会被编译器视为以下两种方式之一:
if (expression1.operator&&(expression2)) ... // operator&& 是个成员函数 if (operator&&(expression1, expression2)) ... // operator&& 是个全局函数
但是,函数调用时,语言规范未明确定义参数的计算顺序,所以也就没办法知道expression1和expression2哪个先计算,这与&&和||操作符的“短路式”计算方式不符。
逗号(,)操作符也有类似的问题,C++规定,表达式内如果含有逗号,则逗号左侧先计算,然后是右侧,最后,整个逗号表达式的结果是右侧的值。而将逗号表达式进行重载后,无法保证这样的计算循序。
C++规定,不能重载以下操作符:
可以重载的操作符有:
然而,可以重载并不意味着可以毫无理由的进行重载,所以,如果没有什么好的理由将某个操作符进行重载,就不要这么做。
08:了解各种不同意义的new和delete
下面的代码:
string *ps = new string("Memory Management");
这里的new是所谓的new表达式,它首先分配内存,然后在分配的内存上调用构造函数。new表达式的行为不可改变。编译器看到上面的语句,它可能产生的代码如下:
void *memory = operator new(sizeof(string)); // get raw memory for a string object call string::string("Memory Management") on *memory; string *ps = static_cast<string*>(memory);
上面的第一句是调用new operator用于执行内存分配。它的原型通常如下:
void * operator new(size_t size);
new operator可以被重载,重载该函数时,第一个参数类型必须总是size_t。operator new的重载版本中,有一个特殊版本称为placement new,比如下面的代码:
Widget * constructWidgetInBuffer(void *buffer, int widgetSize) { return new (buffer) Widget(widgetSize); }
该函数返回一个指向Widget对象的指针,该对象构造于函数第一个参数buffer之上。这里就是使用了placement new,它用于满足对象必须构造于特定地址,或者以特殊函数分配出来的内存上这样的需求。
placement new的实现看起来像这样:
void * operator new(size_t, void *location) { return location; }
类似于new表达式调用operator new,delete表达式会调用operator delete。因此如果ps指向使用new构造出来的string对象,则delete ps;等价于下面的代码:
ps->~string(); operator delete(ps); // 释放内存
如果使用了placement new,则应该避免使用delete表达式,因为delete表达式会调用operator delete,而该内存最初并非是用operator new分配而来的,毕竟placement new只是返回传递给他的指针而已,谁也不知道那个指针是从哪来的:
// functions for allocating and deallocating memory in shared memory void * mallocShared(size_t size); void freeShared(void *memory); void *sharedMemory = mallocShared(sizeof(Widget)); Widget *pw = constructWidgetInBuffer( sharedMemory, 10); //使用placement new ... delete pw; // 未定义! pw从mallocShared得来,而不是operator new pw->~Widget(); freeShared(pw); // 使用与mallocShared对应的freeShared释放内存