LV010-常见数据类型
一、数据类型及标识符
| 分类 | 数据类型 | 标识符 |
| 基本类型 | 整型 | int(基本整形) |
| long(长整型) | ||
| short(短整型) | ||
| unsigned(无符号整型) | ||
| 字符型 | char | |
| 浮点型 | float(单精度) | |
| double(双精度) | ||
| 枚举型 | enum | |
| 构造类型 | 结构体类型 | struct |
| 共用体类型 | union | |
| 数组 | 基本类型和构造类型组成数组 | |
| 空类型 | 空类型 | void |
| 指针类型 | 指针类型 | * |
二、数据类型长度
一种数据类型占用的字节数,称为该数据类型的 长度。例如, short 占用 2 个字节的内存,那么它的长度就是 2 。
| 常用数据类型 | 16 位平台 | 32 位平台 | 64 位平台 | |||
| 字节数 | 位数 | 字节数 | 位数 | 字节数 | 位数 | |
| char unsigned char | 1 | 8 | 1 | 8 | 1 | 8 |
| short unsigned short | 2 | 16 | 2 | 16 | 2 | 16 |
| int unsigned int | 2 | 16 | 4 | 32 | 4 | 32 |
| long unsigned long | 4 | 32 | 4 | 32 | 8 | 64 |
| long long | --- | --- | 8 | 64 | 8 | 64 |
| 指针 | 2 | 16 | 4 | 32 | 8 | 64 |
| bool | 1 | 8 | 1 | 8 | 1 | 8 |
| float | 4 | 32 | 4 | 32 | 4 | 32 |
| double | 8 | 64 | 8 | 64 | 8 | 64 |
三、定点数与浮点数
1. 定点数
数字既包括整数,又包括小数,而小数的精度范围要比整数大得多,所以如果我们想在计算机中,既能表示整数,也能表示小数,关键就在于这个小数点。
于是人们便与计算机约定小数点的位置,且这个位置固定不变。小数点前、后的数字,分别用二进制表示,然后组合起来就可以把这个数字在计算机中存储起来。用这种方法表示的数字叫做 定点数。
定点数如果要表示整数或小数,分为以下三种情况:
| 纯整数 | 例如:100,小数点其实在最后一位,所以忽略不写 |
| 纯小数 | 例如:0.125,小数点固定在最高位 |
| 整数+小数 | 例如:1.28、32.35,小数点在指定某个位置 |
1.1 纯整数
/* 以 1 个字节(8 bit)表示 */
100(D) = 0110 0100(B)1.2 纯小数
/* 以 1 个字节(8 bit)表示 */
0.125(D) = 0.0010 0000(B)
/* ==== ==== ==== ==== ==== ==== ==== ==
0.125 x 2 = 0.250 ----- 取 0 剩 0.250
0.250 x 2 = 0.500 ----- 取 0 剩 0.500
0.500 x 2 = 1.000 ----- 取 1 剩 0.000
0.000 x 2 = 0.000 ----- 取 0 剩 0.000
0.000 x 2 = 0.000 ----- 取 0 剩 0.000
0.000 x 2 = 0.000 ----- 取 0 剩 0.000
0.000 x 2 = 0.000 ----- 取 0 剩 0.000
所以最后就是 0.0010 0000
* ==== ==== ==== ==== ==== ==== ==== == */【说明】小数转二进制方法:乘 2 取整,一直乘到要求的位数。
1.3 整数+小数
我们需要先约定小数点的位置。依旧以 1 个字节( 8 bit )为例,我们可以约定前 5 位表示整数部分,后 3 位表示小数部分。
/* 以 1 个字节(8 bit)表示 */
/* 前 5 位表示整数部分,后 3 位表示小数部分 */
25.125(D) = 11001 001(B)用定点格式来存储小数,优点是精度高,因为所有的位都可以用来存储有效数字。缺点是取值范围太小,不能表示很大或者很小的数。
2. 浮点数
在实际问题中,有很多数据的数量级特别大,小数的取值范围很大,最大值和最小值的差距有上百个数量级,使用定点数来存储将变得非常困难。例如,
这用科学计数法很容易表示,但是计算机中怎么办,没有这么大的数据类型吧。如果真要存,那将会需要很大的一块内存,估计要几十个字节。那计算机不能也学习一下科学计数法吗 🤣?用指数来存储不好吗?
于是,这种以指数的形式来存储小数的解决方案就叫做浮点数。浮点数克服了定点数取值范围太小的缺点。
四、整数
C 语言使用定点数格式来存储 short 、 int 、 long 类型的整数,定点数中的点指的就是小数点。对于整数,可以认为小数点后面都是零。
1. 常见的整型
现在的操作系统中, int 一般 占用 4 个字节( Byte )的内存,共计 32 位( Bit )。使用 4 个字节保存较小的整数绰绰有余,我们正常使用的时候,一般会空闲出两三个字节来,这些字节就白白浪费掉了,不能再被其他数据使用。在单片机和嵌入式系统中,内存都是非常稀缺的资源,所有的程序都在尽力节省内存。
让整数占用更少的内存可以在 int 前边加 short ,让整数占用更多的内存可以在 int 前边加 long 。 short 、 int 、 long 是 C 语言中常见的整数类型,其中 int 称为整型, short 称为短整型, long 称为长整型。
2. 整型的长度
对于上边说三种整型来说,只有 short 的长度是确定的,是两个字节,而 int 和 long 的长度无法确定,在不同的环境下可能会有所不同。C 语言并没有严格规定 short 、 int 、 long 的长度,只做了宽泛的限制:
(1)short 至少占用 2 个字节。
(2)short 的长度不能大于 int。
(3)long 的长度不能小于 int。
总的来说,它们的长度(所占字节数)关系为:short <= int <= long。这说明 short 并不一定真的” 短 “,long 也并不一定真的” 长 “,它们有可能和 int 占用相同的字节数。
在 16 位环境下,short 为 2 个字节,int 为 2 个字节,long 为 4 个字节。对于 32 位的 Windows、Linux 和 OSX,short 为 2 个字节,int 为 4 个字节,long 也为 4 个字节。在 64 位环境下,不同的操作系统会有不同的结果,具体如下所示(长度以字节计):Windows 64 位系统: short 为 2 字节、 int 为 4 字节, long 为 4 字节。类 Unix 系统(包括 Unix、Linux、OSX、BSD、Solaris 等): short 为 2 字节、 int 为 4 字节, long 为 8 字节。
3. 整型的输出
后边我们会接触到 printf 函数来输出一些数据,对于整数来说,常用的格式控制符如下:
| %hd | 输出 short int 类型,hd 是 short decimal 的简写; |
| %d | 输出 int 类型,d 是 decimal 的简写; |
| %ld | 输出 long int 类型,ld 是 long decimal 的简写。 |
4. 整数的符号?
整数,自然会有正负之分,那正负怎么界定呢?C 语言规定,把内存的最高位作为符号位。以 int 为例,它占用 32 位的内存, 0~30 位表示数值, 31 位表示正负号。最高位是 1 表示为负数,最高位是 0 表示为正数。
short 、 int 和 long 类型默认都是带符号位的,符号位以外的内存才是数值位。如果只考虑正数,那么各种类型能表示的数值范围(取值范围)就比原来小了一半。
但是我们很多时候可以确定某个数字只能是正数,比如,人数、字符串的长度、内存地址等,这个时候符号位就是多余的了,那我们就可以删掉符号位,把所有的位都用来存储数值,这样能表示的数值范围更大(大一倍)。 C 语言中通过 unsigned 表示没有符号位,于是就出现了 unsigned short 、 unsigned int 、 unsigned long 。
5. 整数的存储
对于有符号数,都是以 补码 的形式存于内存中。那什么是补码,为什么要用补码呢?
5.1 几个基本概念
- 机器数
机器数就是一个数在计算机中的二进制表示,计算机中机器数的最高位是符号位,正数符号位为 0 ,负数符号位为 1 。机器数包含原码、反码和补码三种表示形式。如:数字 3 若用 8 位二进制数表示,则机器数为 0000 0011 ,数字 -3 若用 8 位二进制数表示,则机器数为 1000 0011 。
- 机器数的真值
真值就是带符号位的机器数对应的真正数值。如:机器数为 0000 0011 则,真值为 3 ,机器数为 1000 0011 ,则真值为 -3 。
5.2 原码、反码、补码
- 原码
若机器字长为 n ,那么一个数的原码就是用一个 n 位的二进制数表示出来的机器数,其中最高位为符号位:正数为 0 ,负数为 1 ,位数不够的用 0 补全。其实就是 原码 = 符号位(0 或 1) + 真值的绝对值 。如(假设机器字长为 8 ): 3 的原码为 0000 0011 , -3 的原码为 1000 0011 。
【注意】 0 的原码有两个: [+0] 原码为 0000 0000 ; [-0] 原码为 1000 0000 。
- 反码
正数的反码就是其 本身,负数的反码为除了 符号位不变 外,其他各位取反。如(假设机器字长为 8 ): 3 的反码为 0000 0011 , -3 的反码为 1111 1100 。
【注意】 0 的反码有两个: [+0] 反码为 0000 0000 ; [-0] 反码为 1111 1111 。
- 补码
正数的补码就是其 本身,负数的补码则是 反码加一。如(假设机器字长为 8 ): 3 的补码为 0000 0011 , -3 的补码为 1111 1101 。
【注意】:
(1)0 的补码只有一个: [0] 补码为 0000 0000 。
(2)在 8 位数据长度下 -128 ,没有原码和反码,补码为 10000000 。
5.3 为什么使用反码和补码
在使用原码进行计算的时候,对于人来说,可以轻易识别符号位,轻松知道正负,然后再对其他位来进行计算,对于计算机的设计来说,识别符号位就是一项复杂的工程了,若是能让符号位直接参与计算,那么这样就可以忽略符号位的识别了。
对于加法来说,符号位有没有影响不大,但是对于减法来说,计算机是将其转换为加法来进行运算,所以若是通过原码来进行计算(符号位直接参与计算)则:
5 - 3 = 2
= 5 + (-3)
= 0000 0101(原码) + 1000 0011(原码)
= 1000 1000(原码)
= -8显然,计算结果理论上为 2 ,但是计算机按照原码计算出来的数值为 -8 ,所以对减法来说,原码计算的方式不行,于是引入反码,若通过反码进行减法计算,则有:
5-3 = 2
= 5 + (-3)
= 0000 0101(原码) + 1000 0011(原码)
= 0000 0101(反码) + 1111 1100(反码)
= 1 0000 0001(反码)
= 0000 0001(反码) + 0000 0001(高位进位,结果要加1)
= 0000 0010(反码,符号位为0,为正数)
= 0000 0010(原码)
= 2【注意】反码计算的运算规则:从低到高位逐列进行计算。 0+0 = 0,0+1 = 1,1+1 = 0(向高位进 1) 。若最高位产生了进位,则最后得到的结果要 加 1。
但是,有一个问题出现了对于相同两个数相减,如:
1 - 1 = 0
= 1 + (-1)
= 0000 0001(原码) + 1000 0001(原码)
= 0000 0001(反码) + 1111 1110
= 1111 1111(反码)
= 1000 0000(原码)
= -0显然,计算出的结果的真值是对的,但是结果却是 -0 ,通过上边已经知道 0 的原码和反码都有 2 个,所以,用反码进行计算时遇上了 0 ,这样的结果就是不合理的了,于是,又引入了补码,则:
1 - 1 = 0
= 1 + (-1)
= 0000 0001(原码) + 1000 0001(原码)
= 0000 0001(反码) + 1111 1110(反码)
= 0000 0001(补码) + 1111 1111(补码)
= 1 0000 0000(补码)
= 0000 0000(补码,最高位进位,舍去进位)
= 0000 0000(最高位为0,是正数)
= 0这样计算结果就没得问题了。
【注意】补码计算时,若最高位产生进位,则舍去进位,注意与反码相区别。
五、小数
整数是以定点数的形式存储,那么小数呢?
1. 表示形式
小数在内存中是以 浮点数 的形式存储的。使用浮点数格式来存储 float 、 double 类型的小数。C 语言标准规定,小数在内存中转换为科学计数法的形式,然后进行存储,具体形式为:
| flt | 要表示的小数。 |
| sign | 表示 flt 的正负号,它的取值只能是 0 或 1:取值为 0 表示 flt 是正数,取值为 1 表示 flt 是负数。 |
| base | 基数,或者说进制,它的取值大于等于 2(例如,2 表示二进制、10 表示十进制、16 表示十六进制……)。数学中常见的科学计数法是基于十进制的,例如 6.93 × 10 13 ;计算机中的科学计数法可以基于其它进制,例如 1.001 × 2 7 就是基于二进制的,它等价于 1001 0000。 |
| mantissa | 尾数,或者说精度,是 base 进制的小数,并且 1 ≤ mantissa < base,这意味着,小数点前面只能有一位数字; |
| exponent | 指数,是一个整数,可正可负,并且为了直观一般采用十进制表示。 |
- base = 10
根据上边的表格,可以知道:
sign = 0
mantissa = 1.9625
base = 10
exponent = 1所以有:
- base = 2:先将 19.625 转换为二进制表示的形式 1 0011.101 。
/* 整数部分 19 */
19 / 2 = 9 ... 1 ----- 取 1 剩 9
9 / 2 = 4 ... 1 ----- 取 1 剩 4
4 / 2 = 2 ... 0 ----- 取 0 剩 2
2 / 2 = 1 ... 0 ----- 取 0 剩 1
1 / 2 = 0 ... 1 ----- 取 1
所以19(D) = 10011(B)
/* 小数部分 0.625 */
0.625 x 2 = 1.250 ----- 取 1 剩 0.250
0.250 x 2 = 0.500 ----- 取 0 剩 0.500
0,500 x 2 = 1.000 ----- 取 1 剩 0.000
所以0.625(D) = 101(B)
故 19.625(D) = 1 0011.101(B)有以下形式:
根据上边的表格,可以知道:
19.625 = 1 0011.101
sign = 0
mantissa = 1.0011101
base = 2
exponent = 4所以有:
【说明】当基数(进制) base 确定以后,指数 exponent 实际上就是小数点的移动位数:
- exponent 大于零, mantissa 中的小数点右移 exponent 位即可还原小数的值;
- exponent 小于零, mantissa 中的小数点左移 exponent 位即可还原小数的值。
2. 内存分配
C 语言中常用的浮点数类型为 float 和 double 。其中 float 始终占用 4 个字节, double 始终占用 8 个字节。浮点数存储时的内存被分成三部分,分别用来存储符号 sign 、尾数 mantissa 和指数 exponent ,当浮点数的类型确定后,每一部分的位数就是固定的。

3. 存储方式
在计算及内部,小数也会被转化为二进制,那么二进制的浮点数是如何存储的呢?
3.1 符号的存储
符号的存储与整数类似,单独分配出一个位( Bit )来,用 0 表示正数,用 1 表示负数。
3.2 尾数的存储
还是以 19.625 为例,我们上边已经知道了转换后的尾数部分为 1.0011101 。
小数转换为浮点格式的二进制数后,尾数部分的取值范围为 1 ≤ mantissa < 2 ,这说明尾数的整数部分一定为 1 ,是一个恒定的值,这样就无需在内存中体现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可。
对于 1.0011101 ,就是把 0011101 放入内存就可以了。
如果 base 采用其它进制,那么尾数的整数部分就不是固定的,它有多种取值的可能,以十进制为例,尾数的整数部分可能是 1~9 之间的任何一个值,这样一来尾数的整数部分就不能省略了,必须在内存中体现出来。所以将 base 设置为二进制就可以节省掉一个位( Bit )的内存,
3.3 指数的存储
指数是一个整数,它有正负之分,所以我们不仅需要存储值,还要存储符号。
short 、 int 、 long 等类型的整数在内存中的存储采用的是补码加符号位的形式,数值在写入内存之前必须先进行转换,读取以后还要再转换一次。但是为了提高效率,避免繁琐的转换,指数的存储并没有采用补码加符号位的形式。那是怎么进行的呢?
以 float 为例,它的指数部分占 8 位,可以表示 0~255 的值。

那么我们就取中间值 127 ,指数在写入内存前先加上 127 ,读取时再减去 127 ,此时正负就很容易区分了。例如 19.625 转换后的指数为 4 ,用 4 加上 127 ,结果就是 131 ,转换为二进制就是 1000 0011 ,这就是指数部分在内存中二进制形式。
那中间值是怎么取的?设中间值为 median ,指数部分占用的内存为 n 位,那么中间值的计算方法如下:
对于 float ,中间值为
我们可以将内存中存储的指数命名为 exp ,那么有内存中的指数等于真实指数加上中间值,即:
4. 验证存储方式
小数的存储还是比较复杂的,我们可以来验证一下,后边我们会学习到结构体,也会接触到位域。
#include <stdio.h>
/* 浮点数结构体 */
/* 小端模式下,靠下的为高位,靠上的为低位*/
typedef struct Float_data
{
unsigned int nMant : 23; /* 尾数部分 */
unsigned int nExp : 8; /* 指数部分 */
unsigned int nSign : 1; /* 符号位 */
}FP_SINGLE;
int main(int argc, const char *argv[])
{
float temp = 19.625;
FP_SINGLE *p = (FP_SINGLE*)&temp;
printf("sign: %x\n", p->nSign);
printf("exp: %x\n", p->nExp);
printf("mant: %x\n", p->nMant);
return 0;
}然后在命令行运行以下命令:
gcc test.c -Wall
./a.out会看到以下输出:
sign: 0
exp: 83
mant: 1d0000我们无法打印二进制数据,但是可以用十六进制数代替,换算一下即可:
sign: 0
exp: 83 = 1000 0011
mant: 1d0000 = 0001 1101 0000 0000 0000 0000六、字符
之前的时候,我一直以为 C 语言中的字符都是以 ASCII 码的形式存储在内存中,但是后来发现,不仅仅是这样。在 C 语言中,只有 char 类型的窄字符才使用 ASCII 编码, char 类型的窄字符串、 wchar_t 类型的宽字符和宽字符串都不使用 ASCII 编码。
1. ASCII 码
上边我们已经见识了一张 ASCII 码表,那么这究竟是什么呢?
一个二进制位( Bit )有 0 和 1 两种状态,一个字节( Byte )有 8 个二进制位,有 256 种状态,每种状态对应一个符号,就是 256 个符号,从 0000 0000 到 1111 1111 。
计算机也只认识 0 和 1 ,那么对于字符来说也就需要转化为二进制的形式了。当初计算机是诞生于美国,在考虑计算机显示文字的问题时,美国制定了一套英文字符与二进制位的对应关系,称为 ASCII ( American Standard Code for Information Interchange ,美国信息交换标准代码)。
标准 ASCII 码规定了 128 个英文字符与二进制的对应关系,占用一个字节(实际上只占用了一个字节的后面 7 位,最前面 1 位统一规定为 0 ),这一位被称为 奇偶校验位。后边的 128 个被称为扩展 ASCII 码。
什么是奇偶校验位?
所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中 1 的个数必须是奇数,若非奇数,则在最高位 b7 添 1 ;偶校验规定:正确的代码一个字节中 1 的个数必须是偶数,若非偶数,则在最高位 b7 添 1 。
2. 宽字符与窄字符
上边我们提到了宽字符和窄字符,这又是什么呢?宽窄字符是与一 个字符所占的字节数有关,如果一个字符只占 1 个字节,那么它就是窄字符,一个宽字符通常占 2 个字节。在 C 语言中, char 类型就是占 1 个字节,它属于窄字符,这种类型的字符可以与 ASCII 一一对应。
我们在以往的编程中,会发现,在 C 语言中是可以使用中文的,但是我们的中文可是远远多于 256 的,也用 ASCII 来对应的话,显然是不可能的。那么中文怎么存储呢?
3. 宽字符的存储
C 语言是一门全球化的编程语言,它支持世界上任何一个国家的语言文化,包括中文、日语、韩语等。只是我们使用英文更多些罢了,而且似乎也会更方便些。上边我们提到了一个问题,那就是中文字符怎么存储,其实不仅仅是中文字符,除了 char 类型的窄字符,那些宽字符怎么存储呢?
需要解决的三个问题:
- (1)足够长的数据类型
char 只能处理 ASCII 编码中的英文字符,是因为 char 类型太短,只有一个字节,容纳不下我们的几万个汉字,要想处理中文字符,必须得使用更长的数据类型。
一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数(就类似于 ASCII 码),所以我们可以用整数类型来存储一个字符,比如 unsigned short 、 unsigned int 、 unsigned long 等。
- (2)选择包含中文的字符集
C 语言规定,对于汉语等 ASCII 编码之外的单个字符,也就是专门的字符类型,要 使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32 ,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。
Unicode 编码则是采用 双字节 16 位来进行编号,可编 65536 字符,基本上包含了世界上所有的语言字符,它也就成为了全世界一种通用的编码。
在真正实现时,微软编译器(内嵌于 Visual Studio 或者 Visual C++ 中)采用 UTF-16 编码,使用 2 个字节存储一个字符,用 unsigned short 类型就可以容纳。 GCC 、 LLVM/Clang (内嵌于 Xcode 中)采用 UTF-32 编码,使用 4 个字节存储字符,用 unsigned int 类型就可以容纳。
- (3)跨平台
不同的编译器可以使用不同的整数类型。如果代码使用 unsigned int 来存储宽字符,那么在微软编译器下就是一种浪费;如果代码使用 unsigned short 来存储宽字符,那么在 GCC 、 LLVM/Clang 下就不够。
为了解决上边的三个问题, C 语言推出了一种新的类型,叫做 wchar_t 。 w 是 wide 的首字母, t 是 type 的首字符, wchar_t 的意思就是宽字符类型。
wchar_t 长度是多少?wchar_t 的长度 由编译器决定:
- 在微软编译器下,它的长度是 2 ,相当于 unsigned short ;
- 在 GCC 、 LLVM/Clang 下,它的长度是 4 ,相当于 unsigned int 。
4. 宽字符的使用
wchar_t 类型位于 <wchar.h> 头文件中,它使得代码在具有良好移植性的同时,也节省了不少内存。要想使用宽字符的编码方式,就得加上 L 前缀,加上 L 前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。
#include <stdio.h>
#include <wchar.h>
#include <locale.h> /* setlocale 函数 */
int main(int argc, const char *argv[])
{
/* 将本地环境设置为 UTF-8 简体中文 */
setlocale(LC_ALL, "zh_CN.UTF-8");
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'繁'; //中文汉字
wchar_t d = L'华'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'♥'; //特殊符号
wchar_t g = L'༄'; //藏文
wprintf(L"Wide chars: %lc %lc %lc %lc %lc %lc %lc\n", //必须使用宽字符串
a, b, c, d, e, f, g);
return 0;
}然后在命令行运行以下命令:
gcc test.c -Wall
./a.out会看到以下输出:
Wide chars: A 9 繁 华 。 ♥ ༄【注意】
(1)宽字符的输出要用到 wprintf 函数。
(2)注意设置语言环境,否则输出的可能全是 ? 。