深度剖析数据在内存中的存储

1.数据类型介绍

  • C语言基本的内置类型:
char        //字符数据类型
short       //短整型
int         //整形
long        //长整型
long long   //更长的整形
float       //单精度浮点数
double      //双精度浮点数
  • 下面是32位机器和64位机器各类型所占不同的空间大小
    在这里插入图片描述

long类型所占字节大小,一般和指针变量所占空间大小一致(即对应不同位的机器,所占空间大小不一致)

  • 类型的意义
    1. 内存分配:
      不同类型占据不同大小的内存空间.通过定义适当的类型,可以分配合适的空间,避免内存溢出和浪费.
    2. 运算操作:
      类型之间可以进行特定的运算操作.例如整型可以用来算数运算,字符类型可以用来处理字符串,浮点类型可以更加相对精确地处理数据.
    3. 类型检查:
      编译器可以通过类型检查来检查程序中类型匹配和操作的合法性.如果出现类型错误,可以发出警告以供修改.
    4. 数据转换:
      C语言存在不同类型的类型转换机制,如隐式类型转换和显式类型转换.这些转换可以方便不同类型之间进行数据传递和操作.
    5. 变量声明和定义:
      变量必须先声明并且定义类型后,才可以使用.这样编译器才可以知道需要给该变量分配多少内存空间,并且告诉编译器如何处理该变量.

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]

在这里插入图片描述

  • 浮点数家族:
float
double
  • 构造类型(自定义类型):
数组类型        // int [10]  int [8]
结构体类型 struct
枚举类型 enum
联合类型 union
  • 指针类型
int *pi;
char *pc;
float *pf;
void *pv;
  • 空类型:

void表示空类型(无类型)
主要用于函数的返回类型,函数的参数,指针类型

//函数的返回类型
void print(int num);
{
    printf("数字是%d", num);
}

//函数的参数
void greet(void) 
{
    printf("Hello, World!\n");
}

//指针类型
void *pv = NULL;    //定义void指针类型,方便对其进行强制类型转换

2. 整型在内存中的存储

一个变量的创建是要在内存中开辟空间的.空间的大小是根据不同的类型而决定的.

  • 一般来说,内存是以字节单位来进行编址的,那么超过一字节大小的类型(例如short,int等等)是怎么在内存中存储的呢?
  • 如果数据有正负,那个负号又是怎么表示的呢?

比如:

int a = 20;
int b = -10;

这里的变量a和变量b都是占据4个字节的空间的,具体在内存中是如何存储的呢?

首先先要了解大小端模式的概念

2.1 大小端介绍

  • 对于跨越多字节的程序对象,我们必须建立两个规则:

这个对象的地址是什么?
我们在存储器如何对这些字节排序?

  • 两个存储模式,大端和小端:

大端(存储)模式:数据的低位保存到内存的高地址上,数据的高位保存到内存的低地址上.
小端(存储)模式:数据的低位保存到内存的低地址上,数据的高位保存到内存的高地址上.

假设变量x类型是int,位于地址0x100处,有一个十六进制值为0x01234567.地址范围0x100 - 0x103的字节顺序依赖于机器的类型:
在这里插入图片描述

注意:在字0x01234567中,高位字节是0x01,而低位字节是0x67.

  • 下面还有关于大小端的名字(endian)起源:
    在这里插入图片描述

  • 下面的代码用来打印不同类型的字节表示

#include <stdio.h>

typedef unsigned char* byte_pointer;

void show_bytes(byte_pointer start, int len)
{
	int i; 
	
	for (i = 0; i < len; i++)
	{
		printf("%.2x ", start[i]);
	}
	printf("\n");
}

void show_int(int x)
{
	show_bytes((byte_pointer)&x, sizeof(int));
}

void show_char(char c)
{
	show_bytes((byte_pointer)&c, sizeof(char));
}

void show_float(float f)
{
	show_bytes((byte_pointer)&f, sizeof(float));
}

int main(void)
{
	char a = 0x98;
	int b = 0x01234567;
	float c = 1.12;
	
	show_char(a);
	show_int(b);
	show_float(c);

	return 0;
}
  1. 将变量的首地址强制转换为unsigned char *,让程序把该地址存储的数据当成一串字节序列,而不是对应的类型变量内容.
  2. 通过遍历这个序列,得到从该变量首地址开始存放的字节顺序.
  3. 结果如下:
    在这里插入图片描述

这里发现,我的机器是小端存储模式,C语言是将该变量的低地址作为变量的首地址,低位0x67存放在了低地址上,即小端存储模式.
4. 同样还可以写出更多类型的打印函数,这里就写了三个类型.

  • 根据上面代码进行判断大小端模式,还是不够直接,下面的代码则更加直接:
#include <stdio.h>

int check_sys()
{
	short a = 0x0011;
	return (*(unsigned char*)&a);
}

int main(void)
{
	int ret = check_sys();

	if (ret)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}

	return 0;
}
  1. check_sys()直接进行判断,定义了一个占据两个字节的变量a,存储数据为0x0011
  2. (*(unsigned char*)&a)先将a对应存储的内容看成是一串字节串,随后将首地址上的一个字节元素进行返回.
  3. 根据小端和大端概念,返回高位0x00的则为大端,返回低位0x11的则为小端
  4. 结果如下:
    在这里插入图片描述

知道了类型在机器中排列的字节顺序,那么如何表示正负呢,下面就是原码,反码,补码相关的知识.

2.2 原码,反码,补码

绝大多数计算机存储整数,都是以补码形式存储在内存空间的.补码是有符号位的.

正整数的原,反,补码都相同.
负整数的原,反,补码各不相同.

原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码

反码
原码的符号位不变,其他位按位取反就可以得到反码

补码
反码+1就得到补码

为什么计算机会使用补码呢?

在计算机系统中,数值一律用补码来表示和存储.原因在于,使用补码,可以将符号位和数值域统一处理.
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程和是相同的,不需要额外的硬件电路(符号位不变,数值位取反加1).

例如:

#include <stdio.h>

int main(void)
{
	int a = 10;
	//补码: 0000 0000 0000 0000 0000 0000 0000 1010 
	//		0x0000000a

	int b = -10;
	//原码: 1000 0000 0000 0000 0000 0000 0000 1010
	//补码: 1111 1111 1111 1111 1111 1111 1111 0110
	//		0xfffffff6

	int c = a + b;
	printf("%d\n", c);

	return 0;
}
  1. 根据调试,观察到a,b对应存储形式是补码形式:
    在这里插入图片描述

  2. 只有通过补码,才能将算数运算运算准确,两数补码相加,得到0x00000000

2.3 练习

2.3.1

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;
}
  1. char类型在C语言中默认是signed,有符号的.

  2. 有符号数(即二进制补码)转换为无符号数,若该类型有w位,正数真值不变,负数真值加上2^w:
    在这里插入图片描述

  3. 类型提升,将符号位扩展至对应类型长度(例如:(char)0xf1 -> (short)0xfff1),原数值大小不变

  4. 所谓的转换,转换的是真值的值,在计算机内部,二进制是不变的.

  5. a大小为-1,b大小为-1,c大小为-1 + 2^8 = 255

  6. 程序运行结果:
    在这里插入图片描述

2.3.2

2.
//输出什么?
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0;
}
  1. char类型占据内存1个字节大小的空间,可存储数字范围为-128 - 127
  2. %u代表以unsigned int打印该数字
  3. a在内存中为0x10,整型提升后为0xffffff10,该数表示无符号数则为2^32 - 1274294967168
  4. 程序运行结果为: 在这里插入图片描述

2.3.3

3.
//输出什么?
#include <stdio.h>
int main()
{
    char a = 128;
    printf("%u\n",a);
    return 0;
}
  1. 每个类型所能存储的数据范围可以认为是一个圈,如超出了存储范围,将该值与该类型所能存储最值的差的补码形式加上该最值.即该变量在内存中真实存储的值.
    在这里插入图片描述

  2. 128超出了-128 - 127的范围,他比1271,0x7f + 0x01 = 0x10

  3. 0x10整型提升为0xffffff10,该数表示无符号数则为2^32 - 1274294967168

  4. 程序运行结果为: 在这里插入图片描述

2.3.4

4.
//输出什么?
#include <stdio.h>
int main(void)
{
	int i = -20;
	unsigned  int  j = 10;
	printf("%d\n", i + j);

	return 0;
}
  1. 有符号类型和无符号类型进行计算,隐式转换将有符号类型转换为无符号类型

  2. 无符号数转换为二进制补码(有符号数),若该数二进制位有w位,若 无符号数大小 =< 2^(w-1),则大小不变;若无符号数大小 < 2^(w-1),则将改值减去2^w
    在这里插入图片描述

  3. 计算结果为-20 + 2^32 + 10 = 2^32 - 10,此值为无符号数的值.最后再将无符号数转换为有符号数的值2^32 - 10 - 2^32 = -10

  4. 程序运行结果为: 在这里插入图片描述

  5. 当然也可以将两数都先转换为补码形式,再进行计算,那样还是没有这样直接.

2.3.5

5.
//输出什么?
unsigned int i;
for(i = 9; i >= 0; i--)
{
    printf("%u\n",i);
}

此程序为死循环,先打印9 8 7 6 5 4 3 2 1 0,随后打印-1 + 2 ^ 32 4294967295,继续不断减小,死循环

在这里插入图片描述

2.3.6

6.
//输出什么?
int main()
{
    char a[1000];
    int i;

    for(i=0; i<1000; i++)
    {
        a[i] = -1-i;
    }

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

    return 0;
}
  1. strlen()从该地址向后遍历,每次增加一个元素的所占的内存空间,同时计数加1,直至该元素为0,返回无符号整数,返回值为计数的结果.

  2. 数组每个元素都是char类型的,即每个元素所能存储范围为-128 - 127

  3. 通过分析,这是该数组的元素内容情况: 在这里插入图片描述

  4. 可得结果255

  5. 程序运行结果为: 在这里插入图片描述

2.3.7

7.
//输出什么?
#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0;
}
  1. unsigned char类型范围为0 - 255,i始终小于255,结果为死循环
  2. 程序运行结果为:
    在这里插入图片描述

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

常见的浮点数:

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

3.1 浮点数存储规则

  • 根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制数浮点数V可以表示成下面的形式:V = (-1)^S * M * 2^E
    举例来说:
    在这里插入图片描述
    在这里插入图片描述

  • IEEE754规定浮点数是由三个部分组成的,分别由上面求出的S,M,E经过转换得到:
    在这里插入图片描述

    尾数的符号位S:

    • 非负数为0,负数为1.

    有偏移的指数E:

    • E为一个unsigned int,这意味着,如果E8位,取值范围是0-255;如果E11位,取值范围是0-2047.
    • 得到最后的指数位,需要将E + Bias,这里的Bias就是偏移量.对于8位的E,Bias = 127 = 0111 1111;对于11位的E,Bias = 1023 = 011 1111 1111
    • 例如:2^10E10,要将其保存成32位浮点数时,必须保存成10 + 127 = 13710001001.

    规格化的尾数M:

    • 1 =< M < 2,M可以写成1.XXXXXX的形式,其中XXXXXX是小数部分.
    • M保存为有效数字位的时候,默认这个数第一位总是1,因此可以被舍去,只保存小数部分XXXXXX.
    • 例如:1.01,只保存01,等到读取的时候,再将第一位的1加上去.这样可以节省一位有效数字.
  • IEEE 754规定

对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M.

在这里插入图片描述

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M.

在这里插入图片描述

  • 将指数E从内存中取出还可以分成三种情况:
    E全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的偏移值减去偏移量127或者1023,得到真实值,再将有效数字M加上第一位的1.
例如:

0.5
整数部分:   0 = 0
小数部分:   0.5 = 1
0.5 = 0.1 = 1 * 2^(-1)

sign:0
biased exponent: -1 + 127 = 126 = 0111 1110
normalised mantisa: 0
单精度浮点数表示为:0 0111110 00000000000000000000000

E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值
有效数字M不再加上第一位的1,而是还原成0.XXXXXX的小数.这样做是为了表示+0-0,以及接近于0的很小的数字.

E全为1

这时,如果有效数字M全为0,表示正负无穷大,正负取决于符号为S

3.2 一个例子

//打印结果是什么?
int main(void)
{
    int n = 9;
    float *pFloat = (float *)&n;    //以单精度浮点数格式读取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. 9int类型存储到n所在空间中,即为0x00000009
  2. float同样是32位,所以*pFloat存放的也是0x00000009
    • printf("n的值为:%d\n",n);即将0x00000009int类型打印,结果为9
    • printf("*pFloat的值为:%f\n",*pFloat);即将0x00000009按单精度浮点数打印,0 00000000 00000000000000000001001,表示一个接近0的浮点数,V = (-1)^0 * 0.00000000000000000001001 * 2^(-126) = 1.001 * 2^(-146),%f只显示6位小数,结果为0.000000
  3. 9.0float存储到指针pFloat所指空间中
    9.0 
    整数部分: 9 = 1001
    小数部分: 0 = 0
    9.0 = 1001.0 = 1.001 * 2^3
    
    sign: 0
    biased exponent: 3 + 127 = 130 = 1000 0010
    normalised mantisa: 001
    单精度浮点数: 0 10000010 00100000000000000000000
    
    所以该空间中存放了0100 0001 0001 0000 0000 0000 0000 00000x61100000
    • printf("num的值为:%d\n",n);即将0x61100000int类型打印,结果为2^30 + 2^24 + 2^20 = 1091567616,该数在int类型范围内,打印1091567616
    • printf("*pFloat的值为:%f\n",*pFloat);即将0x61100000float类型打印,保留小数点后6位,打印9.000000
  4. 该程序运行结果为: 在这里插入图片描述

本章完.

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值