MoreEffectiveC++Item35(操作符)(条款5-8)

时间:2023-03-09 04:38:09
MoreEffectiveC++Item35(操作符)(条款5-8)

条款5 对定制的"类型转换函数"保持警惕

条款6 区别increment/decrement操作符的前值和后置形式

条款7 千万不要重载&&,||,和,操作符

条款8 了解不同意义的 new 和 delete


条款5 对定制的"类型转换函数"保持警惕

1.C++有默认类型转换,如 int---char

2.单自变量constructors(能够以单一变量成功调用起constructor,起构造函数可以有多个参数但是除第一个参数外,其他参数必须有初省值)

 class Name
{
public:
Name(const string & s); //可以将string转化成Name
...
}; class Person
{
public:
Person(string name,int age = );//可以将 string 转化成Person
....
};

对于自定义类型的类型转换,有一个规则"没有任何一个转换程序可以内含一个以上的‘用户定制转换行为’(亦即单自变量constructor亦即隐式类型转换操作符)",

必要的时候编译器可以先进行内置类型之间的转换,再调用带单自变量的构造函数.

或者先调用隐式类型转换操作符在进行内置类型之间的转换.

但不可能连续进行两次用户定制的类型转换.

3.隐式类型转换操作符 关键词 operator + 类型名 (你不能为该函数定义返回值,其返回值已经反映在类型名上)

 class Rational
{
public:
operator double const;//将 Rational转换成double
....
};
//在一下情况会被调用
Rational r(,)// r 的值为1/2
double d = 0.5 * r//先将 Rational转换成double 再进行运算

隐式类型转换操作符type()"需转则转,能转则转".例如设计者定义了一个有理数类Rational,同时定义了 operator int(),而没有定义<<,这种情况下如果对于语句"cout<<a;",编译器应该报错来提醒设计者,但实际上a会被转为int然后输出,这背离了设计者的初衷。

为了防止以上现象出现,我们可以定义一个函数来取代类型转换操作符.虽然使用时有些不便

 class Rational 
{
public:
...
double asDouble() const; //将 Rational 转换为double
}; Rational r(, );
cout << r; // 错误!(编译时不会出错但是打印时会出错) Rationa 对象没有 operator<<
cout << r.asDouble(); // 正确, 用 double 类型 正确打印 r

在C++库函数中的 string 类型没有包括隐式地从 string 转换成 C 风格的 char*的功能,而是定义了一个成员函数 c_str 用来完成这个转换

4.单自变量的构造函数由于隐式的转换可能会出现更加隐蔽的错误

 template<class T>
class Array {
public:
Array(int lowBound, int highBound);
Array(int size);
T& operator[](int index);
bool operator==( const Array<int>& lhs,
const Array<int>& rhs);
...
} Array<int> a();
Array<int> b();
...
for (int i = ; i < ; ++i)
if (a == b[i]) { //判断两个数组是否完全相等(这里的[]被落下 但是编译不会出错 但是与判断两个数组是否完全相等的的意义偏离)
do something for when
a[i] and b[i] are equal;
}
else {
do something for when they're not;
}

5.解决隐式转换带来的不便

a.explicit 关键字

 template<class T>
class Array {
public:
...
explicit Array(int size); // 注意使用"explicit
...
}; if (a == b[i]) ... // 错误! 不可以使用隐式转换

b.编译器不支持explicit

 template<class T>
class Array {
public:
class ArraySize { // 类嵌套类
public:
ArraySize(int numElements): theSize(numElements) {}
int size() const { return theSize; }
private:
int theSize;
};
Array(int lowBound, int highBound);
Array(ArraySize size); // 注意新的声明
...
}; bool operator==( const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a();
Array<int> b();
for (int i = ; i < ; ++i)
if (a == b[i]) ...//现在是一个错误,int调用隐式类型转换 但是不能转换ArraySize,所以会报错

允许编译器执行隐式转换弊大于利,所以非必要不要提供转换函数


条款6 区别increment/decrement操作符的前值和后置形式

 // 前缀形式:增加然后取回值
UPInt& UPInt::operator++()
{
*this += ; // 增加
return *this; // 取回值
} // postfix form: fetch and increment
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this; // 取回值
++(*this); // 增加
return oldValue; // 返回被取回的值 返回值为const类型防止 i++++的调用
}

1.++i 是返回的是原对象的引用,因而可以执行类似于++++a这样的式子.

c++中有一条原则就是“绝对不让用户更改临时对象”(异常处理除外),因为临时对象是由编译器产生的,我们无法主动使用,同时它的生存期也无法由我们来掌握,所以除了用临时对象来充当传入参数或返回值的"载体"外,任何对临时对象的更改都是无意义的,也就被编译器严厉禁止

i++ 如果返回值不用const修饰的话 那么 i++++ 调用时 即调用了 i.operator++(0).operator++(0),第一次++ 返回的是一个临时变量 然而第二次操作用的是该临时变量 所以第二次++ 不产生效果

2.前置++效率高于后置++(中间不产生临时变量)

3.如果要同时定义++i和i++,i++的定义最好以++i为实现基础,这样可以保证它们行为的一致


条款7 千万不要重载&&,||,和,操作符

1.重载&&和||没有办法知道左面表达式与右面表达式 哪一个先计算,所以千万不要重载

2.千万不要重载  ,

 void reverse(char s[])
{
for (int i = , j = strlen(s)-;
i < j;
++i, --j) // 啊! 逗号操作符!
{
int c = s[i];
s[i] = s[j];
s[j] = c;
}
}

for循环最后一个成分必须是一个表达式,如果表达式中含有逗号 那么逗号左侧一定先回被评估 然后以逗号的右侧值为代表

不要重载的意义在于这些你无法控制

3哪些可以重载 ,哪些不可以

 //这些不可以重载
. .* :: ? :
new delete sizeof typeid //这些可以重载 operator new operator delete
operator new[] operator delete[]
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
() []

千万不要让 strlen调用nullptr


条款8 了解不同意义的 new 和 delete

1.new operator = new 分配内存且初始化(调用ctors),不可重载

 string *ps = new string("Memory Management");
//它等于以下代码
void *memory = // 得到未经处理的内存
operator new(sizeof(string)); // 为 String 对象
call string::string("Memory Management") //初始化
on *memory; // 内存中的对象
string *ps = // 是 ps 指针指向
static_cast<string*>(memory); // 新的对象

2.operator new : C++标准库函数,只分配内存不初始化,可重载

//函数 operator new 通常这样声明:
void * operator new(size_t size);//参数 size_t 确定分配多少内存 你能增加额外的参数, 重载函数 operator new,返回 void* 但是第一个参数类型必须是 size_t。

3. delete operator = 析构 + 释放内存  operator delete的作用也就只有一种:释放内存 () 作用相反于 new operator , operator new

4.重载的operator new必须具有void* 返回类型而且第一个参数必须为size_t类型(就算是int类型都不行),此外根据是定位new还是普通new,operator new决定是否增加指针参数(只要满足返回类型为void*而且第一个参数类型为size_t,其他参数随便加,只不过普通new表达式只调用只含有一个size_t参数的operator new版本,而定位new表达式只调用含有一个size_t类型和指针参数类型的operator new版本,自己重载的其他版本可以按需要添加参数

5.operator new[], operator delete[]数组版的operator new 和 operator delete

string *ps = // 调用 operator new[] 以分配足够的内存
new string[]; // 10个string对象的内存 然后针对每个元素调用default ctor
delete [] ps; //为数组中的每一个元素调用dtor 然后调用operator delete[]  释放内存

重载的operator delete必须具有返回类型void,它可以定义为接受单个void*类型形参,也可以定义为接受void* 和size_t类型两个形参,如果提供了size_t类型的形参,就有编译器用第一个形参所指对象的大小自动初始化size_t形参(但是第一个形参不是void*型么?void* 型是不具备所指对象大小的信息的,可能是编译器又在背后做了其他事情吧),对于编译器自动初始化size_t形参的做法,这种类似的做法已经出现过许多次了,比如定位new中的operator的第一个size_t形参,以及后自增操作符大的哑元参数等(编译器确实背着我们干了许多事)

重载的operator new[]必须具有返回类型void*,并且接受的第一个形参类型为size_t,用表示存储特定类型给定数目元素的的数组的字节数值自动初始化操作符的size_t形参”,注意,operator new[]返回的地址之前还有4个字节用来存储元素数目(编译器背着我们做的,就算自己重载了也一样)

注意delete 和 new 搭配使用   operator new与operator delete