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. response - 文件下载

    ## 案例:     * 文件下载需求:         1. 页面显示超链接         2. 点击超链接后弹出下载提示框         3. 完成图片文件下载 * 分析:         1 ...

  2. Spring MVC(三)--控制器接受普通请求参数

    Spring MVC中控制器接受参数的类方式有以下几种: 普通参数:只要保证前端参数名称和传入控制器的参数名称一致即可,适合参数较少的情况: pojo类型:如果前端传的是一个pojo对象,只要保证参数 ...

  3. naturalWidth、naturalHeight来获取图片的真实宽高

    一般在图片放大缩小,或动态插入图片时使用 function imagea(img){ var w = img.naturalWidth; var h = img.naturalHeight; } 注: ...

  4. webServices学习一(了解基础和作用。)

    一.第一部分 1.         带着几个问题学习: l    什么是WebService? l    它能做什么? l    为什么要学习WebService? l    学习WebService ...

  5. django模块安装环境变量

    django 模块 一 安装: 方法一: (在 JetBrains PyCharm 2017.2 软件的) 设置 (里找到) 项目:python +(添加) (搜索) django Install p ...

  6. @ font-face 引入本地字体文件

    @font-face { font-family: DeliciousRoman; src: url('…/Delicious-Roman.otf'); font-stretch: condensed ...

  7. 关于CE的反思

    当你注视着你的分数, 眼眶倏地猛睁. 不会做的题血红一片, 认真做了的题一点墨蓝. 你知道, 你CE了, 你挂了, 你倒数第一了, 你当场去世了. 两小时的努力付诸东流, 线段树的碎片历历在目. 思考 ...

  8. AntColony 磁力搜索引擎的核心

    介绍 AntColony(Github)是findit磁力搜索引擎的核心.用来在DHT网络中,收集活跃资源的infohash,下载并解析资源的种子文件,存入数据库等.AntColony是若干功能的合集 ...

  9. Jeecms网站直接访问html静态页面

    jeecms网站维护,遇到了直接通过链接的方式访问静态页面,jeecms官网也做了详细的解答,但是没有得到满意的结果.但是通过自己的深入研究以及别人的帮助,发现了一个很好的解决方法. 首先说明一下je ...

  10. 2019.8.5 NOIP模拟测试13 反思总结【已更新完毕】

    还没改完题,先留个坑. 放一下AC了的代码,其他东西之后说… 改完了 快下课了先扔代码 跑了跑了 思路慢慢写 来补完了[x 刚刚才发现自己打错了标题 这次考试挺爆炸的XD除了T3老老实实打暴力拿了52 ...