青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-
友链 搜索文章 >>

青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-

【C语言学习笔记】二、数据类型


阅读数: 0次    2024-11-14
字数:10.7k字 | 预计阅读时长:41分钟

1. 注释(Comment)

行注释

1
// 这是一条行注释

块注释

1
2
3
4
/*这是一个块注释
块注释
块注释
*/

被注释掉的语句不会被程序运行,用来对代码或逻辑等进行标注,提高代码的可读性。

2. 进制

2.1 二进制

只有0和1两个数字

十进制:逢十进一

二进制:逢二进一

扩展开来说,0表示false,1表示true

2.2 二进制转十进制

从右到左用二进制的每个数去乘以2的相应次方(次方从0开始),再将其每个数进行相加。

例如:二进制(1101)2(1101)_2(1101)2​转换为十进制

1×20+0×21+1×22+1×23=1+4+8=131\times2^{0}+0\times2^{1}+1\times2^{2}+1\times2^{3}=1+4+8=131×20+0×21+1×22+1×23=1+4+8=13

则二进制(1101)2(1101)_2(1101)2​对应的十进制数为(13)10(13)_{10}(13)10​。

2.3 十进制转二进制

使用“除2取余,逆序排列”法。

用十进制整数除2,可以得到一个商和余数;再用商去除2,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次逆序排列起来组合成二进制数。

例如:十进制(17)10(17)_{10}(17)10​转换为二进制

1

则十进制(17)10(17)_{10}(17)10​对应的二进制数为(10001)2(10001)_2(10001)2​。

对于十进制转其他进制,采用“除(对应进制)取余,逆序排列”法。

2.4 英文表示

二进制数:Binary number

十进制数:Decimal number

十六进制数:Hexadecimal number

2.5 补充知识

详细介绍各种进制之间的转换、小数的进制转换、独热码、BCD码、格雷码、纠错码、校验码等:

2、进制详解(整数部分) - 沐风半岛 - 博客园 (cnblogs.com)

详细介绍带符号的二进制数、原码、反码、补码等:

3、带符号的二进制数(原码、反码、补码) - 沐风半岛 - 博客园 (cnblogs.com)

对于使用补码的更深层次原因以及模算数、进位溢出的介绍:

二进制:有符号整数编码及加减法运算 // 圆方 (lumin.tech)

3. int类型在内存中的表示

二进制中的一个数字位称为binary digit,简称比特(bit)。计算机领域中,我们使用比特作为单位来表示数据量,还会用到一种叫字节(byte)的单位。通常一个字节代表8比特,绝大多数CPU都是以字节为单位处理数据的。内存地址大多也是为每字节赋予一个地址,称为字节编址方式。由8比特组成一个字节是出于2的8次方表达的范围(0~255)比较适合表达文字(英文字母、符号、控制符等)的考虑。

1Bytes = 8bit

1KB = 1024Bytes

1MB = 1024KB

1GB = 1024MB

1K 等于 1024 时1K 等于 1000 时
1 [K]1 024 (2的10次方 )1 000 (10的3次方 )
1 [M]1 048 576 (2的20次方 )1 000 000 (10的6次方 )
1 [G]1 073 741 824 (2的30次方 )1 000 000 000 (10的9次方 )
1 [T]1 099 511 627 776 (2的40次方 )1 000 000 000 000 (10 的12次方 )

一个int类型的数占4个字节(Bytes),每个字节为8位(bit),也就是一个int类型共有32位。

其中最高位为符号位(0为正数,1为负数),其余31位为数据位,则一个int类型的数可以取到的范围为:−2−31→231−1-2^{-31}\to2^{31}-1−2−31→231−1(正数中去除0)。其中负数用补码表示,有关补码的知识可参考"2-5 补充知识"中的链接,点击跳转:2-5 补充知识。

各种数据类型范围可参考:数据类型范围 | Microsoft Learn

4. printf()输出详解

4.1 参数遗漏

使用printf()函数时,要确保转换说明的数量与待打印值的数量相等。

1
2
3
int number = 10;
printf("%d number is: %d\n", number); // 遗漏一个参数
printf("number is: %d\n", number); // 参数无遗漏

当出现参数有遗漏时,被遗漏的打印出的值是内存中的任意值。

4.2 八进制与十六进制的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int number = 10;

// 以十进制(Decimal)表示
printf("%d\n", number); // 输出:10

// 以八进制(Octal)表示
printf("%o\n", number); // 输出:12
// 输出增加“#”标志,使输出显示八进制前缀"0"
printf("%#o\n", number); // 输出:012

// 以十六进制(Hexadecimal)表示(小写/lowercase)
printf("%x\n", number); // 输出:a
// 输出增加“#”标志,使输出显示小写十六进制进制前缀"0x"
printf("%#x\n", number); // 输出:0xa

// 以十六进制(Hexadecimal)表示(大写/uppercase)
printf("%X\n", number); // 输出:A
// 输出增加“#”标志,使输出显示大写十六进制进制前缀"0X"
printf("%#X\n", number); // 输出:0XA

一个转换规范由以下形式的可选和必需字段组成:

%[flags] [width] [.precision] [size] type

即:%[标志] [宽度] [.精度] [大小] 类型

其中设置显示进制前缀的#属于其中的标志字符,除此之外,标志字符还有-(设置左对齐)、+(输出值加正负号前缀)、0(前导零)等,这些标志字符的用法和作用以及其余字符的使用规范,可以参考微软官方链接:格式规范语法:printf 和 wprintf 函数 | Microsoft Learn

5. scanf_s()初步认识

输入一般使用scanf_s(),scanf()由于不安全的问题,已被舍弃。

注意输入时,想要进行输入的参数前要加&(取地址符)

1
2
3
int number = 0;				// 初始化
scanf_s("%d", &number); // 输入
printf("%d\n", number); // 将输入的数字输出

输入输出多个数字:

1
2
3
int number1 = 0, number2 = 0, number3 = 0;				// 初始化
scanf_s("%d %d %d", &number1,&number2,&number3); // 输入
printf("%d %d %d\n", number1, number2, number3); // 输出

6. 整型溢出(integer overflow)

int类型的值占4Byte,1Byte = 8bit也就是一共是32位数据,其中最高位为符号位,其余位为数据位,符号位为0时代表正数,符号位为1时代表负数。一个int类型的数可以取到的范围为:−2−31→231−1-2^{-31}\to2^{31}-1−2−31→231−1(正数中去除0)。其中负数用补码表示。

因此,int类型数据的最大值为231−12^{31}-1231−1,转换为二进制,即第一位符号位为0,其余31位数据为全为1,此时让这个数字再加1,则发生了整型溢出的现象。

此时,二进制下,第一位为1,其余31位全为0。第一位为1代表此时的值为负数,由于负数是由补码表示的,将此第一位为1,剩下31位0的补码转换为原码,即先减去1,得到反码为第一位为0,其余31位为1,然后按位取反得到原码为第一位为1,其余31位为0,即为2−312^{-31}2−31,前面添上负号得到−2−31-2^{-31}−2−31。

可以用八位数据来进行示例方便理解:

八位正数的最大值为255(0111 1111),加一得到用补码表示的负二进制数(1000 0000),这个数减一得到反码(0111 1111),按位取反得到源码256(1000 0000),由于这个数是负数,在前面添一个负号得到真值-256(-1000 0000),为负数的最小值。

有关原码与补码的相关知识,可以参考:

3、带符号的二进制数(原码、反码、补码) - 沐风半岛 - 博客园 (cnblogs.com)

二进制:有符号整数编码及加减法运算 // 圆方 (lumin.tech)

因此int类型的值中,正数的最大值加1,发生整型溢出得到的是负数的最小值。

在此基础上加一,相当于负数的绝对值减一,一直到0。

可以通过编写代码验证:

1
2
3
4
5
6
7
8
9
10
11
int number = 2147483646;
printf("%d\n", number); // 输出:2147483646

number++;
printf("%d\n", number); // 输出:2147483647

number++;
printf("%d\n", number); // 输出:-2147483648

number++;
printf("%d\n", number); // 输出:-2147483647

当负数增加到-1时,对应的二进制补码为32位1,再次增加1时,会变成33位,第一位是1,其余32位全为0的数,但由于int类型只能容纳32位,所以其中最高位的1会被截断并丢弃,剩下32位0,也就是重新变成真值的0。

1
2
3
4
5
6
7
8
9
10
11
int number = -2;
printf("%d\n", number); // 输出:-2

number++;
printf("%d\n", number); // 输出:-1

number++;
printf("%d\n", number); // 输出:0

number++;
printf("%d\n", number); // 输出:1

整数溢出的表现形式可分为:无符号整数上溢、无符号整数下溢、有符号整数上溢、有符号整数下溢。

整数溢出错误会导致软件运算结果出错,1996年亚利安5号运载火箭爆炸,2004年Comair航空公司航班停飞事故都是整数溢出造成的。

目前大多数计算机都有两个专用的处理器标识位来检查溢出情况。

7. unsigned int

值的范围:0到4,294,967,295(232−12^{32}-1232−1)

在printf()输出时使用%u

一般for循环中的index使用unsigned int类型

8. short与unsigned short

short类型占两个字节,范围为-32,768到32,767(−215-2^{15}−215到215−12^{15}-1215−1),在printf()输出时使用%hd

与unsigned int类似,unsigned short的范围为0到65,535(0到216−12^{16}-1216−1),在printf()输出时使用%hu

9. long、long long

long类型比较特殊,占的字节数由CPU架构和操作系统决定,在x86架构中占4个字节,与int类型相同,在x64架构中占8个字节,它的诞生是为了在x86与x64开发过程中做一些适配。

但当使用Microsoft的Visual Studio在Windows平台下查看时,不管输出选择是x64还是x86,long类型的占的字节数都是4。这是为了对老架构有更好的兼容性。在Unix或者类Unix操作系统下查看时,x64下的long类型占8个字节。

long在printf()输出时使用%ld,unsigned long在printf()输出时使用%lu

long long类型占8个字节,在x86与x64中都一样。

long long在printf()输出时使用%lld,unsigned long long在printf()输出时使用%llu

10. size_t类型与sizeof()的使用

1
2
3
4
5
6
7
8
9
10
11
// 输出short类型占的字节数
printf("%zu\n", sizeof(short)); // 输出:2

// 输出int类型占的字节数
printf("%zu\n", sizeof(int)); // 输出:4

// 输出long类型占的字节数
printf("%zu\n", sizeof(long)); // 输出:4

// 输出long long类型占的字节数
printf("%zu\n", sizeof(long long)); // 输出:8

size_t是一个比较特殊的数据类型,可以对计算机里的信息块也就是字节进行计数,在printf()输出时使用%zu,用来表示各个数据类型占的字节数。

sizeof()测量数据类型占的字节数,是在编译时候测量的,架构不同时,输出的结果可能不同。

11. 实际开发过程中的整型声明

在微软官方文档数据类型范围 | Microsoft Learn中,会发现微软定义了一些类型名称,例如__int16、__int32等,这两个分别对应占了两个字节(16bit)的short类型、占了四个字节(32bit)的int类型,使用这种声明可以更好地组织程序,也方便理解。但是这些定义只有在Windows平台下的Microsoft Visual Studio内才可以使用,对于跨平台开发非常不友好。

C99标准之后,真正企业中使用的类型声明是来自一个C语言的标准库文件stdint.h,这个库文件里包含了一些常用的类型定义,使用此头文件里的类型,例如int16_t、int32_t、uint32_t等进行声明,可以在方便组织代码的同时对跨平台应用有很好的支持。

2

如果想使用这些整型声明,需要包含stdint.h库:

1
#include <stdint.h>

在初始化变量时候可以加字面量,这是对数字进行标记,可以提醒开发者这个值的限度,也可以告诉编译器这个数字的类型,并根据这个类型来检查与存储,例如:

1
uint32_t myUInt32 = 4294967295U;	// 在数字后加“U”作为字面量,标注这个数字为无符号

当编译器看到数字后边有字面量“U”,会直接将这个数字看作uint32_t(unsigned int)处理;

也有其他字面量,例如“LL”,编译器会将这个数字看作int64_t(long long)处理;例如“ULL”,编译器会将这个数字看作uint64_t(unsigned long long)处理。这个声明下为防止long类型在不同环境下的不同表示,没有对其进行定义。确保在不同平台上避免溢出问题。

但是,这个新的标准也不能盲目使用,应该先确定项目面向的运行环境是怎么样的,一些情况下,用int可能会比用int32_t更好。例如GitHub上的Linux源代码中,就有很多文件还是使用int而不是新的int32_t可能是有性能、兼容性等多方面考虑。

有的项目也会在自己写的头文件中使用typedef来自定义数据类型,方便使用,例如:

1
2
typedef unsigned int u32;
typedef unsigned short u16;

12. 类型转换(Type Conversion)

隐式类型转换:

1
2
3
4
5
uint16_t smallNum = UINT16_MAX;
uint32_t bigNum = smallNum;

printf("%hu\n", smallNum); // 输出:65535
printf("%u\n", bigNum); // 输出:65535

显示类型转换(强制转换):

1
2
3
4
5
uint16_t smallNum = UINT16_MAX;
uint32_t bigNum = (uint32_t)smallNum;

printf("%hu\n", smallNum); // 输出:65535
printf("%u\n", bigNum); // 输出:65535

虽然隐式类型转换和显式类型转换的效果是一样的,但为了保证代码的可读性与后续维护性,最好全部使用显式类型转换。

在包含头文件stdint.h时,使用字母大写的UINT16_MAX、INT32_MAX可以直接获取对应类型的最大值。

头文件stdint.h中包含的部分定义:

3

类型转换中几种容易导致数据溢出的情况:

  • 无符号到有符号:

    1
    2
    3
    4
    5
    uint32_t uNum = UINT32_MAX;
    int32_t dNum = (int32_t)uNum;

    printf("%u\n", uNum); // 输出:4294967295
    printf("%d\n", dNum); // 输出:-1
  • 有符号到无符号(扩展负数):

    1
    2
    3
    4
    5
    int16_t hdNum = -1;
    uint32_t uNum = (uint32_t)hdNum;

    printf("%hd\n", uNum); // 输出:-1
    printf("%u\n", dNum); // 输出:4294967295
  • 大范围到小范围:

    1
    2
    3
    4
    5
    int64_t lldNum = INT64_MAX;
    int32_t dNum = (int32_t)lldNum;

    printf("%lld\n", lldNum); // 输出:9223372036854775807
    printf("%d\n", dNum); // 输出:-1

这种数据类型转换导致的错误,不一定会得到”-1“,这种错误本质上就是数据溢出,可以参考[6. 整型溢出(integer overflow)](#6.-整型溢出(integer overflow))来找到数据转换过程中数据溢出的规律。

一般只要变量的数据不超出类型限度,转换是可以成功的,例如大范围到小范围:

1
2
3
4
5
int64_t lldNum = 123;
int32_t dNum = (int32_t)lldNum;

printf("%lld\n", lldNum); // 输出:123
printf("%d\n", dNum); // 输出:123

13. 固定宽度整数类型的格式化宏输出(inttypes.h)

使用printf()进行输出时,很容易将各种格式的输出指代代码记错,比如int16_t是%hd、uint64_t是%llu等,这种情况下,引入头文件inttypes.h可以直接在%后使用类似" PRId16 "、" PRIu64 "等进行输出。

1
2
3
4
5
6
7
8
int16_t d16Num = INT16_MAX;
uint64_t u64Num = UINT64_MAX;

//printf("%hd\n", d16Num);
printf("%" PRId16 "\n", d16Num);

//printf("%llu\n", u64Num);
printf("%" PRIu64 "\n", u64Num);

头文件inttypes.h中包含的部分定义:

4

14. least和fast整型的企业用途与区别

  • (u)intN_t:标准整数类型(固定宽度整数类型),固定占用N位,不可以越界,适用于需要精确数据大小的场景。
  • (u)int_leastN_t:至少有N位,可能更多,适用于需要保证最小存储容量的可移植代码场景。
  • (u)int_fastN_t:至少有N位,但是选择运算最快的类型,适用于需要性能敏感的场景。

15. 浮点数

也可以称为是小数,计算机中储存浮点数可以分为符号位、阶码与尾数(基数)。

数据在计算机中都是以二进制进行存储的,IEEE 754标准(电气和电子工程师协会)是目前通用的浮点数表示规范,它为单精度(float)、双精度(double)和扩展精度(如long double)浮点数定义了一套标准化的二进制编码方案。该标准的核心思想是将一个浮点数表示为:

V=(−1)S×M×2EV = (-1)^S \times M \times 2^EV=(−1)S×M×2E

其中:

  • ( S ) 是符号位,表示数值的正负,取值为0(正)或1(负)。
  • ( M ) 是尾数(也称 significand 或 mantissa),表示数值的非零小数部分,通常采用二进制小数形式,包含一个隐含的最高位“1”(对于非规范化数则没有此隐含位)。
  • ( E ) 是阶码(exponent),表示数值的二进制指数,决定了数值的绝对大小。

float类型(单位bit)

符号位(S)阶码(E)尾数(M)
1823

double类型(单位bit)

符号位(S)阶码(E)尾数(M)
11152

因为在任何区间内(如,1.0到2.0之间)都存在无穷多个实数,所以计算机的浮点数不能表示区间内所有的值。浮点数通常只是实际值的近似值。例如,7.0可能被储存为浮点值6.99999。

16. 浮点数存储原理

对于单精度浮点数,使用IEEE(电气和电子工程师学会)格式,具有4个字节,共有32位(bit)数据,其中第1位是符号位,随后8位是阶码位,最后的23位是尾数(基数)位。

在IEEE 754标准中,规定阶码存储采用指数偏移(excess notation)的方法,单精度浮点数存储单元中的指数部分一共有八位,可以表示0-255,一共256个数字,但指数一般包括正数和负数,这是如何存储的呢?标准中规定,将127作为中间数,小于127的数字称为左偏移,表示负幂次,比127小多少,即表示负多少次幂;大于127的数字称为127,表示正幂次,比127大多少,即表示正多少次幂。

例如,指数为-5次幂时,只需要让指数部分为122即可;指数为7次幂时,只需要让指数部分为134即可。

双精度的偏移量的中间数为1023。

可以通过这个视频加深理解:【计算机知识】定点数与浮点数(2)浮点数法表示方法!_哔哩哔哩_bilibili

根据指数和尾数的不同组合,浮点数可以分为规范化和非规范化两种形式:

  • 规范化浮点数:指数不为全0或全1,且尾数的最高有效位为隐含的“1”。也就是尾数部分转换为二进制后,其数字在1.0-2.0之间。这种形式下的单精度浮点数中的尾数位数加上一个隐含的前导1后共有24位,具有最大的动态范围和精度。
  • 非规范化浮点数:指数全为0(对于某些实现可能允许一个特殊值),此时尾数表示为纯小数,没有隐含的最高位“1”。IEEE 754标准规定:非规范形式的浮点数的指数偏移值比规范形式的浮点数的指数偏移值小1,即非规范的偏移值为126。例如,最小的规范形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规范的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。因此这个时候单精度浮点数大小为(−1)S×M×2−126(-1)^S \times M \times 2^{-126}(−1)S×M×2−126。非规范化浮点数主要用于表示非常接近于0的小数值,其动态范围较小,但能提供更精细的低数值表示,但也更容易出现丢失精度的现象。

如果存储比精度更重要,考虑对浮点变量使用float类型。如果精度是最重要的条件,使用double类型。

17. float类型的定义与输出

定义时候在数值后面加上f或者F代表这个数值是float类型,有利于增加代码的可读性。

1
2
3
4
5
6
7
8
float temperature = 36.5f;

float humidity = 48.3f;

float speed_of_sound = 343.5e2f; // 科学计数法,表示:343.5 * 10^2

// 多重定义(只有多个变量为同一种类型时,可以作为同一组时候使用)
float length = 12.34f, width = 23.45f, height = 34.56f;

真实开发中不可以将变量名命名为一些无意义的字符,例如a、b、c、d等,变量名如果没有意义,它就不应该被定义。

浮点数的输出使用%f:

1
2
3
4
5
6
7
printf("Temperature: %f\n", temperature);			// 输出:36.500000

printf("Humidity: %f\n", humidity); // 输出:48.299999

printf("Speed of Sound: %f\n", speed_of_sound); // 输出:34350.000000

printf("Dimensions (L × W × H): %f × %f × %f\n", length, width, height); // 输出:12.340000、23.450001、34.560001

根据输出的结果可以发现,输出的5值与定义时候值的大小有偏差,这种情况便是浮点数存储的丢失精度现象。

18. float丢失精度以及%E与%A科学计数法输出

一个计算机中丢失精度的经典问题:0.1 + 0.2 = 0.30000000000000004

出现丢失精度现象的根本原因是,有些十进制小数无法转换为二进制数:

6

在小数点后四位,连续的二进制数对应的十进制数是不连续的,因此只能增加位数来尽可能近似地表示。例如十进制0.1转换为二进制小数,得到的是0.0001˙1˙0˙0˙0.000 \dot1 \dot1 \dot0 \dot00.0001˙1˙0˙0˙这样的一个循环二进制小数,使用IEEE754表示如下图:

7

同样的方法,0.2用单精度浮点数表示是:0.20000000298023223876953125。所以,0.1 + 0.2的结果是:0.300000004470348358154296875。

8

这也是0.1 + 0.2 = 0.30000000000000004这一经典问题的答案:0.1和0.2作为浮点数,在内存中存储的时候丢失了精度。关于这个问题甚至还有一个专门的网站:Floating Point Math(https://0.30000000000000004.com/)

在使用printf()输出double类型的数据时,使用%lf。而且由于double类型是双精度的,对数字描述的精确度更高,丢失精度的情况也更少:

1
2
3
4
float numFloat = 48.3f;
double numDouble = 48.3;
printf("%f\n", numFloat); // 输出:48.299999
printf("%lf\n", numDouble); // 输出:48.300000

小数默认为double类型,就和整数默认为int类型一样,因此无需像float类型一样在数字之后加后缀。

在输出float类型或double类型时,在%后加.n即可使输出数据保留n位小数:

1
2
3
4
float numFloat = 48.3f;
double numDouble = 48.3;
printf("%.3f\n", numFloat); // 输出:48.300
printf("%.2lf\n", numDouble); // 输出:48.30

如果想要使用printf()打印%,在使用时候写%%即可:

1
2
float numFloat = 48.3f;
printf("Using %%f: %.3f\n", numFloat); // 输出:Using %f: 48.300

C99中引入了一些新特性,比如浮点数输出时,可以使用科学计数法输出:

1
2
3
4
5
6
7
8
9
10
11
float num = 123.456;

printf("Using %%f: %f\n", num); // 输出:123.456001

// %e %E 科学计数法格式化输出
printf("Using %%e: %e\n", num); // 输出:1.234560e+02
printf("Using %%E: %E\n", num); // 输出:1.234560E+02

// %a %A 十六进制浮点数 p计数法
printf("Using %%a: %a\n", num); // 输出:0x1.edd2f20000000p+6
printf("Using %%A: %A\n", num); // 输出:0X1.EDD2F20000000P+6

19. 浮点数overflow上溢与underflow下溢

就像int类型一样,浮点数超过能表示的限度也会出现问题。

在包含头文件float.h时,使用字母大写的FLT_MAX、FLT_MIN、DBL_MAX可以直接获取对应类型的最值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float max_float = FLT_MAX;
printf("Maximum Float: %e\n", max_float); // 输出:3.402823e+38

// OverFlow 上溢
// 此处必须是1000.0f而不能是1000,因为1000默认是int类型的整型
// 浮点类型与整型相乘,会包含隐式类型转换,出现丢失数据的现象
float overflow = max_float * 1000.0f;
printf("Overflow: %e\n", overflow); // 输出:inf

float min_float = FLT_MIN;
printf("Minimum Positive Float: %e\n", min_float); // 输出:1.175494e-38

// UnderFlow 下溢
float underflow = min_float / 1000.0f;
printf("Underflow: %e\n", underflow); // 输出:1.175549e-41

上溢中的输出值inf是infinity的缩写,是浮点数中的一个特殊值,代表无穷大,此时说明数据发生了上溢。

下溢就是想要表示的数的值太小了,和0太接近了,超过了float类型能表示的最小精度,这种情况下,计算机就只好把尾数位向右移,空出第一个二进制位,将隐含的最高位也作为数据的表示位,成为非规范化浮点数。但是与此同时,造成丢失精度,损失了原来末尾有效位上面的数字,然后C编译器本身会产生一些十分接近原来丢失这位的值补在后面,具体生成的值无法控制与计算。

20. Infinity与Nan

20.1 Infinity

当指数位全是1,尾数位全是0时,这样的浮点数表示无穷。根据符号位,有正无穷和负无穷(+infinity和-infinity)。为什么需要无穷?因为计算机资源的限制,没法表示所有的数,当一个数超过了浮点数的表示范围时,就可以用infinity来表示。而数学中也有无穷的概念。

在包含头文件math.h时,使用字母大写的INFINITY可以直接获取infinity的值。

1
2
3
4
5
6
7
8
9
10
11
12
// 正无穷大
float positive_infinity = INFINITY;
printf("Positive Infinity: %f\n", positive_infinity); // 输出:inf

// 负无穷大
float negative_infinity = -INFINITY;
printf("Negative Infinity: %f\n", negative_infinity); // 输出:-inf

// 除以0产生的无穷大
float num = 1.0f;
float infinity = num / 0.0f;
printf("1.0 / 0.0 = %f\n", infinity); // 输出:inf

20.2 Nan

Nan是not-a-number的缩写,即不是一个数。为什么需要它?例如,当对-1进行开根号时,浮点数不知道如何进行计算,就会使用Nan,表示不是一个数。

Nan的具体内存表示是:指数位全是1,尾数位不全是0。

在包含头文件math.h时,使用函数sqrt()可以计算括号里的值的平方根。

1
2
3
4
5
6
7
8
// 0.0 / 0.0产生的值叫Nano
float num = 0.0f;
float nan = num / 0.0f;
printf("0.0 / 0.0 = %f\n", nan); // 输出:-nan(ind)

// 对负数开根号产生的Nan
float negative_sqrt = sqrt(-1.0f);
printf("sqrt(-1.0f) = %f\n", negative_sqrt); // 输出:-nan(ind)

ind是indeterminate的缩写,即无法确定是什么。

21. 最近偶数舍入(银行家舍入)标准

对于舍入,可以有很多种规则,可以向上舍入,向下舍入,向偶数舍入。如果我们只采用前两种中的一种,就会造成平均数过大或者过小,实际上这时候就是引入了统计偏差。如果是采用偶数舍入,则有一半的机会是向上舍入,一半的机会是向下舍入,这样可以避免统计偏差。

因此IEEE 754标准采用最近偶数舍入标准(Round Nearest, ties to Even(RNE)),也叫银行家舍入。

舍入的规则需要区分三种情况:

  • 当具体的值大于中间值的时候,向上舍入
  • 当具体的值小于中间值的时候,向下舍入
  • 当具体的值等于中间值的时候,向偶数舍入

向偶数舍入指的是要保留的最低有效位为偶数,具体规则:

  • 要保留的最低有效位如果为奇数,则向上舍入
  • 要保留的最低有效位如果为偶数,则向下舍入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 当具体的值大于中间值,向上舍入
float num = 3.16;
printf("%.1f\n", num); // 输出:3.2

// 当具体的值小于中间值,向下舍入
num = 3.14;
printf("%.1f\n", num); // 输出:3.1

// 当具体的值等于中间值,向偶数舍入
// 要保留的最低有效位为奇数,向上舍入
num = 3.15;
printf("%.1f\n", num); // 输出:3.2
// 要保留的最低有效位为偶数,向下舍入
num = 3.25;
printf("%.1f\n", num); // 输出:3.2

22. double、long double

double类型占八个字节。float相当于一辆跑车,速度快但精度不高,容易出现丢失精度的现象;double可以比作一辆卡车,速度慢,但精度很高,可以表示的数的范围也更大。具体表示方法可以参考15. 浮点数与16. 浮点数存储原理。

long double类型定义变量时,数字后面必须加L,使用printf()输出时,采用%Lf(L必须大写)作为占位符。

1
2
long double ld_val = 123.321413L;
printf("ld_val: %Lf\n", ld_val);

23. float和double有效精度对比原理与计算

使用代码示例来对比float和double的有效精度。

在包含头文件float.h时,使用字母大写的FLT_DIG、FLT_DIG可以直接获取对应类型的最大有效精度。

1
2
3
4
5
6
7
8
float float_num = 1.0 / 3.0;
double double_num = 1.0 / 3.0;

printf("Float number: %.20f\n", float_num); // 输出:0.33333334326744079590
printf("Double number: %.20lf\n", double_num); // 输出:0.33333333333333331483

printf("Max pracision of float: %d\n", FLT_DIG); // 输出:6
printf("Max pracision of double: %d\n", DBL_DIG); // 输出:15

准确的数字应该是0.3˙0.\dot 30.3˙,此时输出的float类型可以达到的精度是7,double类型可以达到的精度是16。此时会发现两个类型的浮点数的精度看起来都头文件float.h中定义的此类型的最大精度大1,这是为什么呢?

这个问题就涉及到对浮点数表示的深入理解以及浮点数有效精度的计算,对于这个问题,前文18. float丢失精度以及%E与%A科学计数法输出中浅讲了一下,点击跳转:18. float丢失精度以及%E与%A科学计数法输出,不过对于更深层次的认识,这篇文章详细介绍了两种有效精度的计算方法,介绍得非常详细清晰:

IEEE754标准: 三, 为什么说32位浮点数的精度是"7位有效数" - 知乎

这篇文章中还有对浮点数误差的一些补充的原理解释,同样写得非常好:

关于浮点数的原理,误差,以及你想知道的一切 - 知乎

简单来说,误差产生的根本原理,就是计算机中的浮点数是用二进制表示的,所以二进制的数不能完美地表达出所有的十进制小数。在编码的过程中,计算机在储存浮点数之前会先将十进制的数转换为二进制,此时如果遇到比如0.1的十进制数,计算机无法用二进制准确表示,只能对这个数做舍入操作,使用距离这个数最近的二进制数近似表示:00111101110011001100110011001101,也就是0.100000001490116119384765625,这就导致了误差或者说丢失精度的现象。类似十进制中无法准确表示0.6˙0.\dot 60.6˙,只能对其进行舍入操作,用0.667来近似表示。

当然,浮点数中误差产生的原因不止这一条,这只是在所有运算、类型转换都比较常规的情况下,由于浮点数最基础的表示而导致的误差。其他的误差种类如运算误差、平台误差、类型转换误差,可以参考连接:关于浮点数的原理,误差,以及你想知道的一切 - 知乎。

由此看来,可以通过一种巧妙的方法将二进制中的精度映射到十进制中,得出浮点数在十进制下的精度数据。

以float类型为例,前文说过,float类型的32位浮点数在内存中是这样表示的:1位符号位,8位指数位,23位尾数位。其中尾数位中有一个隐含的“1”,使尾数为达到24位。其中符号位控制正负,指数位控制小数点的移动,这两个对于精度的表示没有影响,因此浮点数中只有尾数会影响精度。

那么一共24位二进制数,最多可以表示多少种情况,也就是最多可以表述多少个数呢?没错,是2242^{24}224个。而对应来说,十进制如果为xxx位,最多可以表示的状态数为10x10^x10x。让这两个数字相等,解出xxx,即计算24位二进制数可以表示的状态数,相当于使用多少位十进制数可以表示的状态数。

如果十进制的状态数小于二进制的状态数,即所有十进制数都有对应的二进制数来表示,二进制的精度比十进制大;如果十进制的状态数大于二进制的状态数,即有的十进制数无法使用二进制表示,二进制的精度比十进制小。

10x=224lg⁡10x=lg⁡224xlg⁡10=24lg⁡2x=24lg⁡2≈7.22\begin{aligned} 10^x &= 2^{24} \\ \lg_{}{10^x} &= \lg_{}{2^{24}} \\ x\lg_{}{10} &= 24\lg_{}{2} \\ x &= 24\lg_{}{2} \approx 7.22 \\ \end{aligned}10xlg​10xxlg​10x​=224=lg​224=24lg​2=24lg​2≈7.22​

由于24位二进制数是包含了隐含的“1”的结果,某些情况下不太精确,因此保险起见,float.h文件中将float类型的有效精度定义为6。

或者还有另一种可以得知大概精度的计算方法:由于符号位与指数为不影响精度,因此可以直接将2242^{24}224计算出来,为16 777 216,这个数即为十进制数,为24位二进制数可以表示的最多的数字种类数,16 777 216是8位数,所以32位浮点数的精度最多是7位十进制(0 - 9 999 999),共10 000 000种状态。如果32位浮点数的精度是8位十进制的话(0 - 99 999 999),这一共是100 000 000种状态,大于了32位浮点数能存储的状态上限16 777 216,所以说精度到不了8位十进制数。这只是大致的估算方法,要得到准确的值,还是需要进行精确的对数计算。

类似的,对于double类型,也就是64位浮点数,有1位符号位,11位指数位,52位尾数位。尾数位中包含一个隐含的1,因此为53位尾数位。计算10x=25310^x = 2^{53}10x=253,得到x=53lg⁡2≈15.95x = 53\lg_{}{2} \approx 15.95x=53lg​2≈15.95,由于此时尾数位中含有一个隐含的“1”,保险起见将double类型的有效精度定义为15。

24. Decimal

企业中金融系统中或者银行中保存金钱的数值所使用的类型不是C语言中的浮点数,因为不管是double还是long double,不管尾数有多少位,只要是常规的浮点型,都避免不了会出现丢失精度的现象,这在涉及到金钱的储存时是不可接受的。

所以为了解决这个问题,金融领域中经常使用的类型是decimal,也被称为定点数类型,这种类型在C语言中是没有的,是其他一些语言中特有的,符合某些场合要求不丢失精度存储数据的需求的一中特殊类型。比如MySQL中就有decimal这一类型,在保存比如货币数据时候必须选用这种数据,一定不能选择浮点型。

C语言一般用于编写操作系统等比较底层的场合,因此没有这种专用的数据类型。不过如果非要使用C语言来储存这些有特殊要求的数据,有一些对应的库可以处理这个问题:

  • The GNU MP Bignum Library
  • libdfp/libdfp: Decimal Floating Point C Library

如果有需要的话可以深入了解一下。

25. char与ASCII

char类型用于储存字符(如,字母或标点符号),但是从技术层面看,char是整数类型。

定义char类型时使用单引号将字符包裹起来,printf()输出时使用%c:

1
2
char mych = 'A';
printf("mych: %c\n", mych); // 输出:A

在Visual Studio中,将鼠标光标放在定义的字符附近,可以看到字符对应的int类型的数字:

9

加入直接定义一个int类型的数字,使用%c输出,会发生什么事情呢?是的,输出的是数字对应的字符:

1
2
int mych = 66;
printf("mych: %c\n", mych); // 输出:B

这就涉及到一个关键的编码系统:ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)

每一个ASCII字符占8bit,对于有符号的char类型,第一位是符号位,所以可以用于编码的一共有7bit,因此这种编码中一共有128个字符,这些字符包括两种:

  • 控制字符(0-31和127)。例如,ASCII 13(CR, Carriage Return)表示回车,ASCII 10(LF, Line Feed)表示换行。(有关换行和回车的由来与区别可以参考:1分钟彻底搞懂回车(\r)和换行(\n)的区别_回车的字符-CSDN博客)
  • 可打印字符(32-126)。例如,英文字母大小写、数字、标点、符号等等。

具体编码可参考:ASCII 表 | 菜鸟教程

26. 转义序列

在程序中,如果需要处理一些计算机无法打印出来的字符,如回车、换行、响铃等,就需要用到转义字符。

转义字符以反斜杠\开头,最常见的转义字符就是printf()中的换行\n。

还有其他的一些转义字符,例如\t代表换行:

1
printf("123\t456\t789\n");		// 输出:123     456     789

如果要使用printf()打印一个反斜杠,需要输入两个反斜杠:

1
printf("\\");		// 输出:\

其他更多转义字符可参考:转义序列 | Microsoft Learn

还有一些别的有趣的转义字符:

清除屏幕:可以将前面所有的控制台打印输出全部清除

1
printf("\033[2J");

移动光标到指定位置:如编写记事本程序,需要将光标按照需求移动位置,但由于环境或者编译的原因,在visual studio中运行不是很好

1
printf("\033[%d;%dH", x, y);	// 将光标移动到第x行第y列处

也有一些转义字符导致的特性,比如在C语言程序中想要输出路径,输出\就必须使用\\:

1
printf("C:\\Users\\hc");	// 输出:C:\Users\hc

27. bool类型与实际案例

bool类型,中文叫布尔,占用1位,即1bit的存储空间,用来表示逻辑的true和false,类似开关,使用1代表true,使用0代表false。

C语言中,要使用bool类型,需要引入头文件stdbool.h

给bool类型初始化时候直接使用true或者false即可:

1
2
bool is_true = true;
bool is_false = false;

28. char范围与无符号char

char类型也和int类型类似,分为有符号与无符号,即signed char与unsigned char。

所有带符号的字符值的范围都介于-128和127之间;所有无符号的字符值的范围介于0和255之间。

unsigned char在很多场合是必须的,例如在处理一些输出时候不应该出现负数、在处理网络等字节数据时候可以直接访问原始字节,后面在更深入的学习中可能会使用到。

那么既然char占8bit,而前面说过的uint8_t也占8bit,这两个能否互相替代呢?一般是不可以的,因为uint8_t一般用于数值的计算,而unsigned char一般用于字符的处理。

不过在开发跨平台应用时,有时必须使用uint8_t来代替char,不然可能会由于各个平台对char的定义不同而导致出错。

29. 常量const与#define宏

在定义变量前加一个const即可定义常量,常量定义后就不可改变:

1
const double PI = 3.14;

在函数内部定义的常量只能在函数内部使用。

还有一种定义常量的方法就是宏定义:

1
#define PI 3.14

在代码最开始定义,可以在本文件中所有函数中使用,同时宏定义还方便对常量值进行修改和维护。

30. 第二章结束语

关于数据类型,还有复数与虚数没涉及到,这是因为在C语言中无法计算,除非引入C++,需要的话可以研究一下complex.h头文件,里面包含了C99标准中引入的两个用于描述复数的关键字_Complex和_Imaginary可以用于计算复数。可以参考:C库 —— <complex.h>-CSDN博客

适当休息!多复习!!!多练习!!!

参考链接

  • 关于二进制与十进制互转的方法(简单好学!)_2进制转10进制-CSDN博客
  • 2、进制详解(整数部分) - 沐风半岛 - 博客园 (cnblogs.com)
  • 3、带符号的二进制数(原码、反码、补码) - 沐风半岛 - 博客园 (cnblogs.com)
  • 二进制:有符号整数编码及加减法运算 // 圆方 (lumin.tech)
  • 图灵社区 (ituring.com.cn)
  • 数据类型范围 | Microsoft Learn
  • 格式规范语法:printf 和 wprintf 函数 | Microsoft Learn
  • 整数溢出 - 维基百科,自由的百科全书 (wikipedia.org)
  • float 类型 | Microsoft Learn
  • 浮点数在内存中的存储机制与IEEE 754标准-CSDN博客
  • 【计算机知识】定点数与浮点数(2)浮点数法表示方法!_哔哩哔哩_bilibili
  • 浮点数的深入分析 - Alan_Fire - 博客园
  • 15 张图带你深入理解浮点数
  • 关于浮点值的上溢和下溢和不符合“常识”的情况&整数上溢和整数下溢~_浮点数上溢和下溢-CSDN博客
  • IEEE 754 的舍入规则 | Leodots
  • IEEE754标准: 三, 为什么说32位浮点数的精度是"7位有效数" - 知乎
  • 关于浮点数的原理,误差,以及你想知道的一切 - 知乎
  • The GNU MP Bignum Library
  • libdfp/libdfp: Decimal Floating Point C Library
  • 1分钟彻底搞懂回车(\r)和换行(\n)的区别_回车的字符-CSDN博客
  • ASCII 表 | 菜鸟教程
  • 转义序列 | Microsoft Learn
  • 字符值的范围 | Microsoft Learn
  • C库 —— <complex.h>-CSDN博客
本文来源: 青江的个人站
本文链接: https://hanqingjiang.com/2024/11/14/241114_C_dataType/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
知识共享许可协议
赏

谢谢你请我喝可乐~

支付宝
微信
  • Notes
  • C

扫一扫,分享到微信

微信分享二维码
博客改动备份
【作业】衍射光场分布仿真
  1. 1. 1. 注释(Comment)
  2. 2. 2. 进制
    1. 2.1. 2.1 二进制
    2. 2.2. 2.2 二进制转十进制
    3. 2.3. 2.3 十进制转二进制
    4. 2.4. 2.4 英文表示
    5. 2.5. 2.5 补充知识
  3. 3. 3. int类型在内存中的表示
  4. 4. 4. printf()输出详解
    1. 4.1. 4.1 参数遗漏
    2. 4.2. 4.2 八进制与十六进制的输出
  5. 5. 5. scanf_s()初步认识
  6. 6. 6. 整型溢出(integer overflow)
  7. 7. 7. unsigned int
  8. 8. 8. short与unsigned short
  9. 9. 9. long、long long
  10. 10. 10. size_t类型与sizeof()的使用
  11. 11. 11. 实际开发过程中的整型声明
  12. 12. 12. 类型转换(Type Conversion)
  13. 13. 13. 固定宽度整数类型的格式化宏输出(inttypes.h)
  14. 14. 14. least和fast整型的企业用途与区别
  15. 15. 15. 浮点数
  16. 16. 16. 浮点数存储原理
  17. 17. 17. float类型的定义与输出
  18. 18. 18. float丢失精度以及%E与%A科学计数法输出
  19. 19. 19. 浮点数overflow上溢与underflow下溢
  20. 20. 20. Infinity与Nan
    1. 20.1. 20.1 Infinity
    2. 20.2. 20.2 Nan
  21. 21. 21. 最近偶数舍入(银行家舍入)标准
  22. 22. 22. double、long double
  23. 23. 23. float和double有效精度对比原理与计算
  24. 24. 24. Decimal
  25. 25. 25. char与ASCII
  26. 26. 26. 转义序列
  27. 27. 27. bool类型与实际案例
  28. 28. 28. char范围与无符号char
  29. 29. 29. 常量const与#define宏
  30. 30. 30. 第二章结束语
  31. 31. 参考链接
© 2021-2025 青江的个人站
晋ICP备2024051277号-1
powered by Hexo & Yilia
  • 友链
  • 搜索文章 >>

tag:

  • 生日快乐🎂
  • 新年快乐!
  • 小技巧
  • Linux
  • 命令
  • 语录
  • 复刻
  • Blog
  • Notes
  • Android
  • C
  • FPGA
  • Homework
  • MATLAB
  • Server
  • Vivado

  • 引路人-稚晖
  • Bilibili-稚晖君
  • 超有趣讲师-Frank
  • Bilibili-Frank