数据类型
C 语言的每一种数据,都是有类型(type
)的,编译器必须知道数据的类型,才能操作数据。所谓 “类型”,就是相似的数据所拥有的共同特征,那么一旦知道某个值的数据类型,就能知道该值的特征和操作方式。
变量和常量数据
在程序的指导下,计算机可以做许多事情,如:数值计算、名字排序、执行语言等。要完成这些任务,程序需要使用数据,即承载信息的数字和字符。
有些数据类型在程序使用之前已经预先设定好了,在整个程序的运行过程中没有变化,这些称为 常量(constant
)。其他数据类型在程序运行期间可能会改变或被赋值,这些称为 变量(variable
)。
位、字节和字
位、字节和字是描述计算机数据单元或存储单元的术语。这里主要指存储单元。
最小的存储单元是位(bit
),可以存储 0
或 1
(或者说,位用于设置 “开” 或 “关”)。虽然 1 位存储的信息有限,但是计算机中位的数量十分庞大。位是计算机内存的基本构建块。
字节(byte
)是常用的计算机存储单位。对于几乎所有的机器,1
字节均为 8
位。这是字节的标准定义,至少在衡量存储单位时是这样。既然 1
位可以表示 0
或 1
,那么 8
位字节就有 256
(2
的 8
次方)种可能的 0
、1
的组合。通过二进制编码(仅用 0
和 1
便可表示数字),便可表示 0 ~ 255
的整数或一组字符。
字(word
)是设计计算机时给定的自然存储单位。对于 8 位的微型计算机(如:最初的苹果机),1
个字长只有 8 位。从那以后,个人计算机字长增至 16 位、32 位,直到目前的 64 位。计算机的字长越大,其数据转移越快,允许的内存访问也更多。
数据类型
C 通过识别一些基本的 数据类型 来区分和使用这些不同的数据类型。K&R C
给出了 7
个与类型相关的关键字。C90
标准添加了 2
个关键字,C99
标准又添加了 3
个关键字。
类型关键字 | 类型关键字 | 存储大小 |
---|---|---|
int | signed | _Bool |
long | void | _Complex |
short | _Imaginary | |
unsigned | ||
char | ||
float | ||
double |
int
关键字来表示基本的整数类型。后 3
个关键字(long
、short
和 unsigned
)和 C90
新增的 signed
用于提供基本整数类型的变式,例如:unsigned short int
。char
关键字用于指定字母和其他字符(如:#
、$
、%
和 *
)。另外,char
类型也可以表示较小的整数。float
、double
和 long double
表示带小数点的数。_Bool
类型表示布尔值(true
或 false
),_Complex
和 _Imaginary
分别表示复数和虚数。
通过这些关键字创建的类型,按计算机的存储方式可分为两大基本类型:整数类型 和 浮点数类型。
类型关键字 | 类型关键字 | 存储大小 | 值范围 |
---|---|---|---|
字符型 | char | 1 字节 | -2(8-1) ~ 2(8-1) - 1 即 -128 ~ 127 |
有符号字符型 | signed char | 1 字节 | -2(8-1) ~ 2(8-1) - 1 即 -128 ~ 127 |
无符号字符型 | unsigned char | 1 字节 | 0 ~ 2(8) - 1 即 0 ~ 255 |
整型 | int | 4 字节 | -2(32-1) ~ 2(32-1) - 1 即 -2,147,483,648 ~ 2,147,483,647 |
有符号整型 | signed int | 4 字节 | -2(32-1) ~ 2(32-1) - 1 即 -2,147,483,648 ~ 2,147,483,647 |
无符号整型 | unsigned int | 4 字节 | 0 ~ 2(32) - 1 即 0 ~ 4,294,967,295 |
短整型 | short int | 2 字节 | -2(16-1) ~ 2(16-1) - 1 即 -32,768 ~ 32,767 |
有符号短整型 | signed short int | 2 字节 | -2(16-1) ~ 2(16-1) - 1 即 -32,768 ~ 32,767 |
无符号短整型 | unsigned short int | 2 字节 | 0 ~ 2(16) - 1 即 0 ~ 65,535 |
长整型 | long int | 4 字节 | -2(32-1) ~ 2(32-1) - 1 即 -2,147,483,648 ~ 2,147,483,647 |
有符号长整型 | signed long int | 4 字节 | -2(32-1) ~ 2(32-1) - 1 即 -2,147,483,648 ~ 2,147,483,647 |
无符号长整型 | unsigned long int | 4 字节 | 0 ~ 2(32) - 1 即 0 ~ 4,294,967,295 |
长长整型 | long long int | 8 字节 | -2(64-1) ~ 2(64-1) - 1 即 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
有符号长长整型 | signed long long int | 8 字节 | -2(64-1) ~ 2(64-1) - 1 即 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
无符号长长整型 | unsigned long long int | 8 字节 | 0 ~ 2(64) - 1 即 0 ~ 18,446,744,073,709,551,615 |
单精度浮点数 | float | 4 字节 | |
双精度浮点数 | double | 8 字节 | |
长双精度浮点数 | long double | 16 字节 |
字符类型
字符类型指的是单个字符,类型声明使用 char
关键字。例如:
char c = 'B';
在 C 语言中,用单引号括起来的单个字符被称为 字符常量(character constant
)。
在计算机内部,字符类型使用一个字节(8 位)存储。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如:B
对应整数 66
。
字符类型在不同计算机的默认范围是不一样的。一些系统默认为 -128 ~ 127
,另一些系统默认为 0 ~ 255
。这两种范围正好都能覆盖 0 ~ 127
的 ASCII 字符范围。
只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。例如:
char c = 66;
// 等同于
char c = 'B';
两个字符类型的变量可以进行数学运算,字符类型变量 a
和 b
相加,视同两个整数相加。例如:
char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;
printf("%d\n", a + b); // 输出 133
单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作 转义序列(escape sequence
)。例如:
char t = '\'';
这种转义的写法,主要用来表示 ASCII 码定义的一些无法打印的控制字符,它们也属于字符类型的值。
常见的转义序列:
- \a:警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生
- \b:退格键,光标回退一个字符,但不删除字符
- \f:换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于
\v
- \n:换行符
- \r:回车符,光标移到同一行的开头
- \t:制表符,光标移到下一个水平制表位,通常是下一个 8 的倍数
- \v:垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列
- \0:null 字符,代表没有内容。注意,这个值不等于数字 0
转义写法还能使用八进制和十六进制表示一个字符。
- \nn:字符的八进制写法,nn 为八进制值
- \xnn:字符的十六进制写法,nn 为十六进制值
以下四种写法都是等价的:
char x = 'B';
char x = 66;
char x = '\102'; // 八进制
char x = '\x42'; // 十六进制
整数类型
简介
整数类型用来表示较大的整数,类型声明使用 int
关键字。例如:
int a;
不同计算机的 int
类型的大小是不一样的。比较常见的是使用 4
个字节(32 位)存储一个 int
类型的值,但是 2
个字节(16 位)或 8
个字节(64 位)也有可能使用。它们可以表示的整数范围如下:
- 16位:-32,768 ~ 32,767
- 32位:-2,147,483,648 ~ 2,147,483,647
- 64位:-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
signed 和 unsigned
C 语言使用 signed
关键字,表示一个类型带有正负号,包含负值;使用 unsigned
关键字,表示该类型不带有正负号,只能表示零和正整数。
对于 int
类型,默认是带有正负号的,也就是说 int
等同于 signed int
。由于这是默认情况,关键字 signed
一般都省略不写,但是写了也不算错。例如:
signed int a;
// 等同于
int a;
int
类型也可以不带正负号,只表示非负整数。这时就必须使用关键字 unsigned
声明变量。例如:
unsigned int a;
整数变量声明为 unsigned
的好处是,同样长度的内存能够表示的最大整数值,增大了一倍。比如:16 位的 signed int
最大值为 32,767
,而 unsigned int
的最大值增大到了 65,535
。
unsigned int
里面的 int
可以省略,所以上面的变量声明也可以写成下面这样:
unsigned a;
字符类型 char
也可以设置 signed
和 unsigned
。例如:
signed char c; // 范围是 -128 ~ 127
unsigned char c; // 范围是 0 ~ 255
注意
C 语言规定 char 类型默认是否带有正负号,由当前系统决定。这就是说,char
不等同于 signed char
,它有可能是 signed char
,也有可能是 unsigned char
。这一点与 int
不同,int
就是等同于 signed int
整数的子类型
如果 int
类型使用 4
个或 8
个字节表示一个整数,对于小整数,这样做很浪费空间。另一方面,某些场合需要更大的整数,8
个字节还不够。为了解决这些问题,C 语言在 int
类型之外,又提供了三个整数的子类型。这样有利于更精细地限定整数变量的范围,也有利于更好地表达代码的意图。
- short int(简写为 short):占用空间不多于
int
,一般占用2
个字节(整数范围为 -32768 ~ 32767) - long int(简写为 long):占用空间不少于
int
,至少为4
个字节 - long long int(简写为 long long):占用空间多于
long
,至少为8
个字节
short int a;
long int b;
long long int c;
默认情况下,short
、long
、long long
都是带符号的(signed
),即 signed
关键字省略了。它们也可以声明为不带符号(unsigned
),使得能够表示的最大值扩大一倍。例如:
unsigned short int a;
unsigned long int b;
unsigned long long int c;
C 语言允许省略 int
,所以变量声明语句也可以写成下面这样:
short a;
unsigned short a;
long b;
unsigned long b;
long long c;
unsigned long long c;
不同的计算机,数据类型的字节长度是不一样的。确实需要 32 位整数时,应使用 long
类型而不是 int
类型,可以确保不少于 4
个字节;确实需要 64 位的整数时,应该使用 long long
类型,可以确保不少于 8
个字节。另一方面,为了节省空间,只需要 16 位整数时,应使用 short
类型;需要 8 位整数时,应该使用 char
类型。
整数类型的极限值
有时候需要查看,当前系统不同整数类型的最大值和最小值,C 语言的头文件 limits.h
提供了相应的常量,比如:SCHAR_MIN
代表 signed char
类型的最小值 -128,SCHAR_MAX
代表 signed char
类型的最大值 127。
为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。
- SCHAR_MIN,SCHAR_MAX:signed char 的最小值和最大值
- SHRT_MIN,SHRT_MAX:short 的最小值和最大值
- INT_MIN,INT_MAX:int 的最小值和最大值
- LONG_MIN,LONG_MAX:long 的最小值和最大值
- LLONG_MIN,LLONG_MAX:long long 的最小值和最大值
- UCHAR_MAX:unsigned char 的最大值
- USHRT_MAX:unsigned short 的最大值
- UINT_MAX:unsigned int 的最大值
- ULONG_MAX:unsigned long 的最大值
- ULLONG_MAX:unsigned long long 的最大值
整数的进制
C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。
八进制使用 0
作为前缀,比如:017
、0377
。例如:
int a = 012; // 八进制,相当于十进制的 10
十六进制使用 0x
或 0X
作为前缀,比如:0xf
、0X10
。例如:
int a = 0x1A2B; // 十六进制,相当于十进制的 6699
有些编译器使用 0b
前缀,表示二进制数,但不是标准。例如:
int x = 0b101010;
注意
不同的进制只是整数的书写方法,不会对整数的实际存储方式产生影响。
所有整数都是二进制形式存储,跟书写方式无关。
不同进制可以混合使用,比如:10 + 015 + 0x20
是一个合法的表达式。
printf()
的进制相关占位符如下:
- %d:十进制整数
- %o:八进制整数
- %x:十六进制整数
- %#o:显示前缀 0 的八进制整数
- %#x:显示前缀 0x 的十六进制整数
- %#X:显示前缀 0X 的十六进制整数
int x = 100;
printf("dec = %d\n", x); // 100
printf("octal = %o\n", x); // 144
printf("hex = %x\n", x); // 64
printf("octal = %#o\n", x); // 0144
printf("hex = %#x\n", x); // 0x64
printf("hex = %#X\n", x); // 0X64
浮点数类型
任何有小数点的数值,都会被编译器解释为浮点数。所谓 “浮点数” 就是使用 m * be 的形式,存储一个数值,m
是小数部分,b
是基数(通常是 2
),e
是指数部分。这种形式是精度和数值范围的一种结合,可以表示非常大或者非常小的数。
浮点数的类型声明使用 float
关键字,可以用来声明浮点数变量。例如:
float c = 10.5;
float
类型占用 4
个字节(32 位),其中 8 位存放指数的值和符号,剩下 24 位存放小数的值和符号。float
类型至少能够提供(十进制的)6 位有效数字,指数部分的范围为(十进制的)-37 ~ 37
,即数值范围为 10-37 ~ 1037。
有时候,32 位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。
- double:占用
8
个字节(64 位),至少提供 13 位有效数字 - long double:通常占用
16
个字节
注意
由于存在精度限制,浮点数只是一个近似值,它的计算是不精确的,比如 C 语言里面 0.1 + 0.2
并不等于 0.3
,而是有一个很小的误差。例如:
if (0.1 + 0.2 == 0.3) // false
C 语言允许使用科学计数法表示浮点数,使用字母 e
来分隔小数部分和指数部分。例如:
double x = 123.456e+3; // 123.456 x 10^3
// 等同于
double x = 123.456e3;
e
后面如果是加号 +
,加号可以省略。注意:科学计数法里面 e
的前后,不能存在空格。
另外,科学计数法的小数部分如果是 0.x
或 x.0
的形式,那么 0
可以省略。例如:
0.3E6
// 等同于
.3E6
3.0E6
// 等同于
3.E6
布尔类型
C 语言原来并没有为布尔值单独设置一个类型,而是使用整数 0
表示伪,所有非零值表示真。例如:
int x = 1;
if (x) {
printf("x is true!\n");
}
变量 x
等于 1
,C 语言就认为这个值代表真,从而会执行判断体内部的代码。
C99 标准添加了类型 _Bool
,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用 0
表示伪,1
表示真。例如:
_Bool isNormal;
isNormal = 1;
if (isNormal)
printf("Everything is OK.\n");
头文件 stdbool.h
定义了另一个类型别名 bool
,并且定义了 true
代表 1
、false
代表 0
。只要加载这个头文件,就可以使用这几个关键字。例如:
#include <stdbool.h>
bool flag = false;
加载头文件 stdbool.h
以后,就可以使用 bool
定义布尔值类型,以及 false
和 true
表示真伪。
字面量的类型
字面量(literal
)指的是代码里面直接出现的值。例如:
int x = 123;
x
是变量,123
就是字面量。
编译时,字面量也会写入内存,因此编译器必须为字面量指定数据类型,就像必须为变量指定数据类型一样。
一般情况下,十进制整数字面量(比如:123
)会被编译器指定为 int
类型。如果一个数值比较大,超出了 int
能够表示的范围,编译器会将其指定为 long int
。如果数值超过了 long int
,会被指定为 unsigned long
。如果还不够大,就指定为 long long
或 unsigned long long
。
小数(比如:3.14
)会被指定为 double
类型。
字面量后缀
有时候,程序员希望为字面量指定一个不同的类型。比如:编译器将一个整数字面量指定为 int
类型,但是程序员希望将其指定为 long
类型,这时可以为该字面量加上后缀 l
或 L
,编译器就知道要把这个字面量的类型指定为 long
。例如:
int x = 123L;
这里 123L
写成 123l
,效果也是一样的,但是建议优先使用 L
,因为小写的 l
容易跟数字 1
混淆。
八进制和十六进制的值,也可以使用后缀 l
和 L
指定为 Long
类型。例如:
int y = 0377L;
int z = 0x7fffL;
如果希望指定为无符号整数 unsigned int
,可以使用后缀 u
或 U
。例如:
int x = 123U;
L
和 U
可以结合使用,表示 unsigned long
类型。L
和 U
的大小写和组合顺序无所谓。例如:
int x = 123LU;
对于浮点数,编译器默认指定为 double
类型,如果希望指定为其他类型,需要在小数后面添加后缀 f
(float)或 l
(long double)。
科学计数法也可以使用后缀。例如:
1.2345e+10F
1.2345e+10L
常用的字面量后缀:
- f 和 F:
float
类型 - l 和 L:对于整数是
long int
类型,对于小数是long double
类型 - ll 和 LL:
Long Long
类型,比如:3LL
- u 和 U:表示
unsigned int
,比如:15U
、0377U
u
还可以与其他整数后缀结合,放在前面或后面都可以,比如:10UL
、10ULL
和 10LLU
都是合法的。
溢出
每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做 向上溢出(overflow
);小于最小值,叫做 向下溢出(underflow
)。
一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。例如:
unsigned char x = 255;
x = x + 1;
printf("%d\n", x); // 0
变量 x
加 1
,得到的结果不是 256
,而是 0
。因为 x
是 unsigned char
类型,最大值是 255
(二进制 11111111
),加 1
后就发生了溢出,256
(二进制 100000000
)的最高位 1
被丢弃,剩下的值就是 0
。
unsigned int ui = UINT_MAX; // 4,294,967,295
ui++;
printf("ui = %u\n", ui); // 0
ui--;
printf("ui = %u\n", ui); // 4,294,967,295
常量 UINT_MAX
是 unsigned int
类型的最大值。如果加 1
,对于该类型就会溢出,从而得到 0
;而 0
是该类型的最小值,再减 1
,又会得到 UINT_MAX
。
溢出很容易被忽视,编译器又不会报错,所以必须非常小心。例如:
for (unsigned int i = n; i >= 0; --i) // 错误
上面代码表面看似乎没有问题,但是循环变量 i
的类型是 unsigned int
,这个类型的最小值是 0
,不可能得到小于 0
的结果。当 i
等于 0
,再减去 1
的时候,并不会返回 -1
,而是返回 unsigned int
的类型最大值,这个值总是大于等于 0
,导致无限循环。
为了避免溢出,最好方法就是将运算结果与类型的极限值进行比较。例如:
unsigned int ui;
unsigned int sum;
// 错误
if (sum + ui > UINT_MAX) too_big();
else sum = sum + ui;
// 正确
if (ui > UINT_MAX - sum) too_big();
else sum = sum + ui;
变量 sum
和 ui
都是 unsigned int
类型,它们相加的和还是 unsigned int
类型,这就有可能发生溢出。但是,不能通过相加的和是否超出了最大值 UINT_MAX
,来判断是否发生了溢出,因为 sum + ui
总是返回溢出后的结果,不可能大于 UINT_MAX
。正确的比较方法是,判断 UINT_MAX - sum
与 ui
之间的大小关系。
下面是另一种错误的写法。
unsigned int i = 5;
unsigned int j = 7;
if (i - j < 0) // 错误
printf("negative\n");
else
printf("positive\n");
上面示例的运算结果,会输出 positive。原因是变量 i
和 j
都是 unsigned int
类型,i - j
的结果也是这个类型,最小值为 0
,不可能得到小于 0
的结果。正确的写法如下:
if (j > i) // ....
sizeof 运算符
sizeof
是 C 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。它的参数可以是数据类型的关键字,也可以是变量名或某个具体的值。例如:
// 参数为数据类型
int x = sizeof(int); // 4 或 8
// 参数为变量
int i;
sizeof(i); // 4 或 8
// 参数为数值
sizeof(3.14); // 8
sizeof
运算符的返回值,C 语言只规定是无符号整数,并没有规定具体的类型,而是留给系统自己去决定,sizeof
到底返回什么类型。不同的系统中,返回值的类型有可能是 unsigned int
,也有可能是 unsigned long
,甚至是 unsigned long long
,对应的 printf()
占位符分别是 %u
、%lu
和 %llu
。这样不利于程序的可移植性。
C 语言提供了一个解决方法,创造了一个类型别名 size_t
,用来统一表示 sizeof
的返回值类型。该别名定义在 stddef.h
头文件(引入 stdio.h
时会自动引入)里面,对应当前系统的 sizeof
的返回值类型,可能是 unsigned int
,也可能是 unsigned long
。
C 语言还提供了一个常量 SIZE_MAX
,表示 size_t
可以表示的最大整数。所以,size_t
能够表示的整数范围为 [0, SIZE_MAX]
。
printf()
有专门的占位符 %zd
或 %zu
,用来处理 size_t
类型的值。例如:
printf("%zd\n", sizeof(int));
不管 sizeof
返回值的类型是什么,%zd
占位符(或 %zu
)都可以正确输出。
如果当前系统不支持 %zd
或 %zu
,可使用 %u
(unsigned int)或 %lu
(unsigned long int)代替。
类型的自动转换
某些情况下,C 语言会自动转换某个值的类型。
赋值运算
赋值运算符会自动将右边的值,转成左边变量的类型。
浮点数赋值给整数变量
浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。例如:
int x = 3.14;
变量
x
是整数类型,赋给它的值是一个浮点数。编译器会自动把3.14
先转为int
类型,丢弃小数部分,再赋值给x
,因此x
的值是3
。这种自动转换会导致部分数据的丢失(
3.14
丢失了小数部分),所以最好不要跨类型赋值,尽量保证变量与所要赋予的值是同一个类型。整数赋值给浮点数变量
整数赋值给浮点数变量时,会自动转为浮点数。例如:
float y = 12 * 2;
变量
y
的值不是24
,而是24.0
,因为等号右边的整数自动转为了浮点数。窄类型赋值给宽类型
字节宽度较小的整数类型,赋值给字节宽度较大的整数变量时,会发生类型提升,即窄类型自动转为宽类型。例如:
char x = 10; int i = x + y;
变量
x
的类型是char
,由于赋值给int
类型,所以会自动提升为int
。宽类型赋值给窄类型
字节宽度较大的类型,赋值给字节宽度较小的变量时,会发生类型降级,自动转为后者的类型。这时可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。例如:
int i = 321; char ch = i; // ch 的值是 65 (321 % 256 的余值)
变量
ch
是char
类型,宽度是8
个二进制位。变量i
是int
类型,将i
赋值给ch
,后者只能容纳i
(二进制形式为101000001
,共 9位)的后八位,前面多出来的二进制位被丢弃,保留后八位就变成了01000001
(十进制的65
,相当于字符A
)。浮点数赋值给整数类型的值,也会发生截值,浮点数的小数部分会被截去。
double pi = 3.14159; int i = pi; // i 的值为 3
上面示例中,
i
等于3
,pi
的小数部分被截去了。
混合类型的运算
不同类型的值进行混合计算时,必须先转成同一个类型,才能进行计算。转换规则如下:
整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。例如:
3 + 1.2 // 4.2
int
类型的3
会先转成float
的3.0
,再进行计算,得到4.2
。不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型。比如:
float
转为double
,double
转为long double
。不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。比如:
short
转为int
,int
转为long
等,有时还会将带符号的类型signed
转为无符号unsigned
。
下面例子的执行结果,可能会出人意料。
int a = -5;
if (a < sizeof(int))
do_something();
变量 a
是带符号整数,sizeof(int)
是 size_t
类型,这是一个无符号整数。按照规则,signed int
自动转为 unsigned int
,所以 a
会自动转成无符号整数 4294967291
(转换规则是 -5
加上无符号整数的最大值,再加 1
),导致比较失败,do_something()
不会执行。
所以,最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将 signed int
转为 unsigned int
,可能不会得到预期的结果。
整数类型的运算
两个相同类型的整数运算时,或者单个整数的运算,一般来说,运算结果也属于同一类型。但是有一个例外,宽度小于 int
的类型,运算结果会自动提升为 int
。例如:
unsigned char a = 66;
if ((-a) < 0) printf("negative\n");
else printf("positive\n");
变量 a
是 unsigned char
类型,这个类型不可能小于 0
,但是 -a
不是 unsigned char
类型,会自动转为 int
类型,导致上面的代码输出 negative
。
unsigned char a = 1;
unsigned char b = 255;
unsigned char c = 255;
if ((a - 5) < 0) do_something();
if ((b + c) > 300) do_something();
表达式 a - 5
和 b + c
都会自动转为 int 类型,所以函数do_something()会执行两次。
函数 函数的参数和返回值,会自动转成函数定义里指定的类型。
int dostuff(int, unsigned char);
char m = 42; unsigned short n = 43; long long int c = dostuff(m, n); 上面示例中,参数变量m和n不管原来的类型是什么,都会转成函数dostuff()定义的参数类型。
下面是返回值自动转换类型的例子。
char func(void) { int a = 42; return a; } 上面示例中,函数内部的变量a是int类型,但是返回的值是char类型,因为函数定义中返回的是这个类型。
类型的显式转换 原则上,应该避免类型的自动转换,防止出现意料之外的结果。C 语言提供了类型的显式转换,允许手动转换类型。
只要在一个值或变量的前面,使用圆括号指定类型(type),就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。
(unsigned char) ch 上面示例将变量ch转成无符号的字符类型。
long int y = (long int) 10 + 12; 上面示例中,(long int)将10显式转为long int类型。这里的显示转换其实是不必要的,因为赋值运算符会自动将右边的值,转为左边变量的类型。
可移植类型 C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。
程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件stdint.h创造了一些新的类型别名。
(1)精确宽度类型(exact-width integer type),保证某个整数类型的宽度是确定的。
int8_t:8位有符号整数。 int16_t:16位有符号整数。 int32_t:32位有符号整数。 int64_t:64位有符号整数。 uint8_t:8位无符号整数。 uint16_t:16位无符号整数。 uint32_t:32位无符号整数。 uint64_t:64位无符号整数。 上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果int类型为32位,int32_t就会指向int;如果long类型为32位,int32_t则会指向long。
下面是一个使用示例。
#include <stdio.h> #include <stdint.h>
int main(void) { int32_t x32 = 45933945; printf("x32 = %d\n", x32); return 0; } 上面示例中,变量x32声明为int32_t类型,可以保证是32位的宽度。
(2)最小宽度类型(minimum width type),保证某个整数类型的最小长度。
int_least8_t int_least16_t int_least32_t int_least64_t uint_least8_t uint_least16_t uint_least32_t uint_least64_t 上面这些类型,可以保证占据的字节不少于指定宽度。比如,int_least8_t表示可以容纳8位有符号整数的最小宽度的类型。
(3)最快的最小宽度类型(fast minimum width type),可以使整数计算达到最快的类型。
int_fast8_t int_fast16_t int_fast32_t int_fast64_t uint_fast8_t uint_fast16_t uint_fast32_t uint_fast64_t 上面这些类型是保证字节宽度的同时,追求最快的运算速度,比如int_fast8_t表示对于8位有符号整数,运算速度最快的类型。这是因为某些机器对于特定宽度的数据,运算速度最快,举例来说,32位计算机对于32位数据的运算速度,会快于16位数据。
(4)可以保存指针的整数类型。
intptr_t:可以存储指针(内存地址)的有符号整数类型。 uintptr_t:可以存储指针的无符号整数类型。 (5)最大宽度整数类型,用于存放最大的整数。
intmax_t:可以存储任何有效的有符号整数的类型。 uintmax_t:可以存放任何有效的无符号整数的类型。 上面的这两个类型的宽度比long long和unsigned long更大。