改善C++ 程序的150个建议学习之建议17:提防隐式转换带来的麻烦

原创 2013年09月11日 09:47:36
建议17:提防隐式转换带来的麻烦
在C/C++语言的表达式中,允许在不同类型的数据之间进行某一操作或混合运算。当对不同类型的数据进行操作时,首先要做的就是将数据转换成为相同的数据类型。C/C++语言中的类型转换可以分为两种,一种为隐式转换,而另一种则为建议11中提及的显式强制转型。显式强制转型在某种程度上还有一定的优点,对于编写代码的人来说使用它能够很容易地获得所需类型的数据,对于阅读代码的人来说可以从代码中获知作者的意图。而隐式转换则不然,它让发生的一切变得悄无声息,在编译时这一切由编译程序按照一定规则自动完成,不需任何的人为干预。存在大量的隐式转换也是C/C++常受人诟病的焦点之一。隐式转换虽然带来了一定的便利,使编码更加简洁,减少了冗余,但是这些并不足以让我们完全接受它,因为隐式转换所带来的副作用不可小觑,它通常会使我们在调试程序时毫无头绪。就像下面的代码片
段所示:
void Function(char c);
int main()
{
long para = 256;
Function(para);
return 0;
}

上述代码片段中的函数调用不会出现任何错误,编译器给出的仅仅是一个警告。可是细心的程序员一眼就能看出问题:函数Function(char c)的参数c是一个char型,256绝不会出现在其取值区间内。但是编译器会自动地完成数据截断处理。编译器悄悄完成的这种转换存在着很大的不确定性:一方面它可能是合理的,因为尽管类型long大于char,但para中很可能存放着char类型范围内的数值;另一方面para的值的确可能是char无法容纳的数据,这种“暗地里的勾当”一不小心便会造成一个非常隐蔽、难以捉摸的错误。C/C++隐式转换主要发生在以下几种情形。

基本类型之间的隐式转换

int ival = 3; 
double dval = 3.1415 
cout<<(ival + dval)<<endl; //ival被提升为double类型:3.0
extern double sqrt(double); 
sqrt(2); //2被提升为double类型: 2.0
在编译这段代码时,编译器会按照规则自动地将ival转换为与dval相同的double类型。
C语言规定的转换规则是由低级向高级转换。两个通用的转换原则是:
(1)为防止精度损失,类型总是被提升为较宽的类型。
(2)所有含有小于整型类型的算术表达式在计算之前其类型都会被转换成整型。
这两点在C++中依旧有效,这已无须多言。它最直接的害处就是有可能导致重载函数产生二义性,如下所示:
void Print(int ival);
void Print(float fval);
int ival = 2;
float fval = 2.0f;
Print(ival); // OK, int-version
Print(fval); // OK, float-version
Print(1); // OK, int-version
Print(0.5); // ERROR!!
参数0.5应该转换为ival还是fval?这是编译器没法搞明白的一个问题。
T* 指针到 void* 的隐式转换
在C语言中,标准允许T*与void*之间的双向转换,这也就间接导致了各种数据类型之间的隐式转换是被允许的,无论是从低级到高级,还是从高级到低级。这样的转换存在着太多的不安全因素,所以到了C++中,双向变单向,只允许T*隐式地转换为void*了,示
例代码如下所示:
char* pChar = new char[20];
void * pVoid = pChar;
non-explicit constructor接受一个参数的用户定义类对象之间隐式转换先看如下代码:
class A 

public: 
A(int x):m_data(x){}
private:
int m_data;
}
void DoSomething(A aObject);
DoSomething(20);
在上面的代码中,调用DoSomething()函数时会发现实参与形参类型不一致,但是因为类A的构造函数只含有一个int类型的参数,所以编译器会以20为参数调用A的构造函数,以便构造临时对象,然后传给DoSomething()函数。不要为此而感到惊讶,其实编译器比想像的还要聪明:当无法完成直接隐式转换的时候,它不会罢休,它会尝试使用间接的方式。所以,下面的代码也是可以被编译器接受的:
void DoSomething(A aObject);
float fval = 20.0f;
DoSomething(fval);
这是一个多么奇妙的世界。这样的隐式转换在某些时候会变得相当微妙,一个误用也许会引起难以捉摸的错误。另外,由于在隐式转换过程中需要调用类的构造函数、析构函数,如果这种转换代价很大,那么这样的隐式转换将会影响系统性能。当然,我们熟知的隐式转换还包括“子类到基类的隐式转换”和“const到non-const的同类型隐式转换”。不过这两种转换是比较安全的,所以在这里就不再详细讨论。如果试图禁止所有的隐式类型转换,那么为了维持函数使用代码的简洁性,函数必须对所有的类型执行重载。这将是一个十分庞大且毫无技术含量的重复性工程,这不仅大大增加了函数实现的负担,重复的代码也严重偏离了DRY原则。说明 DRY— Don’t Repeat Yourself Principle,直译为“不要重复自己”。简而言之,就是不要写重复的代码。DRY利用的方法就是抽象:把共同的事物抽象出来,把代码抽取到一个地方去,这样就可以避免重复写代码。
C/C++对于这个问题采取的策略是“把问题交给程序员全权处理”。程序员既然享受了隐式变换所带来的便利,那么如果出现错误也是程序员需要负责的问题。权利与义务对等,这也算得上合情合理。但在程序员的眼里,这样的处理方式却不能让他们满意。后来C++设计者意识到了这个问题,于是提供了控制隐式转换的两条有效途径:使用具名转换函数来看一段代码:
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1)
:m_num(numerator),m_den(denominator){}
operator double() const
{
return ((double)m_num/(double)m_den);
}
private:
int m_num;
int m_den;
};
Rational r(1,2);
cout<<r<<endl;

上面代码的本意是打印类似n/m的形式,可是结果输出的却是0.5。问题出现在哪里?当调用operator<<时,编译器会发现没有合适的函数存在,所以它就试图找到一个合适的隐式类型转换顺序,以使函数得到正常调用。本来程序中并不存在将Rational转为其他类型的转换规则,但是Rational::operator double函数告诉编译器Rational类型可以转换为double类型,所以就有了上述结果的出现。为了避免此类问题的出现,建议使用非C/C++关键字的具名函数,代码如下所示:

class Rational
{
public:
Rational(int numerator = 0, int denominator = 1);
operator as_double() const; 
private:
int m_num;
int m_den;
};
Rational r(1,2);
cout<<r<<endl; // 提示无operator<<Rational重载函数
使用 explicit 限制的构造函数
这种方式针对的是具有一个单参数构造函数的用户自定义类型。代码如下所示:
class Widget

public:
Widget( unsigned int factor);
Widget( const char* name,const Widget* other = NULL);
};
上述代码片段中,用户自定义类型Widget的构造函数可以是一个参数,也可以是两个参数。具有一个参数时,其参数类型可以是unsigned int,亦可以是char*。所以这两种类型的数据均可以隐式地转换为Widget类型。控制这种隐式转换的方法很简单:为单参数的构
造函数加上explicit关键字:
class Widget 
{
explicit Widget(unsigned int factor);
explicit Widget(const char*name,const Widget* other = NULL);
};

请记住:提防隐式转换所带来的微妙问题,尽量控制隐式转换的发生;通常采用的方式包括:

(1)使用非C/C++关键字的具名函数,用operator as_T()替换operato T()(T为C++数据类型)。

(2)为单参数的构造函数加上explicit关键字。

《编写高质量代码:改善C++程序的150个建议》读书笔记1

《编写高质量代码:改善C++程序的150个建议》读书笔记1
  • jiangwei0512
  • jiangwei0512
  • 2016年02月28日 16:41
  • 382

编写高质量代码-改善C++程序的150个建议-1

最近从图书馆借了《编写高质量代码-改善C++程序的150个建议》这本书。感觉不错,将内容摘录出来,纪录自己学习点滴。也喜欢与大家共同分享。 另外,我也找到了高清pdf。 已上传到我的资源里了。   ...
  • hitwhylz
  • hitwhylz
  • 2013年10月09日 15:55
  • 2505

《编写高质量代码改善C++程序的150个建议》摘录

内容摘自《编写高质量代码改善C++程序的150个建议》:   第一部分 语法篇   第1章   从C继承而来的 建议0:不用让main函数返回void          main函数的返回类型是int...
  • fengbingchun
  • fengbingchun
  • 2013年06月28日 09:09
  • 6055

改善C#编程的50个建议(1-5)

翻译自Effective C# 1、属性(properties) 抽象属性可以放在接口的定义里。如: public interface INameValuePair     {         s...
  • edcvf3
  • edcvf3
  • 2014年03月19日 00:14
  • 1523

【备注】【C42】《编写高质量代码:改善Python程序的91个建议》PDF

内容简介: 在通往“Python技术殿堂”的路上,本书将为你编写健壮、优雅、高质量的Python代码提供切实帮助!内容全部由Python编码的实践组成,从基本原则、惯用法、语法、库、设计模式、内部机...
  • tianshilirenwei
  • tianshilirenwei
  • 2016年11月27日 23:34
  • 104

读书笔记----《编写高质量代码:改善Java程序的151个建议》第一章

第一章 通用方法和准则 包名全小写,类名首字母全大写,常量全部大写并用下划线分隔,变量采用驼峰命名法(Camel Case)命名。Long型数据标志使用L代替l,使用O(字母O)时加注释。 ran...
  • syney120
  • syney120
  • 2017年12月26日 19:25
  • 33

代码质量优先——《编写高质量代码:改善c程序代码的125个建议》

高质量的代码不但可以促进团队合作、减少bug处理、降低维护成本,对程序员自身的成长也是至关重要的...
  • dongfeng9ge
  • dongfeng9ge
  • 2016年04月13日 15:42
  • 1796

编写高质量代码--改善Java程序的151个建议--读书笔记

--警惕自增陷阱 i++表示先赋值后自增,而++i表示先自增后赋值。下面的代码返回结果为0,因为lastAdd++有返回值,而返回值是自增前的值(在自增前变量的原始值会存在一个内存空间,当自增结束后...
  • u011680118
  • u011680118
  • 2015年08月24日 21:11
  • 579

编写高质量代码 改善Python程序的91个建议

编写高质量代码 改善Python程序的91个建议第1章 引论建议1:理解Pythonic概念建议2:编写Pythonic代码建议3:理解Python与C语言的不同之处建议4:在代码中适当添加注释建议5...
  • theforever
  • theforever
  • 2016年07月26日 14:30
  • 1370

读-秦小波-编写高质量代码:改善java程序的151个建议

有些建议不太用到,但是用到的时候如果不注意就会进坑,所以书名改成java的151个坑更合适。 不要在常量和变量中出现易混淆的字母包名全小写,类名首字母全大写,常量全大写下划线分割,变量驼峰; 字母l作...
  • xiaoxufox
  • xiaoxufox
  • 2016年12月21日 10:06
  • 1058
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:改善C++ 程序的150个建议学习之建议17:提防隐式转换带来的麻烦
举报原因:
原因补充:

(最多只允许输入30个字)