C语言从青铜到王者第五篇·数据在内存中的存储
Posted ·潇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言从青铜到王者第五篇·数据在内存中的存储相关的知识,希望对你有一定的参考价值。
本篇前言
从本篇开始,我们要开始逐渐和内存打交道了。想学好C语言,打牢编程基本功,我们心中一定要时刻有内存的概念。
文章目录
数据类型及其意义
整型与浮点型
char
short
int
long
long long
float
double
构造(自定义)类型
数组类型
结构体类型
struct
枚举类型
enum
联合类型
union
空类型
void
void test (void) 函数返回类型 函数参数
void* p 指针
指针类型
以上所有类型 *
数据类型的意义
数据类型的意义有两个:
1.决定为变量开辟内存空间的大小
2.决定看代内存中0/1序列的视角
这两句话到底是什么意思,相信看完本文你就清楚了
整型数据在内存中的存储
整型家族
char
unsigned char
signed char
short
unsigned short
signed short
int
unsigned int
signed int
long
unsigned long
signed long
为什么char
类型是整型家族呢?
因为字符型数据是按照ASCII码值存储在内存中的,而ASCII码值也是整数,所以char
类型也是整型家族的一员
整型数据在内存中的存储
任何数据在内存中都是以二进制序列存储。整数的二进制序列有三种形式,分别是原码、反码、补码
整数在内存中是以补码的形式存储的,比如看下面的a:
#include<stdio.h>
int main()
int a = -10;
return 0;
为什么-10在内存中存储为f6 ff ff ff
的形式呢?
- 详解:
-10的原码:
10000000 00000000 00000000 00001010
-10的反码:
11111111 11111111 11111111 11110101
-10的补码:
11111111 11111111 11111111 11110110
换算成16进制:
ff ff ff f6
为什么内存中存补码而不存原码?
比如我们想计算 1 - 1这个算数
由于计算器底层没有减法器,我们需要用加法器代替减法器
1 - 1 → 1 + (-1)
用原码:
1 :00000000 00000000 00000000 00000001
-1 :10000000 00000000 00000000 00000001
相加:10000000 00000000 00000000 00000010
结果是 -2 ,不符合结果
用补码:
1 : 00000000 00000000 00000000 00000001
-1 :11111111 11111111 11111111 11111111
相加:1 00000000 00000000 00000000 00000000
由于只有32位,首位丢弃:
结果:00000000 00000000 00000000 00000000
结果为0,符合结果
出现这一现象的本质原因是
1.补码可以将二进制数据的符号位和数值位统一处理,可以将加法和减法统一处理
2.补码和原码的相互转换,其运算过程是相同的,不需要额外的电路(原码→取反+1→补码 、补码→取反+1→原码)
这也是为什么密码学家发明补码的一个原因
大小端字节序
刚刚-10的例子的结果同学们一定有疑问:
-10的16进制补码:ff ff ff f6
而编译器中内存值:
为什么顺序正好反过来了呢?
这就涉及到了大小端字节序的问题
我们把一个内存单元看成一个整体,1字节8bit位,8位二进制即二位16进制,所以两个16进制的数字就表示一字节的大小,也就是一个内存单元的大小。而将这些单元编号(也就是标上地址)的顺序是可以不同的:
大端字节序:高位数字放在高地址(符合我们的阅读习惯)
小端字节序:高位数字反而放在低地址(我的编译器的内存中存储类型)
为什么有大小端?
内存中,每个地址单元都对应一个内存单元,大小为1字节8bit。如果存储的类型都是8bit大小,也就不需要对内存单元进行排序,但是C语言中还有16bit的short类型,32bit的int类型等等超过一个内存单元大小的数据类型,现在流行的32位64位处理器的寄存器宽度也大于一个字节,所以我们不得不面临将多字节安排的问题
我写的判断自己编译器大小端的代码:
#include<stdio.h>
int main()
int a =1;
char* p = &a;
if (*p)
printf("小端");
else
printf("大端");
return 0;
实例操练
题一:请手算下面的程序的输出结果
#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;
- 详解:
-1的补码:
11111111 11111111 11111111 11111111
由于a、b、c三个变量都是一字节
截断:
11111111
则a、b、c变量位置内存值为:
11111111
a、b为有符号char,所以最高位为符号位(char是有符号还是无符号,是由编译器决定的,但是大部分编译器是有符号)
1 1111111
由于是以%d打印,需要进行整型提升
a、b整型提升后补码:
11111111 11111111 11111111 11111111
打印出原码的十进制:
-1
c为无符号char,所以所有位都是数值位
c 整型提升后补码:
00000000 00000000 00000000 11111111
原码:
00000000 00000000 00000000 11111111
十进制:
255
题二:请手动计算下面的程序的输出结果
#include<stdio.h>
int main()
char a = -128;
printf("%u", a);
return 0;
- 详解:
-128的二进制原码:
10000000 00000000 00000000 10000000
反码:
11111111 11111111 11111111 01111111
补码:
11111111 11111111 11111111 10000000
放入char中发生截断:
10000000
由于%u打印,所以整型提升,且无符号位
11111111 11111111 11111111 10000000
由于无符号数的原反补码相同
总结:
1.数值是以补码在内存中操作(截断、整型提升)的
2.
printf
中的%d等类型决定的是最后看待内存中补码的角度和是否需要整型提升
题三:请手动计算下面的程序的输出结果
#include<stdio.h>
int main()
int i = -20;
unsigned int j = 10;
printf("%d\\n", i + j);
return 0;
- 详解:
-20的二进制序列
10000000 00000000 00000000 00010100
反码
11111111 11111111 11111111 11101011
补码
11111111 11111111 11111111 11101100
10的二进制序列
原反补码
00000000 00000000 00000000 00001010
i + j
补码相加
11111111 11111111 11111111 11110110
反码
11111111 11111111 11111111 11110101
原码
10000000 00000000 00000000 00001010
%d为有符号整型,结果为
-10
题四:请手动计算下面的程序的输出结果
#include<stdio.h>
int main()
unsigned int i;
for (i = 9; i >= 0; i--)
printf("%u\\n", i);
return 0;
感性的定性判断一下:i
是无符号数,无符号数一定大于等于0,所以代码会死循环
- 详解:
9的二进制序列
00000000 00000000 00000000 00001001
以u%看待,就是9
同理程序依次打印出87654321
i = 0
时,打印出0
00000000 00000000 00000000 00000000
然后i--
:
11111111 11111111 11111111 11111111
由于是无符号数,所以原反补码相同
它的十进制为:
232-1
所以这整个循环就是:
232-2→
232-3→
…→
1→
0→
232-1→
232-2→
…(死循环)
题五:请手动计算下面的程序的输出结果
#include<stdio.h>
#include<string.h>
int main()
char a[1000];
int i;
for (i = 0; i < 1000; i++)
a[i] = -1 - i;
printf("%d", strlen(a));
return 0;
- 详解:
-1的二进制序列补码
11111111 11111111 11111111 11111111
放入a[0]截断
11111111
原码:
10000001
a[0]是-1
-2的二进制补码
11111111 11111111 11111111 11111110
截断后原码
10000010
a[1]是-2
一直到-127
原码
10000000 00000000 00000000 01111111
反码
11111111 11111111 11111111 10000000
补码
11111111 11111111 11111111 10000001
截断后
10000001
原码
11111111
a[126]是-127
-128
原码
10000000 00000000 00000000 10000000
补码
11111111 11111111 11111111 10000000
截断
10000000
原码的值为-128(特殊序列)
-129
原码
10000000 00000000 00000000 10000001
补码
11111111 11111111 11111111 01111111
截断
01111111
原码的值为127
循环为
-1 → -128 → 127 → 0
当a为0时候,就是strlen的结束标志
所以前面有128+127一共255个元素
char类型存储的数据范围
内存中补码:00000000 → 11111111
正数:00000000 → 01111111 即 0 → 127
负数:11111111 → 1000001 即原码 10000001 → 11111111 即 -1 → -127
特殊补码序列 10000000 由于数值位无法再减去1 所以直接规定 10000000 的原码值为 -128
因为-128本身的原码:
10000000 00000000 00000000 10000000
反码:
11111111 11111111 11111111 01111111
补码:
11111111 11111111 11111111 10000000
装入char类型中发生截断:
10000000
也就是特殊序列10000000
所以char的取值范围是 -128 → 127
浮点型数据在内存中的存储
浮点型家族
float
double
常见的浮点数:
3.1415926
1E10 (1×10^10)
引例
#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,打印出正整数9
但二、三、四的结果就很让人匪夷所思了
下面就来解析为什么会出现这样的结果
浮点型数据是怎么放入内存的
浮点数在内存中的存储是由IEEE(电气电子工程协会)的754标准规定的:
任意一个浮点数可以表示成
(-1)^s * M * 2^E
(-1)^s 表示符号位 s=1就是负数 s=0就是正数
M表示有效数字 1 <= M < 2
E表示指数位
下面详细说一下这是啥意思。
首先我们要学会将小数转换成二进制数。
我们以前都学过科学计数法,这种计算小数的方法其实就是用2作为底数的科学计数法
举例说明怎么转换:
现有浮点数 十进制表示法为 5.5
5的二进制:101(整数除以2,结果的余数作为每一次的结果,除数再除以2,直到除数为0)
0.5的二进制:0.1(小数乘以2,结果的整数位作为每一次的结果,小数再乘以2,直到小数为0)
所以5.5的二进制表示就是101.1
科学计数法就是 1.011×2^2
则5.5表示成 (-1)^s * M * 2^E
就是
(-1)^0 * 1.011 * 2^2
s=0
M=1.011
E=2
拿到了这三个参数后,我们看看它们是怎么存入内存的
754规定:
对于float类型,一共分配4字节32bit内存
第1位为s位,第2-9位 8bit 为 E,第10-32位 23bit 为M
对于double类型,一共分配8字节64bit内存
第1位为s位,第2-12位 11bit 为 E,第13-64位 52bit 为M
S的值:正数放0 负数放1 与整数的符号位意义相同
M的值:由于M一定是1.xxxxxx的形式,所以1可以不存,只存后面的数字(为了增加存储的有效数字量,增加精度),后面的数字直接顺序排列在M的位置上
E的值:由于E是整数,可正可负 8bit的范围是0-255,11bit的范围是0-2047,所以E的值必须在原来整数的基础上加上127和1023,这样E表示的范围就是-127 → 128和-1023 → 1024
综上,可以知道float和double表示的数字范围
紧接上文,5.5的三个参数为
s=0
M=1.011
E=2
拿到了这三个参数后,经过处理:
s=0,E=2+127=129=10000001,M=011
所以总序列为
0 10000001 01100000000000000000000
再把二进制换成16进制
0100 0000 1011 0000 0000 0000 0000 0000
40 b0 00 00
再按照小端字节序排序
00 00 b0 40
来见证奇迹吧:
实验证明浮点型数据确实是这样存储的
浮点型数据是怎么从内存中拿出来的
我们已经知道了浮点型数据是怎么放入内存中,现在来探讨浮点型数据是怎么拿出来的
浮点数的取出,其实就是存入的反操作,但是有以下不同的情况
情况一:E全为0
指数E的真实值为-127或-1023,此时的浮点数无限接近于0,此时M的值还原时不再+1,表示为很小的数字,显示出来就是0
情况二:E全为1
表示±无穷大
情况三:E不全为0且不全为1
E-127得到E的真实值 M+1得到M的真实值 最后得到真实的浮点数
现在终于可以把引例重新拿过来看看:
#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;
9的二进制位:
0000000 0000000 0000000 00001001
按浮点型存储划分
0 00000000 00000000000000001001
可见E为全0,符合情况一,所以打印出来是0.000000(后面的位数显示不出来)
而9.0的二进制位:
1.001×2^3 s=0 E=130 M=001
0 10000010 00100000000000000000000
再以有符号整型的十进制读出
这样第三个结果也就出来了
至此,上面程序结果的解析也就全部完成,浮点数在内存中的存储也讲解完毕
回顾一下,本文重点讲解了整型和浮点型数据在内存中的存储形式。现在我们知道这些数据是如何被存放在内存中的了。从本文可以看出,计算机和人脑的思维方式差别还是很大的,当“人脑”用“电脑”的方式思考问题是不是多少有点“烧脑”呢?
以上是关于C语言从青铜到王者第五篇·数据在内存中的存储的主要内容,如果未能解决你的问题,请参考以下文章