(深度剖析数据在内存中的存储) C语言从入门到入土(进阶篇)

目录

1. 数据类型介绍

1.1 类型的基本归类

2. 整形在内存中的存储

2.1 原码、反码、补码

这里同时补充一下源码求补码的两种方法:

2.2 大小端介绍

2.3 练习 (每题都能让你意想不到)

3. 浮点型在内存中的存储

3.1 一个例子

 3.2 浮点数存储规则

扫描二维码关注公众号,回复: 13476361 查看本文章

3.3对浮点数存储的补充


与坚持信念者同行!  与坚持梦想者同行!

1. 数据类型介绍

前面我们已经学习了基本的内置类型:
char         // 字符数据类型
short       // 短整型
int         // 整形
long         // 长整型
long long   // 更长的整形
float       // 单精度浮点数
double       // 双精度浮点数
//C 语言有没有字符串类型?
以及他们所占存储空间的大小。

C99之前没有 longlong

int 在16位操作系统下 占2字节  32位 4字节  64位 4字节 (VS)

在新的标准下还有long double这样的类型

 布尔类型 _Bool  是用来表示真假的,早期人们是用 1  0  来表示真假 现在:

定义了ture(1) 和 false(0)   其实本质上_Bool 本质上就是 int 的重命名

当然用时要有头文件

我们一般说的int long 都是 unsigned int(long),short也是。但是对于char

是没有规定是 signed char 还是 unsigned char ,但是常见的编译器里面我们用

的时候他是signed char。

类型的意义:
1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
2. 如何看待内存空间的视角。

1.1 类型的基本归类

整形家族:
char
unsigned char
signed char
short
unsigned short [ int ]
signed short [ int ]
int
unsigned int
signed int
long
unsigned long [ int ]
signed long [ int ]

当我们打印有符号时应该用%d打印,用无符号时应该用%u

 这里虽然是-10,但是我们是用%d来解析,就是有符号

这里我们改为%u:

 大家看下图就很好理解了

 也就是说一开始的数字先放入先前开辟的空间当中同时转化为二进制数,然后再存储到内存中时才会看类型来看是有符号还是无符号,有符号就看符号位,无符号就直接把存进去的补码当成源码。

浮点数家族:

float
double 

构造类型:

> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union 

构造类型也叫自定义类型,比如int arr【10】  和 int arr【5】 就是两种类型,我们把函数名这里是arr除去,留下的就是类型名,这里是int 【10】 和 int 【5】 ,所以是自定义类型。

指针类型

int * pi ;
char * pc ;
float* pf ;
void* pv

 空类型:

void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。

2. 整形在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
比如:
int a = 20 ;
int b = - 10 ;

我们知道为 a 分配四个字节的空间。

那如何存储?
下来了解下面的概念:

2.1 原码、反码、补码

计算机中的整数有三种表示方法,即原码、反码和补码。
三种表示方法均有 符号位 数值位 两部分,符号位都是用 0 表示 ,用 1 表示 ,而数值位
负整数的三种表示方法各不相同。
原码
直接将二进制按照正负数的形式翻译成二进制就可以。
反码 
将原码的符号位不变,其他位依次按位取反就可以得到了。
补码
反码 +1 就得到补码。
正数的原、反、补码都相同。
对于整形来说:数据存放内存中其实存放的是补码。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统 一处理;
同时,加法和减法也可以统一处理( CPU 只有加法器 )此外,补码与原码相互转换,其运算过程 是相同的,不需要额外的硬件电路。

 我们看看在内存中的存储:

 

 

整数在内存中存的是补码,这是为什么呢?

这是因为:

这里同时补充一下源码求补码的两种方法:

法一:就是我们通识的源码得到反码就是符号位不变其他位按位取反然后反码得到补码就是反码加一。由补码得到源码就是逆着回去。

法二:这是作者比较喜欢用的。其实我们由补码得到源码一样可以按源码得到补码的方法,也就是说可以补码符号位不变,其他位按位取反得到反码,然后反码加一得到源码。这很神奇,大家可以自行验证哈!

 然后接着正文:

我们可以看到对于a和b分别存储的是补码。但是我们发现顺序有点不对劲

这是又为什么?

2.2 大小端介绍

什么是大端小端:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址 中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位 , ,保存在内存的高地 址中。

为什么有大端和小端:

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元 都对应着一个字节,一个字节为8 bit 。但是在 C 语言中除了 8 bit char 之外,还有 16 bit short 型, 32 bit long 型(要看具体的编 译器),另外,对于位数大于 8 的处理器,例如 16 位或者 32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如 何将多个字节安排的问题。因此就 导致了大端存储模式和小端存储模式。
例如:一个 16bit short x ,在内存中的地址为 0x0010 x 的值为 0x1122 ,那么 0x11 高字节, 0x22 为低字节。对于大端 模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式, 刚好相反。我们常用的 X86 结构是 小端模式,而 KEIL C51 则为大端模式。很多的 ARM DSP 都为小端模式。有些 ARM 处理器还可以 由硬件来选择是大端模式还是小端 模式。

百度2015年系统工程师笔试题:

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
#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; }
解释:
要判断我们就拿出第一个字节,看是0还是1。
其实对于上面我们可以先从简单的实现:

 法二:

 对法二简化:

 再简化:

2.3 练习 (每题都能让你意想不到)

1.
//输出什么?
#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; 
}

然后a是char所以a里面是 1111 1111(放的时候只看空间大小,只有在用的时候才看是有符号还是无符号)
所以:(都是补码现在)

 

现在我们要打印(也就是用,现在就要看有符号还是无符号了),然后现在是只有char,而打印要%d,所以要发生整型提升(有符号是补符号位无符号是补0)。

例a: 1111 1111 1111 1111 1111 1111 1111 1111(整型提升后的a,补码)

       1111 1111 1111 1111 1111 1111 1111 1110  反码

       1000 0000 0000 0000 0000 0000 0000 0001   源码

a和b是一样的

现在是c:

上面说到现在char里面都是 1111 1111 而c是无符号的所以最高位不是符号位

     :0000 0000 0000 0000 0000 0000 1111 1111   补码,而无符号数源反补相等

   所以c打印出来就是255。

2.
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0; 
}

1000 0000 0000 0000 0000 0000 1000 0000  -128的源码

1111 1111 1111 1111 1111 1111 0111 1111  反码

1111 1111 1111 1111 1111 1111 1000 0000  补码

a->   1000 0000

%u 是无符号整型打印  所以要发生整型提升

这里要注意:我们虽然是要打印无符号整型,但是可不就是整型提升的时候也按照无符号整型来提升,整型提升的时候看的是存他这个数字的类型,这里是char,也就是有符号,所以:

1111 1111 1111 1111 1111 1111 1000 0000 补码

这里又要注意了:虽然此时算出了补码,但是我们是不需要求源码的,因为我们是要无符号去打印,所以此时的补码就是源码,而此时的数字我们通过计算器就可以得到我们上面开始的结果。

3.
int i= -20;
unsigned  int  j = 10;
printf("%d\n", i+j); 
//按照补码的形式进行运算,最后格式化成为有符号整数

 

(-20的补码)

相加:

1111  1111  1111  1111  1111 1111 1111 0110   补码

1000 0000 0000 0000 0000 0000 0000 1001   反码

1000 0000 0000 0000 0000 0000 0000 1010   源码 

                          所以是 -10 

4.
unsigned int i;
for(i = 9; i >= 0; i--) {
    printf("%u\n",i);
}

 (死循环)

 因为这是无符号的i所以就不会有负数,即i>=0一定恒成立,为什么到0之后就变成那么大的数,因为0--到-1,即32个全1,但是这里是无符号数,所以就变成了全1的正数

5.
int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
   {
        a[i] = -1-i;
   }
    printf("%d",strlen(a));
    return 0; 
}

 打印出来是255,为什么呢?

目前我们可以知道前面的,但是先不要写后面的,因为我们也不知道后面应该是什么

在这里我们先讨论1个字节(char)到底可以放什么数值,注意:下面都是写的补码

 

 (这里对于-128对应的1000 0000是规定的,因为是不能和我们其他一样进行计算的)

这里我们来写一个-128的源码反码和补码,(这里是写了9 位),但是我们得到他的反码 1 1000 0000 然后存到char里面,也是我们上图中的 1000 0000,

所以这里规定1000 0000 (补码)是-128本身也是没有错的。

这里我们在 1111 1111      再加1,就变成 1 0000 0000 丢掉前面的1 变成 0000 0000,发现又回到了图的最上面的数,也就是说,这是一个循环:

这个个人认为还是比较重要的,但还是重在理解

我们上面推了char的,我们可以推演short也是一样的:

 PS:对于各类型的取值范围,我们可以通过#include<limits.h>我们可以右击,转到文档,我们就可以看到此编译器的各个类型的范围及各个定义。这里是vs的:

再回到我们的题目:

 我们这里是strlen求字符串长度,这个函数关注的是\0,也就是0,我们到0停止,-1到-128共128个字符,然后127到0128个字符,加起来长度就是255。

6.
#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0; 
}

这题会死循环,因为这是是无符号char,是0到255的取值范围,所以i是到不了256,即i<=255恒成立

3. 浮点型在内存中的存储

常见的浮点数:
3.14159
1E10
浮点数家族包括: float double long double 类型。
浮点数表示的范围: float.h 中定义

 PS:第一个是字面浮点型,第二个是科学计数法即1.0乘10的10次方。在浮点型(小数)后面加上f是float类型,不然默认为double类型。

3.1 一个例子

浮点数存储的例子:
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; 
}
输出的结果是什么呢?

这里我们先说结论,方面后续的理解:当定义一个整型然后用整型打印时值是不变的,浮点型也一样,但是定义整型来打印浮点型或者定义浮点型打印整型值是会变的,因为整型和浮点型的存储方式是不一样的,同时还要注意,只有整型才有源码反码补码,对于浮点型是不适用的。

这里先问大家一个问题,5.5的十进制数用二进制数怎么表示,是101.101?答案是错的,应该是101.1,为什么呢,这里的第一个1的权重是2的平方,0是2的一次方,1是2的0次方,最后一个是2的-1次方也就是2分之1,也就是0.5,其实和10进制数是相似的大家可以自行比较。

 3.2 浮点数存储规则

num *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
详细解读:
根据国际标准 IEEE (电气和电子工程协会) 754 ,任意一个二进制浮点数 V 可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^s 表示符号位,当 s=0 V 为正数;当 s=1 V 为负数。
M 表示有效数字,大于等于 1 ,小于 2
2^E 表示指数位。

举例来说:

十进制的 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

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 舍去以后,等于可以保存 24 位有效数字。
至于指数 E ,情况就比较复杂。
首先, E 为一个无符号整数( unsigned int
这意味着,如果 E 8 位,它的取值范围为 0~255 ;如果 E 11 位,它的取值范围为 0~2047 。但是,我们 知道,科学计数法中的 E 是可以出 现负数的,所以 IEEE 754 规定,存入内存时 E 的真实值必须再加上一个中间数,对于 8 位的 E ,这个中间数 127 ;对于 11 位的 E ,这个中间 数是 1023 。比如, 2^10 E 10 ,所以保存成 32 位浮点数时,必须保存成 10+127=137 ,即 10001001
然后,指数 E 从内存中取出还可以再分成三种情况:
E 不全为 0 或不全为 1
这时,浮点数就采用下面的规则表示,即指数 E 的计算值减去 127 (或 1023 ),得到真实值,再将
有效数字 M 前加上第一位的 1
比如:
0.5 1/2 )的二进制形式为 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 的很小的数字。
E 全为 1
这时,如果有效数字 M 全为 0 ,表示 ± 无穷大(正负取决于符号位 s );
好了,关于浮点数的表示规则,就说到这里。
解释前面的题目:
下面,让我们回到一开始的问题:为什么 0x00000009 还原成浮点数,就成了 0.000000
首先,将 0x00000009 拆分,得到第一位符号位 s=0 ,后面 8 位的指数 E=00000000 ,最后 23 位的有效数 M=000 0000 0000 0000 0000
1001
9 -> 0000 0000 0000 0000 0000 0000 0000 1001
由于指数 E 全为 0 ,所以符合上一节的第二种情况。因此,浮点数 V 就写成:
V=( - 1)^0 × 0.00000000000000000001001×2^( - 126)=1.001×2^( - 146)
显然, V 是一个很小的接近于 0 的正数,所以用十进制小数表示就是 0.000000
再看例题的第二部分。
请问浮点数 9.0 ,如何用二进制表示?还原成十进制又是多少?
首先,浮点数 9.0 等于二进制的 1001.0 ,即 1.001×2^3。
9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130
那么,第一位的符号位 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

3.3对浮点数存储的补充

这里用16进制看(E存进去是加上了127的因为是32位)

 同时我们去看一下浮点数里面的二进制(只有E里面)有没有大小端的存储形式。

 本来我们的数字应该是0x41 10 00 00 但是这是是反的也就是小端存储。然后我们再往上述说的进行补充,我们说源码反码补码是针对整型的,浮点型有自己的存储方式,但是浮点数在内存中的存储也按大小端的存储方式。

今天的内容就到这里了哈!!!肝了好几天,又难又多QAQ,累坏了实在QAQ

要是认为作者有一点帮助你的话!

就来一个点赞加关注吧!!!当然订阅是更是求之不得!

最后的最后谢谢大家的观看!!!

你们的支持是作者写作的最大动力!!!

下期见哈!!!

猜你喜欢

转载自blog.csdn.net/weixin_62700590/article/details/121507964