《More Effective C++》 条款5 谨慎定义类型转换函数
---恢复内容开始---
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,
// 然后做乘法
完整示例:
#include<iostream>
using namespace std; class Rational{
public:
Rational(int numerator=,int denominator=):n(numerator),d(denominator){} operator double() const
{
return (double)n/d;
}
private:
int n,d;
friend ostream& operator<<(ostream& s,const Rational& r); }; ostream& operator<<(ostream& s,const Rational& r)
{
s<<r.n<<"/"<<r.d;
return s;
}
int main()
{
Rational r(,);
cout<<r<<endl;
double d=0.5*r;
cout<<d<<endl; }
以上这些说明只是一个复习,我真正想说的是为什么你不需要定义各中类型转换函数。
根本问题是当你在不需要使用转换函数时,这些的函数缺却能被调用运行。结果这些不正确的程序会做出一些令人恼火的事情,而你又很难判断出原因。
让我们首先分析一下隐式类型转换运算符,它们是最容易处理的。假设你有一个如上所述的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++标准库(参见条款49和35)委员会工作的人员是在此领域最有经验的,他们加在库函数中的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>对象(见条款19)。
通过不声明运算符(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(参见条款2),两个“>”字符间的空格不能漏掉,如果这样写语句:
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对象能帮你更好地控制软件的在某些方面的行为,否则你就不能控制这些行为,比如在上面的情况里,这种行为是指隐式类型转换,所以它值得你去学习和使用。你可能会问你如何去学习它呢?一种方法是转向条款33;它专门讨论proxy classes。
在你跳到条款33之前,再仔细考虑一下本条款的内容。让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。
《More Effective C++》 条款5 谨慎定义类型转换函数的更多相关文章
- Effective C++ -----条款46:需要类型转换时请为模板定义非成员函数
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”.
- Effective C++ -----条款45:运用成员函数模板接受所有兼容类型
请使用member function templates(成员函数模板)生成”可接受所有兼容类型“的函数. 如果你声明member templates 用于“泛化copy构造”或“泛化assignme ...
- Effective C++ -----条款35:考虑virtual函数以外的其他选择
virtual函数的替代方案包括NVI手法及Strategy设计模式的多种手法.NVI手法自身是一个特殊形式的Template Method设计模式. 将机能从成员函数移到class外部函数,带来的一 ...
- 转载: C++ 转换构造函数 和 类型转换函数
1.对于系统的预定义基本类型数据,C++提供了两种类型转换方式:隐式类型转换和显式类型转换. ,sum; double b=5.55; sum=a+b;//-------(1) std::cout&l ...
- [一道搜狗输入法的面试题]C++转换构造函数和类型转换函数
今天面试遇到一道有关C++转换构造函数的题目,之前经常见到默认构造函数.拷贝构造函数.析构函数,但是从没听说过转换构造函数,隐式转换函数也是一样,C++的确是够博大精深的,学习之路很长啊! 其实我们已 ...
- 21.C++- "++"操作符重载、隐式转换之explicit关键字、类的类型转换函数
++操作符重载 ++操作符分为前置++和后置++,比如: ++a; a++; ++操作符可以进行全局函数或成员函数重载 重载前置++操作符不需要参数 重载后置++操作符需要一个int类型的占位参数 ...
- C++解析(20):智能指针与类型转换函数
0.目录 1.智能指针 2.转换构造函数 3.类型转换函数 4.小结 1.智能指针 内存泄漏(臭名昭著的Bug): 动态申请堆空间,用完后不归还 C++语言中没有垃圾回收机制 指针无法控制所指堆空间的 ...
- C++ 类类型转换函数explicit 关键字
标准数据之间会进行 隐式类型安全转换. 转换规则: 隐式类型转换的问题: #include <iostream> #include <string> using namesp ...
- C++中的类型转换函数
1,转换构造函数可以将普通的基础类型转换为当前的类类型,也有能力将其它类类 型的对象转换为当前的类类型: 2,问题: 1,类类型是否能够类型转换到普通类型? 1,可以的: 3,类型转换函数: 1,C+ ...
随机推荐
- "ORA-00942: 表或视图不存在 "的原因和解决方法
"ORA-00942: 表或视图不存在 "的原因和解决方法 采用Oracle数据库,使用Powerdesigner设计,生成Sql文件导入后查询出现“ORA-00942: 表或 ...
- jQuery给CheckBox添加事件
<asp:CheckBox ID="ckbTable" runat="server" Checked="false" /> &l ...
- easyui-treegrid移除树节点出错
easyui-treegrid移除树节点出错 >>>>>>>>>>>>>>>>>>>& ...
- HTML5吧
一.为了能使IE9以下的IE浏览器也能支持html5的标签,所以首先得在文档头部用条件注释的方法引入那段著名的代码. 1 2 3 <!--[if lt IE 9]> <script ...
- [IO] C# INI文件读写类与源码下载 (转载)
/// <summary> /// 类说明:INI文件读写类. /// 编 码 人:苏飞 /// 联系方式:361983679 /// 更新网站:[url]http://www.sufei ...
- [DEncrypt] RSACryption--RSA加密/解密字符串 (转载)
点击下载 RSACryption.zip 这个类是关于加密,解密的操作,文件的一些高级操作1.RSACryption RSA 的密钥产生2.RSACryption RSA的加密函数3.RSACrypt ...
- C#当中的多线程_线程同步
第2章 线程同步 原来以为线程同步就是lock,monitor等呢,看了第二章真是大开眼界啊! 第一章中我们遇到了一个叫做竞争条件的问题.引起的原因是没有进行正确的线程同步.当一个线程在执行操作时候, ...
- Windows下的进程【一】
什么是进程?进程就是一个正在运行的程序的实例,由两部分组成: 内核对象.操作系统用内核对象对进程进行管理,内核对象是操作系统保存进程统计信息的地方. 地址空间.其中包含所有可执行文件或DLL模块的代码 ...
- 这次是C#中的接口
接口的出现,是为了解决C#中不允许多重继承的问题. 1.什么是接口? 我觉得可以把接口理解为对一组方法声明进行的统一命名,但这些方法没有提供任何实现. 通过接口,就可以对方法进行统一管理,避免了在每种 ...
- 2014-11-9------- 设有一数据库,包括四个表:学生表(Student)、课程表(Course)、成绩表(Score)以及教师信息表(Teacher)。
一. 设有一数据库,包括四个表:学生表(Student).课程表(Course).成绩表(Score)以及教师信息表(Teacher).四个表的结构分别如表1-1的表(一)~表( ...