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

来源:互联网 发布:网络流行歌曲2009 编辑:程序博客网 时间:2024/05/20 09:07
建议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关键字。
原创粉丝点击