实用经验 16 提防隐式转换带来的麻烦

在C/C++中,类型转换发生在这种情况下:为了实现不同类型的数据之间进行某一操作或混合运算,编译器必须把他们转成同一种类型的数据。C/C++语言中的类型转换分为两种,一种是隐式转换,特指那些由编译器替我们完成的类型转换。另一种是显示转换,特指那些由开发人员显示进行的数据类型转换。

说明

  • 隐式转换在编译过程中由编译器按照一定的规则自动完成,无需任何人为干预。
  • 显示转换由人为因素显示干预完成,与隐式转换相比,显式转换使得开发人员更容易获取所需类型的数据;阅读代码的人也更易明白开发人员的真实意图。

存在大量的隐式转化也是C/C++语言常受人诟病的焦点之一。隐式转换虽然可以给开发人员带来一定的便利,使代码更加简洁,减少不必要冗余。但隐式转化所带来的副作用也是不可小觑的。它常常使我们变得苦不堪言。

隐式转换场景

C/C++的隐式转换主要发生在下面几种情况下,详细来看看:

1. 内置类型间的隐式转化

内置数据类型的隐式转化,发生在下面这些条件下:在混合类型表达式中,操作数被转换成相同的类型;用作 if 语句或循环语句的条件时,被转换为bool类型;用于switch语句时,转为整数类型;用来初始化某个变量(包括函数实参、return语句),转为变量的类型。内置类型转化时,转化级别如图2-7所示。且在隐式转化时,总是由低级别到高级别转化。

在这里插入图片描述

图2-7 内置类型转化级别

同时内置类型的级别还遵循下述8条规则:

  • 除char和signed char外,任何两个有符号的整数类型都具有不同的级别(Rank)。
  • 有符号的整数类型的级别高于比它小的有符号的整数类型的级别。  无符号整数类型的级别等于其对应的有符号整数类型的级别。
  • 标准整数类型的级别高于同样大小的扩展整数类型的级别。(比如在long long为64位,且__int64为扩展整数类型情况下,前者高于后者)。
  • 布尔类型bool的级别低于任何一个标准的整数类型级别。
  • char16_t、char32_t、wchar_t的级别等于它们底层类型的级别。
  • 相同大小的两个有符号的扩展整数类型间的级别高低,由实现定义。
  • 整数类型级别低于浮点数级别,而双精度的级别高于单精度浮点数的级别。

隐式转化规则

  • 为防止精度损失,类型总是被提升为较高级别的类型。
  • 所有含有小于整型类型的算术表达式在计算之前其类型均被转化为整型。

关于内置类型转换在编码中,隐式转化的过程及产生的副作用。我们可以参考下面这段代码:

int    nValue = 100;
float  fValue = 50.1233f;
double dValue = 100;
cout << (nValue + fValue) << endl;  // nValue 被提升为float类型,提升后值为100.0。
void Output(double dPutValue);
void Output(float fPutValue);

Output(dValue);    // 调用double类型的Output函数
Output(fValue);    // 调用float类型的Output函数
Output(100.4);     // 调用错误,编译器无法决定到底调用哪个函数版本。

2. non-explicit构造函数接受一个参数的用户定义类对象之间的隐式转化
我们首先看下面这段代码。

// CTest测试类
class CTest
{
public:
    CTest (int n) { m_nNum = n; }  //普通构造函数
    virtual  ~ CTest () { }
private:
    int m_nNum;
};

void Func(CTest test); // 完成某一功能的一个函数

int main()
{
    Func(100);   // 将100作为参数传给Func,并执行函数。
}

在上述代码中,当调用Func()函数时会发现形式参数和实参的类型不匹配。但是编译器发现形参类CTest类有一个只有一个int类型参数的构造函数,所以编译器会以100为实参调用CTest的构造函数构造临时对象,然后将此临时对象传给Func()函数。也许你会惊讶:编译器为我们悄无声息的做了这么多工作,真是奇妙。这些工作发生的是那么悄无声息,要是一个误用了,会不会引起难以捉摸的错误啊?答案是肯定的。

控制隐式转化

其实这些问题是共存的,编译器给你提供了隐式转化的方便,所以如果出现了错误也是程序员需要负责的问题。权利和义务对等原则在这儿依然适用。为了体现这种对等原则。如程序员想避免隐式转化代码的麻烦,必须履行相应的义务—限制隐式转化。C++提供了两种有效途径控制隐式转化。

1. 根据需要自定义具名转换函数

class String
{
publicoperator const char*();   // 在需要情况下, String 对象可以转成 const char* 指针。
}// 上面的定义将使很多愚蠢的表达式通过编译 ( 编译器启用了隐式转换 ) .
// 假设s1和s2均是String类型字符串。
int x = s1 - s2;          // 可以编译,但行为不确定
const char* p = s1 - 5;   // 可以编译,但行为不确定
p = s1 + '0';             // 可以编译,但不是你期望的结果
if (s1 == "0" ){}         // 可以编译,但不是你期望的结果

为了避免此类问题的出现,建议使用自定义转换具名函数代替转换操作符。如下述代码就是一段较好的代码。

class String
{
public: 
    const char* as_char_pointer() const; // String 对象转成 const char* 指针。
};
// 假设s1和s2均是String类型字符串。
int x = s1 - s2;       	 //  编译错误
const char* p = s1 - 5;	 //  编译错误
p = s1 + '0';         	     //  编译错误
if (s1 == "0") {}	         //  编译错误

2. 使用explicit限制的构造函数
这种方法针对的是具有一个单参数构造函数的用户自定义类型。代码如下:

class Widget
{ 
public:
    Widget(unsigned int widgetizationFactor);
    Widget(const char* name, const Widget* other = 0)};

Widget widget1 = 100;     // 可编译通过
Widget widget1 = “my window”;// 可编译通过

上述代码中,unsigned int 类型和char*类型变量都可以隐式转化为Widget类型对象。为了控制这种隐式转化,C++引入explicit关键字,在构造函数声明时添加explict关键字可禁止此类隐式转化。在看添加了explict关键字的上述代码编译情况

class Widget
{ 
public:
    explicit  Widget(unsigned int widgetizationFactor);
    explicit  Widget(const char* name, const Widget* other = 0);
};

Widget widget1 = 100;        // 编译错误
Widget widget1 = “my window”;// 编译错误

最后,给大家讲述一个重要的隐式转化理念。如果有这么一个问题:隐式转化过程中,转型真是什么都没做吗,仅仅是告诉编译器把某种类型视为另外一种类型?我想大部分程序员会说是的,他们提供的原因是我在编程过程中隐式类型转换时,转型确实什么都没有作。如果你持有这样观念,那你需要注意了。这个观念是错误的。也许你不同意我的说法,但是你看完下面两个例子,你一定会惊呆的。

示例一如下:

Class CBaseA
{
Public:
    virtual void Func1() {}
};
Class CBaseB
{
Public:
    virtual void Func2() {}
};
Class CDrived:public CBaseA, public CBaseB 
{
Public:
    virtual void Func1() {}
    virtual void Func2() {}
};

CDrived d;
CDrived*pd = &d;
CBaseB* pb = &d;
printf(“d’s location is %d\r\n”, pd);
printf(“d’s location is %d\r\n”, pb);

现在问题是:代码中pd和pb两个指针的值是相等的吗?你可以再VS2010或者GCC上验证一下,我在VS2010上运行的结果是:

d's location is 2685200
d's location is 2685204

我们分析一下,上述示例为什么会由这样的运行结果:我们仅仅是建立一个基类指针指向一个子类对象,然后再建立一个子类指向此子类,最后两个指针的值就不同了。这种情况下两者会有一个偏移量在运行时会施加于子类指针上,用以取得正确的基类指针值。

不仅多重继承对象拥有一个以上的地址,即使在单一继承对象中也会发生这样的现象。所以请你注意了,在类型隐式转化过程中,你应该避免作出“对象在C++中如何分布布局”的假设。

示例二如下:

char cValue1 = 255;
printf("cValue1 = %d\r\n", cValue1);

现在这段代码的运行结果呢?编译器会输出“cValue1 = 255”吗?如果你把这段代码在VS2010编译器上运行一下,我想你会发现输出结果不是“cValue1 = 255”而是“cValue1 = -1”。我们分析一下为何会出现这样的问题:

  1. printf在执行时,输入参数的类型时int型,而cValue1的类型却是char型。所以函数printf在执行时cValue1会提升int型临时变量temp。
  2. temp在提升时遵循这样的规则,数据提升时进行符号位扩展,cValue1=255=1111,1111B符号位为1。所以提升为int类型时temp=1111,1111,1111,1111,1111,1111,1111,1111B。temp的原码是-1。所以上述代码输出-1。

注意:整型值类型提升规则:补码进行符号位扩展,得到的补码值即使提升后的变量值。

上面的两个例子,仅仅是隐式转化过程中最常见的两类隐式转化。编译器进行的隐式转化还有很多种。通过上述两个简单的例子,我们可以看出编译器在隐式转化时,并不是我们想象的什么都不做,仅仅是赋值而已。在隐式转化过程中会进行很多微妙的处理。这才是我们在使用隐式转化时,应注意的东西。

请谨记

  • 在使用编译器隐式类型转化时,请小心在小心。能减少隐式转化使用时尽量减少隐式转换的使用。
  • 除非你明确知道隐式转换时编译器发生什么时,在你编程时请不要对编译器隐式转换进行任何的假设。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值