C++编译器能够在两种数据类型之间进行隐式转换(implicit conversions),它继承了C语言的转换方法,例如允许把char隐式转换为int和从short隐式转换为double。因此当你把一个short值传递给准备接受double参数值的函数时,依然可以成功运行。C中许多这种可怕的转换可能会导致数据的丢失,它们在C++中依然存在,包括int到short的转换和double到char的转换。

你对这些类型转换是无能为力的,因为它们是语言本身的特性。不过当你增加自己的类型时,你就可以有更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换。

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

class Name { // for names of things

public:

Name(const string& s); // 转换 string 到

// Name

...

};

class Rational { // 有理数类

public:

Rational(int numerator = 0, // 转换int到

int denominator = 1); // 有理数类

...

};

隐式类型转换运算符只是一个样子奇怪的成员函数:operator 关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。例如为了允许Rational(有理数)类隐式地转换为double类型(在用有理数进行混合类型运算时,可能有用),你可以如此声明Rational类:

class Rational {

public:

...

operator double() const; // 转换Rational类成

}; // double类型

在下面这种情况下,这个函数会被自动调用:

Rational r(1, 2); // r 的值是1/2

double d = 0.5 * r; // 转换 r 到double,

// 然后做乘法

以上这些说明只是一个复习,我真正想说的是为什么你不需要定义各种类型转换函数。

根本问题是当你在不需要使用转换函数时,这些的函数缺却会被调用运行。结果,这些不正确的程序会做出一些令人恼火的事情,而你又很难判断出原因。

让我们首先分析一下隐式类型转换运算符,它们是最容易处理的。假设你有一个如上所述的Rational类,你想让该类拥有打印有理数对象的功能,就好像它是一个内置类型。因此,你可能会这么写:

Rational r(1, 2);

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

}; // 成double

这个成员函数能被显式调用:

Rational r(1, 2);

cout << r; // 错误! Rationa对象没有

// operator<<

cout << r.asDouble(); // 正确, 用double类型

//打印r

在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再会发生,这点损失是值得的。一般来说,越有经验的C++程序员就越喜欢避开类型转换运算符。例如在C++标准库(参见Effective C++条款49和M35)委员会工作的人员是在此领域最有经验的,他们加在库函数中的string类型没有包括隐式地从string转换成C风格的char*的功能,而是定义了一个成员函数c_str用来完成这个转换,这是巧合么?我看不是。

通过单参数构造函数进行隐式类型转换更难消除。而且在很多情况下这些函数所导致的问题要甚于隐式类型转换运算符。

举一个例子,一个array类模板,这些数组需要调用者确定边界的上限与下限:

template<class T>

class Array {

public:

Array(int lowBound, int highBound);

Array(int size);

T& operator[](int index);

...

};

第一个构造函数允许调用者确定数组索引的范围,例如从10到20。它是一个两参数构造函数,所以不能做为类型转换函数。第二个构造函数让调用者仅仅定义数组元素的个数(使用方法与内置数组的使用相似),不过不同的是它能做为类型转换函数使用,能导致无穷的痛苦。

例如比较Array<int>对象,部分代码如下:

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" 应该是 "a[i]"

do something for when

a[i] and b[i] are equal;

}

else {

do something for when they're not;

}

我们想用a的每个元素与b的每个元素相比较,但是当录入a时,我们偶然忘记了数组下标。当然我们希望编译器能报出各种各样的警告信息,但是它根本没有。因为它把这个调用看成用Array<int>参数(对于a)和int(对于b[i])参数调用operator==函数,然而没有operator==函数是这样的参数类型,我们的编译器注意到它能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数。然后编译器如此去编译,生成的代码就象这样:

for (int i = 0; i < 10; ++i)

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

每一次循环都把a的内容与一个大小为b[i]的临时数组(内容是未定义的)比较。这不仅不可能以正确的方法运行,而且还是效率低下的。因为每一次循环我们都必须建立和释放Array<int>对象(见条款M19)。

通过不声明运算符(operator)的方法,可以克服隐式类型转换运算符的缺点,但是单参数构造函数没有那么简单。毕竟,你确实想给调用者提供一个单参数构造函数。同时你也希望防止编译器不加鉴别地调用这个构造函数。幸运的是,有一个方法可以让你鱼肉与熊掌兼得。事实上是两个方法:一是容易的方法,二是当你的编译器不支持容易的方法时所必须使用的方法。

容易的方法是利用一个最新编译器的特性,explicit关键字。为了解决隐式类型转换而特别引入的这个特性,它的使用方法很好理解。构造函数用explicit声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法:

template<class T>

class Array {

public:

...

explicit Array(int size); // 注意使用"explicit"

...

};

Array<int> a(10); // 正确, explicit 构造函数

// 在建立对象时能正常使用

Array<int> b(10); // 也正确

if (a == b[i]) ... // 错误! 没有办法

// 隐式转换

// int 到 Array<int>

if (a == Array<int>(b[i])) ... // 正确,显式从int到

// Array<int>转换

// (但是代码的逻辑

// 不合理)

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

// 同样正确,同样

// 不合理

if (a == (Array<int>)b[i]) ... //C风格的转换也正确,

// 但是逻辑

// 依旧不合理

在例子里使用了static_cast(参见条款M2),两个“>”字符间的空格不能漏掉,如果这样写语句:

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

这是一个不同的含义的语句。因为C++编译器把“>>”做为一个符号来解释。在两个“>”间没有空格,语句会产生语法错误。

如果你的编译器不支持explicit,你不得不回到不使用成为隐式类型转换函数的单参数构造函数。

我前面说过复杂的规则决定哪一个隐式类型转换是合法的,哪一个是不合法的。这些规则中没有一个转换能够包含用户自定义类型(调用单参数构造函数或隐式类型转换运算符)。你能利用这个规则来正确构造你的类,使得对象能够正常构造,同时去掉你不想要的隐式类型转换。

再来想一下数组模板,你需要用整形变量做为构造函数参数来确定数组大小,但是同时又必须防止从整数类型到临时数组对象的隐式类型转换。你要达到这个目的,先要建立一个新类ArraySize。这个对象只有一个目的就是表示将要建立数组的大小。你必须修改Array的单参数构造函数,用一个ArraySize对象来代替int。代码如下:

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); // 注意新的声明

...

};

这里把ArraySize嵌套入Array中,为了强调它总是与Array一起使用。你也必须声明ArraySize为公有,为了让任何人都能使用它。

想一下,当通过单参数构造函数定义Array对象,会发生什么样的事情:

Array<int> a(10);

你的编译器要求用int参数调用Array<int>里的构造函数,但是没有这样的构造函数。编译器意识到它能从int参数转换成一个临时ArraySize对象,ArraySize对象只是Array<int>构造函数所需要的,这样编译器进行了转换。函数调用(及其后的对象建立)也就成功了。

事实上你仍旧能够安心地构造Array对象,不过这样做能够使你避免类型转换。考虑一下以下代码:

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" 应该是 "a[i]";

// 现在是一个错误。

为了调用operator==函数,编译器要求Array<int>对象在”==”右侧,但是不存在一个参数为int的单参数构造函数。而且编译器无法把int转换成一个临时ArraySize对象然后通过这个临时对象建立必须的Array<int>对象,因为这将调用两个用户定义(user-defined)的类型转换,一个从int到ArraySize,一个从ArraySize到Array<int>。这种转换顺序被禁止的,所以当试图进行比较时编译器肯定会产生错误。

ArraySize类的使用有些象一个有目的的帮手,这是一个更通用技术的应用实例。类似于ArraySize的类经常被称为proxy classes,因为这样类的每一个对象都为了支持其他对象的工作。ArraySize对象实际是一个整数类型的替代者,用来在建立Array对象时确定数组大小。Proxy对象能帮你更好地控制软件的在某些方面的行为,否则你就不能控制这些行为,比如在上面的情况里,这种行为是指隐式类型转换,所以它值得你去学习和使用。你可能会问你如何去学习它呢?一种方法是转向条款M30;它专门讨论proxy classes。

在你跳到条款M30之前,再仔细考虑一下本条款的内容。让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。

More Effective C++之Item M5:谨慎定义类型转换函数相关推荐

  1. Effective C++ 之 Item 6 : 若不想使用编译器自动生成的函数,就该明确拒绝

    Effective C++ chapter 2. 构造 / 析构 / 赋值运算 (Constructors, Destructors, and Assignment Operators) Item 6 ...

  2. More Effective C++之 Item M30:代理类

    虽然你和你的亲家可能住在同一地理位置,但就整个世界而言,通常不是这样的.很不幸,C++还没有认识到这个事实.至少,从它对数组的支持上可以看出一些迹象.在FORTRAN.BASIC甚至是COBOL中,你 ...

  3. 《Effective C#》Item 20:区分接口实现与虚函数重载

    本文的英文标题为"Distinguish Between Implementing Interfaces and Overriding Virtual Functions",但是文 ...

  4. 定义python函数时如果没有return_定义 Python 函数时,如果函数中没有 return 语句,则默认返回空值 None 。_学小易找答案...

    [多选题]因发现核酶而共享诺贝尔化学奖的科学家是(). [简答题]如果是六角梅花,你还可以用什么方法完成? [填空题]如果函数中没有 return 语句或者 return 语句不带任何返回值,那么该函 ...

  5. python如何定义自定义函数_python类中系统自定义函数

    标签: python类 双下划线特殊函数 今天遇到python类内部系统以双下划线开头和结尾的情况,特去学习一部分的应用情况,顺便记录一下.python 的OOP编程中,每个类拥有一些系统特殊定义的函 ...

  6. VB谨慎使用IsMissing函数

    在VB6中提供了一个很好用的函数叫IsMissing,可以用来判断用户是否对缺省参数赋值,比如有以下一个函数体: 代码如下: Public Property Get Item(Optional ByR ...

  7. python 类函数 成员函数_python内置函数类型,如何为新类型定义成员函数?

    但是如何定义成员函数,以便类可以等效为:class X(object): a = 1 def get_a(self): return self.a 这个问题是由向给定的自定义枚举类型添加iterabl ...

  8. 【C语言】函数 ---- 函数的嵌套调用和链式访问、函数的声明和定义、变量的声明和定义、函数递归与迭代、递归时的栈溢出问题

    函数 一.函数的嵌套调用和链式访问 1.嵌套调用 2.链式访问 2.1strlen()函数 2.2printf()函数 二.函数的声明和定义 1.函数声明和定义的介绍 2.函数声明和定义的使用 三.变 ...

  9. Go 学习笔记(15)— 函数(01)[函数定义、函数特点、多值返回、实参形参、变长参数,函数作为参数调用]

    1. 函数定义 Go 语言最少有个 main() 函数.函数声明告诉了编译器函数的名称,返回类型和参数. func funcName(parameter_list)(result_list) {fun ...

最新文章

  1. redis为什么是单线程_面试官:Redis单线程为什么执行效率这么高?
  2. Android-ConvenientBanner轻松实现广告头效果
  3. 【转】深入理解JavaScript闭包(closure)
  4. Facebook对memcached的提升
  5. hdu3068 求一个字符串中最长回文字符串的长度 Manacher算法
  6. 【PID优化】基于matlab天牛须算法PID控制器优化设计【含Matlab源码 1312期】
  7. 热点|国家动物健康与食品安全创新联盟成立科技创新赋能委员会 并首发动物源食品信任追溯平台...
  8. 英特尔服务器主板型号参数对照表,Intel主板芯片组参数速查表(201805版)
  9. 三维扫描仪[10]——如何设计一台云台式扫描仪(代码详解)
  10. STM32制作FLASH字库
  11. 毕业就去当网红?先听听8位95后网红的口述
  12. 魔方还原算法(一) 概述
  13. 春招秋招,什么是群面和无领导小组讨论
  14. 史上最全maven教程
  15. 商城系统必备营销工具(五)——积分商城
  16. linux入门指南(基础教程)
  17. 高考落榜怎么办,奉劝学弟学妹们一些事情,请一定要擦亮眼睛
  18. 如何阅读书籍 学习、使用技术的四种层次
  19. 插入排序java_排序算法之直接插入排序Java实现
  20. cad怎样弄出放线的坐标_怎么把要放线的坐标标注出?

热门文章

  1. python硬件编程_树莓派c语言 设置并使用树莓派进行Python和C语言编程 - 硬件设备 - 服务器之家...
  2. ubuntu下系统python与anaconda下python
  3. 六个计算机硬件商标名称,2.注册类别中有6个商标(计算机外围设备
  4. 缓存穿透 雪崩 击穿
  5. 什么是FFmpeg?
  6. 如何使用Corona SDK构建公路道奇
  7. 根目录,子目录,当前目录 宿主目录、根目录及/home
  8. php 中大括号,PHP中大括号的作用总结
  9. 原生H5+JS+CSS实现音乐播放器(上下一首,随机单曲循环播放,进度条,播放列表)
  10. vs2013的简单安装