C语言中的类型

类型表示范围速查

注意开发平台的类型精确定义

不同的平台(操作系统、编译器)上对类型的定义不尽相同,要注意其中的精确定义,以免踩坑。

【例】一个常见的例子就是:32位机器上的int类型占4个字节,而64位CPU上的int类型占8个字节。

【例】另一个常见的例子就是:32位机器上的指针类型占4个字节,而64位CPU上的指针类型占8个字节。

【例】还有一个比较极端的例子,曾经在嵌入式平台上使用linux-arm-gcc进行开发时,发现该编译器对char默认的定义是unsigned的。 好可怕,给char类型变量赋值一个负数,会得到错误的结果,而且编译器不会报错或者警告 幸好恰巧使用了“==”进行判别,编译器提出了警告,才得以提前发现这个问题。

char a = -1;
printf("%d\n",a ); //x86的gcc打出-1,而linux-arm-gcc打印出255。
if(a == -10)
printf("error") // 编译警告,不同类型之间相互比较

【解法】有一种方法可以从源头上避免踩坑,那就是使用stdint.h头文件中的精确类型定义。

stdint.h包含了“int8_”,“uint8_”,“int16_t”,“uin16_t”,“in32_t”,“uin32_t”,“int64_t”,“uint64_t”这几种类型,使用这种类型就完全没有歧义的,推荐使用。

浮点转定点中的四舍五入

浮点数转定点数不能直接截取,必须四舍五入,否则精度会丢失,有时候会丢失很多!

【例】例如将浮点数转换成16Q8的定点数,当输入0.035时,0.035*256.0=8.96,直接截取就变成了8,然而8对应着0.03125,9对应着0.03515625,很明显应该是9更合适。 我们自然而然地想到+0.5,例如第二行代码所示,但是这样仍然是有问题的,就是在使用负数的时候。 例如输入-0.6,那么照理说-0.6更接近-1,然而-0.6+0.5=-0.1会截取成0,没有错(int)(-0.1)=0,在VS和gcc上是这样的。

#include <stdio.h>
int main(){
printf("%d,%d", (int)0.1, (int)(-0.1));
}

输出结果:

0,0

【解法】一种解法是做判断,感觉不是很优雅,每次做个计算都得if一下。

#define Q168_FROM_FLOAT(x) ( (int16_t)((x)*256.0) ) // 丢失精度
#define Q168_FROM_FLOAT(x) ( (int16_t)((x)*256.0+0.5) ) // 负数不准
#define Q168_FROM_FLOAT(x) ( (int16_t)((x)>=0?(x)*256.0+0.5:(x)*256.0-0.5) ) //完美
// 测试
printf("%d,%d\n", Q168_FROM_FLOAT(0.35), Q168_FROM_FLOAT(-0.35))
//结果为 9和-9,正确!

另一个float不够用的例子

以纳秒为单位UTC时间数值上特别大,举个例子(946,685,275,874,349,312,这一连串是一个数字),大概是30年左右。

intptr_t类型

很多32位平台的程序喜欢使将指针赋值给整数,这样就不需要二级指针,例如

int func(uint32_t *addr)
{
void * data = malloc (1000);
*addr = (uint32_t) data;
}

如果需要在函数里面开内存,输出内存地址,那么要么使用二级指针,要么用这种转地址的方式。 但是,这种方式在跨平台时存在风险,在64位系统中,指针的大小可能是64位的,这样赋值就会出错。 仍然想用这种方式的话,使用intptr_t类型,兼容32和64位,在C99中支持,在<stdint.h>中定义。

枚举(enumeration)

使用enum代替整数的好处

防止数组访问溢出 假设在函数func里操作一个size为N的数组,函数传入需要操作的index,那么在没有前置判断溢出的情况下,需要加入判断是否溢出的语句。

int a[10]
int func(int x)
{
if(x>=10)
{
return 1;
}
printf("%d",a[x]);
}

如果数组的size不大,而且具有明确的含义,例如某种过程处理通道的个数,可以考虑使用枚举来代替。 使用枚举的好处是,枚举变量一定不会超出定义域的范围,在编译阶段就决定了不会产生数组的越界。

字面常量整型表达式溢出

即使变量长度没有问题,常量表达式也会溢出,如下代码所示。 想在想要使用纳秒来表达8个小时的时长,第一种计算方法是错误的,“8”,“3600“以及”1000000000“都是int32_t, 它们每个都在int32_t的表达范围内,但是乘起来就会超出。因为三个常量都是int32_t类型,所以编译器最终选择输出int32_t类型,最终常量溢出。 哪怕前面用了uin64_t的变量来接受赋值,但是右边的计算已经溢出了。

第二种方法是正确的,随便在其中一个数字后面加上LL,那么编译器最终会选择LL类型输出,也就是uint64_t,这样计算就不会溢出。

#include <iostream>
#include <stdint.h>
using namespace std;
int main(){
uint64_t x = 8 * 3600 * 1000000000;
cout<<"x="<<x<<endl;
uint64_t y = 8 * 3600 * 1000000000LL;
cout<<"y="<<y<<endl;
}
x=18446744071658864640
y=28800000000000

一个uint64_t无法满足需求的实际例子。

使用纳秒来表示从公元0年到公元2000年的时间。

需要表达的时间大致为:2000*365*24*3600*1e9 = 63,072,000,000,000,000,000

而uint64能表达的最大数值为:numeric_limits<uint64_t>::max() = 18,446,744,073,709,551,615

显然,uint64_t无法表达这个数。

所以在使用ns来表示时间的时候,一定要注意溢出,连uint64都不够用。