符号&强制转换

  2020/09/05,最后一版~

一、符号 & 变量类型

1.1 b1<<1 和 b1<<=1

  可能有人一眼就看出差异,这是因为有得对比;这里还是提醒一下,实际编写代码时往往会拐不过弯、忽略这点细节。

  • b1<<1,等价于其结果左移1位,但是b1值没有发生改变
  • b1<<=1,等价于b1 = b1<<1;b1<<1表示b左移1位(二进制),b1值发生了变化

1.2 int 和 int32_t 、uint32_t

  首先先区分有符号和无符号。uint和int之间不仅有符号,还有补码。不是只差最高位的符号位。
至于int32_t、uint32_t,是typedef重定义来的。代码如下:

1
2
3
4
5
6
typedef unsigned char           uint8_t;
typedef signed char int8_t;
typedef unsigned short uint16_t;
typedef signed short int16_t;
typedef unsigned int uint32_t;
typedef signed int int32_t;

  在嵌入式开发的话,为了提高兼容性和阅读理解,推荐统一使用该重定义。而且该重定义是有C的标准库的,提高代码的兼容性;常见的C的标准库举例如下:

1
2
3
#include <stdint.h>         //标准库文件采用<>,自己编写的文件引用时采用""
#include <stdbool.h>
#incldue <stddef.h>

常见的给宏定义赋常量,会使用类似如下方法:

1
#define MINI_UNIT   (1ul)

  1ul说明这个常量1是unsigned long,用于明确计算值的范围。

1.3 少用printf

  有可能打印出来的类型都弄错了= =

  为什么呢?很多学生写完代码,直接用 printf 打印出来,发现结果不对。不会看变量的值,内存的值。只知道 printf 出来结果不对,却不知道为什么不对,怎么解决。这种情况还算好的。

  往往很多时候 printf 出来的结果是对的,然后呢,学生也理所当然的认为程序没有问题。 是这样吗?
  打印结果对,并不代表程序真正没有问题。所以,以后尽量不要用 printf 函数,要去看变量的值、内存的值。

二、类型转换的来源

  计算机硬件进行算术操作时,要求各操作数的类型具有相同的大小(存储位数)及存储方式。例如,由于各操作数大小不同,硬件不能将 char 型( 1 字节)数据与 int 型( 2 或 4 字节)数据直接参与运算;由于存储方式的不同,也不能将 int 型数据与 float 型数据直接参与运算。

  由于 C 语言编程的灵活性,在一个表达式或一条语句中,允许不同类型的数据混合运算。C 语言的灵活性与计算机硬件的机械性是一对矛盾,如处理不好,将会产生错误结果。

  • 对于某些类型的转换编译器可隐式地自动进行,不需人工干预,称这种转换为自动类型转换
  • 而有些类型转换需要编程者显式指定,通常,把这种类型转换称为强制类型转换

2.1 自动类型转换(隐式)

  不同数据类型之间的差别在于数据的表示范围及精度上,一般情况下,数据的表示范围越大、精度越高,其类型也越“高级”。

  • 常见类型级别从低到高依次为:
    char -> short -> int -> unsigned int -> long -> unsigned long -> double
  • 浮点型级别从低到高依次为:
    float -> double

不同类型间的混合运算,较低类型将自动向较高类型转换(精度提高不影响结果)。

2.2 强制类型转换

  为了给程序设计人员提供更多的类型转换控制权限,使程序设计更加灵活,转换的目的更加清晰,C 语言提供了可显式指定类型转换的语法支持,通常称之为强制类型转换。

  • 强制类型转换常见的用法:
    • (1)数据的 高类型一般不会强制转换成低类型,因为可能会丢失一部分数据;
    • (2)一般是低类型强制转换成高类型,防止数据溢出;
    • (3)提高函数指针的适用性
  • 原因如下:
    • (1)高类型转低类型,往往是函数传参,让函数处理更加明确范围;
    • (2)低类型转高类型,防止数据溢出;
    • (3)强制转换函数指针能够让回调方式更加多样性。

也许有人会问,那一开始都为高类型就不需要强制转换了吗?

  很简单,能低类型处理能够更好体现对应代码块功能性。当外部需要调用该变量,再强制转换成想要(高)数据类型,防止计算过程中数据溢出。这种灵活性,还能实现节省内存

三、类型转换例子

3.1 常见类型转换(算术运算式)

  • (int)(a + b)
  • (int)a + b

前者是(a+b)共同强制转换成整形常数,后者是a强制成整形 加上b的值。举例如下:

1
2
3
4
float a = 5.1, b = 2.2;

(int)(a + b) = 7
(int)a + b = 7.2 //低类型自动转成高类型计算

同理

1
2
int a = 2,b = 3;
(float)(a+b)/2 = 2.5

3.2 float、double类型的近似问题

  float double这类的数据是近似值,有精度问题。也就是说打印出制来的 8.0000 未必是 8.00000。
  8.00000实际上可能是是7.99999999999872812850 ,所以如果进行强制转换会是转为int的7。

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
int temp,i;
double a=2.4568;
unsigned char b[5];
for(i=0;i<5;i++)
{
temp=(int)a;
b[i]=temp+'0';
a=(a-temp)*10;
printf("%.20f %d\n",a,(int)a);
}
b[5]='\0';
puts(b);
}

结果如下:

1
2
3
4
5
6
4.56799999999999872813 4
5.67999999999998728129 5
6.79999999999987281285 6
7.99999999999872812850 7
9.99999999998728128503 9
24567

  一般来说 要把浮点转为int 要取得最近似的值,大都是采用(int)(a+0.5) 从而达到一种四舍五入的效果。

3.3 (隐式)强制转换

  • 算术运算式中,低类型能够转换为高类型(比较熟悉)
  • 赋值表达式中,表达式的值 (自动)转换为 左值(变量)的类型
  • 函数调用时,实参(自动)转化为形式参数的类型
  • 函数返回值,return 表达式 (自动)转化为返回值的类型

  举一个简单直白的例子如下:

1
2
3
4
5
float a = 5.1, b = 2.2;
int c = 0;

c = (int)a + b; //等效于下面式子,结果虽然为7.2,但最后赋值经过强制转换,c = 7
c = (int)((int)a + b); //与上述式子等效,c = 7

  相当于赋值操作总有一个整体的强制转换(隐藏)。再举一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 2,b = 3;
float c = 0;

c = (float)(a+b)/2; //c = 2.500000
c = (float)((float)(a+b)/2); //如同上式

c = (a+b)/(float)2; //在整个式子计算中,结果计算中较低类型自动转换较高类型
c = (float)((a+b)/(float)2); //因此两式子结果均为 c = 2.5000000

c = (a+b)/2; //毫无意外,c = 2.000000;等效下面式子
c = (float)((a+b)/2); //经典错误,很多人容易把强制转换放错位置
//最后一条式子是经典错误,尤其是式子比较复杂时,容易理解错误并放错位置
//导致最终结果并不是想要的值

  再举一个因(隐藏)强制转换导致的常见例子如下;右值超出左值类型范围,将把该右值截断后,赋给左值。所得结果可能毫无意义。

1
2
3
char c;     //char 占8位,表示范围-127〜128
c = 1025; //1025 对应二进制形式:100 0000 0001,超出了8位
printf("%d",c) ; //以十进制输出c的值

  该输出结果为 1,因为只取 1025 低 8 位 0000 0001(值为1),赋给字符型变量 c,故得到毫无意义的值。

四、double 转 uint32_t 等于?

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdint.h>

int main()
{

double i = -3;
uint32_t j = 0;
int32_t k = 0;
uint8_t z = 0;
int8_t y = 0;

j = (uint32_t)i;
k = (int32_t)i;
z = (uint8_t)i;
y = (int8_t)i;

printf("%f\n",i);
printf("%d\n",j);
printf("%u\n",j);
printf("%d\n",k);
printf("%d\n",z);
printf("%u\n",z);
printf("%d\n",y);

return 0;
}

那么,以上例子的 j 值究竟是多少?答案是 4294967293。全部结果如下:

1
2
3
4
5
6
7
-3.000000
-3
4294967293
-3
253
253
-3

  如果你以为上面的答案就是结束了?那你就大错特错了;上面的答案可能不够准确,在x86平台上是-3的补码,在ARM平台上是0。


这里要如何解释该现象呢?首先明确一些概念:

  • 有符号类型和无符号类型混合运算时,所有的操作数都自动转换为无符号类型(在编译器中);从这个意义上讲,无符号数的运算优先级要高于有符号数
  • 由于负数在底层存储是补码形式,从而方便机器放入运算器进行计算(基础知识)
  • 因此导致了,有符号数与无符号数不能直接一起混合运算

  举个明显的例子:

1
2
3
4
5
6
7
8
9
uint32_t a = 20;
int32_t b = -130;

if(a > b)
printf("%o\n", a);
else
printf("%d\n", b);

//最终打印出来的是b的值,因为编译器把 b 转化为无符号数处理,因此 b > a

我们在学校学到的知识是基于x86平台的,即:

  • 整型负数的类型转换,是在其补码上做高位的去除和填补
  • 浮点型负数的类型转换,是取其整数部分的补码做高位去除和填补。
  • 当负数是整数时,无符号整数只是换了一种表达方式来解释它的补码,所以 -1 是 4294967295。
  • 当负数是浮点数时,浮点数的存储是尾数+指数的方式
  • 如果直接将浮点数的二进制数据(补码)转为无符号整数,这就依赖平台的实现了;具体的ARM会转成0,而x86会转成整数部分。

  举一个stm32在keil平台的测试情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
double i = -12.456;
uint32_t j = 0;
int32_t k = 0;
uint16_t p = 0;
int16_t q = 0;
uint8_t y = 0;
int8_t z = 0;

j = (uint32_t)i;
k = (int32_t)i;
p = (uint16_t)i;
q = (int16_t)i;
y = (uint8_t)i;
z = (int8_t)i;

最后的结果如下:

1
2
3
4
5
6
7
i = -12.456;
j = 0x00000000;
k = 0xfffffff4;
p = 0x0000;
q = 0xfff4;
y = 0x00;
z = 0xf4;

  这里总结一下强制转换 & 类型的要点:

  • 有符号数 & 无符号数 不要混合运算;运算前明确转换成同种类型,不然可能因为有符号数自动转为无符号数导致获取数值异常问题
  • 强制转换只能 截取或提高 精度(二进制机器码存储长度),并不能改变数据的正负;例如 -1强制转换为正数,会变成一个很大的数(补码);举例如下:
    • (uint32_t)-3,0xfffffffd;即4294967293
    • (uint16_t)-3, 0xfffd;即65533
  • 因此,无符号整形 & 浮点型 不能直接强制转换;因为不但涉及到精度,还涉及到正负问题。且浮点型的补码和整数补码方式不一样,不同硬件平台可能出现不同结果

五、强制转换 实现 地址(指针)跳转

1
#define jump(TargetAddr)   (*((void   TargetAddr)))()

  第一个(( void( * )( )) ,意思为强制类型转换为一个无形参,无返回值的函数指针,(*(TargetAddr))为跳转地址,但是函数指针变量不能为常数所以要加((void( * )( )) 进行强制类型转换。最后一个()为执行的意思。

整个宏定义目的是为了跳转到一个绝对地址执行函数。用处如下:

  1. 在单片机中可以实现软件复位,比如跳转到0地址。
  2. 如果程序是由多个程序合并的(bootloader跳转),跳转到某一个确定的用户程序地址执行。
  3. 如果flash空间足够大的话,甚至还可以实现当多份不相同的代码合并为一份后,在软件上做逻辑跳转,好处是新程序不必为旧程序做大量的兼容工作,通常旧程序含有大量的前人的(坏)编程习惯。可以选择执行想要的版本软件程序。
-------------本文结束感谢您的阅读-------------