第二章:操作符
条款五.对定制的类型转换函数保持警觉
一个类中的隐式类型转换符的形式是:
operator type();
这是一个有点奇怪的函数:关键词operator加上一个类型名称再加上一个括号。例如有理数类:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
operator double() const;
};
这个隐式转换就将有理数的分数形式转换为小数。考虑下面的语句:
Rational r(1, 2);
cout << r;
这个时候操作符operator<<
虽然无法接受Rational
,但是经过隐式转换将Rational
转换为double
,这个时候就能调用double
,实际输出就是0.5。这可能是我们没有预料到的函数调用。解决的办法就是以一个对等的函数来取代类型转换操作符。
double Rational::asDouble() const; //将Rational转换为double
直接调用这个函数就好了:cout << r.asDouble();
有一个例子说明了这个观点,在string
类中,并没有类型转换操作符将其转换为char *
,实际提供的是一个成员函数c_str()
。
另外一种隐式类型转换就是通过单自变量的构造函数来完成,例如一个Array类:
template<typename T>
class Array {
public:
Array(int size);
T& operator[](int index);
};
考虑对Array<int>
对象的比较操作:
bool operator==(const Array<int>& lhs, const Array<int>& rhs);
Array<int> a(10), b(10);
for(int i = 0; i < 10; ++i) {
if(a == b[i]){ //本来应该式a[i] == b[i]
...
}
}
上面的代码并不会报错,原因就在于单自变量的构造函数的隐式类型转换,实际产生的代码如下:
if(a == static_cast<Array<int>>(b[i]))
避免这种类型的隐式类型转换的方法就是使用explicit
关键字,explicit
关键字的作用就是抑制编译器的隐式类型转换,但是显式的转换是可以的。
explicit Array(int size);
if(a == b[i]) //编译就会报错
还有一种解决办法就是使用代理类“proxy class”,例如:
class Array {
public:
Array(ArraySize size); //其它一样
};
class ArraySize {
public:
ArraySize(int numEle) : theSize(numEle) {}
int size() const { return theSize; }
private:
int theSize;
}
这个时候ArraySize
就是代理类
if(a == b[i]) //编译出错,因为无法两次调用隐式类型转换。
隐式类型转换害处大于好处,请谨慎使用。
条款六:区分前置/后置操作符
这里以前置++和后置++为例:
后置++通常会带有一个int型的形参:
T& operator++() { //前置++
*this += 1;
return *this;
}
T& operator++(int) { //后置++
T temp = *this; //临时对象
++(*this); //调用前置++
return temp;
}
前置++将对象作为左值返回,左值:可以取地址/有明确内存范围的值;
后置++将对象的原始值副本作为右值返回,右值:无法取地址/需要存储到对象的值。
举个例子:
int i = 0;
i++++; //错误: (i++)++
++++i; //正确:++(++i)
第一个表达式式错误的,第一个后置++返回右值,而右值调用后置++是错误的。为了避免这种情况,将后置++的返回值声明为const即可。
const T& operator++(int);
条款七:千万不要重载&& || ,
这三个操作符
c++对于真假值表达式采取的评估方式是“骤死式”,即一旦表达式的真假值确定,后面未检验的部分就不会再考虑了。
char* p;
if(p != 0 && strlen(p) > 10) ...
无需担心strlen(p)
中p是否为NULL。
C++允许重载&& ||
操作符,但是函数调用语意会取代骤死式语意:
if(expression1 && expression2) ...
重载,那么实际是下面两种情况:
if(expression1.operator&&(expression2));
if(operator&&(exoression1, expression2));
函数调用操作执行之前,所有的参数都已经评估完成,就不存在什么骤死式语意。
对于逗号,总是左侧的先被评估,然后右侧的再被评估,最后以右侧评估结果作为代表,如果是操作符重载,不一定能达到这个效果。
无法重载的操作符:
. .* :: ?:
new delete sizeof typeid
static_cast dynamic_cats const_cast reinterpret_cast
可以重载的操作符有:
operator new operator delete
operator new[] operator delete[]
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
() []
条款八:了解各种不同意义的new和delete
三种new及其对应的delete:
new operator;
operator new;
placement operator new;
- new operator即是最常见的new操作符,C++语言内建的操作符。
new操作符的过程就是:先配置内存,再调用构造函数(内置类型直接赋值)。(如果配置内存失败,内存还是需要释放的) 。
对应的delete的过程就是:先调用析构函数(内置类型没有这一步),再释放内存。 - operator new的作用就是分配内存,函数原型为:
void * operator new(size_t size); //size就是分配的内存的大小
- placement operator new就是Operator new预先定义好的重载,函数原型如下:
void * operator new(size_t, void * p) { return p; }
Operator new得到的资源要进行释放,也必须使用对应的Operator delete。一般情况下都是先调用析构函数,在调用Operator delete。
new和数组。
更加底层的内容还是参见执行期语意学第二节new和delete。