一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)

一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)

:scissors: 写在前面

经过上篇博客的学习,你已经知道了数据的运算, 那数据在内存中又是如何存储的呢? 今天bug郭就带你一起学习数据在内存中的储存!

@TOC

:100: 本章重点

数据类型详细介绍

整形在内存中的存储:原码、反码、补码

大小端字节序介绍及判断

浮点型在内存中的存储解析

:book: 数据类型介绍

那些我们学过的C语言数据类型,你还记得多少,我们一起来整理一一下吧:book:

:eye:内置类型

char //字符型 1byte

int //整型 4byte

short//短整型2byte

long //长整型4/8byte

long long //更长的整型8byte

float //单精度浮点型 4byte

double//双精度浮点型8byte

//C语言中无字符串类型

类型的意义 之前的博客中已经介绍过了

类型可以决定该类型的变量在内存中创建内存空间的大小

类型可以决定指针访问的权限,加减指针的位移

我们可以根据我们变量的大小合理选择类型,创建空间大小。 不同的数据类型根据它们的字节大小,需要占用不同空间大小的内存空间

类型的基本归类

整型家族

char

signed char

unsigned char

short

signed short [int]

unsigned short [int]

int

signed int

unsigned int

long

signed long [int]

unsigned long [int]

long long

signed long long [int]

unsigned long long [int]

注意:字符型也归类为整型家族,每个类型都有有符号类型和无符号类型。

浮点数家族

float

double

构造类型

//结构体类型

struct

//枚举类型

enum

//联合类型

union

指针类型

char*

int*

float*

void*

空类型

void

void空类型 通常使用在函数的参数,返回值,指针。

:tm:整形在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。 那接下来我们来看看整型是如何存储的。 例如:

int a=1;

int b=-3;

我们已经知道整型占用内存空间为4个字节。那么是如何分配储存的! 我们先来了解一下计算机中有符号数的三种表示方法: 原码,反码,补码

计算机中有符号数有三种表示方法,原反补。

这三种表示方法,都是由符号位和数值位组成,符号位1表示负数,0表示正数,数值位各不相同! 原码

直接将数据通过二进制正负的形式翻译过来的的二进制位

反码

由原码,符号位不变,数值位按位取反。

补码

反码+1得到补码!

正数的原反补相同

==数据是以补码的形式在内存中存储== 为啥是补码呢? 学过计算机原理的同学肯定了解,因为计算机的CPU中运算器(ALU)只能进行加法!所以负数要转化成加法运算,而补码很好的解决了这个问题!

:heavy_check_mark: 大小端

根据我们之前博客的学习:eye:,避免bug,调试技巧我们已经知道了,调试窗口,可以查看变量的地址和内存,我们&x可以查看到x在计算机中内存的储存。

int x=1;

//x为整型有32二级制位

//而每4个二进制位是一个16进制位,

//x=1的16进制表示方法:00 00 00 01

而我们看到vs下x的内存,低位01却存在最左边。 为啥会存到最左边呢? 我们可以看到x占用4个字节空间,地址从左往右依次递增!低地址存低位字节数据,高地址存高位字节数据。 这就是我们所介绍的小端存储。 而大端存储,不言而喻就是,高地址存低位,低地址存高位! 总结:

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

为啥会有大小端

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

总结: 计算机寄存器宽度大于 一个字节,那么就多个字节类型数据的存储就产生了不一样的大小端存储模式。

:old_key:判断大小端

我们已经知道有大小端两种存储模式,而我们要如何判断一台机器是小端存储,还是大端储存呢?也就是判断当前机器的字节序?

我们可以设计几个程序,来验证该不同机器的字节序。 设计思路 我们可以想办法将某一地址处存的字节数据拿出即可判断,如果高地址低字节位,说明是小端存储,否者就是大端存储模式。

//代码一

//利用char*指针得到低地址的字节数据

#include

int main()

{

int a=1;

int *pa=&a;

//利用char*存储a第一个字节的低地址

char*pc=(char*)pa;

printf("%d",*pc);//访问这个字节的地址,打印数据

return 0;

}

低地址打印了低字节位,说明bug郭的机器是采用小端存储模式! 我们刚刚是说写个程序,判断字节序,所以我们需要封装一下!

//代码1

#include

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;

}

我们之前还了解到了一个C语言自定义类型联合体,我们后期还会详细介绍! 联合体就是一块空间,多个变量联合使用,共同占用一块空间!当我们访问其中一个变量,该空间就存储着该变量! 我们可以利用联合体这一特性来判断字节序

//代码2

int check_sys()

{

union

{

int i;

char c;

}un;

un.i=1;

return un.c;

}

学会了吗,这就是大小端的判断!

:punch: 小试牛刀

到这里我们已经学习了整型在内存中如何存储,我们来写几个练习巩固一下吧!

练习题目

下面一共7道题目 大家可以试着练习一下,我会给大家一一讲解

//练习1

//输出什么?

#include

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

#include

int main()

{

char a = -128;

printf("%u\n",a);

return 0;

}

//练习3

#include

int main()

{

char a = 128;

printf("%u\n",a);

return 0;

}

//练习4

#include

int main()

{

int i= -20;

unsigned int j = 10;

printf("%d\n", i+j);

//按照补码的形式进行运算,最后格式化成为有符号整数

}

//练习5

#include

int main()

{

unsigned int i;

for(i = 9; i >= 0; i--)

{

printf("%u\n",i);

}

return 0;

}

//练习6

int main()

{

char a[1000];

int i;

for(i=0; i<1000; i++)

{

a[i] = -1-i;

}

printf("%d",strlen(a));

return 0;

}

//练习7

#include

unsigned char i = 0;

int main()

{

for(i = 0;i<=255;i++)

{

printf("hello world\n");

}

return 0;

}

:pushpin: 练习讲解

//练习1

//输出什么?

#include

int main()

{

char a = -1;

//-1 : 补码 11111111 11111111 11111111 11111111

//截断放入 a: 11111111

signed char b = -1;

//-1截断放入b中 b: 11111111

unsigned char c = -1;

// 同理 c: 11111111

printf("a=%d,b=%d,c=%d", a, b, c);

//a和b是有符号字符型,%d打印整型提升补充符号位后

//补码 11111111 11111111 11111111 11111111

//得到原码:10000000 00000000 00000000 00000001

//所以a和b打印结果是-1

//而c是无符号字符型,所以整型提升,补充0

//00000000 00000000 00000000 11111111

//转换原码 00000000 00000000 00000000 11111111

//所以c的打印结果是 225

return 0;

}

运行结果

//练习2

#include

int main()

{

char a = -128;

//-128 原码:00000000 00000000 00000000 10000000

//补码: 11111111 11111111 11111111 10000000

//截断存入char a 中 10000000

printf("%u\n", a);

//%u无符号的形式打印

//a是有符号char 整型提升补充符号位

// 11111111 11111111 11111111 10000000

//而%u默认该数据为无符号数据,所以认为a的原码补码相同

//打印结果 4294967168

return 0;

}

运行结果

//练习3

#include

int main()

{

char a = 128;

//128 00000000 00000000 00000000 10000000

//截断存入char a :10000000

printf("%u\n", a);

//整型提升char a有符号,补符号位

//11111111 11111111 11111111 10000000

//%u无符号的形式打印,认为该数据原码补码相同

//打印 4294967168

return 0;

}

看到练习3的结果和练习2的结果一样,一个是-128,一个是128 但以%u打印了一样的结果! 因为无论是128还是-128截断后存储到a都是相同的二进制位!

//练习4

#include

int main()

{

int i = -20;

//-20 原码:10000000 00000000 00000000 00010100

//补码 : 11111111 11111111 11111111 11101100

unsigned int j = 10;

//10 : 00000000 00000000 00000000 00001010

printf("%d\n", i + j);

//i+j补码:11111111 11111111 11111111 11110110

//原码: 10000000 00000000 00000000 00001010

// 打印 -10

//按照补码的形式进行运算,最后格式化成为有符号整数

}

运行结果

//练习5

#include

int main()

{

unsigned int i;

//无符号int i 所以始终大于等于0

for (i = 9; i >= 0; i--)

{

printf("%u\n", i);

//无法退出循环

}

return 0;

}

unsigned int范围:0~2^32 代码会发生死循环!

运行结果

//练习6

#include

int main()

{

char a[1000];

int i;

for (i = 0; i < 1000; i++)

{

a[i] = -1 - i;

//a[i]是字符型,范围为-128~127

//超过会进行截断存入a[i]中

//当-129存入a[128]中截断

//-129 :原 10000000 00000000 00000000 10000001

// 补码 11111111 11111111 11111111 01111111

//截断 后存入a[128]中 01111111

//此时a[128]符号位为0 故存入的为 127

//后面数据同理

//当a[255]=-256

// -256 原 10000000 00000000 00000001 00000000

//补码 : 11111111 11111111 11111111 00000000

// 故此时a[255]存入的是 0

}

printf("%d", strlen(a));

//strlen 遇到'\0'停止计数,也就arr[255],所以返回长度为255

return 0;

}

char中的范围就是这样的,所以但一个数据小于-128时下一个数据就是127 大于127下一个数据就是-128 运行结果

//练习7

#include

unsigned char i = 0;

//unsigned char 范围为0~255

int main()

{

for (i = 0; i <= 255; i++)

{

printf("hello world\n");

//当i=255时,i++后,i循环回到i=0

//所以该代码会发生死循环

}

return 0;

}

运行结果 这就是所以练习的答案了,是不是还有点意犹未尽!如果还没学会可以多看几遍!

:star: 重点归纳总结

计算机中数据的存储和计算都是以补码的形式进行的!

整型提升还有截断的对象也是针对补码。

无符号整型提升,二级制位补充0,有符号整型提升,二进制位补充符号位。

%u(无符号打印)自动认为打印的数据是无符号数据,所以存储的补码也就是原码,%d(有符号打印)认为打印的数据是有符号类型的,要将数据转换成原码打印输出!

:sweat_drops: 浮点型在内存中的存储

我们已经学会了整型在内存中的存储,你肯定会好奇,浮点型数据该怎样存储在内存中呢?

常见的浮点数:

3.14159 1E10 2.7

浮点数家族包括:

float、double、long double 类型。

浮点数表示的范围: vs中float.h有详细介绍浮点数的表示范围,有兴趣的伙伴可以期查阅一下,bug郭截取了一段供大家参考:

// float.h

//

// Copyright (c) Microsoft Corporation. All rights reserved.

//

// Implementation-defined values commonly used by sophisticated numerical

// (floating point) programs.

//

#pragma once

#ifndef _INC_FLOAT // include guard for 3rd party interop

#define _INC_FLOAT

#include

#pragma warning(push)

#pragma warning(disable: _UCRT_DISABLED_WARNINGS)

_UCRT_DISABLE_CLANG_WARNINGS

_CRT_BEGIN_C_HEADER

#ifndef _CRT_MANAGED_FP_DEPRECATE

#ifdef _CRT_MANAGED_FP_NO_DEPRECATE

#define _CRT_MANAGED_FP_DEPRECATE

#else

#ifdef _M_CEE

#define _CRT_MANAGED_FP_DEPRECATE _CRT_DEPRECATE_TEXT("Direct floating point control is not supported or reliable from within managed code. ")

#else

#define _CRT_MANAGED_FP_DEPRECATE

#endif

#endif

#endif

大家肯定会疑问,这是个啥,看不懂啊,其实bug郭也看不懂,哈哈哈,不过问题不大!

浮点型的其类型说明符有float 单精度说明符,double 双精度说明符。在Turbo C中单精度型占4个字节(32位)内存空间,其数值范围为3.4E-38~3.4E+38,只能提供七位有效数字。双精度型占8 个字节(64位)内存空间,其数值范围 1.7E-308~1.7E+308,可提供16位有效数字。

兄弟们,我们写个代码看看,你就了解了浮点型!

#include

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;

}

输出结果会是怎么样的?会是像我们整形数据那样分析吗? 结果肯定是否定的!

运行结果: 可以看到打印结果完全出乎我们意料,num和*pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大? 要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。所以我们可以知道,浮点型数据和整型数据在计算机中有着不一样的存储方式!

:thumbsup: 浮点数存储方式介绍

根据国际标准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。

对于double 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);

好了,关于浮点数的表示规则,就说到这里。

学到这我们了解了浮点数据的存储方式,就可以把刚刚的运行结果解释清楚了! 解析

#include

int main()

{

int n = 9;

//n 9 00000000 00000000 00000000 00001001

float* pFloat = (float*)&n;

//*pFloat :00000000 00000000 00000000 00001001

//利用存储公式 (-1)^s * M *2^E

//所以 s 为 0为正数

//E 为 00000000 全为0 E无需减去127,M无需加上1.

//M 000000 00000000 00001001

//所以*pFloat 是一个无限接近0的数

printf("n的值为:%d\n", n);

//n的打印结果肯定是9

printf("*pFloat的值为:%f\n", *pFloat);

//所以打印结果为0.000000

*pFloat = 9.0;

//9.0 存入 float中

//9.0: 00001001.0 向左移动3位 00001.001*2^3

//s为0 E为3 3+127=130

// s 0

// E 10000010

// M 001后面添20个0补齐 00100000000000000000000

//所以存入 *pFloat 数据为

// 0 10000010 00100000000000000000000

printf("num的值为:%d\n", n);

//*pFloat解引用操作,将n的值也改变了

//补码 01000001 00010000 00000000 00000000

//转换成10进制 1091567616

printf("*pFloat的值为:%f\n", *pFloat);

return 0;

}

让我们回到一开始的问题: 为什么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。 那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010。 所以,写成二进制形式,应该是s+E+M,即 01000001 00010000 00000000 00000000

这个32位的二进制数,还原成十进制,正是1091567616 。

:trophy: 总结

浮点数存入时,由于指数有可能是负数,所以统一单精度指数加上127,双精度加上1023存入E中

当M有二进制位不足时采用右边补位补0补齐M

E全为0和1时为特殊情况!

兄弟们看到这里那就收藏一下吧!

相关推荐

阿根廷历史足坛十大前锋,排名不是梅西也不是马拉多纳,猜猜是谁
程序员:光明前景与挑战共存
beat365在线体育打不开

程序员:光明前景与挑战共存

📅 07-21 👁️ 1399
魔兽狼人哪个版本有的
365手机卫士

魔兽狼人哪个版本有的

📅 07-07 👁️ 7767