操作符
主旨:讨论“重载操作符”被调用的时机、方法、它们的行为,应如何与其它操作符产生关系,以及如何获取“重载操作符”的控制权
条款5:对定制的“类型转换函数”保持警觉
故名思意,就是在自己提供的类上编写某些函数以便为了编译器拿来作为隐式类型转换用时一定要多加注意,最好不要做这样的操作
两种函数可以做到上述点:
- 单自变量constructors 核心点: 调用构造函数 构造函数存在参数
- 隐式类型转换操作符(member function) 核心点: operator type() const : operator double() const;
//单自变量constructors
class A
{
public:
A(int&); //可以把int 转为A类的对象
};
class B
{
public:
B(string& s1="",string& s2=""); //可以把string 转换为B类的对象
};
//隐式类型转换操作符
class C
{
public:
C(int l, int r)
{
char datas[100];
sprintf_s(datas, "%d.%d", l, r);
_data = atof(datas);
}
operator double() const
{
return _data;
}
private:
double _data;
};
C r(1,23); //1.23
double d = 0.5 * r //0.5*1.23
建议理由:
可能在未预期的情况下,此类函数可能会被调用,而且其结果可能是不正确、不直观的程序行为,很难调试
解决方案:
1.对于隐式转换操作符:以功能相同的另一个函数取代类型转换操作符
例如:
- QString -> string 要调用 toStdString()
- string -> char* 要调用 c_str()
2.对于单自变量:声明explicit关键字,编译器便不能因隐式转换的需要而去调用它,但是允许用户显式类型转换
#include <iostream>
using namespace std;
template<class T>
class Array
{
public:
Array(int size):_size(size) //未声明exlicit则注释2正常编译通过
{
data = new T[size];
}
const int size()
{
return _size;
}
T& operator [](int i)
{
return data[i];
}
//bool operator==(const Array<T>& lhs)
//{
// if (size() != lhs.size())
// return false;
// for (int i = 0; i < size(); i++)
// {
// if (data[i] != lhs[i])
// return false;
// }
// return true;
//}
private:
T* data;
int _size;
};
bool operator==(const Array<int>& lhs, const Array<int>& rhs)
{
Array<int>* nConstL = const_cast<Array<int>*>(&lhs);
Array<int>* nConstR = const_cast<Array<int>*>(&rhs);
if (nConstL->size() != nConstR->size())
return false;
for (int i = 0; i < nConstL->size(); i++)
{
if((*nConstL)[i] != (*nConstR)[i])
return false;
}
return true;
}
int main(int argc,char*argv[])
{
Array<int> a(10);
Array<int> b(10);
for (int i = 0; i < 10; i++)
{
a[i] = i;
b[i] = i;
}
for (int i = 0; i < 10; i++)
{
if (a[i] == b[i]) //注释2 if (a == b[i]) 同样能编译通过
cout << a[i]<<" = "<<b[i] << endl;
}
cout << (b == a);
return 0;
}
因此为了防止编译器因为单自变量隐式转换,建议有参的构造函数都加一个explicit
3.使用代理类完成单自变量数据的获取【很重要的技术】
对上述代码中Arrray类增加一个 ArraySize类,专门用于保存传递的数组大小的值
//衔接上述Array声明
class Array
{
public:
Array(ArraySize size); //将单自变量的传递改为代理类的传递
private:
ArraySize m_size;
//嵌套到Array表示之和Array一起使用
class ArraySize{
public:
ArraySize(int size):theSize(size){}
private:
int theSize;
};
}
之所以这种方法能够阻止隐式转换,是因为编译器最多只能进行一次隐式转换,超过一次则认为失败
例如: Array<int> a(10); 能够做到 int -> arraySize,但不能做到 int -> arraySize -> Array的转换,因为它需要编译器隐式转换两次
条款6:区别++/--操作符的前置(prefix)和后置(postfix)形式
前置式(累加然后取出): type& operator++() //返回一个reference
后置式(取出然后累加):const type operator++(int) //返回一个 const对象
如上所示,后置式的形参int没有实际意义,它是为了填补一个语法上的漏洞:重载函数是以其参数类型来区分彼此的,然而不论前置
式还是后置式,都没有参数,因此给后置式加上一个int参数,并在编译时默认设置为0
class UPInt;
//前置式:累加然后取出
UPInt& UPInt::operator++()
{
*this +=1;
return *this;
}
//后置式:取出然后累加
const UPInt UPInt::operator++(int i)
{
UPInt oldVa = *this;
++(*this);
return oldVa;
}
但从效率点出发建议使用前置式,因为后置式会生成临时对象
条款7:千万不要重载 '&&' '||' ',' 三个操作符
原因,语言对于真假值表达式采用“骤死式”评估,一旦该表达式的真假值确定,即使表达式部分尚未检验,整个评估工作即结束
如果对此操作符进行重载,则会从根本层面改变整个游戏规则,“函数调用”语义会取代“骤死式”语义
条款8:了解各种不同意义的new和delete
- new 即 new operator是由语言内建的操作符:实现两方面动作:分配内存,调用constructor
- 我们能够改变的是分配内存部分调用的分配行为,new operator会调用函数operator new,执行必要的分配内存动作
//new operator 在实现分配内存时调用的函数 operator new原型
void * opearator new(size_t size);
//使用 operator new 创建内存
void *rawMemory = operator new(sizeof(type));
可以看出operator new函数唯一的作用就是分配内存空间,而将内存转换为一个对象时 new operator的责任
假设创建一个string对象如下:
//创建一个字符串对象
string * ps = new string("hello world");
//编译器产生的代码
void * memory = operator new(sizeof(string));
call string::string("Memory Management") on * memory //将内存中的对象初始化[]
string *ps = static_cast<string*>(memory);
上述调用 constructor只允许编译器调用,这也是为什么创建一个heap-based object必须使用 new operator
删除和内存释放,需要注意的是必须配对使用删除动作
- malloc -> free
- new -> delete
- operator new [] -> operator delete []
- operator new -> operator delete [注意:如果只调用内存分配动作而没有调用构造函数,则此操作和 malloc free相同]
如果使用了 placement new 应该避免对那块内存使用 delete operator,因为此操作会调用operator delete 来释放内存,
但是该内存对象的分配可能并不是由operator new函数调用分配的,因为 placement new 只是负责接收指针而已。