数据类型
C 语言的每一种数据,都是有类型(type)的,编译器必须知道数据的类型,才能操作数据。一旦知道某个值的数据类型,就能知道该值的特征和操作方式。
基本数据类型有三种:字符(char)、整数(int)和浮点数(float)。复杂的类型都是基于它们构建。
字符类型
字符类型指的是单个字符,类型声明使用char
关键字。
在计算机内部,字符类型使用一个字节(8位)存储。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。
char val = 'B';
上面示例声明了变量val
是字符类型,并将其赋值为字母B
。
C 语言规定,字符常量必须放在单引号里面。
Q:不能放双引号里面吗?
A:
C 语言规定字符常量必须放在单引号 ('
) 里面。字符常量代表单个字符,它可以是字母、数字、特殊符号或者是转义字符(以反斜杠\
开始的具有特殊含义的序列),但无论是什么字符,都必须用单引号括起来。
Q:字符和字符串的区别?
A:
字符常量和字符串常量在C语言中具有明显的区别?:
- 形式/定义
- **字符常量:**由单引号 (
'
) 包围的一个字符,例如'A'
、'5'
或者转义字符'\n'
(换行符)等。字符常量实际代表了字符对应的ASCII码或Unicode编码(取决于编译器)。 - 字符串常量:由双引号
(")
包围的一串字符序列,例如"Hello"
或"World"
。字符串常量实际上是一个字符数组,其中包含了所有指定的字符,并且末尾有一个隐含的空字符(\0
),用来标记字符串的结束。
"Hello" 是一个字符串常量,包含字符 H、e、l、l、o 和结束符 \0。
- 长度
- 字符常量:长度固定为
1
,即一个字符。 - 字符串常量:长度可变,包括所有可见字符以及末尾的空字符,所以它的存储需求至少比显示的字符多一个字节。
- 赋值与使用
- 字符常量:可以直接赋值给字符类型变量,如
char ch = 'A';
。 - 字符串常量:不能直接赋值给字符类型变量,必须通过指向字符的指针或字符数组来存储,如
char str[] = "Hello";
或char* pStr = "Hello";
。
- 运算
- 字符常量:可以参与算术和逻辑运算,其本质会被转换成对应的整数值(ASCII码或Unicode码点)。每个字符对应一个整数(由 ASCII 码确定),比如
B
对应整数66
。
char c = 66;
// 等同于
char c = 'B';
char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;printf("%d\n", a + b); // 输出 133
- 字符串常量:不直接参与算术运算,但可以进行字符串连接操作(通过库函数实现),比较操作(如
strcmp()
),以及访问其单个字符等。
整数类型
整数类型是用来存储整数值的数据类型,它们不包含小数部分。类型声明使用int
关键字。
int a;
不同计算机上的int类型的大小不一样。int
类型的大小取决于具体的计算机体系结构、操作系统以及所使用的编译器。
在大多数现代32位和64位计算机系统中,int
类型通常被定义为32
位(4
字节),在某些情况下,特别是对于嵌入式系统和其他特殊的硬件架构,int
类型可能只有16
位(2
字节)或其它大小。
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。
由于signed int需要用一位来区分正负,所以它所能表示的最大正数小于unsigned int,后者充分利用了所有位来表示正数,从而获得了更大的数值范围。
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
类型之外,又提供了三个整数的子类型。这样有利于更精细地限定整数变量的范围,也有利于更好地表达代码的意图。
int类型在不同的计算机系统和编译器中所占用的字节数量是为了适应不同环境下的需求以及历史演进过程。int类型占用
4
个或8
个字节的选择是由系统设计者和编译器开发者综合考虑硬件特性、性能优化以及软件生态兼容性等因素决定的。
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;
推荐使用C99标准引入的 <stdint.h> 头文件中的固定宽度整数类型。这样可以更加精确地控制数据类型的字节长度:
- 确保32位整数时,应使用int32_t类型。
- 确保64位整数时,应使用int64_t类型。
- 如果只需要16位整数,应使用int16_t类型。
- 需要8位整数时,理论上可以使用int8_t类型,但实际上在C语言中,int8_t对应的就是signed char类型。
关于long类型和short类型:
long
类型原本是用来表示比int类型更长的整数,但在不同的编译器和平台上,long类型的大小并不统一,它可能占用4个字节(32位)或8个字节(64位),并不能确保一定是32位。short
类型则一般占用2个字节(16位),但它的确切大小也依赖于编译器和平台。
因此,除非特别了解目标平台上的long
和shor
t的确切大小,否则最好还是使用<stdint.h>
提供的标准类型以确保可移植性和准确性。
整数类型的极限值
有时候需要查看,当前系统不同整数类型的最大值和最小值,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 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。
- 八进制(Octal)数:在数字前面放置前缀
0
。例如,十进制数10
在八进制下表示为 012,十进制数8在八进制下不能直接表示(因为8不是合法的八进制数字),对应的八进制数为 010。 - 十六进制(Hexadecimal)数:在数字前面放置前缀
0x
或0X
(不区分大小写)。例如,十进制数10在十六进制下表示为 0xA 或 0xA,十进制数8在十六进制下表示为 0x8。
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
浮点数类型
任何有小数点的数值,都会被编译器解释为浮点数。
浮点数的类型声明使用float
关键字,可以用来声明浮点数变量。
float c = 10.5;
float类型占用4
个字节(32位),其中8
位存放指数的值和符号,剩下24
位存放小数的值和符号。float类型至少能够提供(十进制的)6
位有效数字,指数部分的范围为(十进制的)-37
到37
。
有时候,32位浮点数提供的精度或者数值范围还不够
double
:占用8个字节(64位),至少提供13位有效数字。long double
:通常占用16个字节。
注意,由于存在精度限制,浮点数只是一个近似值,它的计算是不精确的,比如 C 语言里面0.1 + 0.2并不等于0.3,而是有一个很小的误差。
#include <stdio.h>int main() {float a = 0.1f;float b = 0.2f;float c = a + b;printf("0.1 + 0.2 = %.20f\n", c); // 输出结果可能不是0.30000000000000000000if (c == 0.3f) {printf("true.\n");} else {printf("false.\n");}return 0;
}
布尔类型
C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0
表示伪,所有非零值表示真。
int x = 1;
if (x) {printf("x is true!\n");
}
C99
标准添加了类型_Bool
,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0
表示伪,1
表示真,下面是一个示例。
#include <stdio.h>int main() {_Bool isNormal;isNormal = 1;if(isNormal) {printf("isNormal is true \n");}return 0;
}
头文件stdbool.h
定义了另一个类型别名bool,并且定义了true
代表1
、false
代表0
。只要加载这个头文件,就可以使用这几个关键字。
#include <stdio.h>
#include <stdbool.h>int main() {bool flag = true;if(flag) {printf("flag is true \n");}return 0;
}
字面量的类型
字面量(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;
上面代码中,字面量123有后缀L
,编译器就会将其指定为long
类型。这里123L写成123l,效果也是一样的,但是建议优先使用L,因为小写的l容易跟数字1混淆。
八进制和十六进制的值,也可以使用后缀l和L指定为 Long 类型,比如020L
和0x20L
。
如果希望指定为无符号整数unsigned int
,可以使用后缀u
或U
。
int x = 123U;
L
和U
可以结合使用,表示unsigned long
类型。L
和U
的大小写和组合顺序没有影响。
int x = 123LU;
对于浮点数,编译器默认指定为 double
类型,如果希望指定为其他类型,需要在小数后面添加后缀f
(float
)或l
(long double
)。
float a = 0.1f;
总结一下,常用的字面量后缀有下面这些。
f
和F
:代表float
类型l
和L
:代表对于整数是long int
类型,对于浮点数是long double
类型ll
和LL
:代表Long Long
类型u
和U
:代表unsigned int
u
还可以与其他整数后缀结合,放在前面或后面都可以,比如10UL
、10ULL
和10LLU
都是合法的。
溢出
每一种数据类型都有数值范围,如果存放的数值超过这个范围(小于最小值或大于最大值),就需要更多的二进制去存储,此时就会发生溢出。
大于最大值,叫向上溢出(overflow),小于最小值,叫向下溢出(underflow)。
一般来说,编译器不会去处理溢出报错,继续正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。
#include <stdio.h>int main() {unsigned char x = 255; // 255 (十进制) = 11111111 (二进制)x = x + 1;printf("%d\n",x);// 0return 0;
}
x
是 unsigned char
类型,最大值为255
,经过自增操作,得到的结果不是256
,而是0
,这是因为加了1
发生了溢出,256
(二进制100000000
)的最高位1
被丢弃,剩下的值就是0
。
#include <stdio.h>
#include <limits.h>
int main() {unsigned int x = UINT_MAX;x++;// %u正确显示无符号整数的值printf("x = %u\n",x); // x= 0x--;printf("x = %u\n", x); // x = 4294967295return 0;
}
上面示例中,常量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
,导致无限循环。
#include <stdio.h>
#include <limits.h>
unsigned int safe_unsigned_add(unsigned int a, unsigned int b) {if(b >UINT_MAX - a) {// 如果b>a容纳的最大增量而不溢出,则表明加法会导致溢出printf("Error: Unsigned integer addition would overflow!\n");}else {return a + b;}
}
int main() {unsigned int ui = 4294967290U; // 接近 UINT_MAX 的一个数unsigned int addend = 5U;unsigned int sum = safe_unsigned_add(ui, addend);if(sum!==UINT_MAX) {printf("Sum: %u\n", sum);}return 0;
}
在这个例子中,safe_unsigned_add
函数首先检查 b
是否大于 UINT_MAX - a
,如果是,则说明加上 a
后会超出 unsigned int
类型的最大值(由 UINT_MAX
定义)。如果不是,则安全地执行加法运算。这样可以有效地防止无符号整数加法溢出的发生。注意,这种方法适用于无符号整数,而对于有符号整数,需要采用不同的比较方式来判断是否会发生溢出。
sizeof 运算符
sizeof
是 C 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。它的参数可以是数据类型的关键字,也可以是变量名或某个具体的值。
// 参数为数据类型
int x = sizeof(int); // int类型占用的字节数量(通常是4或8)
// 参数为变量
int i;
sizeof(i);// 整数变量占用字节数量(通常是4或8)
// 参数为数值
sizeof(3.14);// 返回浮点数3.14占用的字节数量,由于浮点数的字面量一律存储为 double 类型,所以会返回8,因为 double 类型占用的8个字节。
计算基本数据类型大小:
#include <stdio.h>int main() {printf("Size of int: %zu bytes\n", sizeof(int)); // 输出 int 类型所占的字节数printf("Size of char: %zu bytes\n", sizeof(char)); // 输出 char 类型所占的字节数printf("Size of double: %zu bytes\n", sizeof(double)); // 输出 double 类型所占的字节数return 0;
}
计算变量的大小:
#include <stdio.h>int main() {int myInteger = 0;char myChar = 'A';float myFloat = 3.14f;printf("Size of myInteger: %zu bytes\n", sizeof(myInteger));printf("Size of myChar: %zu bytes\n", sizeof(myChar));printf("Size of myFloat: %zu bytes\n", sizeof(myFloat));return 0;
}
printf()有专门的占位符%zd或%zu,用来处理size_t类型的值。
在C语言中,为了提高程序的可移植性和一致性,sizeof
运算符返回一个无符号整数类型,但具体类型取决于编译器和目标平台。为了方便编程和保证代码跨平台兼容性,C语言标准库提供了size_t
这一类型别名,它被定义在<stddef.h>
头文件中,用于表示对象大小或数组长度等与系统相关的、非负的、无符号整数值。
size_t
通常足够大,足以存储任何对象的大小,包括指针和大型数组的尺寸。因此,在打印sizeof
的结果时,建议使用%zu
作为printf
系列函数的格式化字符串占位符,这与size_t
类型是匹配的:
#include <stdio.h>int main() {int someInt;printf("Size of int: %zu bytes\n", sizeof(someInt));return 0;
}
类型的自动转换
某些情况下,C 语言会自动转换某个值的类型。
赋值运算
赋值运算符会自动将右边的值,转成左边变量的类型。
浮点数赋值给整数变量
浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。
#include <stdio.h>int main() {int i = 3.14;printf("%d\n",i);// 3 return 0;
}
编译器会自动把3.14
先转为int
类型,丢弃小数部分,再赋值给x
,因此x
的值是3
。
注意,舍弃小数部分时,不是四舍五入,而是整个舍弃,这个过程可以被视为向下取整(truncation)
整数赋值给浮点数变量
#include <stdio.h>int main() {float i = 3 * 3;printf("%f\n",i); // 9.000000return 0;
}
窄类型赋值给宽类型
#include <stdio.h>int main() {// 假设char类型是8位,short类型是16位,int类型是32位 实际上这些大小依赖于具体的编译器和机器架构// 示例1:char类型赋值给int类型char c = 128; // 假设char是有符号类型,128在char类型中可能会表示为负数int i = c; // 在这里,c自动转换为int类型printf("char to int: %d\n", i); // 输出可能为-128,因为char类型可能以补码形式表示128// 示例2:short类型赋值给int类型short s = 32767; // 假设short是有符号类型,这是short的最大正值int si = s; // 在这里,s自动转换为int类型printf("short to int: %d\n", si); // 输出32767// 示例3:char与int类型混合运算char ch = 100;int num = ch * 10; // 在乘法运算中,ch被隐式提升为int类型printf("char mixed with int: %d\n", num); // 输出1000,因为ch提升为int类型后再进行乘法运算return 0;
}
宽类型赋值给窄类型
double wide = 3.1415926535;
int narrow;// 窄类型赋值
narrow = wide; // 实际上,narrow会接收到double变量wide的整数部分,即3,小数部分被截断// 输出结果
printf("Wide value: %.10f\n", wide);
printf("Narrow value: %d\n", narrow);
double
类型的wide
赋值给int
类型的narrow
时,narrow
只能存储整数值,因此3.1415926535
的小数部分会被丢弃,arrow
最终得到的值是3
。
混合类型的运算
整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。
3 + 1.2 // 4.2
上面示例是int
类型与float
类型的混合计算,int
类型的3
会先转成float
的3.0
,再进行计算,得到4.2
。
最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int转为unsigned int,可能不会得到预期的结果。
函数
函数的参数和返回值,会自动转成函数定义里指定的类型。
int dostuff(int, unsigned char);char m = 42;
unsigned short n = 43;
long long int c = dostuff(m, n);
上面示例中,参数变量m和n不管原来的类型是什么,都会转成函数dostuff()定义的参数类型。
类型的显式转换
原则上,应该避免类型的自动转换,防止出现意料之外的结果。C 语言提供了类型的显式转换,允许手动转换类型。
只要在一个值或变量的前面,使用圆括号指定类型(type),就可以将这个值或变量转为指定的类型,这叫做**“类型指定”(casting)。**
#include <math.h>float f = 3.7;
int i;i = (int)f; // 这里i将被赋值为3,而不是四舍五入后的4
i = (int)f;就是类型指定,
可移植类型
C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。
程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件stdint.h
创造了一些新的类型别名。
int8_t:8位有符号整数。
int16_t:16位有符号整数。
int32_t:32位有符号整数。
int64_t:64位有符号整数。
uint8_t:8位无符号整数。
uint16_t:16位无符号整数。
uint32_t:32位无符号整数。
uint64_t:64位无符号整数。