05:谨慎定义类型转换函数

有两种函数允许编译器进行隐式类型转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构造函数。该函数可以是只定义了一个参数,也可以是定义了多个参数但第一个参数以后的所有参数都有缺省值。

隐式类型转换运算符的形式是:operator type()。不用定义函数的返回类型,因为返回类型就是type。例如为了允许Rational(有理数)类隐式地转换为double类型,可以如此声明Rational类:

class Rational {
public:
operator double() const; // 转换Rational类成double类型
}; Rational r(, );
double d = 0.5 * r; // 转换 r 到double, 然后做乘法

为什么最好不要提供任何类型转换函数?问题在于当你在不需要使用转换函数时,这些函数却可能会被调用运行。其结果可能是不正确的,又很难调试。

首先看一下隐式类型转换运算符,比如对于上面定义的Rational类,你想让该类拥有打印有理数对象的功能:

Rational r(, );
cout << r; // 应该打印出"1/2"

但是你忘了为Rational对象定义operator<<,你可能想打印操作将失败,因为没有合适的operator<<被调用。但是当编译器调用operator<<时,会发现没有这样的函数存在,但是它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。编译器最终会发现它能调用Rational::operator double函数来把r转换为double类型。所以上述代码打印的结果是一个浮点数,而不是一个有理数,这是一种非预期的行为。

解决方法是用不使用语法关键字的等同的函数来替代转换运算符。例如为了把Rational对象转换为double,用asDouble函数代替operator double函数:

class Rational {
public:
...
double asDouble() const;
}; Rational r(, ); cout << r; // 错误! Rationa对象没有 operator<<
cout << r.asDouble(); // 正确, 用double类型打印r

在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再会发生,这点损失是值得的。一般来说,越有经验的C++程序员就越喜欢避开类型转换运算符。例如在C++标准库中的string类型没有包括隐式地从string转换成C风格的char*的功能,而是定义了一个成员函数c_str用来完成这个转换。

通过单参数构造函数进行隐式类型转换更难消除,而且在很多情况下这些函数所导致的问题要甚于隐式类型转换运算符。举一个例子,一个array类模板,这些数组需要调用者确定边界的上限与下限:

template<class T>
class Array {
public:
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]) { // 哎呦! "a" 应该是 "a[i]"
do something for when a[i] and b[i] are equal;
}
else {
do something for when they're not;
}
}

上面的for循环本意是想用a的每个元素与b的每个元素相比较,但当录入a时,却忘记了数组下标。这种情况下我们希望编译器找出这种错误,但是它根本没有。因为它把这个调用看成用Array<int>参数(参数a)和int参数(参数b[i])调用operator==函数,虽然没有operator==函数是这样的参数类型,编译器注意到它能通过调用Array<int>构造函数转换int类型到Array<int>类型。编译器如此去编译,生成的代码就像这样:

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

可以使用explicit关键字解决这个问题:只要将constructors声明为explicit,编译器便不能因隐式类型转换的需要而调用它们。不过显式类型转换仍然是允许的:

template<class T>
class Array {
public:
...
explicit Array(int size);
...
};
Array<int> a(); // 没问题,explicit ctors可以像往常一样作为对象构造之用
Array<int> b(); // 也没问题 if (a == b[i]) ... // 错误! 无法将int隐式转换为Array<int> if (a == Array<int>(b[i])) ... // 没问题,显式转换(但是代码逻辑上存疑) if (a == static_cast< Array<int> >(b[i])) ... //同样没问题 if (a == (Array<int>)b[i]) ... // C旧式转换也没问题

还有一种不使用explicit的方法,它利用这样的规则:没有任何一个转换过程(sequence of conversions)可以内含一个以上的“用户定制转换行为”(调用单参数构造函数或隐式类型转换运算符)。考虑Array template,现在需要一种方法,不但允许以一个整数作为构造函数的参数来指定数组大小,又能阻止一个整数被隐式转换为一个临时性Array对象:

template<class T>
class Array {
public:
class ArraySize { // this class is new
public:
ArraySize(int numElements): theSize(numElements) {}
int size() const { return theSize; }
private:
int theSize;
}; Array(ArraySize size); // note new declaration
...
};

现在,当调用”Array<int> a(10);”时,你的编译器寻找拥有单一int类型参数的构造函数,但是这种构造函数不存在,不过编译器知道它能将int转换为一个ArraySize对象,而该对象正是Array<int>构造函数的参数,所以编译器执行了这样的转换,因而该语句得以成功。再看下面的代码:

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对象,然后再根据这个临时对象产生Array<int>对象,因为那将调用两个用户定制转换行为,这样的转换是禁止的。

类似ArraySize这样的类,往往被称为proxy classes,因为它的每一个对象都是为了其他对象而存在的,好像其他对象的代理人一样。

06:区别increment/decrement操作符的前置和后置形式

重载++或--操作符时,因为++或--的前置式和后置式都是没有参数的,因此无法以参数来区分前置还是后置,所以规定重载时后置式有一个int参数:

class UPInt {
public:
UPInt& operator++(); // 前置++
const UPInt operator++(int); // 后置++ UPInt& operator--(); // 前置--
const UPInt operator--(int); // 后置--
...
}; // prefix form: increment and fetch
UPInt& UPInt::operator++()
{
*this += ;
return *this;
} // postfix form: fetch and increment
const UPInt UPInt::operator++(int)
{
const UPInt oldValue = *this;
++(*this);
return oldValue;
} UPInt i;
++i; // 调用i.operator++();
i++; // 调用i.operator++(0);
--i; // 调用i.operator--();
i--; // 调用i.operator--(0);

需要注意的是,后置操作符函数没有使用它的参数,但如果没有在函数里使用参数,许多编译器会报警。为了避免编译器报警,惯常的做法是省略掉不想使用的参数名称。

注意,这些操作符前置与后置形式返回值类型是不同的。前置形式返回一个引用,后置形式返回一个 const 类型。如果后置形式返回的不是一个const对象的话,则像i++++(等价于i.operator++(0).operator++(0))这样的动作就合法了,这就和内建类型的行为不一致了;而且即便能够两次实施increment操作符,第二个operator++所改变的对象也是第一个operator++返回的对象,而不是原对象。

单以效率而言,UPInt 的调用者应该尽量使用前置 increment,少用后置 increment,因为后置实现必须产生一个临时对象,而前置式就没有如此的临时对象。因此,当处理用户定义的类型时,尽可能地使用前置increment,因为它的效率更高。

上面的代码还有一个问题,后置与前置 increment 操作符,它们除了返回值不同外,所完成的功能是一样的,那么如何确保后置 increment和前置 increment 的行为一致呢?为了确保行为一致,必须遵循一个原则:后置increment 和 decrement 应该根据它们的前置形式来实现。这样仅仅需要维护前置版本。

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

&&和||操作符采用“短路式”计算左右表达式的值,但是一旦要对这俩操作符进行重载,也就是说:

if (expression1 && expression2) ...

会被编译器视为以下两种方式之一:

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

但是,函数调用时,语言规范未明确定义参数的计算顺序,所以也就没办法知道expression1和expression2哪个先计算,这与&&和||操作符的“短路式”计算方式不符。

逗号(,)操作符也有类似的问题,C++规定,表达式内如果含有逗号,则逗号左侧先计算,然后是右侧,最后,整个逗号表达式的结果是右侧的值。而将逗号表达式进行重载后,无法保证这样的计算循序。

C++规定,不能重载以下操作符:

可以重载的操作符有:

然而,可以重载并不意味着可以毫无理由的进行重载,所以,如果没有什么好的理由将某个操作符进行重载,就不要这么做。

08:了解各种不同意义的new和delete

下面的代码:

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

这里的new是所谓的new表达式,它首先分配内存,然后在分配的内存上调用构造函数。new表达式的行为不可改变。编译器看到上面的语句,它可能产生的代码如下:

void *memory = operator new(sizeof(string)); // get raw memory for a string object

call string::string("Memory Management") on *memory; 

string *ps = static_cast<string*>(memory);

上面的第一句是调用new operator用于执行内存分配。它的原型通常如下:

void * operator new(size_t size);

new operator可以被重载,重载该函数时,第一个参数类型必须总是size_t。operator new的重载版本中,有一个特殊版本称为placement new,比如下面的代码:

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

该函数返回一个指向Widget对象的指针,该对象构造于函数第一个参数buffer之上。这里就是使用了placement new,它用于满足对象必须构造于特定地址,或者以特殊函数分配出来的内存上这样的需求。

placement new的实现看起来像这样:

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

类似于new表达式调用operator new,delete表达式会调用operator delete。因此如果ps指向使用new构造出来的string对象,则delete ps;等价于下面的代码:

ps->~string();
operator delete(ps); // 释放内存

如果使用了placement new,则应该避免使用delete表达式,因为delete表达式会调用operator delete,而该内存最初并非是用operator new分配而来的,毕竟placement new只是返回传递给他的指针而已,谁也不知道那个指针是从哪来的:

// functions for allocating and deallocating memory in shared memory
void * mallocShared(size_t size);
void freeShared(void *memory); void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = constructWidgetInBuffer( sharedMemory, ); //使用placement new
...
delete pw; // 未定义! pw从mallocShared得来,而不是operator new pw->~Widget();
freeShared(pw); // 使用与mallocShared对应的freeShared释放内存

More Effective C++: 02操作符的更多相关文章

  1. ###《More Effective C++》- 操作符

    More Effective C++ #@author: gr #@date: 2015-05-21 #@email: forgerui@gmail.com 五.对定制的"类型转换函数&qu ...

  2. Effective C++: 02构造、析构、赋值运算

    05:了解C++默默编写并调用哪些函数 1:一个空类,如果你自己没声明,编译器就会为它声明(编译器版本的)一个copy构造函数.一个copy assignment操作符和一个析构函数.此外如果你没有声 ...

  3. Effective Java 02 Consider a builder when faced with many constructor parameters

    Advantage It simulates named optional parameters which is easily used to client API. Detect the inva ...

  4. Effective Java Index

    Hi guys, I am happy to tell you that I am moving to the open source world. And Java is the 1st langu ...

  5. Effective C++(10) 重载赋值操作符时,返回该对象的引用(retrun *this)

    问题聚焦: 这个准则比较简短,但是往往就是这种细节的地方,可以提高你的代码质量. 细节决定成败,让我们一起学习这条重载赋值操作符时需要遵守的准则吧. 还是以一个例子开始: Demo // 连锁赋值 x ...

  6. More Effective C++ - 章节二 : 操作符(operators)

    5. 对定制的 "类型转换函数" 保持警觉 允许编译器执行隐式类型转换,害处多过好处,不要提供转换函数,除非你确定需要. class foo { foo(int a = 0, in ...

  7. [Effective JavaScript 笔记]第33条:使构造函数与new操作符无关

    当使用函数作为一个构造函数时,程序依赖于调用者是否记得使用new操作符来调用该构造函数.注意:该函数假设接收者是一个全新的对象. 一个例子 function User(name,pwd){ this. ...

  8. 《Effective C++ 》学习笔记——条款02

    ****************************  一. Accustoming Yourself to C++ **************************** 条款02: Pref ...

  9. C++ 重载操作符- 02 重载输入输出操作符

    重载输入输出操作符 本篇博客主要介绍两个操作符重载.一个是 <<(输出操作符).一个是 >> (输入操作符) 现在就使用实例来学习:如何重载输入和输出操作符. #include ...

随机推荐

  1. 关于JEECMS套站工具的使用要点

      第一步:在[界面—资源]下面引入资源文件(js,css,img…) 第二步:在[界面—模板]下面将网站的入口页面写在[index]文件下 此时修改index页面中的 js,css,图片 的路径,路 ...

  2. KOA 学习(七) 路由koa-router

    一.基本用法 var app = require('koa')(); var router = require('koa-router')(); router.get('/', function *( ...

  3. getBoundingClientRect介绍

    getBoundingClientRect用于获取元素相对与浏览器视口的位置 由于getBoundingClientRect()已经是w3c标准,所以不用担心兼容,不过在ie下还是有所区别 { top ...

  4. leetcode 665

    665. Non-decreasing Array Input: [4,2,3] Output: True Explanation: You could modify the first 4 to 1 ...

  5. php用mysql方式连接数据库出现Deprecated报错

    以上是用php5.5 连接mysql数据库时报的错. 于是我用php5.4 连接正常没有报错. 这与mysql版本无关系,php 5.x版本,如5.2.5.3.5.4.5.5,怕跟不上时代,新的服务器 ...

  6. Markdown文档使用

    Markdown使用 一.markdown标题:1级-6级 一级 #空格 二级 ##空格 三级 ###空格 ... 六级 ######空格 二.代码块 print("hello world! ...

  7. 访问者模式(Visitor、Element、accept、ObjectStructure、)(操作外置,与数据结构分离)

    访问者模式表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作.从定义可以看出结构对象是使用访问者模式的必备条件,而且这个结构对象必须存在遍历自身各个 ...

  8. Python科学计算生态圈--Scipy

  9. win10 系统同步时间出错

    设置->时间和语言->区域和语言->其他日期,区域和时间设置->设置时间和日期->Internet时间->更改设置 应该会有两个服务器,分别更新下时间,哪个正确就用 ...

  10. MySQL Daemon failed to start错误解决办法是什么呢?

    首先我尝试用命令:service mysql start 来启动服务,但是提示: MySQL Daemon failed to start 一开始出现这个问题我很方,然后开始查,说什么的都有,然后看到 ...