More Effective C++ 02 操作符

2. 操作符

条款 05:对定制的“类型转换函数”保持警觉

C++ 允许编译器在不同类型之间执行隐式转换。也可能会发生遗失信息的转换,包括将 int 转换为 short,将 double 转换为 char 等等。

自定义类型可以进行隐式转换的函数

自定义类型中有两种函数允许编译器执行隐式转换:单自变量构造函数和隐式类型转换操作符。单自变量构造函数是指能够以单一自变量成功调用的 constructor。此 constructor 可能声明拥有单一参数,也可能拥有多个参数,并且除了第一个参数外都有默认值。

隐式转换操作符,是一个成员函数,其形式是:关键词 operator 之后加上一个类型名称。你不能为此函数指定返回值类型,因为其返回值类型基本上已经表现于函数名称上。例如:

class Rational {
public:
	operator double() const;  // 将 Rational 转换为 double
};
隐式转换操作符

假设,你希望像内置类型一样地输出 Rational object 内容:

Rational r(1, 2);
cout << r;  // 应该输出 1/2

在本例中,即使没有适当的 operator<< 可以调用,但是可以通过将 r 隐式转换为 double 的方法顺利执行。这也显示了隐式类型转换操作符的缺点:它们的出现可能导致错误(非预期)的函数被调用。

解决上述问题的方法就是以功能对待的另一个函数取代类型转换操作符,但是这也会带来一些不便:

class Rational {
public:
	double asDouble() const;  // 将 Rational 转换为 double
};
// 如何的成员函数必须被明确调用
Rational r(1, 2);
cout << r;  // 错误!Rational 没有 operator<<
cout << r.asDouble();  // 正确

一般而言,C++ 程序员应该尽可能避免适用类型转换操作符

单自变量构造函数

通过单自变量 constructor 完成的隐式转换,较难消除。参考下面例子:

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(10), b(10);
...
for (int i = 0; i < 10; i++) {
	if (a == b[i])  // 应该是 a[i] 而不是 a
		...
}

由于有 Array(int size) 的单变量构造函数,它会将 int 转换为 Array<int> object,因而产生类似这样的代码:

for (int i = 0; i < 10; i++)
	if (a == static_cast<Array<int>>(b[i]))

于是每一次循环都会拿 a 的内容和一个大小为 b[i] 的临时数组进行比较。但是有时真的需要提供一个单自变量 constructor 给你的用户使用,但是又不想发生上述问题,解决这一问题的方法有两个:一个是简易法,另一个可在你编译器不支持简易法时适用。

简易法是最新的 C++ 特性:关键词 explicit,这个特性阻止隐式类型转换,只允许显式类型转换

C++ 中有一条规则是:没有任何一个转换程序可以内含一个以上的用户定制转换行为(入,单自变量构造函数或隐式类型转换操作符)。可以利用这一规则解决上述问题:

template<class T>
class Array {
public:
	class ArraySize {
	public:
		ArraySize(int numElements) : theSize(numElements) { }
		int size() const { retrun theSize; }
	private:
		int theSize;
	};
	Array(int lowBound, int highBound);
	Array(ArraySize size);  // 注意这个新声明
};

此时:

Array<int> a(10);  // 正确
for (int i = 0; i < 10; i++)
	if (a == b[i])  // 错误

编译器需要一个类型为 Array<int> 的对象在 == 右侧,但是此时没有类型为接受单一自变量 int 的构造函数。此外,编译器不能考虑将 int 转换为一个临时性的 ArraySize 对象,然后再根据这个临时对象产生必要的 Array<int> 对象,因为这会调用两个用户定制转换行为。如此的转换程序是禁止的,所以编译器会对以上代码发出错误信息。


条款 06:区别递增/递减操作符的前置和后置形式

重载函数是以其参数类型来区分彼此的,然而不论 increment 或 decrement 操作符的前置式或后置式,都没有参数。这一问题的解决是通过,让后置式有一个 int 自变量,并且在它被调用时,编译器默默地为该 int 指定一个 0 值

class UPInt {
public:
	UPint& operator++();  // 前置式 ++
	const UPInt operator++(int);  // 后置式 ++
	
	UPint& operator--();  // 前置式 --
	const UPInt operator--(int);  // 后置式 --
	
	UPint& operator+=();  // += 操作符
};

递增操作符的前置式意义是累加然后取出,后置式意义式取出然后累加。它们成为了前置式和后置式操作符实现的规范:

// 前置式:累加然后取出
UPint& UPint::operator++() {
	*this += 1;  // 累加
	return *this;  // 取出
}
// 后置式:取出然后累加
const UPInt UPInt::operator++(int) {
	UPInt oldValue = *this;  // 取出
	++(*this);  // 累加
	return oldValue;  // 返回先被取出的值
}

请注意:后置式操作符并没有使用其参数,其参数的唯一目的只是为了区分前置式和后置式

后置式操作符

让后置式 increment 操作符必须返回一个对象(代表旧值),原因很清楚。但为什么是个 const 对象的原因,请参考下述代码:

UPInt i;
i++++;  // 实施后置递增操作符两次
// 这和以下动作相同
i.operator++(0).operator++(0);

有两个理由使我们不应该这样操作。第一,它和内置类型的行为不一致

int i;
i++++;  // 错误,++++i 合法

第二个理由是,即使能够实行两次后置式 increment 操作符,第二个 operator++ 所改变的对象是第一个 operator++ 返回的对象,而不是原对象,即使合法,i 也只被累加了一次而已。所以最好的办法就是禁止它合法化。

如何选择

单以效率因素来看,UPInt 用户应该更喜欢前置式,而非后置式,因为后置式返回一个临时对象,需要构造也就需要析构。所以除非真的需要后置式行为,否则尽量使用前置式操作。

操作前置式和后置式需要坚持一个原则,后置式操作符的实现应以其前置式兄弟为基础。如此一来你就只需维护前置式版本,因为后置式版本会自动调整一致的行为。


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

&& 和 || 操作符

C++ 允许你为“用户定制类型”定做 && 和 || 操作符,做法是对 operator&& 和 operator|| 两函数进行重载。但是如果你决定重载这两个函数,你必须知道,你正在从根本层面改变整个游戏规则。

如果你承重载 operator&&,下面这个式子:

if (expression1 && expression2)

会被编译器视为以下二者之一:

if (expression1.operator&&(expression2))
	// 假设 operator&& 是个成员函数
if (operator&&(expression1, expression2))
	// 假设 operator&& 是个全局函数

虽然看起来没有什么,但实际上语义已经发生了改变。第一,当函数调用动作被执行式,所有参数值都必须已经评估完成;第二,C++ 语言规范并未明确定义函数调用动作中各参数的评估顺序,所以没办法知道 expression1 和 expression2 哪个会先被评估。这与原始评估法有明确对比,后者总是由左向右评估其自变量。

, 操作符

在 C++ 中,表达式如果内含逗号,那么逗号左侧会先被评估,然后逗号的右侧再被评估;最后,整个逗号表达式的结果以逗号右侧的值为代表

如果你重载 ,操作符的话,你就必须要模仿这些行为,但事实上,你没有办法做到。如果你把操作符写成一个 non-member 函数,你绝对无法保证左侧表达式一定比右侧表达式更早被评估,因为两个表达式都被当做函数调用时的自变量。如果将操作符写成一个 member 函数,依然无法做到逗号左侧操作数先被评估,因为编译器并不强迫做这样的事。

更多可参考 C++ Primer 14 重载运算与类型转换


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

new operator 和 operator new

当你写出这样的代码:

string *ps = new string("Memory Mangement");

你所使用的 new 是所谓的 new operator,这个操作符是内置的,就像 sizeof 那些,不能被改变意义。它的动作分为两个方面:第一,它分配足够的内存,用来放置某类型的对象;第二,它调用一个 constructor,为刚分配的内存中的那个对象设定初值。new operator 总是做这两件事,无论如何你不能够改变其行为。

而你能改变的是用来容纳对象的那块内存的分配行为。new operator 调用某个函数,执行必要的内存分配动作,你可以重写或重载那个函数,改变其行为。这个函数叫做 operator new。

函数 operator new 通常声明如下:

void* operator new(size_t size);

此函数返回一个指针,指向一块原始的、未设初值的内存。函数中的 size_t 参数标识需要分配多少内存。你可以将 operator new 重载,加上额外参数,但第一参数的类型必须是 size_t。你也可以像调用其他函数一样调用它:

void *rawMemory = operator new(sizeof(string));

和 malloc 一样,operator new 的唯一任务就是分配内存取得 operator new 返回的内存并将之转换为一个对象,是 new operator 的责任

当你的编译器看到下面句子时:

string *ps = new string("Memory Mangement");

它会产生一些代码,或多或少反映以下行为:

void *memory = operator new(sizeof(string));  // 取得原始内存,用来放置一个 string 对象
call string::string("Memory Mangement") on *memory;  // 将内存中的对象初始化
string *ps = static_cast<string*>(memory);  // 让 ps 指向新完成的对象

上述第二个步骤,调用一个 constructor,但是程序员并没有权力这么做。

placement new

有时你会有一些分配好的原始内存,你需要在上面构建对象。有一个特殊版本的 operator new,称为 placement new,允许你这么做:

Widget* constuctWidgetInBuffer(void* buffer, int widgetSize) {
	return new(buffer) Widget(widgetSize);
}

此函数返回一个指向 Widegt 对象的指针,它被构造于传递给此函数的一块内存缓冲区上。被调用的 operator new 除了接受 size_t 自变量外,还接受一个 void* 参数,指向一块内存,准备用来接收构造好的对象。这也的 operator new 就是所谓的 placement new,看起来像这样:

void* operator new(size_t, void* location) {
	return location;
}

operator new 的目的时要为对象找到一块内存,然后返回一个指针指向它。在 placement new 的情况下,调用者已经知道指向内存的指针了,因为调用者知道对象应该放在哪里。因此 placement new 唯一需要做的就是将它获得的指针再返回。

如果你希望将对象产生于 heap,请使用 new operator。它不但分配内存而且为该对象调用一个 constructor。如果你只是打算分配内存,请调用 operator new,那就没有任何 constructor 会被调用。如果你打算再 heap object 产生时自己据欸的那个内存分配方式,倾斜一个自己的 operator new,并使用 new operator,它将会自动调用你所写的 operator new。如果你打算在已分配(并拥有指针)的内存中构造对象,请使用 placement new。

删除与内存释放

为了避免资源泄露,每一个动态分配行为都必须匹配一个相应单相反的释放动作。函数 operator delete 对于内置的 delete operator,就好像 operator new 和 new operator 一样。

面对 delete operator 时,你的编译器必须能够析构 ps 所指对象,还要释放被该对象占用的内存。内存释放动作是由 operator delete 执行,通常声明如下:

void operator delete(void *memoryToBeDeallocated);

因此,下面这个动作:

delete ps;

会造成编译器产生近似这样的代码:

ps->~string();  // 调用对象的析构函数
operator delete(ps);  // 释放对象所占用的内存

这呈现了一个暗示就是,如果你只打算处理原始的、未设初值的内存,应该完全回避 new operator 和 delete operator,而是调用 operator new 已取得内存并以 operator delete 归还给系统。这组行为类似于 malloc 和 free。

如果你使用 placement new 再某块内存中产生对象,你应该避免对那块内存使用 delete operator。因为 delete operator 会调用 operator delete 来释放内存,单是该内存内含的对象并非是由 operator new 分配得来的,毕竟 placement new 只是返回它所接收到的指针而已。所以为了抵消该对象的构造函数的影响,你应该直接调用该对象的析构函数。

数组

参考下面例子:

string *ps = new string[10];  // 分配一个对象数组

上述使用的 new 仍然是 new operator,但是略有不同,其内存不再是由 operator new 分配,而是使用一个名为 operator new[] 的函数负责(通常称为 array new)。 operator new[] 也可以被重载,这使得你可以夺取数组的内存分配权。

数组版与单一对象版的 new operator 的第二个不同是,它所调用的 constructor 数量。数组版 new operator 必须针对数组中的每一个对象调用一个 constructor。

同样的道理,当 delete operator 被用于数组,它会针对数组中的每个元素调用其析构函数,然后再调用 operator delete[] 释放内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值