More Effective C++ (运算符)
4.1:谨慎定义类型转换函数
<1>容易的方法是利用一个最新的编译器特性:explicit关键字
<2>C++编译器把">>"作为一个符号来解释,在两个">"间没有空格,语句会产生语法错误。
<3>隐式类型转换函数
示例代码如下:
1 #include<iostream> 2 using namespace std; 3 4 template<class T> 5 class Array 6 { 7 public: 8 class ArraySize 9 { 10 public: 11 ArraySize(int numElements):theSize(numElements) 12 { 13 cout<<"flag ArraySize(int numElements)"<<endl; 14 } 15 int size() const 16 { 17 cout<<"flag size()"<<endl; 18 return theSize; 19 } 20 private: 21 int theSize; 22 }; 23 24 Array(int lowBound,int highBound) 25 {}; 26 27 Array(ArraySize arsize) 28 { 29 cout<<"flag Array"<<endl; 30 }; 31 }; 32 void main() 33 { 34 Array<int> a(10); 35 } 36 37 /* 38 flag ArraySize(int numElements) 39 flag Array 40 */
4.2:自增,自减操作符前缀形式和后缀形式
<1>重载函数间的区别决定于它们的参数类型上的差异,但是不论increament或者decrement的前缀还是后缀都只有一个参数。
为了解决这个语言问题,C++规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0作为int参数的值给该函数。
<2>前缀自增:增加然后取回;后缀自增:取回然后增加
<3>前缀形式
示例代码如下:
1 UPInt & UPInt::operator++() 2 { 3 *this += 1; //增加 4 return *this; //取回值 5 } 6 const UPInt UPInt::operator++(int) 7 { 8 UPInt oldValue = *this; //保留原值 9 ++(*this); //增加 10 return oldValue; //返回原值 11 }
<4>后缀操作符函数没有使用它的参数。这个参数仅仅只是为了区别前缀函数调用。
<5>如果在函数内部没有使用参数,许多编译器会显示警告信息。为了避免这些信息,最常用的方式就是省略掉参数名称
<6>很明显后缀函数必须返回一个对象,但是为什么是const对象呢?假设不是const:
假设不是 const 对象,下面的代码就是正确的:
这组代码与下面的代码相同:
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 就是例子。 (更多的例子参见 Effective C++ 条款 21)
如果你很关心效率问题, 当你第一次看到后缀 increment函数时,你可能觉得有些问题。
这个函数必须建立一个临时对象以做为它的返回值, (参见条款M19) ,上述实现代码建立了一个显式的临时对象(oldValue) ,这个临时对象必须被构造并在最后被析构。前缀 increment 函数没有这样的临时对象。
由此得出一个令人惊讶的结论,如果仅为了提高代码效率,UPInt 的调用者应该尽量使用前缀 increment,少用后缀 increment,除非确实需要使用后缀 increment。
让我们明确一下,当处理用户定义的类型时,尽可能地使用前缀 increment,因为它的效率较高。
我们再观察一下后缀与前缀 increment 操作符。它们除了返回值不同外,所完成的功能是一样的,即值加一。
简而言之,它们被认为功能一样。那么你如何确保后缀increment和前缀increment的行为一致呢?
当不同的程序员去维护和升级代码时,有什么能保证它们不会产生差异?除非你遵守上述代码里的原则,这才能得到确保。
这个原则是后缀 increment和 decrement 应该根据它们的前缀形式来实现。你仅仅需要维护前缀版本,因为后缀形式自动与前缀形式的行为一致。
正如你所看到的,掌握前缀和后缀 increment和decrement 是容易的。
一旦了解了他们正确的返回值类型以及后缀操作符应该以前缀操作符为基础来实现的规则,就足够了。
4.3:不要重载“&&”,“||”,或“,”
<1>char *p; if((p!=0)&&(strlen(p)>10))......
注意这个if条件的写法顺序
<2>运算符重载要求:
4.4:理解各种不同含义的new和delete
<1>new operator 称为new操作符与operator new称为new操作
string *ps = new string("Memory Management");
使用的是new operator ,即就是new操作符,这种用法是不可改变的,功能总是相同的。
完成两部分任务:第一,分配足够的内存以便容纳所需类型的对象;第二,它调用构造函数初始化内存中的对象。
而我们所能改变的是如何为对象分配内存。
new操作符调用一个函数来完成必需的内存分配,你能够重写或重载这个函数来改变它的行为。
new操作符为分配内存所调用函数的名字是 operator new。
原型如下:
void * operator new(size_t size);
调用形式与其他函数一样:
void *rawMemory=operator new(sizeof(string));
placement new旨在完成一种情况,那就是假如我们已经申请到了内存,想要在该内存上构建对象,就选择placement new
三种情况代码如下:
1 #include<iostream> 2 using namespace std; 3 4 class Test 5 { 6 int a; 7 public: 8 Test(int data = 10):a(data) 9 { 10 cout<<"Construction :"<<this<<endl; 11 } 12 ~Test() 13 { 14 cout<<"Destroy :"<<this<<endl; 15 }; 16 }; 17 void main() 18 { 19 //new操作符(new operator) 20 Test *pa = new Test[2]; //完成两部分任务 21 delete []pa; 22 pa = NULL; 23 24 //new函数(operator new) 25 void *pb = operator new(sizeof(Test)); //仅仅只是分配一块内存,类似于malloc 26 operator delete(pb); 27 pb = NULL; 28 29 //placement new 30 void *suffer = malloc(sizeof(Test)); 31 Test *pc = new(suffer) Test(100); //在已经申请的内存上建立自己的对象 32 pc->~Test(); 33 free(suffer); 34 suffer = NULL; 35 }
总结:
如果想在堆上创建一个对象,应该用new操作符,它分配内存,同时又为对象调用构造函数。
如果仅仅想分配内存,就用operator new函数,它不会调用构造函数
如果你想定制自己的在堆对象被建立时的内存分配过程,应该重载写你自己的operator new函数,new操作符会调用你定制的operator new
如果想在一块已经分配好的内存里建立一个对象,使用placement new
<2>delete操作符与operator delete的关系与new操作符与operator new的关系一样
delete p;
导致编译器生成类似的代码:
p->~Class();
operator delete(p);