- 条款5:对定制的“类型转换函数”保持警觉
- 条款6:区别increment/decrement操作符的前置(prefix)和后置(postfix)形式
- 条款7:千万不要重载&&,||和,操作符
- 条款8:了解各种不同意义的new和delete
条款5:对定制的“类型转换函数”保持警觉
- C++允许编译器在不同类型之间执行隐式转换(implicit conversions)。它继承C的传统,允许将char转换为int,将short转换为double。然而还有令人害怕的转型,包括将int转换为short,将double转换为char,这些类型转换会遗失信息。所以我们可以使用自己的类型。两种函数允许编译器执行这样的转换:单自变量constructors和隐式类型转换操作符。所谓的单自变量constructor是指能够以单一自变量成功调用的constructors: ,而隐式类型转换操作符,是一个拥有奇怪名称的member function:关键词operator后面加上一个类型名称,你不能为此函数指定返回值类型,因为其返回值类型已经表现在函数名称上了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Name
{
public:
// string to Name
Name(const std::string &s);
...
};
class Rational
{
public:
...
// Rational to double
operator double() const;
}1
2
3
4
5
6
7
8class Rational
{
public:
...
operator double() const;
};
Rational r(1, 2); // r == 1/2
double d = 0.5 * r; // 将r转为double,然后执行乘法运算 - 假设有一个class用来表现分数(rational numbers)。你希望像内建类型一样地输出Rational objects内容。 假设忘了给Rational写一个operator<<,上述打印不但不会出错,编译器在面对上述动作,它会想尽各种办法(包括找出一系列可接受的隐式类型转换)让函数调用动作成功。而之间的代码发现,只要调用了Rational::oeprator double,将r转换为double,调用动作就能成功,却又有隐式类型转换操作符的缺点:它的出现可能到导致错误(非预期)的函数被调用。于是有了解决办法:
1
2Rational r(1, 2);
std::cout << r;必须明白调用类型转换函数虽然有点不便,却可以避免默默调用了不想调用的函数。这也就是为什么标准程序库的string类型并未含有从string object到C-style char*的隐式转换函数,而是提供了一个显式的member function1
2
3
4
5
6
7
8class Rational
{
public:
...
double asDouble() const;
};
std::cout << r; // error
std::cout << r.asDouble(); // ok, 以double形式输出rc_str
。
在单自变量constructors,这些函数造成的隐式类型转换的情况更难对付:这样的错误编译器并没有提醒,它一声不吭,编译器注意到它,发现只要调用Array<int> constructor(需要一个int自变量)就可以将int转为Array<int> object。于是它放手去做。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20template<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(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
if (a == b[i]) // 如果打算写a[i] == b[i]
{
do something for when a[i] and b[i] are equal;
}
else
do something for when they're not;
有两种办法阻止编译器这么做:一个是简易法,另一个可在编译器不支持简易法的情况下使用。
简易法是使用C++特征:关键词explicit
。只要将constructors声明为explicit,编译器就能因隐式转换而调用它,不过显式类型转换是允许的(没发现)。有个解决方案:编译器需要一个Array<int>的对象在==右边,但此时没有这样隐式转换的constructor。更不能将int转换一个临时性的ArraySize对象,再根据这个临时对象产生必要的Array<int>对象,所以报错。类似ArraySize这样的classes,往往被称为proxy classes,因为它的每一个对象都是为了其他对象而存在的,好像其他对象的代理人(proxy)一样。这是一个很值得学习的技术。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22template<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(10);
Array<int> b(10);
...
for(int i = 0; i < 10; ++i)
if (a == b[i]) // error
条款6:区别increment/decrement操作符的前置(prefix)和后置(postfix)形式
- 重载函数是以其参数类型来区分彼此的,然而不论increment或decrement操作符的前置式或后置式,都没有参数,为了填平这个语言学上的漏洞,只好让后置式有一个int自变量,并且让它被调用时,编译器默默地为该int指定一个0值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class UPInt
{
public:
UPInt& operator++(); // 前置式++
const UPInt operator++(int); // 后置式++
UPInt& operator--(); // 前置式--
const UPInt operator--(int); // 后置式--
UPInt& operator+=(int); // +=操作符
...
};
UPInt i;
++i; // i.operator++();
i++; // i.operator++(0);
--i; // i.operator--();
i--; // i.operator--(0); - 所谓的increment操作符的前置式意义“increment and fetch“(积累然后取出),后置式意义”fetch and increment“(取出然后累加): 为什么后置increment操作符返回的对象是const呢?以下:
1
2
3
4
5
6
7
8
9
10
11UPInt& UPInt::operator++()
{
*this += 1;
return *this;
}
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this;
++(*this);
return oldValue;
}1
2
3UPInt i;
i++++; // 后置increment操作符两次
i.operator(0).operator++(0);
条款7:千万不要重载&&,||和,操作符
- 操作符重载也不是任何都能用来重,它也是有底线的,你不能重载以下操作符: 可以重载的有:
1
2
3. .* :: ?: new
delete sizeof typeid static_cast
dynamic_cast const_cast reinterpret_cast1
2
3
4
5
6
7operator new operator delete
operator new[] operator delete[]
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
() []
条款8:了解各种不同意义的new和delete
new operator和operator new之间是有差别的。
1
string *ps = new string("Memory Management");
所使用的new是所谓的new operator。这个操作符是语言内建的,跟sizeof一样不能改变意义。它的分为两方面:分配足够的内存用来存放某类型的对象;而后调用一个constructor为刚才分配的内存中的那个对象设定初值。
我们能够重写改函数,改变其行为。这个函数名称叫operator new。它的函数原型是:1
void * operator new(size_t size);
size_t参数表示需要分配多少内存。然后返回一块原始的内存。和malloc一样,operator new唯一任务是分配内存。但它不知道什么是constructors。当你想要调用一个constructor,有个特殊版本的operator new,称为placement new,允许你这么做:
1
2
3
4
5
6
7
8
9
10class Widget
{
public:
Widget(int widgetSize);
...
};
Widget* constructorWidgetInBuffer(void *buffer, int widgetSize)
{
return new(buffer) Widget(widgetSize);
}这是new operator的用法之一,指定一个额外自变量(buffer)作为new operator隐式调用operator new时所用。于是,被调用的operator new除了几首size_t变量外,还接受了一个void*参数,这就是所谓的placement new,看起来像这样:
1
2
3
4void* operator new(size_t, void *location)
{
return location;
}如果你希望将对象产生于heap,请使用new operator,它不但分配内存而且为对象调用一个constructor。如果只是打算分配内存,调用operator new,那没有任何constructor被调用。如果打算在heap objects产生时决定内存分配方式,写一个自己的operator new。
为了避免resource leaks(资源泄露),每一个动态分配行为都必须匹配一个相应但相反的释放动作。内存释放动作是由operator delete执行:
1
2
3
4string *ps;
...
delete ps;
void operator delete(void *memoryToBeDeallocated);因此,
delete
动作会产生类似代码:1
2ps->~string();
operator delete(ps);这里呈现的暗示是,如果只打算处理原始的、未设初值的内存,应该回避new operator和delete operators。如果使用了placement new产生对象,避免对该内存使用delete operator。因为delete operator会调用operator delete来释放内存,但该内存并非由operator new分配得来的。placement new只是返回它接受的指针而已,所以未了抵消该对象的constructor的影响,应该直接调用该对象的destructor。
面对数组情况,也有所不同:
1
string *ps = new string[10];
上述的new仍然是new operator,但由于诞生的是数组,所以new operator的行为不再以operator new分配,而是由一个名为operator new[]负责分配(通常称为array new)。同样道理,当delete operator被用于数组,它会针对数组中的每个元素调用其destructor,然后调用operator delete[]释放内存。
总结以下,new operator和delete operator都是内建操作符,无法为你所控制,但是它们所调用的内存分配/释放函数则不然。