C语言修炼——内存函数与数据的存储

发布于:2024-04-19 ⋅ 阅读:(22) ⋅ 点赞:(0)

内存函数介绍:

一、memcpy使用和模拟实现

在这里插入图片描述

  • 函数memcpysource的位置开始向后复制num个字节的数据到destination指向的内存位置
  • destinationsource所指向内存空间中存储的两段数据的类型互不相干,数据在内存中的存储都是以二进制文本的形式,memcpy是从字节的层面复制所有的二进制信息
  • 这个函数在遇到'\0'的时候并不会停下来,而是复制完指定的字节数
  • 如果sourcedestination有任何的重叠,复制的结果都是未定义的
  • 为了避免overflows,需要确保destinationsource指向的数组的大小至少为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,53,4,5,6,7时,假如我们从前往后复制,先复制1,2到了3,4的位置,3,4被替换,我们继续想要复制3,4,55,6,7时就找不到3,4,因为3,4已经被替换为1,2,所以我们在3,4位置找到的是1,2,最终复制到5,6位置的还是1,2,再复制元素到75的位置找到的是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

数据在内存中的存储形式是二进制文本,本质是由01组成的一串数字。一个字节有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 010x00 00 00 020x00 00 00 030x00 00 00 04,因为一个int类型占4个字节,这里我们需要修改这16个字节。需要注意的是value接收的值需要满足在0~255,如果超出就会造成数据丢失,因为一个字节最多有两个十六进制数,而两个十六进制数能表示的最大数字为0xff,即255
例如:当我们把257传给value时,想要以十六进制数完整表示257需要3位数,即0x101,但由于一个字节只有两位十六进制数,所以计算机只会取低位,即0x01,最终把0x01放入arr数组的十六个字节中。

四、memcmp的使用

在这里插入图片描述

  • ⽐较从ptr1ptr2指针指向的位置开始,向后的num个字节
  • 与字符串比较函数strcmp,strncmp不同,memcmp遇见'\0'并不会停止比较,会比较完所有字节
  • 如果ptr1当中的字节小于ptr2当中的字节则返回值<0;如果ptr1ptr2当中的所有字节均相等则返回值=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语言中数据同样有低位有高位,同样是从右往左是从低到高。所以0x0000000a0a是最低位字节,相比起来其他的都是高位字节。
大小端举例:
例如:⼀个 16bitshortx,在内存中的地址设为0x0010x设为0x1122,那么0x11为⾼字节,0x22为低字节。对于⼤端模式,就将0x11放在低地址中,即0x0010中,0x22放在⾼地址中,即0x0011中。对于⼩端模式,刚好相反。我们常⽤的x86结构是⼩端模式,⽽KEIL C51则为⼤端模式。很多的ARMDSP都为⼩端模式。有些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用了联合体的知识,ic共用了地址,相当于存储i用了4个字节与地址,而c的存储也放在i的地址中,数据字节序有大小端,但地址都是从低到高,所以char c会与int i共用的是i的4个字节的地址中的低地址。当我们修改i1时,通过c我们可以知道i低地址处存放的值,是i4个字节的高字节还是低字节,进而我们可以判断系统的大小端。

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:
	最高位当作符号位,所以从0000000101111111都是正数的补码,正数的原、反、补码相同,从
0000000101111111也是一系列正数的原码,这些正数大小为从1127;
	从1000000011111111都是负数,负数的补码符号位不变其他位依次取反再加1得到原码,我们从
下11111111往上到10000001求原码可以得到原码1000000111111111,数据大小为-1-127;
	对于10000000,它也是负数,这个补码的反码是11111111,反码加1,求得的原码应该是100000000,
占9个bit位,为了不造成数据丢失且不浪费10000000这个补码,所以C语言规定10000000这个补码就是
-128。
	所以signed char数据的大小是-128~127。
对于unsigned char:
	没有符号位,所以补码从0000000011111111,同样是原码、反码,可以表示的数据大小是0~255。
对于charchar究竟是有符号的char还是无符号的char,具体是取决于编译器的。

对于整数-1的补码1111 1111 1111 1111 1111 1111 1111 1111char asigned char bunsigned char c仅能存储一个字节,默认取低位存储,即取靠右的1111 1111放入a,b,c
printf中,%d打印有符号十进制整数,而整数有4个字节,char类型只有一个字节,所以需要整型提升后打印。

在这里插入图片描述
原文链接:C语言整型提升的规则及样例详解

在VS上,char默认是有符号的,所以char asigned 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 10000000128是正数,原、反、补码相同,所以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~127unsigned 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只能计算下标从0254共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 111111114,294,967,295的补码,所以这里又是一个死循环,从4,294,967,2950

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 + 1a数组首元素的地址强制类型转换为整型并加1,再把int类型的(int)a + 1强制类型转换成int*类型,相当于原本指向a数组首元素的地址的指针向后指向高一个字节的地址,即int* ptr2 = (int*)( (char*)&a[0] + 1 )

%x是以十六进制整数打印,对于ptr1[-1],打印的是a[3],十六进制打印是0x00000004;对于*ptr2*ptr2 = 0x02000000
在这里插入图片描述
由于%x打印的十六进制整数会省略前面的0x0,所以最终打印结果为:
在这里插入图片描述

三、浮点数在内存中的存储

常⻅的浮点数:3.14159、1E10(即1.0 * 10^10)等,浮点数家族包括:floatdoublelong 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的地址,解引用应该得到什么值呢?如果按之前所说,指针类型决定了指针可以访问的字节数,而intfloat都占用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个结果我们只对了一半。
其实对于floatint类型的变量来说,它们两个内存中的字节内容的理解方式是不同的。
哲学点来说,一千个人心中有一千个哈姆雷特,不同的两个人对同一件事情的理解方式,读取方式是不同的。
也就是说虽然我们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。