C/C++数据类型转换

前言

在最近的编程实践中,遇到了一个的 bug,定位问题时才发现问题根源在于忽略了 C/C++ 中的自动数据类型转换。由于编译时没有开启严格的编译检查,使得这个潜在的问题顺利通过了编译阶段,却在程序运行时引发了异常行为,给调试工作带来了不小的困扰。在此,我决定将这次对 C/C++ 数据类型转换问题的研究过程记录下来,以便日后回顾,同时也希望能给其他开发者提供一些参考和警示。

先看一个案例:

#include <vector>
#include <iostream>

int main() {
    // 定义一个buffer,里面存放tcp流式数据
    std::vector<uint8_t> buffer = {0x01, 0x02, 0x03};
    // 定义一个check函数,模拟以下功能:
    // check函数里面根据通信协议去检查buffer是否符合协议,
    // 如果不符合协议,需要丢弃多少字节(返回负数),如果符合协议,需要拿出多少字节(返回正数)
    // 此处模拟一种情况,返回-1,即需要从buffer中丢弃1个字节
    auto check = [](std::vector<uint8_t>&)->int{ return -1; };

    int len = check(buffer);
    if (len < 0) {
        // 省略丢数据逻辑
        std::cout << "discard " << -len << " bytes" << std::endl;
    }

    if (len > buffer.size()) {
        // 需要的数据长度大于buffer长度
        std::cout << "the buffer does not have enough data" << std::endl;
    }

	// ...
}

看一下打印:

$ g++ test.cc 
$ ./a.out 
discard 1 bytes
the buffer does not have enough data

根据打印可以看到 **(len > buffer.size())**竟然是true,这里len是 -1,而buffer.size()是3,那么为什么 -1>3 会是true呢,我们在日常写程序时如何避免发生这种情况呢,这就要说到C/C++中的类型转换。

C/C++中的类型转换主要分为:隐式类型转换 & 显示类型转换(即强制类型转换)。

隐式类型转换

在C Primer Plus中对隐式类型转换介绍了一下几大规则:

  1. 在表达式中,char/unsigned char/signed char/unsigned short/signed short都会被系统自动转成int,但是需要注意的是:如果short比int短,则unsigned short或者short类型将被转换为int;如果两种类型的长度相同,则unsigned short类型将被转换为unsigned int而不是int,这种规则确保了在对unsigned short进行提升时不会损失数据精度。
  2. 包含两种数据类型的任意运算中,两个值会被分别转成两种类型中的高级别的数据类型,类型的级别从低到高:long double > double > float > unsigned long long > long long > unsigned long > long > unsigned int > int,有一个特殊情况,当long和int的大小相同时,unsigned int比long级别高,还有就是short和char类型没有出现是因为它们已经被OS转成了int或unsigned int ,(如:混合运算转换过程 3+4/5.0F+6-9.0,先计算4/5.0F,4转成float参与运算得到0.8F,3+0.8F,3转成float参与运算得到3.8F,3.8F+6得到9.8F,9.8F-9.0因为浮点数默认是double型,9.8F被转成double型9.8参与运算得到double型0.8,可以看出混合运算转换过程是一步一步进行的)
  3. 在赋值语句中,会被转换成赋值运算符左侧的类型,可能升级,也可能降级,不考虑四舍五入
  4. 作为函数参数时,char和short会被自动转换成int,float会被转成double

总结一下上面的规则:
在包含两种数据类型的运算中

  • 如果有一个操作数的类型是long double,则将另一个操作数转换为long double
  • 否则,如果有一个操作数的类型是double,则将另一个操作数转换为double
  • 否则,如果有一个操作数的类型是float,则将另一个操作数转换为float
  • 否则,操作数都是整型,因此执行整型提升:在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
    如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型
  • 否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型
  • 否则,将两个操作数都转换为有符号类型的无符号版本

光说无用,上代码示例:
long double和double以及float这几个转换基本上不会出问题,这里就不举例了。
重点关注整形提升

test1
int main() {
    short a = 1;
    short b = 2;
    short c = a + b;
}

C++程序取得a和b的值,并将它们转换为int。然后程序将结果转换为short类型赋给c。通常将计算机最自然的类型选择为int类型,这意味着计算机使用这种类型时,运算速度可能最快。

test2
int main() {
    unsigned int a = 9;
    int b = -10;
    std::cout << (a + b) << std::endl;
}

输出:

$ g++ test2.cc 
$ ./a.out 
4294967295

输出结果 a + b = 9 + (-10) 竟然等于4294967295,这明显不符合预期
分析一下计算过程:
a和b数据类型不同,根据整数提升规则(如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型),此处将int转为unsigned int,即 b 转换为了正数变成 4294967296 - 10 = 4294967286 (这个转换可以根据反码补码自己算),a + b = 9 + 4294967286 = 4294967295,结果一致。

test3
int main() {
    unsigned short a = 9;
    int b = -10;
    std::cout << (a + b) << std::endl;
}

输出:

$ g++ test3.cc 
$ ./a.out 
-1

相比于test2,把a的类型从unsigned int改成unsigned short,为什么结果就变成了-1,unsigned int和unsigned short都是无符号类型,为什么结果却不一样,分析一下:
根据整数提升规则(如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型),实际上此处是将 a 转换成了 int,而不是向test2一样,将 b 转换为 unsigned short,这是因为 int 的级别要比 unsigned short 高,所以需要注意的是,无符号和有符号运算时,并不是一定将有符号转为无符号。a 转换为 int 后还是 9,再与 b 相加时,b无需转换,因此 a + b = 9 + (-10) = -1。

test4
int main() {
    unsigned int a = 9;
    long b = -10;
    std::cout << (a + b) << std::endl;
}

这个例子,猜一下输出什么?
-1 还是 4294967295
答案是都有可能

  • 在32位系统中,long 和 int 都是 4 Byte,根据前面提高的规则 当long和int的大小相同时,unsigned int比long级别高,因此这里将 b 转为为 unsigned int,a + b = 9 + 4294967286 = 4294967295。
  • 在64位系统中,有的编译器 long 是 4 Byte,有的编译器 long 是 8 Byte (64位系统中,long不一定是8字节,这个自己查资料),当 long 为 8 Byte时,long 的级别比 unsigned int 高,因此将 a 转换为 long,a + b = 9 + (-10) = -1。
test5

再看一个容易出错的例子:

int main() {
    unsigned int a = 10;
    unsigned int b = 20;
    if (a - b < 0) {
        std::cout << "a - b < 0 is true" << std::endl;
        std::cout << "a - b = " << (a - b) << std::endl;
    } else {
        std::cout << "a - b < 0 is false" << std::endl;
        std::cout << "a - b = " << (a - b) << std::endl;
    }
}

猜一下,输出什么

$ g++ test5.cc 
$ ./a.out 
a - b < 0 is false
a - b = 4294967286

看到这个结果,好像觉得不意外,但是又觉得哪里有点不对,难道两个无符号数相减,一定被转换成了一个无符号数?
研究一下C/C++里面的减法运算规则(这里只研究整数之间的运算规则):

  • 对于有符号整数(如 int, long),减法遵循普通的算术规则,但要考虑溢出问题。当减法结果超出范围时,会发生溢出,导致未定义行为。
  • 对于无符号整数(如 unsigned int, unsigned long),减法是在模运算的意义下进行的。

对于无符号整数(如 unsigned int, unsigned long),减法是在模运算的意义下进行的,这是什么意思?还是上面的例子,这里 a - b,其实被转换成了 a + (-b) = a + (2^32 - 20) = 2 ^32 - 10 = 4294967286,减法被转成了加法。

那么无符号数和有符号数之间相减呢?这其实又回到了整数提升问题,会先将有符号数转为无符号数(符合低级别向高级别转换规则时),转换完后,又回到了刚才两个无符号数相减的问题。

显示类型转换(强制类型转换)

强制类型转换一般不会出现问题,此处列举一个容易出错的例子:

强转指针类型/强转地址类型,进行指针/地址类型的转换,需要注意两点,一是指针的类型,决定了指针的读写方式(也就是一次可以操作多少字节的数据),另一点是一定不要越界访问

int a = 12;
double *p = (double *)&a;
*p = 12.3;

这样的操作是非法的,int a; 只有 4Byte,double *p 一次可以操作 8Byte,明显是越界操作了

int a = 12;
float *p = (float *)&a;
*p = 23.2;

这样的操作是合法的(int和float都占 4Byte),但因为float和int在内存中的存储方式不同,所以输出的数据可能与想象中的结果不同,但这是合法的操作

double d = 12.3;
int *p = (int *)&d;
*p = 34;                       // 操作前 4Byte
*(p + 1) = 45;                 // 操作后 4Byte
*(int *)((short*)p + 1) = 56;  // 操作中间 4Byte

可以看出指针的操作是很灵活的,但是一定注意不要越界进行读写操作

C++类型转换

四大运算符:const_cast 、reinterpret_cast 、dynamic_cast 、static_cast
C++11中引入了更安全的类型转换方式,之后再介绍

总结

回过头再看最开始给的示例,出现 -1>3 的原因也明了了:
变量 len 是 int 类型,而 buffer.size() 是 std::size_t 类型,size_t 是什么类型,在64位系统里面其实是 unsigned long int 类型,也就是说,这两个数比较时,会将 len 转成 unsigned long int 类型,len是负数时,会被转成一个很大的整数,因此会大于buffer.size(),这就出现了bug,那么如何避免写出这样的bug呢,尽量使用强制类型转换,不要使用自动类型转换给自己挖坑,如 if (len > (int)buffer.size()) 就不会再出现问题(真的没有任何问题吗?如果buffer.size()超过了 int 的范围呢,钻牛角尖了,真有这种情况,需要自己在强制类型转换时,注意转换精度丢失的问题)

补充

C/C++字面量类型:
整形字面量默认 int
浮点型字面量默认 double

int main() {
    auto a = 10;
    auto b = 10.0;
    std::cout << sizeof(a) << std::endl;
    std::cout << sizeof(b) << std::endl;
}

输出:

$ g++ test6.cc 
$ ./a.out 
4
8
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值