操作符
条款5:谨慎定义类型转换函数
1.C++编译器能够在两种数据类型之间进行隐式转换(implicit conversions),它继承了C语言的转换方法。这种可怕的转换可能会导致数据的丢失
2.所提到的是语言本身的特性,你无能为力。不过当你定义自己的类型时,就有更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换
3.有两种函数允许编译器进行这些转换:单参数构造函数(single-argument-constructors)和隐式类型转换运算符
4.单参数构造函数:只定义了一个参数的函数或虽定义了多个参数但第一个参数以后的所有参数都有缺省值
temlplate<typename T>
class Array
{
public:
Array(intlowBound, int hightBound);
Array(intsize);
T&operator[](int index);
};
Array<int> a(10);
Array<int> b(10);
for (int i = 0; i < 10; ++i)
{
if (a ==b[i]) // 哎呦! "a" 应该是 "a[i]"
{
dosomething for when
a[i] andb[i] are equal;
}
else
{
dosomething for when they're not;
}
}
我们的编译器注意到它能通过调用 Array<int>构造函数能转换 int 类型到 Array<int>类型,这个构造函数只有一个 int 类型的参数。然后编译器如此去编译,生成的代码就象这样:
if (a == static_cast< Array<int> >(b[i]))
5.隐式类型转换操作符只是一个样子奇怪的成员函数:operator关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字
class Rational
{
public:
Rational(int numerator = 0, intdenominator = 1);
operatordouble() const; //不需要返回类型,函数名称就是返回类型
};
Rational r(1, 2);
double d = 0.5 * r; //r 转换为double类型
标题是要谨慎定义类型转换函数,为什么要谨慎呢,就是说这些定义的类型转换函数有可能会产生不是你想要的结果,比如你不想进行转换的时候,却给你转换了。如下情况:
cout << r; //Rational并没有定义operator << 运算符,但是,编译器会找到operator double()转换函数,将r转换为double输出。但这并不是你想要的结果。
解决方法是用不使用语法关键字的等同的函数来替代转换运算符。如定义一个toDouble()函数来代替operator double()转换函数。STL中的string就提供了一个c_str()函数来转换到char*。
6.说了上面几条,根本问题是当你在不需要使用转换函数时,这些函数却会被调用运行。它表明了隐式类型转换的缺点:它们的存在将导致错误的发生
7.解决6的办法是,用不使用语法关键字的等同的函数来替代转换运算符(显式调用转换成员函数),虽然这种显式转换函数的使用不方便,但是函数被悄悄调用的情况不再会发生
8.通过单参数构造函数进行隐式类型转换更难消除解决办法:(1)explicit关键字 (2)使用代理类来代替构造函数的单参数
条款6:自增和自减操作符前缀形式与后缀形式的区别
1.句法问题:重载函数间的区别决定于它们的参数类型上的差异,但是不论是increment或decrement的前缀还是后缀都只有一个参数。为了解决这个语言问题,C++规定后缀形式有一个int类型的参数,当函数被调用时,编译器传递一个0做为int参数的值给该函数
class UPInt
{
public:
UPInt&operator++(); // ++前缀
const UPIntoperator++(int); // ++后缀
UPInt&operator--() // --前缀
const UPIntoperator--(int); // --后缀
UPInt&operator +=(int) // +=操作符,UPInts
};// 注意返回值类型,前缀形式返回一个引用,后缀形式返回一个const类型
2.从你开始做C程序员那天开始,你就记住increment的前缀形式有时叫做“增加然后取回”,后缀形式叫做“取回然后增加”
UPInt& UPInt::operator++()
{
*this +=1; // 增加
return*this; // 取回
}
const UPInt UPInt::operator++()
{
UPInt oldValue= *this; // 取回值
++(*this); // 增加
returnoldValue;
}
3.后缀操作符函数没有使用它的参数。它的参数只是用来区分前缀和后缀函数调用。如果没有在函数里使用参数,许多编译器会显示警告信息,很令人讨厌。为了避免这些警告信息,一种经常使用的方法是省略掉你不想使用的参数名称
4、后缀式返回const对象,原因是 :使该类的行为和int一致,而int不允许连续两次自增后缀运算;连续两次运算实际只增一次,和直觉不符,如UPInt u; u++++;++++u则合法
5、前缀比后缀效率更高,因为后缀要返回对象,而前缀只返回引用另外,可以用前缀来实现后缀,以方便维护。
6、后置式increment和decrement操作符的实现应以其前置兄弟为基础。
条款7:不要重载&&,||,或者,
1.与C一样,C++使用布尔表达式短路求值法(short-circuit evaluation)。这表示一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算,这是所谓的“骤死式”。
2.如果你重载&&或||,就没有办法提供给程序员他们所期望和使用的行为特性,因为你以函数调用法替代了短路求值法,极大地改变了游戏规则,从此“函数调用 语义”会取代“骤死式 语义”。如:
operator &&()有两种情况,一是全局重载,二是作为类成员函数;如果是全局重载,那么调用本质为 "operator &&(expression1, expression2)",而C++并没有规定参数的计算顺序,所以没有办法保证重载的&&保持原意不变。成员函数形式为expression1.operator&&(expression2),跟全局问题一样
3.当函数调用动作被执行,所有参数值都必须评估完成,也就没什么骤死式语义;C++语言规范没有定义函数参数的计算顺序,这与骤死式评估法形成一个明确的对比,骤死式总是由左向右评估其自变量。
4.C++的逗号表达式规则:一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。
5.提到逗号表达式的规则,主要是要重载,你需要模仿这个行为特性。不幸的是你无法模仿
6.你不能重载的操作符:. .* :: ?: new deletesizeof typeid static_cast const_castdynamic_cast reinterpret_cast
7.你可以重载的操作符:operator new operator delete operator new[] operator delete[]
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^=&= |= << >> >>= <<= == != <= >= && ||++ -- , ->* -> () []
8.能重载操作这些操作符不是去重载的理由。操作符重载的目的是使程序更容易阅读,书写和理解,而不是用你的知识去迷惑其他人。如果没有一个好理由重载操作符,就不要重载
条款8:理解各种不同含义的new和delete
1.new操作符(newoperator)和new操作(operatornew)
2.new操作符完成的功能分两部分,第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。你所能改变的是如何为对象分配内存。无论如何你不能改变其行为。
3.new操作符调用一个函数来完成必需的内存分配,你能够重新或重载这个函数来改变它的行为
4.new操作符为分配内存所调用函数的名字是operator new
5.operator new声明:
void* operator new( size_t size );
返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化内存。你能增加额外的参数重载函数operator new,但是第一个参数类型必须是size_t。
像malloc一样,operator new的职责是分配内存,它对构造函数一无所知。
6.有时你有一些已经被分配但是尚未处理的(raw)内存,你需要在这些内存中构造一个对象,你可以使用一个特殊的operator new,它被称为placement new,它唯一需要做的就是将它获得的指针再返回。
7.为了使用placement new,你必须使用语句#include<new>
8.如果你想在堆上建立一个对象,应该用new操作符。它既分配内存又为对象调用构造函数。如果你仅仅想分配内存,就应该调用operator new函数,它不会调用构造函数。如果你想定制自己的堆对象被建立时的内存分配过程,你应该写你自己的operator new函数,然后使用new操作符,new操作符会调用你定制的operator new。如果你想在一块已经获得指针的内存里建立一个对象,应该用placement new
9.为了避免内存泄漏,每个动态内存分配必须与一个等同相反的deallocation对应。函数operator delete与delete操作符的关系与operator new与new操作符的关系一样。
10、operator new[]与operator delete[]和new与delete相类似。注意数组所调用的constructor和destructor的数量。
11、delete操作符作为new操作符对应的相反行为,做的事情自然跟new相反,1调用析构函数,2释放已经分配的内存。如果你重载了operator new,则你必须重载operator delete来对应,否则有可能导致new和delete行为的不一致性。什么样的new对应什么样的delete,即单个对象的new对应单个对象的delete,new[]对应delete[]。
12、注意,尽量不要重载全局的new和delete,这样会造成程序的不兼容性,除非这是你想要的结果。