C语言修炼——内存函数与数据的存储
目录
内存函数介绍:
一、memcpy使用和模拟实现
- 函数
memcpy
从source
的位置开始向后复制num
个字节的数据到destination
指向的内存位置 destination
与source
所指向内存空间中存储的两段数据的类型互不相干,数据在内存中的存储都是以二进制文本的形式,memcpy
是从字节的层面复制所有的二进制信息- 这个函数在遇到
'\0'
的时候并不会停下来,而是复制完指定的字节数 - 如果
source
和destination
有任何的重叠,复制的结果都是未定义的 - 为了避免
overflows
,需要确保destination
与source
指向的数组的大小至少为num
- 对于重叠的内存,需要用
memmove
进行复制
memcpy
的使用:
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0 };
memcpy(arr2, arr1, 20);//从arr1复制20个字节,5个int元素到arr2
for (int i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);//1 2 3 4 5 0 0 0 0 0
}
return 0;
}
memcpy
的模拟实现:
void* memcpy(void* dst, const void* src, size_t count)
{
void* ret = dst;//记录dst的初始位置,便于函数返回值
assert(dst && src);//确保没有空指针
/*
* copy from lower addresses to higher addresses从低到高地址复制
*/
while (count--)
{//char*的访问与运算都是1个字节,强制类型转换为char*便于操作
*(char*)dst = *(char*)src;
dst = (char*)dst + 1;
src = (char*)src + 1;
}
return ret;
}
二、memmove的使用和模拟实现
- 和
memcpy
的差别就是memmove
函数处理的源内存块和⽬标内存块是可以重叠的 - 如果源空间和⽬标空间出现重叠,就得使⽤
memmove
函数处理
memmove
的使用:
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr1 + 2, arr1, 20);//从arr1[0]开始复制20个字节到arr1[2]后
for (int i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);//1 2 1 2 3 4 5 8 9 10
}
return 0;
}
memmove
的模拟实现:
void* memmove(void* dst, const void* src, size_t count)
{
void* ret = dst;
if (dst <= src || (char*)dst >= ((char*)src + count))
{
/*
* Non-Overlapping Buffers无重叠部分
* copy from lower addresses to higher addresses
*/
while (count--)//从前往后复制
{
*(char*)dst = *(char*)src;
dst = (char*)dst + 1;
src = (char*)src + 1;
}
}
else
{
/*
* Overlapping Buffers重叠部分
* copy from higher addresses to lower addresses
*/
dst = (char*)dst + count - 1;
src = (char*)src + count - 1;
while (count--)//从后往前复制
{
*(char*)dst = *(char*)src;
dst = (char*)dst - 1;
src = (char*)src - 1;
}
}
return ret;
}
需注意重叠部分的复制,如果继续从前往后复制会造成source
指向空间中靠后数据被更改,当我们想复制source
指向空间中靠后数据到destination
时就会丢失数据,造成source
指向空间中靠前数据的重复复制。例如:arr[] = { 1,2,3,4,5,6,7,8,9,10 }
,当我们从arr[0]
开始复制20个字节,5个int
元素到arr[2]
,即复制1,2,3,4,5
到3,4,5,6,7
时,假如我们从前往后复制,先复制1,2
到了3,4
的位置,3,4
被替换,我们继续想要复制3,4,5
到5,6,7
时就找不到3,4
,因为3,4
已经被替换为1,2
,所以我们在3,4
位置找到的是1,2
,最终复制到5,6
位置的还是1,2
,再复制元素到7
时5
的位置找到的是1
,所以我们最终得到的不是1,2,1,2,3,4,5,8,9,10
而是1,2,1,2,1,2,1,8,9,10
。
因此我们可以采取从后往前复制的方式处理重叠部分。
三、memset的使用
memset
是⽤来设置内存的,将内存中的值以字节为单位设置成想要的内容。
char str[] = "Hello World";
memset(str, 'X', 6);
printf("%s\n", str);
结果:
XXXXXXWorld
数据在内存中的存储形式是二进制文本,本质是由0
,1
组成的一串数字。一个字节有8个比特位,也就是8个二进制数,8个二进制数可以化为2个十六进制数(因为4个二进制数可以表示的数字为0~15,十六进制数在0~15之间)。
我们以十六进制来表示字节信息,每一个字节的8个比特位换成十六进制都是0xXX
(X为一个十六进制数)。
字符'X'
的ASCII码是0x58
,所以value
接收的值是0x58
。所以上述代码memset
所做得操作是将str
指向空间的前6个字节全部修改为0x58
,这样如果我们以char
类型去读取这一个字节那么这个字节代表的信息就是'X'
。
int arr[] = { 1, 2, 3, 4 };
memset(arr, 0, 16);
for (int i = 0; i < 4; i++)
{
printf("%d ", arr[i]);
}
结果:
0 0 0 0
1,2,3,4
在内存中的存储信息分别是0x00 00 00 01
,0x00 00 00 02
,0x00 00 00 03
,0x00 00 00 04
,因为一个int
类型占4个字节,这里我们需要修改这16个字节。需要注意的是value
接收的值需要满足在0~255
,如果超出就会造成数据丢失,因为一个字节最多有两个十六进制数,而两个十六进制数能表示的最大数字为0xff
,即255
。
例如:当我们把257
传给value
时,想要以十六进制数完整表示257
需要3位数,即0x101
,但由于一个字节只有两位十六进制数,所以计算机只会取低位,即0x01
,最终把0x01
放入arr
数组的十六个字节中。
四、memcmp的使用
- ⽐较从
ptr1
和ptr2
指针指向的位置开始,向后的num
个字节 - 与字符串比较函数
strcmp,strncmp
不同,memcmp
遇见'\0'
并不会停止比较,会比较完所有字节 - 如果
ptr1
当中的字节小于ptr2
当中的字节则返回值<0
;如果ptr1
与ptr2
当中的所有字节均相等则返回值=0
;如果ptr1
当中的字节大于ptr2
当中的字节则返回值>0
memcmp
的使用:
#include <stdio.h>
#include <string.h>
int main()
{
char buffer1[] = "DWgaOtP12df0";
char buffer2[] = "DWGAOTP12DF0";
int n;
n = memcmp(buffer1, buffer2, sizeof(buffer1));
if (n > 0)
printf("'%s' is greater than '%s'.\n", buffer1, buffer2);
else if (n < 0)
printf("'%s' is less than '%s'.\n", buffer1, buffer2);
else
printf("'%s' is the same as '%s'.\n", buffer1, buffer2);
return 0;
}
结果:
数据在内存中的存储:
一、整数在内存中的存储
整数的二进制表⽰⽅法有三种,即原码、反码和补码。
有符号的整数,三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0
表⽰“正”,⽤1
表
⽰“负”,最⾼位的⼀位是被当做符号位,剩余的都是数值位。
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
对于整形来说:数据存放内存中其实存放的是补码。
为什么在计算机系统中,数值⼀律⽤补码来表⽰和存储呢?
原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器);此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
例如:-10
翻译成二进制是1000 0000 0000 0000 0000 0000 0000 1010
,这是原码;除符号位依次取反,得到反码1111 1111 1111 1111 1111 1111 1111 0101
;反码加1得到补码1111 1111 1111 1111 1111 1111 1111 0110
,所以整型-10
真正存储在内存中的是它的补码1111 1111 1111 1111 1111 1111 1111 0110
。
二、大小端字节序和字节序判断
当我们了解了整数在内存中存储后,我们调试看⼀个细节:
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
在VS2022x86环境下调试的时候,我们可以看到在a
中的0x11223344
这个数字是按照字节为单位倒着存储的。这是为什么呢?
因为只要有多个字节,那么就一定会存在字节存储顺序的问题。而且如果计算机处理器位数大于8,例如16位或者32位的处理器,寄存器宽度会大于1个字节,那么必然存在一个如何将多个字节安排的问题。就像这里有一个数字1018,1,0,1,8
分别代表一个字节,我们可以正着放进[]
中,这样[1018]
存储;也可以反着放进[]
中,这样[8101]
存储;还可以这样子[0181]
存储,把最高位放在最右边。为了统一与规范存储顺序,我们才需要去划分出大小端字节序,而我们学习时使用时也需要遵循这个标准。
2.1 什么是大小端?
⼤端(存储)模式:
- 是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:
- 是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
上述概念需要记住,⽅便分辨⼤⼩端。
高低位字节举例:
对于10
,它的原、补码是0000 0000 0000 0000 0000 0000 0000 1010
,十六进制表示是0x0000000a
。和数学中的数字一样有个、十、百、千分位,个位是低位,往上是高位,在C语言中数据同样有低位有高位,同样是从右往左是从低到高。所以0x0000000a
中0a是最低位字节
,相比起来其他的都是高位字节。
大小端举例:
例如:⼀个 16bit
的short
型x
,在内存中的地址设为0x0010
,x
的值设为0x1122
,那么0x11
为⾼字节,0x22
为低字节。对于⼤端模式,就将0x11
放在低地址中,即0x0010
中,0x22
放在⾼地址中,即0x0011
中。对于⼩端模式,刚好相反。我们常⽤的x86
结构是⼩端模式,⽽KEIL C51则为⼤端模式。很多的ARM,DSP都为⼩端模式。有些ARM处理器还可以由硬件来选择是⼤端模式还是⼩端模式。
2.2 练习
2.2.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;
}
我们来看一下这条语句return (*(char*)&i)
什么意思:
首先整型元素i
被赋值整数1,1在内存中是0x00000001
。
其中0x01
在低字节,现在我们取出i
的地址,访问到的是4个地址(i
有4个字节分别有4个地址),如果我们不强制类型转换,&i
的类型默认为int*
,解引用*
访问到的还是4个地址,但如果我们把&i
强制类型转换成char*
类型,我们再解引用*
就只能访问一个地址(默认取低地址),只得到该地址上的字节信息。
这时,如果代码运行的系统环境是大端字节序,那么(char*)&i
取到的低地址存储的是高字节0x00
;如果代码运行的系统环境是小端字节序,那么(char*)&i
取到的低地址存储的是低字节0x01
。
//代码2
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
代码2用了联合体的知识,i
与c
共用了地址,相当于存储i
用了4个字节与地址,而c
的存储也放在i
的地址中,数据字节序有大小端,但地址都是从低到高,所以char c
会与int i
共用的是i
的4个字节的地址中的低地址。当我们修改i
为1
时,通过c
我们可以知道i
低地址处存放的值,是i
4个字节的高字节还是低字节,进而我们可以判断系统的大小端。
2.2.2 练习2
//判断打印结果
#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;
}
首先这题我们需要明确一个概念,那就是在C语言中字符本质上其实是整数。
所以我们可以按照整数在内存中的存储规则去分析一下a,b,c
的存储:
首先-1
是一个整数,化成二进制形式是1000 0000 0000 0000 0000 0000 0000 0001
,这是原码,1111 1111 1111 1111 1111 1111 1111 1111
是它的补码。那么放进a,b,c
的又是什么呢?我们必须先弄清楚char
究竟是有符号的,还是无符号的以及有符号char
与无符号char
的区别。
char
类型详解:
char类型变量为1个字节,有8个bit位:
补码数据范围是:
00000000
00000001
00000010
00000011
...
01111111
10000000
10000001
10000010
10000011
...
11111111
对于signed char:
最高位当作符号位,所以从00000001到01111111都是正数的补码,正数的原、反、补码相同,从
00000001到01111111也是一系列正数的原码,这些正数大小为从1到127;
从10000000到11111111都是负数,负数的补码符号位不变其他位依次取反再加1得到原码,我们从
下11111111往上到10000001求原码可以得到原码10000001到11111111,数据大小为-1到-127;
对于10000000,它也是负数,这个补码的反码是11111111,反码加1,求得的原码应该是100000000,
占9个bit位,为了不造成数据丢失且不浪费10000000这个补码,所以C语言规定10000000这个补码就是
-128。
所以signed char数据的大小是-128~127。
对于unsigned char:
没有符号位,所以补码从00000000到11111111,同样是原码、反码,可以表示的数据大小是0~255。
对于char:
char究竟是有符号的char还是无符号的char,具体是取决于编译器的。
对于整数-1
的补码1111 1111 1111 1111 1111 1111 1111 1111
,char a
,signed char b
与unsigned char c
仅能存储一个字节,默认取低位存储,即取靠右的1111 1111
放入a,b,c
。
在printf
中,%d
打印有符号十进制整数,而整数有4个字节,char
类型只有一个字节,所以需要整型提升后打印。
原文链接:C语言整型提升的规则及样例详解
在VS上,char
默认是有符号的,所以char a
与signed char b
中存储的11111111
是有符号位的,整型提升需要补符号位,补成11111111 11111111 11111111 11111111
,而这个补码的原码是10000000 00000000 00000000 00000001
,即-1
,最终a,b
打印结果是-1
。
而unsigned char c
中存储的11111111
是无符号位的,整型提升需要补0
,补成00000000 00000000 00000000 11111111
,这个补码被当作有符号整型的补码时是表示正数,所以原、反、补码相同,这个原码表示的就是正整数255
,所以c
打印结果是255
。
最终结果:
2.2.3 练习3
再来两道类似的题目巩固练习2的知识。
//代码1
//判断打印结果
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
首先128
是整数,二进制表示为00000000 00000000 00000000 10000000
,128
是正数,原、反、补码相同,所以128
的补码也为00000000 00000000 00000000 10000000
,放入char a
中,取低位的8个bit位,即10000000
。
格式符%u
打印无符号十进制整数,char a
需要整型提升,在VS上char
默认是有符号的,所以整型提升补符号位,补成11111111 11111111 11111111 10000000
,这段补码被当作无符号整型的补码,无符号整型原、反、补码相同,所以11111111 11111111 11111111 10000000
表示的是4,294,967,168
。
最终打印结果:
//代码2
//判断打印结果
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
首先-128
是整数,二进制表示为10000000 00000000 00000000 10000000
,-128
是负数,-128
的二进制形式就是原码,取反加1得到补码11111111 11111111 11111111 10000000
,放入char a
中,取低位的8个bit位,即10000000
。
格式符%u
打印无符号十进制整数,char a
需要整型提升,在VS上char
默认是有符号的,所以整型提升补符号位,补成11111111 11111111 11111111 10000000
,这段补码被当作无符号整型的补码,无符号整型原、反、补码相同,所以11111111 11111111 11111111 10000000
表示的是4,294,967,168
。最终打印结果与代码1一致。
2.2.4 练习4
#include <stdio.h>
int main()
{
char a[1000];
for (int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
前面我们已经说过signed char
可以存储的数据大小是-128~127
,unsigned char
可以存储的数据大小是0~255
,而在VS上,char
默认是有符号的char
,所以当i
等于0~127
时,char a[1000]
数组都可以正常存储,但当i=128
时,a[128]
需要存储-129
这个整数,很明显-129
超出了char
的存储范围,必定会造成数据损失,那么怎么损失的呢?
首先-129
需要转换成补码的形式,即11111111 11111111 11111111 01111111
,存入char
类型元素a[128]
中,取低位的8个bit位,即01111111
,而这个补码进去后会被当成有符号的char
的补码,翻译过来真正表示的是127
,相当于char a[128] = 127
。
后面i
继续增大,当i=129
时,a[129]
需要存储-130
,-130
转换的补码是11111111 11111111 11111111 01111110
,存入a[129]
的是01111110
,相当于char a[129] = 126
。我们可以找到规律当i
等于128~255
时,a[i]
等于127~0
,也就是说当i = 255
时,a[255] = 0
,对于char
变量来说这里的0
是某个字符的ASCII码值,而这个字符其实是'\0'
,也就是说当i = 255
时,a[255] = '\0'
。
strlen
是求字符串长度的函数,遇到'\0'
停止,所以我们可以知道,strlen
只能计算下标从0
到254
共255个字符元素,当下标为255
时,arr[255] = '\0'
所以strlen
会直接返回,最终得到的字符串长度为255。
最终打印结果:
2.2.5 练习5
//代码1
//判断打印结果
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0;i <= 255;i++)
{
printf("hello world\n");
}
return 0;
}
容易知道unsigned char
类型的变量i
的数据大小永远在0~255
之间。
i
先存储0
的补码00000000
,不断加1,一直到255
的补码11111111
,此时再加1,本应是9个bit位的1 00000000
,但char
变量最多存储8个bit位,所以1
会被舍弃,取低位的8个bit位,于是i
又从00000000
开始轮回,所以i
始终<=255
,最终会死循环打印hello world\n
。
//代码2
//判断打印结果
#include <stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
首先我们可以判断,当i
等于9~0
时,都可以正常打印,但继续i--
呢?
容易知道对于0
的补码00000000 00000000 00000000 00000000
,减1可以得到11111111 11111111 11111111 11111111
即4,294,967,295
的补码,所以这里又是一个死循环,从4,294,967,295
到0
。
2.2.6 练习6
//判断输出结果
//X86环境 ⼩端字节序
#include <stdio.h>
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;
}
对于ptr1
:
&a + 1
可以取到整个a
数组的地址,加1指向a
数组后高一个字节的地址,用int*
强制类型转换int (*)[4]
类型的数组指针&a + 1
。
对于ptr2
:
(int)a + 1
将a
数组首元素的地址强制类型转换为整型并加1,再把int
类型的(int)a + 1
强制类型转换成int*
类型,相当于原本指向a
数组首元素的地址的指针向后指向高一个字节的地址,即int* ptr2 = (int*)( (char*)&a[0] + 1 )
。
%x
是以十六进制整数打印,对于ptr1[-1]
,打印的是a[3]
,十六进制打印是0x00000004
;对于*ptr2
,*ptr2 = 0x02000000
。
由于%x
打印的十六进制整数会省略前面的0x
与0
,所以最终打印结果为:
三、浮点数在内存中的存储
常⻅的浮点数:3.14159、1E10(即1.0 * 10^10)等,浮点数家族包括:float
、double
、long double
类型。
浮点数表⽰的范围:float.h
中定义。
我们可以在电脑中查找到float.h
这个文件,打开如下:
3.1 练习
#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("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
判断打印结果。
对于第一个printf
,我们可以确定,整型n
用%d
打印,可以正常打印得到9
;
对于第二个printf
,我们用float*
类型的指针存储了n
的地址,解引用应该得到什么值呢?如果按之前所说,指针类型决定了指针可以访问的字节数,而int
与float
都占用4个字节,也就是说int*
和float*
类型的访问权限是一致的,都是4个字节,现在对pFloat
解引用确实可以访问到n
的4个字节的内容,但是int*
和float*
的访问方式是否一致呢?用%f
对该内容打印,%f
用来输出实数,以小数形式输出,默认情况下保留小数点后6位,那么结果是不是9.000000
呢?
对于第三个printf
,我们用指针pFloat
修改n
的值为浮点数9.0
,再用n
去打印,打印结果是否是9
呢?
对于第4个printf
,我们对指针pFloat
解引用,打印结果是否是9.000000
呢?
直接看结果:
我们发现,4个结果我们只对了一半。
其实对于float
与int
类型的变量来说,它们两个内存中的字节内容的理解方式是不同的。
哲学点来说,一千个人心中有一千个哈姆雷特,不同的两个人对同一件事情的理解方式,读取方式是不同的。
也就是说虽然我们float*
的指针明明与int*
的指针的访问权限都是4个字节,但是访问方式是不同的,对于相同的字节内容,解读结果自然不同。当n
中存储的是整数9
时,我们用浮点数的方式去读取,或者当n
中存储的是浮点数9.0
时,我们用整数的方式去读取,这两种操作都是错误的,错误的行为自然导致错误的结果。
3.2 浮点数的存储
那么浮点数在内存中的表示方法到底与整数有何不同呢?
根据国际标准IEEE(电⽓和电⼦⼯程协会)754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
V = (−1)S ∗ M ∗ 2E
- (−1)S 表⽰符号位,当S=0,V为正数;当S=1,V为负数
- M 表⽰有效数字,M是⼤于等于1,⼩于2的
- 2E 表示指数位
举例来说:
⼗进制的5.0,写成⼆进制是101.0,相当于1.01×2^2。
那么,按照上⾯V的格式,可以得出S=0,M=1.01,E=2。
⼗进制的-5.0,写成⼆进制是-101.0,相当于-1.01×2^2。那么,S=1,有效数字M=1.01,E=2。
IEEE 754规定:
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
3.2.1 浮点数存储过程
IEEE 754对有效数字M和指数E,还有⼀些特别规定。
前⾯说过,1≤M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表⽰⼩数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的⽬的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于存储在内存中的虽然是23位,但实际上保存了24位有效数字。
⾄于指数E,情况就⽐较复杂
⾸先,E是⼀个⽆符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如,210的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001
。
3.2.2 浮点数读取过程
指数E从内存中取出还可以再分成三种情况:
E不全为0或不全为1:
这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。
⽐如:0.5的⼆进制形式为0.1,由于规定正数部分必须为1,即将⼩数点右移1位,则为1.0*2-1,其阶码为-1+127(中间值)=126,表⽰为01111110
,⽽尾数1.0去掉整数部分为0
,补⻬0到23位00000000000000000000000
,则其⼆进制表⽰形式为:
0 01111110 00000000000000000000000
E全为0:
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。
0 00000000 00100000000000000000000
E全为1:
这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s)。
0 11111111 00010000000000000000000
3.3 题目解析
下⾯,让我们回到⼀开始的练习:
#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("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
先看第1环节,为什么9当成浮点数读取,就成了0.000000?
9以整型的形式存储在内存中,得到如下⼆进制序列:
0000 0000 0000 0000 0000 0000 0000 1001
⾸先,将9的⼆进制序列按照浮点数的形式拆分,得到第⼀位符号位S=0,后⾯8位的指数E=00000000,最后23位的有效数字M=000 0000 0000 0000 0000 1001。
由于指数E全为0,所以符合E为全0的情况。因此,浮点数V就写成:
V = (-1)0 × 0.00000000000000000001001 × 2-126 = 1.001 × 2-146
显然,V是⼀个很⼩的接近于0的正数,所以⽤⼗进制⼩数表⽰就是0.000000…1001,只取小数点后六位得0.000000。
再看第2环节,浮点数9.0,为什么整数打印是1091567616?
⾸先,浮点数9.0等于⼆进制的1001.0,即换算成科学计数法是:1.001×2^3。
所以:9.0 = (−1)0 × 1.001 × 23,
那么,第⼀位的符号位S=0,有效数字M等于001后⾯再加20个0,凑满23位,指数E等于3+127=130,即10000010
所以,写成⼆进制形式,应该是S+E+M,即
0 10000010 001 0000 0000 0000 0000 0000
这个32位的⼆进制数,被当做整数来解析的时候,就是整数在内存中的补码,翻译成原码表示的正是1091567616。
原文地址:https://blog.csdn.net/Meanlong_/article/details/137724638
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!