操作符
条款5: 对定制的“类型转换函数”保存警觉
C++允许编译器在不同类型之间执行隐式转换(implicit conversions),你可以选择是否提供某些函数,供编译器拿来作为隐式转换之用;
两种函数允许编译器执行隐式转换:
第一种:单自变量constructors( 构造函数 ),也就是指能够以单一变量成功调用的构造函数,也可能拥有多个参数,并且除了第一个参数外都有默认值
class Name{
public:
Name(const string& s); //可以把string 转换为 Name
...
};
class Rational{
public:
Rational(int numerator = 0, //可以把int转换为Rational,此处为 int numerator = 0, 笔者感觉是不是应该是int numerator,
int denominator = 1);
...
};
第二种:隐式类型转换操作符,也就是关键词operator之后加上类型名称;
class Rational{
public:
...
operator double() const; //将Rational转换为 double
};
Rational r(1, 2); //r 为 1/2
double d = 0.5 * r; //将r转换为double然后执行乘法操作
当存在这些函数的时候,会在意料之外的时候被调用;
Rational r(1, 2); //r为 1/2;
cout<< r; //r会先被转换为double,然后再调用operator<<(因为Rational没有定义operator<<, 所以编译器会执行转换动作匹配操作符)
解决办法: 以功能对等的另一函数取代类型转换操作符
class Rational{
public:
...
double asDouble() const; //将Rational转换为double
};
Rational r(1, 2);
cout << r; //错误,Rational没有定义<<操作符
cout << r.asDouble(); //正确,以double的形式输出r
对于单(多)参数构造函数的同样会出现意料之外的情况:
template<class T>
class Array{
public:
Array(int lowBound, int highBound); //构造函数,不会导致隐式转换
Array(int size); //会产生隐式转换的构造函数,把int转换为Array
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[i] == b[i]的,发生错误的时候并没有报错,虽然不存在这样的operator == 函数可被调用,但是 a 的类型为int可以被转换为Array<int>
...;
else
...;
}
所以会产生类似如下的代码
for(int i =0; i < 10; ++i){
if(a == static_cast<array<int> >(b[i])) //每一次的迭代都会把b[i]转换为arry<int>然后再调用对应的operator ==来比较,每次都会产生一个array<int>的数组,然后销毁数组对象
...
}
解决方法:
方法一: 使用关键词explicit ,将构造函数声明为explicit,编译器就不能因隐式类型转换的需要而调用了,不过显式类型转换仍然是被允许的;
template<class T>
class Array{
public:
explicit Array(int size);
...
}
Array<int> a(10); //没问题,正常调用单参数构造函数
Array<int> b(10);
if(a == b[i]) ... //错误,无法进行隐式转换
if(a == array<int>(b[i])) ... //没问题,调用了构造函数
if(a == static_cast<array<int> >(b[i])) ... //没问题,进行了显式转换
if(a == (Array<int>)b[i] ) ... //没问题,进行了c风格的显式转换
方法二:如果编译器不支持关键字explicit,那就把类设计得使希望拥有的“对象构造行为”合法化,并让不希望允许的隐式构造行为非法化(没有任何一个转换的程序(过程)可以内含一个以上的“用户定制的转换行为(也就是当自变量构造函数或隐式类型转换操作符)”);
template<class T>
class Array{
public:
class ArraySize{ //内部代理类,见条款30
public:
ArraySize(int numElements): theSize(numElements){ } //接受一个int作为单参数构造函数
int size() const{ return theSize; }
private:
int theSize;
};
Array(int lowBound, int highBound);
Array(ArraySize size); //自变量为内部类的构造函数
...
};
Array<int> a(10); //当编译器被要求调用自变量为int的构造函数时,并不存在这样的构造函数,但是,编译器知道能将int自变量转换为一个临时的ArraySize对象,于是执行了转换动作,所以构造函数以及附随的对象构造行为成功调用
现在隐式转换的行为会被阻止了,因为没有单一自变量为int的构造函数,而且编译器不能将int转换为一个临时的ArraySize对象,然后再把这个临时对象产生必要的Array<int>对象,那将调用两个用户定制转换行为,一个将int转换为ArraySize,一个将ArraySize转换为Array<int>,这种转换是被禁止的;
bool operator ==(const Array<int>& lhs, const Array<int>& rhs);
Array<int> a;
Array<int> b;
...
for(int i = 0; i < 10; ++i)
if(a == b[i]) ... //错误,其中的隐式转换涉及到了两个转换动作,这是不被允许的,从结果上来说,隐式转换被阻止了
条款 6:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式
重载函数是通过参数类型来区分彼此的,然而无论是 increment 或 decrement 操作符的前置或是后置形式,都没有参数,为了填平语言学上的漏洞,所以,后置式会有一个int 自变量,并在调用时,编译器默认为int指定一个0值:
class UPInt{
public:
UPInt& operator++ (); //前置式(prefix)++
UPInt& operator-- (); //前置式(prefix)--
const UPInt operator++ (int); //后置式(postfix) ++
const UPInt operator--(int); //后置式(postfix) --
UPInt& operator+= (int); //+=操作符,结合UPInts 和 ints
...
};
UPInt i;
++i; /*调用i.operator++()*/ i++; //调用i.operator++(0)
--i; /*调用i.operator--()*/ i--; //调用i.operator--(0)
所谓increment操作符的前置式意义是“increment and fetch”(累加然后取出),后置式的意义"fetch and increment"(取出之后再累加);
//前置式
UPInt& UPInt::operator++(){
*(this) += 1; //累加(increment)
return this; //取出(fetch)
}
//后置式
const UPInt UPInt::operator++(int){ //返回的对象必须是const对象,因为如果是non-cosnt对象,会被允许 UPInt i; i++++; ,这与内建类型ints的行为不一致,而且实际上只是被累加一次而已,与期待的作用不一致
UPInt oldValue = *this; //取出(fetch)
++(*this); //累加(increment)
return oldValue; //返回旧的值
}
因为后置式会产生临时对象,所以在处理用户定制类型时,
尽可能的使用前置式increment;
后置式increment和decrement操作符的实现应以其前置式兄弟为基础实现,如此就只需维护前置式版本,因为后置式版本会自动调整为一致的行为;
const UPInt UPint::operator++(int){
UPInt oldValue (*this);
++(*this); //以前置式为基础实现后置,保证行为一致
return oldValue;
}
条款 7:千万不要重载&&, || 和,操作符
C++对于“真假表达式”采用所谓的“骤死式”评估方式,就是一旦表达式的真假确定以后,即使还有部分未被检查,整个评估工作就结束了,很多程序都是依赖于“骤死式”的形式进行判断的;
char *p;
if( (p != 0) && (strlen(p) > 10)) ... //在strlen中p是不会为null指针的,因为如果p为null指针,那么strlen将不会被调用
int rangeCheck(int index){ //如果index 小于lowBound,那么就不会和upperBound比较
if((index < lowerBound) || (index > upperBound)) ...
}
假如重载了 && 和 || 运算符,那么“函数调用”语义将会取代“骤死式”语义,就会导致产生重大区别,当函数调用动作被执行时,所有参数值的评估都必须完成,而且,因为语言规范并未明确定义函数调用动作中各个参数的评估顺序,所以没办法知道expression1和expression2哪个会被先评估(骤死式的是从左到右评估其自变量)
if(expression1 && expression2) ...
//会被编译器视为以下两者之一
if(expression1.operator&&(expression2)) ... //假如 operator&& 是成员函数
if(operator&&(expression1, expression2)) ... //假如 opertotr&& 是全局函数
逗号运算符用来构成表达式,表达式先评估逗号左侧的值,然后再评估逗号右侧的值,整个表达式的值以逗号右侧的值为代表,如果把操作符写出一个非成员函数,将无法保证左侧表达式一定比右侧表达式更早被评估,即使,写出成员函数也是无法保证这一事实,所以,重载后的逗号表达式和默认版本的行为不一致,不推荐重载逗号运算符;
//将字符串s的字符顺序颠倒
void reverse(char s[]){
for(int i = 0, j = strlen(s) - 1; i < j; ++i, --j){ //先初始化i,然后初始化j, 先++i再--j
int c = s[i];
s[i] = s[j];
s[j] = c;
}
}
条款 8:了解各种不同意义的new和delete
new有 new operator 和 operator new,new operator就是语言内建的操作符,像sizeof一样不能改变意义,动作分为两部分,第一,分配足够的内存,用来放置某类型的对象,第二,调用构造函数,在刚刚分配的内存中对那个对象设置初值;new operator 调用某个函数,执行必要的内存分配动作,我们可以重写或重载那个函数,改变其行为,这个函数的名称就叫operator new;
用new operator获得一个对象:
string *ps = new string("Memory Management");
函数operator new通常声明为:
void* operator new(size_t size); //返回类型为void*, 指向一块原始,未设初值的内存;
如果希望定制自己的operator new,可以对operator new进行重载,加上额外的产生;
void* operator(size_t size, int, ...); //第一参数必须为size_t(保存一致性)
和maclloc一样,operator new 的唯一任务是分配内存,取得operator new返回的内存并将之转换为一个对象,是new operator的责任,所以会产生类似如下的代码:
void *memory = operator mew (sizeof(string));
call string::string("Memory Management") on *memory; //程序员没办法这么做,这是编译器的工作
string *ps = static_cast<string>(memory);
Placement new
特殊版本的operator new,称为placement new,可以允许你在分配好的原始内存上构建对象;
class Widget{
public:
Widget(int widgetSize);
...
};
Wiget* constructWidgetInBuffer(void *buffer, int widgetSize){
return new (buffer) Widget(widgetSize);
}
constructWidgetInBuffer函数乍一看下有些奇怪,这是new operator 的用法之一,其中指定一个额外自变量(buffer)作为new operator“隐式调用 operator new”时所用,于是,被调用的operator new 除了接受“一定得有的size_t自变量“以外,还接受了void* 参数,指向一块准备用来接收构造好的对象的原始内存,这样的operator new就是所谓的placement new;
void* operator new(size_t, void* location){ //size_t因为不使用所以不赋予名称
return location; //operator new的目的是为对象找到一块内存,因为调用者知道对象应该放在哪里,所以placement new唯一需要做的是将它获得的指针返回
}//placement new是C++标准库的一部分,欲使用plcaement new,必须先#include<new>
总而言之,如果希望对象产生于heap(堆内存),使用new operator,不但分配内存还调用构造函数,如果只是单纯分配内存,可以使用operator new,如果打算在heap object(堆对象)产生时决定内存分配方式,可以自己写一个operator new,并使用operator new,如果打算在已分配的内存中构造对象,可以使用placement new;
删除(Deletion)和内存释放(Deallocation)
为了避免资源泄漏,每一个动态分配的对象必须匹配一个相应但相反的释放动作,函数operaotr delete对于内建的operator delete,就类似于operator new 对应于 new operator:
string *ps;
delete ps; //使用delete operaotr
//会产生类似以外的代码
ps->~string(); //调用析构函数
operator delete(ps);//释放分配的内存
//operator delete 一般声明如下
void operator delete(void *memoryToBeDeallocated);
如果只打算处理原始内存,未设初值的内存,应该使用operator new取得内存并以operator delete 归还系统,在C++中相当于调用了malloc/free;
void *buffer = operator new(50 * sizeof(char)); //分配内存
...
operator delete(buffer); //释放内存
如果使用了placement new 在某内存块中产生对象,应该避免对那块内存使用delete operaotr,因为delete operator 会调用operator delete来释放内存,但是该内存内含的对象并非是由operator new分配得来的,所以指针从何而来不得而知,毕竟placement new只是返回他接受的指针而已;
void* mallocShared(size_t size); //分配shared memory 中的内存函数
void freeShared(void* memory); //释放shared memory 内存函数
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = constructWidgetInBuffer(sharedMemory, 10); //placement new
...
delete pw; //无定义,因为sharedMemory来自mallocShared,不是opertor new
pw->~Widget(); //可以,析构pw指向的Widget对象,但是并未释放Widget占用的内存
freeShared(pw); //可以,释放pw所指向的内存,不调用析构函数
数组(Arrays)
多个对象(数组)的情况,new 仍然是 new operator ,但是由于诞生的是数组所以new operator 的行为与先前产生单一对象的情况略有不同,内存不再以operator new分配,而是由其“数组版”,名为operator new[] 分配(通常称为“array new”), ”数组版“与“单一对象版本”另外一个不同的是,new operator会针对数组中每一个元素调用一个构造函数,同时,”数组版本“也可以重载而且和“单一版本”有着相同的重载限制;
string *ps = new string[10]; //调用operator new[] 来分配足够的内存然后对每个元素调用string的默认构造函数
delete ps; //为数组每个元素调用析构函数,然后调用operator delete[] 以释放内存