1. 整数在内存中的存储
在讲解操作符的时候,我们有两篇文章讲过了下面的内容:整数的2进制表示方法有三种,即 原码、反码和补码,分别是C语言第2节:数据类型和变量和C语言第10节:详解操作符,点击链接🔗即可查看之前的内容,这里我们再复习一下。
1.1 原码(Sign-Magnitude Representation)
- 定义:最高位作为符号位,
0
表示正数,1
表示负数,剩下的位表示数值的绝对值。 - 特点:
- 正数的原码和其二进制表示相同。
- 负数的原码是符号位为
1
,后面加上绝对值的二进制。
示例(8位):
+5
:00000101
-5
:10000101
问题:存在两个零(+0
和 -0
),运算复杂。
1.2 反码(One’s Complement)
- 定义:正数的反码与原码相同;负数的反码是对其原码除符号位外按位取反。
- 特点:
- 用于解决两个零的问题,但仍需特殊处理。
示例(8位):
+5
:00000101
(同原码)-5
:- 原码:
10000101
- 反码:
11111010
(符号位不变,其他位取反)
- 原码:
问题:仍需特殊规则处理零,运算复杂。
1.3 补码(Two’s Complement)
- 定义:正数的补码与原码相同;负数的补码是对其绝对值的二进制表示按位取反后加1。
- 特点:
- 简化了加减法运算。
- 只有一个零(解决了两个零的问题)。
- 符号位直接参与运算。
示例(8位):
+5
:00000101
(同原码)-5
:- 原码:
10000101
- 反码:
11111010
- 补码:
11111011
(反码加1)
- 原码:
优势:
- 运算简单。
- 广泛用于现代计算机系统。
- 计算机存储的是补码
1.4 总结对比
表示方式 | 最高位 | 正数表示 | 负数表示 | 零的表示 | 应用场景 |
---|---|---|---|---|---|
原码 | 符号位 | 与二进制相同 | 符号位+绝对值二进制 | +0 和 -0 | 理论学习 |
反码 | 符号位 | 与二进制相同 | 按位取反 | +0 和 -0 | 早期计算机 |
补码 | 符号位 | 与二进制相同 | 按位取反+1 | 唯一的零 | 现代计算机实际使用 |
对于整形来说:数据存放内存中其实存放的是二进制的补码。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储。
原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
2. 大小端字节序和字节序判断
其实超过一个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储,下面是内容讲解
2.1 什么是字节序?
字节序(Endianness)是多字节数据在内存中存储的顺序,即如何将一个多字节的数据(如 int
, float
)的每个字节存放在连续的内存单元中。
2.2 分类:大端和小端
-
大端序(Big Endian):
- 高位字节存储在低地址,低位字节存储在高地址。
- 类似于我们平常书写数字的方式(从高位到低位)。
例子:
0x12345678
(32位整数)-
内存存储顺序:
12 34 56 78
(地址从低到高)我这里没有对应的设备,无法实现,请见谅
-
小端序(Little Endian):
- 低位字节存储在低地址,高位字节存储在高地址。
- 这是Intel x86架构等常见硬件的默认字节序。
例子:
0x11223344
(32位整数)-
内存存储顺序:
44 33 22 11
(地址从低到高)
2.3 字节序的影响与大小端的成因
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit
位,但是在C语言中除了 8bit
的char
之外,还有16 bit
的short
型,32 bit
的long
型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
- 在大端系统中:
- 数据按照从高位到低位的顺序存储。
- 网络字节序(Network Byte Order)通常采用大端。
- 在小端系统中:
- 数据按照从低位到高位的顺序存储。
- 小端序是现代PC和嵌入式系统的常见存储方式。
2.4 字节序的具体存储示例
示例:存储 int x = 0x12345678;
假设变量 x
的地址是 0x1000
,int
类型为 4 字节:
地址 | 0x1000 | 0x1001 | 0x1002 | 0x1003 |
---|---|---|---|---|
大端序 | 0x12 | 0x34 | 0x56 | 0x78 |
小端序 | 0x78 | 0x56 | 0x34 | 0x12 |
2.5 总结
特点 | 大端序(Big Endian) | 小端序(Little Endian) |
---|---|---|
存储顺序 | 高位字节存储在低地址,高地址存储低位字节 | 低位字节存储在低地址,高地址存储高位字节 |
常见应用 | 网络协议、部分嵌入式系统 | x86、ARM(默认小端)、现代PC架构 |
优点 | 人类易读(类似自然书写方式) | 存取效率高(低位先存取) |
2.6 练习
2.6.1 练习1
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)- 百度笔试题
答:大端字节序存储:把一个数据的低位字节的数据存放在高地址处,高位字节的数据存放在低地址处;小端序节序存储:把一个数据的低位字节的数据存放在低地址处,高位字节的数据存放在高地址处。
//代码1
#include <stdio.h>
int check_sys()
{int i = 1;return (*(char *)&i);
}
int main(){int ret = check_sys();if(ret == 1){printf("小端\n");}else{printf("大端\n");}return 0;
}
解释:
- 在函数check_sys中,
int
型变量i
的值为1,那么它在内存中的表示为0x00000001
- 如果是大端,那么最低位置存储的值是
0
- 如果是小端,那么最低位置存储的值是
1
//代码2
int check_sys()
{union{int i;char c;}un;un.i = 1;return un.c;
}
解释:
-
联合体的所有成员共享同一块内存区域。(以后会讲解联合体)
-
当
i = 1
时,其二进制表示为:0x00000001
(在 32 位或 16 位系统中)。 -
系统的字节序决定了最低字节的位置:
- 小端序:最低有效字节
0x01
存储在低地址。 - 大端序:最高有效字节存储在低地址,
0x01
存储在高地址。
- 小端序:最低有效字节
-
如果
un.c
的值为1
,说明低地址存储低字节,即小端序。如果
un.c
的值为0
,说明低地址存储高字节,即大端序。
2.6.2 练习2
//VS2022
#include <stdio.h>
int main()
{char a= -1;signed char b=-1;unsigned char c=-1;printf("a=%d,b=%d,c=%d",a,b,c);return 0;
}
讲解:
2.6.2.1 a
和 b
在带符号类型中,计算机使用 补码 来表示负数。我们通过以下步骤理解:
-1
的补码表示:- 原码:对于
-1
,其原码是10000001
。第一个1
是符号位,表示负数,后面 7 个0
表示数值 1。 - 反码:反码是对原码中除符号位外的每一位取反,
-1
的反码是11111110
。 - 补码:补码是反码加 1,
-1
的补码是11111111
。
- 原码:对于
- 如何存储
-1
:a
和b
都是带符号类型(8 位),所以它们的值-1
会被存储为11111111
(补码表示)。- 在 8 位补码表示下,
11111111
是-1
的补码表示。无论是char
还是signed char
,它们会存储为11111111
,并且打印出来时将被解释为-1
。
2.6.2.2 c
在无符号类型中,补码 不用于表示负数。无符号类型直接按二进制表示数值,不允许表示负数。因此,对于 unsigned char
,负数会转换为它对应的无符号值。
-1
的处理:- 当
-1
被赋给unsigned char
时,-1
会被转化为 8 位的补码表示(即11111111
)。 - 然而,由于
unsigned char
类型不能表示负数,它会把这个补码当作无符号数值11111111
来处理,即255
(这是11111111
的无符号表示)。
- 当
- 如何存储
-1
:- 对于
unsigned char c
,-1
经过转换后会存储为11111111
,但它表示的值是255
,这是因为unsigned char
类型解释的是 8 位二进制数的无符号值。
- 对于
tips : 为什么a、b、c二进制存储一致?原因在于计算机底层只存储二进制数据,而数据类型只是用来告诉编译器如何解释这些二进制数据。
2.6.2.3 整形提升与输出
在printf
函数中,给它传递参数的时候,会将a
、b
、c
进行整形提升。由于整形提升的规则,尽管我们传递的是 char
和 unsigned char
类型变量,但它们在传递给 printf
时会被提升为 int
和 unsigned int
。
a
和 b
的输出
a
是char
类型,printf
期望一个带符号int
,因此a
在传递给printf
时会被提升为int
。由于a
存储的是-1
(在补码中表示为11111111
),它在整形提升后仍然是-1
(补码为11111111 11111111 11111111 11111111
)。因此,a
打印出来时是-1
。b
是signed char
类型,同样会被提升为int
类型,在传递给printf
时也会是-1
。因此,b
也打印出-1
。
c
的输出(无符号整数 %d
)
c
是unsigned char
类型,根据整形提升规则,c
会被提升为unsigned int
类型。尽管c
的存储值是11111111
(即255
),它会被提升为unsigned int
,因此c
被提升后作为无符号整数255
传递给printf
,最终输出255
。
最终输出结果为:
a=-1,b=-1,c=255
2.6.3 练习3
//VS2022
#include <stdio.h>
int main()
{char a = -128;printf("%u\n",a);return 0;
}
解释:
根据上一个题的方法,我们可以轻松的得到
- a的原码为
10000000
- a的反码为
11111111
- a的补码为
10000000
(如果不知道怎么计算,可以先计算4字节存储的原码反码补码,再进行截断来得到1字节的原码反码补码)
整形提升(Integer Promotion)
-
在调用
printf("%u\n", a)
时,a
作为参数传递,会发生整形提升。 -
而MSVC中
char
是有符号类型(signed char
),并且补码最高位为1(即负数),整形提升后会用符号扩展填充高位。 -
提升后的补码为
11111111 11111111 11111111 10000000
(即int
类型的-128),当这个int
类型的-128
被打印为无符号整数时,它会被解释为一个 无符号整数,即4294967168
。
输出结果:
4294967168
#include <stdio.h>
int main()
{char a = 128;printf("%u\n",a);return 0;
}
解释:
- 有符号char范围是
-128 ~ 127
。赋值128
超出了signed char
的范围,因此发生溢出。由于char
是 1 字节(8 位),只保留 低 8 位有效值,多余的高位会被丢弃,因此二进制存储为10000000
。而二进制10000000
对应的补码解释为-128
。因此,a
实际存储的值是-128
。 a
的原码、反码、补码-
a的原码为
10000000
-
a的反码为
11111111
-
a的补码为
10000000
-
整形提升(Integer Promotion)
-
在调用
printf("%u\n", a)
时,a
作为参数传递,会发生整形提升。 -
而MSVC中
char
是有符号类型(signed char
),并且补码最高位为1(即负数),整形提升后会用符号扩展填充高位。 -
提升后的补码为
11111111 11111111 11111111 10000000
(即int
类型的-128),当这个int
类型的-128
被打印为无符号整数时,它会被解释为一个 无符号整数,即4294967168
。
输出结果:
4294967168
2.6.4 练习4
#include <stdio.h>
int main()
{char a[1000];int i;for(i=0; i<1000; i++){a[i] = -1-i;}printf("%zd",strlen(a));return 0;
}
解释:
- 很明显,for循环尝试把-1~-1000的值存储在a这个字符数组里面
-
但是有符号char范围是
-128 ~ 127
。赋值小于-128
的数字超出了signed char
的范围,因此发生溢出,发生溢出时,会按照补码规则截断。我们同样可以按照原码反码补码的方式来分析到底存储的值是什么。 -
这里我们以-129为例,
-129
的原码是10000000 00000000 00000000 10000001
反码是11111111 11111111 11111111 01111110
补码是11111111 11111111 11111111 01111111
,存储在1字节的char
类型变量里的补码则是01111111
,对应存储的数据实际上是127。同理可得-130为126······右图引用《C++ Primer Plus》中的插图,以短整型为例直观感受溢出时实际的值 -
所以溢出的具体表现为超过
-128
的值会从127
开始循环。a[127] = -128
,a[128] = 127
,a[129] = 126
,依此类推。 -
strlen
计算字符串长度的方式是:从数组的第一个字符开始,逐个检查字符是否是\0
(ASCII 值为 0)。一旦遇到\0
,停止计数,返回当前的字符数。根据赋值-1 - i
的计算:当 i = 255时:-1 - 255 = -256按char
类型截断后,值为 0(补码为 00000000)。因此,a[255]
是第一个\0
。从a[0]
~a[255]
,一共有256个字符,除去a[255]
的第一个\0
,一个255个
程序最终输出:
255
2.6.5 练习5
#include <stdio.h>unsigned char i = 0;
int main()
{for(i = 0;i<=255;i++){printf("hello world\n");}return 0;
}
解释:
(1) 循环条件和增量
- 循环从
i = 0
开始,每次执行i++
,直到条件i <= 255
不再成立。 - 循环条件
i <= 255
看起来是希望i
的值从0
到255
,总共执行256
次。
(2) 循环的陷阱:i
的类型是 unsigned char
unsigned char
的最大取值为255
。- 当
i
增加到255
时,再执行i++
时会导致溢出。- 因为
unsigned char
只有 8 位,当i = 255
时再加1
,溢出后会变为0
,而不是256
。(可以自己用原码反码补码进行分析)
- 因为
- 这意味着变量
i
的值会从255
回到0
,并进入下一次循环。
(3) 无限循环的原因
- 由于
i
是unsigned char
,它的取值范围是0 ~ 255
。 - 当
i = 255
时,再加1
会导致溢出并重新回到0
。 - 因为
for
循环的条件是i <= 255
,而i
的值一直在0 ~ 255
之间变化,因此循环永远不会终止,会陷入 无限循环。
(4) 程序的输出
- 在每次循环中,都会执行
printf("hello world\n");
。 - 因为变量
i
永远不会超出0 ~ 255
的范围,并且i <= 255
一直成立,所以程序会无限输出"hello world"
。
#include <stdio.h>
int main()
{unsigned int i;for(i = 9; i >= 0; i--){printf("%u\n",i);}return 0;
}
解释:
1. 变量 i
的类型
- 这里
i
被定义为unsigned int
,即 无符号整数,它的取值范围是从0
到2^32 - 1
(通常是0 ~ 4294967295
)。 - 因为它是无符号的,所以
i
无法表示负值。
2. 循环行为分析
- 初始化:
i
从9
开始。 - 循环条件:
i >= 0
,因为i
是无符号整数,这个条件实际上永远为真,因为无符号整数的最小值是0
。 - 步进:每次循环后
i--
,意味着i
的值会递减。
- 当
i
从9
递减到0
,打印0
后,接下来执行i--
。 - 溢出行为:
i
是无符号类型,所以当i
从0
递减时,会发生 溢出,使得i
变为其最大值,即4294967295
。(可以自己用原码反码补码进行分析) - 因此,
i
从4294967295
继续递减,每次仍满足i >= 0
的条件,导致无限循环。
3. 输出结果
- 循环开始时,程序依次打印
9
到0
。 - 然后,由于
i
溢出变为4294967295
,程序会继续无限递减并打印这些值。
2.6.6 练习6
#include <stdio.h>
//X86环境 小端字节序
int main()
{int a[4] = { 1, 2, 3, 4 };int *ptr1 = (int *)(&a + 1);int *ptr2 = (int *)((int)a + 1);printf("%x,%x", ptr1[-1], *ptr2);return 0;
}
代码输出的结果是啥?
1. ptr1[-1]
&a + 1
:&a
是指向整个数组的指针,其类型是int (*)[4]
,即指向 4 个整数的数组。&a + 1
指向的是数组a
之后的内存位置,相当于原来地址加上16
字节(4 个int
,每个 4 字节)。
ptr1 = (int *)(&a + 1)
:- 将
&a + 1
强制转换为int *
,所以ptr1
指向数组a
之后的位置,即a[4]
的地址。
- 将
ptr1[-1]
:ptr1[-1]
等价于*(ptr1 - 1)
,即指向a[3]
,即数组a
的最后一个元素。- 因此,
ptr1[-1]
的值是4
。
2. *ptr2
-
(int)a
:a
是数组名,指向数组的首地址,其类型是int *
。(int)a
是将a
的地址强制转换为int
类型。需要注意,这里直接将一个指针转换为整数,这种操作在不同的编译器和系统环境中可能会有不同的行为。
-
(int)a + 1
:- 将地址值加
1
,相当于将原来的首地址偏移了 1 个字节。
- 将地址值加
-
(int *)((int)a + 1)
:- 现在
(int)a + 1
是一个偏移后的整数值,将它转换回int *
。 - 这样
ptr2
指向的实际上是一个不对齐的位置,因为原始地址加1
字节后再去读取int
类型,可能会导致未对齐访问。
- 现在
-
数组
a
的第一个元素是1
,在内存中按小端存储,即内存中的布局是从低字节到高字节的存储,其十六进制表示为:01 00 00 00
-
偏移后的地址访问:
(int)a + 1
后的地址指向第二个字节(即00
)。- 因为
ptr2
被解释为int *
类型,所以它试图从这个地址读取 4 个字节。 - 由于这个地址是字节级的偏移,读取到的值为
2000000
。
3. 浮点数在内存中的存储
浮点数在 C 语言中的存储遵循 IEEE(电气和电子工程协会) 754 标准,主要包括 单精度浮点数(32 位) 和 双精度浮点数(64 位) 两种格式。这里将详细介绍浮点数的存储方式、组成部分以及如何存储和表示浮点数。
3.1 IEEE 754 标准
IEEE 754 标准是浮点数在计算机中表示的标准。它定义了浮点数的表示格式,分为两种主要格式:
- 单精度(32 位):用于存储较低精度的浮点数,使用 32 位来存储。
- 双精度(64 位):用于存储更高精度的浮点数,使用 64 位来存储。
3.2 浮点数的组成部分
浮点数的存储由三部分组成:符号位、指数部分 和 尾数部分(也称为有效数字)。
V = ( − 1 ) S ∗ M ∗ 2 E V = (-1) ^ S * M * 2 ^ E V=(−1)S∗M∗2E
(1) 符号位(S)
- 用于表示浮点数的正负。
- 如果符号位是
0
,表示正数。 - 如果符号位是
1
,表示负数。
- 如果符号位是
(2) 指数部分(E)
-
指数部分表示浮点数的 范围,它采用 偏移量表示法。
偏移量的作用是将指数部分的值映射到一个正数范围(无符号整数),这样就不需要直接存储负数的指数值。
偏移量(bias)对于不同的浮点数格式是不同的:
- 单精度浮点数:偏移量为 127(即
2^7 - 1
)。 - 双精度浮点数:偏移量为 1023(即
2^10 - 1
)。
- 单精度浮点数:偏移量为 127(即
(3) 尾数部分(M)
-
也叫有效数字,表示浮点数的精确值。尾数通常是一个二进制的小数。
- 在单精度浮点数中,尾数使用 23 位。
- 在双精度浮点数中,尾数使用 52 位。
-
M是大于等于1,小于2的。
3.3 浮点数存储方式
3.3.1 单精度浮点数(32 位)的存储结构
单精度浮点数占 32 位,其中:
- 1 位用于符号位(S)
- 8 位用于指数部分(E)
- 23 位用于尾数部分(M)
其存储结构如下:
符号位 (S) | 指数部分 (E) | 尾数部分 (M) |
---|---|---|
1 位 | 8 位 | 23 位 |
3.3.2 双精度浮点数(64 位)的存储结构
双精度浮点数占 64 位,其中:
- 1 位用于符号位(S)
- 11 位用于指数部分(E)
- 52 位用于尾数部分(M)
其存储结构如下:
符号位 (S) | 指数部分 (E) | 尾数部分 (M) |
---|---|---|
1 位 | 11 位 | 52 位 |
3.4 浮点数表示方法(规格化与非规格化)
在 IEEE 754 标准中,浮点数分为 规格化数(Normalized)和 非规格化数(Denormalized)。
3.4.1 规格化数(Normalized)
- 在规格化数中,尾数部分(M)始终以
1
开头。即,尾数部分通常表示为1.xxxxxx
(二进制)。这个1
是 隐含的,在存储时不会显式表示。只保存后面的xxxxxx
部分。(比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。) - 指数部分是存储一个偏移量,通常加上一个常数(例如:对于 32 位的浮点数,偏移量为 127;对于 64 位的浮点数,偏移量为 1023)。
例如,浮点数 1.25
可以表示为:
1.25 = 1.01 × 2^0
该值在内存中的表示为:
- 符号位
S = 0
(表示正数) - 指数部分
E = 127
(偏移量表示法) - 尾数部分
M = 01000000000000000000000
(隐含 1)
3.4.2 非规格化数(Denormalized)
- 当指数部分为全
0
时,表示非规格化数。在这种情况下,尾数部分的前导1
不再隐含,实际存储的尾数以0
开头。这样做是为了表示±0
,以及接近于0的很小的数字。 - 非规格化数可以表示非常接近零的小数值,但精度较低。
3.4.3 特殊数
在 IEEE 754 标准中,当 指数部分全为 1 时,表示两种特殊的浮点数:无穷大(Infinity)和 NaN(Not a Number,非数)。
这两种特殊值通常用于表示计算中出现的错误或溢出的结果。
3.4.3.1 指数部分全为 1 的含义
(1) 单精度浮点数(32 位)
单精度浮点数的存储格式如下:
- 1 位符号位
- 8 位指数部分
- 23 位尾数部分(有效数字)
如果指数部分的 8 位全为 1,即 11111111
,我们需要根据符号位和尾数部分的值来区分具体情况:
- 符号位 = 0,尾数部分全为 0:表示正无穷大(
+∞
)。 - 符号位 = 1,尾数部分全为 0:表示负无穷大(
-∞
)。 - 尾数部分不全为 0:表示
NaN
(非数),这是一个特殊的数值,表示不合法或未定义的数学操作(例如 0 除以 0)。
(2) 双精度浮点数(64 位)
双精度浮点数的存储格式如下:
- 1 位符号位
- 11 位指数部分
- 52 位尾数部分
如果指数部分的 11 位全为 1,即 11111111111
,同样地:
- 符号位 = 0,尾数部分全为 0:表示正无穷大(
+∞
)。 - 符号位 = 1,尾数部分全为 0:表示负无穷大(
-∞
)。 - 尾数部分不全为 0:表示
NaN
(非数)。
3.4.3.2 解释:无穷大(Infinity)和 NaN
(1) 无穷大(Infinity)
- 无穷大用于表示超出浮点数表示范围的值,例如在除法中出现溢出或者数字超出了可以表示的范围。
+∞
:符号位为0
,指数部分为11111111
,尾数部分为00000000000000000000000
。-∞
:符号位为1
,指数部分为11111111
,尾数部分为00000000000000000000000
。
(2) NaN(Not a Number)
- NaN 表示无效或未定义的数值,通常出现在无法计算或出现数学错误的情况下。例如,尝试计算
0 / 0
、∞ - ∞
、sqrt(-1)
等。 - NaN 的符号位可以是
0
或1
,但尾数部分不能全为 0。- 常见的 NaN 格式为:指数部分为全
1
,尾数部分非全零。
- 常见的 NaN 格式为:指数部分为全
3.4.3.3 举个例子
假设我们使用单精度浮点数(32 位)表示:
(1) 正无穷大(+∞)
- 符号位:
0
(表示正数) - 指数部分:
11111111
(表示∞
) - 尾数部分:
00000000000000000000000
(尾数全为 0)
二进制表示:0 11111111 00000000000000000000000
(2) 负无穷大(-∞)
- 符号位:
1
(表示负数) - 指数部分:
11111111
(表示∞
) - 尾数部分:
00000000000000000000000
(尾数全为 0)
二进制表示:1 11111111 00000000000000000000000
(3) NaN(非数)
- 符号位:
0
(可以是0
或1
) - 指数部分:
11111111
(表示特殊情况) - 尾数部分:非全
0
(尾数部分有有效数字)
二进制表示(一个示例):0 11111111 10000000000000000000000
(10000000000000000000000
作为 NaN
的一部分)
3.5 浮点数表示的例子
3.5.1单精度浮点数:1.25
假设我们有浮点数 1.25
,我们想将它表示为一个单精度浮点数。
- 浮点数转换为二进制:
1.25
转换为二进制是1.01
,即1.01 × 2^0
。 - 规格化表示:
这是一个规格化浮点数,因为尾数是1.01
。根据 IEEE 754 标准,尾数是1.xxxx
格式。 - 指数部分:
指数为0
,但需要加上偏移量127
(单精度浮点数的偏移量),所以存储的指数部分是127
,即二进制01111111
。 - 尾数部分:
尾数部分是1.01
,由于隐含的1
,存储的实际尾数部分是01000000000000000000000
(23 位)。 - 最终表示:
- 符号位:
0
(正数) - 指数部分:
01111111
- 尾数部分:
01000000000000000000000
- 符号位:
在内存中的存储方式是:
符号位 (S) | 指数部分 (E) | 尾数部分 (M) |
---|---|---|
0 | 01111111 | 01000000000000000000000 |
32 位二进制表示:0 01111111 01000000000000000000000
3.5.2 双精度浮点数:1.25
对于双精度浮点数,存储格式是相同的,只是占用 64 位空间。
- 浮点数转换为二进制:
1.25
转换为二进制是1.01 × 2^0
。 - 规格化表示:
规格化浮点数,尾数是1.01
,隐含的1
。 - 指数部分:
偏移量为1023
(双精度的偏移量),因此存储的指数部分是1023 + 0 = 1023
,即二进制01111111111
。 - 尾数部分:
尾数部分为1.01
,实际存储尾数部分为0100000000000000000000000000000000000000000000000000
(52 位)。 - 最终表示:
- 符号位:
0
(正数) - 指数部分:
01111111111
- 尾数部分:
0100000000000000000000000000000000000000000000000000
- 符号位:
在内存中的存储方式是:
符号位 (S) | 指数部分 (E) | 尾数部分 (M) |
---|---|---|
0 | 01111111111 | 0100000000000000000000000000000000000000000000000000 |
64 位二进制表示:0 01111111111 0100000000000000000000000000000000000000000000000000
3.6 小练习
#include <stdio.h>
int main()
{int n = 9;float *pFloat = (float *)&n;printf("n的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);*pFloat = 9.0;printf("n的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);return 0;
}
这段代码输出什么?
n的值为:9
*pFloat的值为:0.000000
n的值为:1091567616
*pFloat的值为:9.000000
3.6.1 题目解析
9以整型的形式存储在内存中,得到如下二进制序列:
0000 0000 0000 0000 0000 0000 0000 1001
3.6.1.1 int n = 9;
这一行代码定义了一个整数变量 n
并初始化为 9
。
n
的内存表示是00000000 00000000 00000000 00001001
(在 32 位机器上,9 的二进制表示为00000000 00000000 00000000 00001001
,具体根据平台的大小端可能会有所不同)。- 也就是说,
n
在内存中的字节顺序是:0x09 0x00 0x00 0x00
。
3.6.1.2 float *pFloat = (float *)&n;
这行代码做了一个 强制类型转换,将 n
的地址(&n
)强制转换为 float *
类型。
&n
是变量n
的地址,它是int *
类型,但我们将其转换成了float *
类型。- 类型转换:虽然
n
是一个int
类型(4 字节),但我们将它的地址当作float
类型来访问。注意:float
类型占用 4 字节,但它的存储方式与int
类型可能不同,float
类型的存储采用 IEEE 754 标准,而int
类型是按照补码表示的。因此,这两种类型在内存中的解释方式会不同。
3.6.1.3 printf("n的值为:%d\n", n);
这行代码输出变量 n
的值。在此时,n
的值是 9
,输出的结果为:
n的值为:9
3.6.1.4 printf("*pFloat的值为:%f\n", *pFloat);
*pFloat
解引用指针pFloat
,并将其作为float
类型的值来打印。- 由于
pFloat
是指向n
的float *
指针,程序会把n
的内存内容按照float
类型进行解释。n
的内存内容是0000 0000 0000 0000 1 0000 0000 0000 1001
,当以float
类型读取时,程序会将这 4 个字节解析为一个float
数值,而不是int
数值。 - 首先,将9 的二进制序列按照浮点数的形式拆分,得到第一位符号位
s =0
,后面8位的指数E=00000000
,最后23位的有效数字M=000 0000 0000 0000 0000 1001
。 - 由于
0000 0000 0000 0000 1 0000 0000 0000 1001
的二进制位在 IEEE 754 标准下对应的浮点数值非常接近1.401298e-45
(这是一个非常小的数,接近于零),输出的值是非常小的浮点数,而不是预期的9.0
。
因此,输出为:
*pFloat的值为:0.000000
3.6.1.5 *pFloat = 9.0;
这行代码通过 pFloat
指针将 9.0
赋值给 n
。
-
由于
pFloat
指向n
,这实际上是修改了n
的值,赋值为9.0
。 -
这里,
9.0
被赋给了n
,但是由于pFloat
是float *
类型的指针,赋值时会将9.0
转换为其 IEEE 754 浮点数的二进制表示,并写回到n
的内存位置。 -
首先,浮点数9.0 等于二进制的1001.0,即换算成科学计数法是:
9.0 = ( − 1 ) 0 ∗ 1.001 ∗ 2 3 9.0 = (-1)^0 * 1.001 * 2^3 9.0=(−1)0∗1.001∗23 -
9.0 在 IEEE 754 单精度浮点数下的表示是:
0 10000010 001 0000 0000 0000 0000 0000
。程序会将这四个字节写入n
的内存。
3.6.1.6 printf("n的值为:%d\n", n);
这行代码再次打印 n
的值。此时,n
被视作一个 int
类型,但它的内存内容被修改为 9.0
的浮点数二进制表示。因此,输出的 n
的值实际上是浮点数的二进制表示作为整数解释的结果。
- 浮点数
9.0
的 IEEE 754 单精度浮点数表示是0 10000010 001 0000 0000 0000 0000 0000
,它的字节顺序会被解释为一个int
类型。 0 10000010 001 0000 0000 0000 0000 0000
在内存中按int
类型解释时,实际上是1091567616
。
因此,输出为:
n的值为:1091567616
3.6.1.7 printf("*pFloat的值为:%f\n", *pFloat);
- 现在,
*pFloat
是将n
的内存内容作为float
类型读取。 - 由于
n
的内存内容现在已经是浮点数9.0
的二进制表示,*pFloat
会正确地输出9.0
。
因此,输出为:
*pFloat的值为:9.000000
—完—