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 | typedef unsigned char uint8_t; |
在嵌入式开发的话,为了提高兼容性和阅读理解,推荐统一使用该重定义。而且该重定义是有C的标准库的,提高代码的兼容性;常见的C的标准库举例如下:
1 |
常见的给宏定义赋常量,会使用类似如下方法:
1 |
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 | float a = 5.1, b = 2.2; |
同理
1 | int a = 2,b = 3; |
3.2 float、double类型的近似问题
float double这类的数据是近似值,有精度问题。也就是说打印出制来的 8.0000 未必是 8.00000。
8.00000实际上可能是是7.99999999999872812850 ,所以如果进行强制转换会是转为int的7。
举例如下:
1 |
|
结果如下:
1 | 4.56799999999999872813 4 |
一般来说 要把浮点转为int 要取得最近似的值,大都是采用(int)(a+0.5) 从而达到一种四舍五入的效果。
3.3 (隐式)强制转换
- 算术运算式中,低类型能够转换为高类型(比较熟悉)
- 赋值表达式中,表达式的值 (自动)转换为 左值(变量)的类型
- 函数调用时,实参(自动)转化为形式参数的类型
- 函数返回值,
return 表达式
(自动)转化为返回值的类型
举一个简单直白的例子如下:
1 | float a = 5.1, b = 2.2; |
相当于赋值操作总有一个整体的强制转换(隐藏)。再举一个例子如下:
1 | int a = 2,b = 3; |
再举一个因(隐藏)强制转换导致的常见例子如下;右值超出左值类型范围,将把该右值截断后,赋给左值。所得结果可能毫无意义。
1 | char c; //char 占8位,表示范围-127〜128 |
该输出结果为 1,因为只取 1025 低 8 位 0000 0001(值为1),赋给字符型变量 c,故得到毫无意义的值。
四、double 转 uint32_t 等于?
举例如下:
1 |
|
那么,以上例子的 j 值究竟是多少?答案是 4294967293。全部结果如下:
1 | -3.000000 |
如果你以为上面的答案就是结束了?那你就大错特错了;上面的答案可能不够准确,在x86平台上是-3的补码,在ARM平台上是0。
这里要如何解释该现象呢?首先明确一些概念:
- 有符号类型和无符号类型混合运算时,所有的操作数都自动转换为无符号类型(在编译器中);从这个意义上讲,无符号数的运算优先级要高于有符号数
- 由于负数在底层存储是补码形式,从而方便机器放入运算器进行计算(基础知识)
- 因此导致了,有符号数与无符号数不能直接一起混合运算
举个明显的例子:
1 | uint32_t a = 20; |
我们在学校学到的知识是基于x86平台的,即:
- 整型负数的类型转换,是在其补码上做高位的去除和填补
- 浮点型负数的类型转换,是取其整数部分的补码做高位去除和填补。
- 当负数是整数时,无符号整数只是换了一种表达方式来解释它的补码,所以 -1 是 4294967295。
- 当负数是浮点数时,浮点数的存储是尾数+指数的方式
- 如果直接将浮点数的二进制数据(补码)转为无符号整数,这就依赖平台的实现了;具体的ARM会转成0,而x86会转成整数部分。
举一个stm32在keil平台的测试情况:
1 | double i = -12.456; |
最后的结果如下:
1 | i = -12.456; |
这里总结一下强制转换 & 类型的要点:
- 有符号数 & 无符号数 不要混合运算;运算前明确转换成同种类型,不然可能因为有符号数自动转为无符号数导致获取数值异常问题
- 强制转换只能 截取或提高 精度(二进制机器码存储长度),并不能改变数据的正负;例如
-1
强制转换为正数,会变成一个很大的数(补码);举例如下:- (uint32_t)-3,0xfffffffd;即4294967293
- (uint16_t)-3, 0xfffd;即65533
- 因此,无符号整形 & 浮点型 不能直接强制转换;因为不但涉及到精度,还涉及到正负问题。且浮点型的补码和整数补码方式不一样,不同硬件平台可能出现不同结果
五、强制转换 实现 地址(指针)跳转
1 |
第一个(( void( * )( ))
,意思为强制类型转换为一个无形参,无返回值的函数指针,(*(TargetAddr))
为跳转地址,但是函数指针变量不能为常数所以要加((void( * )( ))
进行强制类型转换。最后一个()
为执行的意思。
整个宏定义目的是为了跳转到一个绝对地址执行函数。用处如下:
- 在单片机中可以实现软件复位,比如跳转到0地址。
- 如果程序是由多个程序合并的(bootloader跳转),跳转到某一个确定的用户程序地址执行。
- 如果flash空间足够大的话,甚至还可以实现当多份不相同的代码合并为一份后,在软件上做逻辑跳转,好处是新程序不必为旧程序做大量的兼容工作,通常旧程序含有大量的前人的(坏)编程习惯。可以选择执行想要的版本软件程序。