剖析数据在内存中的存储

1.数据类型的介绍

C语言内置的数据类型:

char                //字符类型

short               //短整型

int                   //整型

long                //长整型

long long        //更长的整型

float                //单精度浮点数类型

double            //双精度浮点数类型    

类型的意义:

1.使用类型来创建变量,在内存中开辟一块空间(类型的大小决定了开辟空间的大小)

2.不同的数据有不同的类型,使操作更加灵活

1.1类型的基本归类

整型:

char

        unsigned char

        signed char

short

        unsigned short

        signed short

int

        unsigned int

        signed int

long

        unsigned long

        signed long

long long

        unsigned long long

        signed long long

浮点数类型:

float

double

构造类型:

数组类型

结构体类型 struct

枚举类型 enum

联合体类型 union 

指针类型:

char* pc

short* ps

int* pi

long* pl

float* pf

double* pd 

空类型:

void 

void类型可用于函数的参数,返回的类型,指针的类型。

2.整型在内存中的存储 

前面说过,类型可以创建一个变量,并在内存中开辟相应大小的空间,那么数据在内存中是怎么存储的呢?

要先了解几个概念:原码、反码和补码

2.1原码、反码、补码

计算机中的整数有3种表示的方法,分别是原码、反码、补码。

3种表示方法均有符号位和数值位两部分,符号位用‘0’表示正,用'1'表示负。数值位则是具体的数值转化为二进制位。符号位占一个位,数值位占31个位。

剖析数据在内存中的存储_第1张图片

(图中一个方块为一个bit位)

原码:

将数值按照正负数的形式转化为二进制位

反码:

将原码的符号位不变,其他位按位取反

补码:

反码加1就是补码

例如

-10的原码为10000000000000000000000000001010

         反码为11111111111111111111111111110101

         补码为11111111111111111111111111110110

 20的原码为00000000000000000000000000010100

         反码为01111111111111111111111111101011

         补码为01111111111111111111111111101100

正数的原码、反码、补码都相同。

虽然整数有3种的表示方法,但是整数在内存中只存储它的补码。

原因如下:

在计算机系统中,整数的数值一律用补码来表示和存储,我们输入时其实输入的是原码。使用补码可以将符号位和数值位统一处理;

加法和减法也可以统一处理(CPU只有加法器),而且补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

 接下来看看-10和20在内存中的存储

剖析数据在内存中的存储_第2张图片

剖析数据在内存中的存储_第3张图片

 可以看到内存中存放的是补码,但是顺序好像有点不太对。

这里又引入一个概念,叫做大小端存储模式。

2.2大小端存储模式介绍

什么是大端小端:

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

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

(这里的低位或者高位都至少为1个字节即8个bit位的大小)

例如0xff112233的高位是0xff,而低位是0x33

 为什么会有大端小端:

在计算机地址中,是以字节为单位的,每个地址单元都对应着一个字节,而一个字节为8bit位。但是在C语言中除了8bit的char,还有16bit和short和32bit的long(大小根据具体的编译器决定),对于位数大于8位的处理器,比如16位和32位的处理器,由于寄存器宽度大于1个字节,那么必然存在着如何将多个字节安排的问题,所以就产生了大小端的存储模式。

 这里举个例子,用short类型创建一个变量a,假设其值为0x1122,&a为0x0001

大端存储模式如图(一个小方块为一个地址单元,即一个字节大小)

剖析数据在内存中的存储_第4张图片

 小端存储模式如图(一个小方块为一个地址单元,即一个字节大小)

剖析数据在内存中的存储_第5张图片

 

 我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。而有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

 设计一个小程序来判断当前机器的字节序

思路:创建一个变量a并赋值1存入内存中,其16进制为0x00000001,如果是小端模式,那么a的首地址里存放的是0x01,而如果是大端模式,那么a的首地址里存放的是0x00,对于如何访问a的首地址里的空间,可以用char类型的指针。

代码:

#include 

int main()
{
	int a = 1;
	char* p = (char*) & a;
	int ret = *p;
	if (ret == 1)
	{
		printf("小端");
	}
	else
	{
		printf("大端");
	}
	return 0;
}

也可以将对大小端的判断写成一个int类型的函数,是大端,那么返回0,是小端,那么返回1

#include 

int check()
{
	int a = 1;
	return *(char*)&a;
}

int main()
{
	int ret = check();
	if (ret == 1)
	{
		printf("小端");
	}
	else
	{
		printf("大端");
	}
	return 0;
}

练习

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;
}

-1的补码是11111111111111111111111111111111,将这个数放入char类型的a中,要进行截断,因为a只有一个字节即8个位的大小,所以最后a中存放的是11111111

以此类推,可以知道b中存放的是11111111,c中存放的是11111111,有所区别的是,a和b的最高位都是符号位,c则是无符号数。

而打印的时候是以%d的形式打印的,%d打印的是有符号的整数,计算机认为a,b,c都是有符号整数,所以a,b,c都要发生整型提升,对于a来说,a是有符号数,整型提升的时候看符号位是0还是1,是1的话高位补1,是0的话高位补0,所以a整型提升后为11111111111111111111111111111111,b也是有符号数,所以和a的整型提升的结果一样,而c是无无符号数,整型提升的时候高位直接补0就可以了,所以c整型提升的结果是00000000000000000000000011111111,而这些都是补码,是在计算机中发生的运算,最终打印出来我们看到的是原码,所以要转换为原码。

a的原码为10000000000000000000000000000001

b的原码为10000000000000000000000000000001

c的原码为00000000000000000000000011111111

最终输出的结果为a=-1,b=-1,c=255

2.

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

这里也是用一样的方法,先把-128的补码11111111111111111111111110000000写出来,然后截断成10000000放入a中,%u打印的是无符号整数,按照a的类型来发生整型提升为11111111111111111111111110000000,最后打印。

结果为4,294,967,168

3.

#include 
int main()
{
  char a = 128;
  printf("%u\n",a);
  return 0;
}

128的补码是00000000000000000000000010000000,截断后是10000000,整型提升后为11111111111111111111111110000000,%u会将其格式化为无符号数然后打印,所以结果为4,294,967,168

4.

#include

int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	return 0;
}

整数存入整型中不用截断,因为都是4个字节的大小,而 i + j 的运算可以转化为补码进行运算,最后格式化为有符号整数并打印。

-20的补码为11111111111111111111111111101100

 10的补码为00000000000000000000000000001010

相加为1111 1111 1111 1111 1111 1111 1111 0110

按照%d的形式打印,%d会认为其是个有符号数,所以还要把他的原码写出来,是

1000 0000 0000 0000 0000 0000 0000 1010

所以结果是-10

5.

#include 

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

对于这个题,可以画一个图来理解

剖析数据在内存中的存储_第6张图片

对于unsigned int 类型来说,它的原码、反码、补码都是一样的,图中按照顺时针的顺序,它的取值范围是0——4,294,967,295(这里换算成十进制,图中是二进制)。

而对于int 类型来说,从补码到原码需要换算(图中写的是补码),所以它的取值范围是 -2,147,483,648——2,147,483,647

题中当循环 i 从0开始减1的时候,相当于在内存中 i 的补码由00000000000000000000000000000000变成了11111111111111111111111111111111,所以 i 会始终保持在>=0的范围内。

因此输出结果为死循环并且一直按顺序打印数字。

6.

#include 

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

最终输出的是strlen(a),所以要在数组a中找到’\0‘的位置。

同样的可以画一个char类型的取值范围图来理解

剖析数据在内存中的存储_第7张图片

 -1-i的循环即数字按照图中逆时针来变化(-128减1变成127),当i为255的时候,存入a[255]中的数据刚好为0,根据ASCII码表,0的值对应的就是'\0',所以strlen计算的是0——254的字符个数,为255。

7.

#include 

unsigned char i = 0;
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world\n");
	}
	return 0;
}

unsigned char类型的取值范围图为

剖析数据在内存中的存储_第8张图片

 所以运行结果为死循环打印hello world\n

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

1.浮点数存储的例子

#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;
}

 输出的结果是什么呢?

 剖析数据在内存中的存储_第9张图片

 结果是否和你想的一样呢?

如果不是的话,我们还需了解浮点数存储的规则。

2.浮点数存储规则

我们需要理解浮点数在计算机内部的表示方法

根据国际标准IEEE (电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

(-1)^S*M*2^E(这里的^是次方,不是按位异或)

(-1)^S中的S表示符号位,当S为0,V(浮点数)为正数,当S为1,V为负数

M表示有效数字,范围为大于等于1,小于2

2^E中的E表示指数位

举个例子:

 十进制的5.0写成二进制是101.0,即(-1)^0*1.01*2^2,则S = 0, M = 1.01, E = 2

十进制的-5.0写成二进制是-101.0,即(-1)^1*1.01*2^2,即S = 1, M = 1.01, E = 2

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

剖析数据在内存中的存储_第10张图片 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数位E,剩下的52位是有效数字位M

剖析数据在内存中的存储_第11张图片

 IEEE 754对有效数字M和指数E,还有一些特别的规定

对于有效数字M

M的取值范围是1<=M<2,所以M可以写成1.XXXXXX的形式,其中XXXXXX表示小数部分。

IEEE 754规定,在计算机内部保持M时,默认这个数的第一位总是1,所以这个数可以被舍去,只保存后面的XXXXXX部分。

例如在保存1.01的时候,只保存小数点后面的01,等到读取出来的时候,再把第一位的1加上去,这样做的目的,是可以节省1位有效数字,可以提高精度。以32位浮点数为例,用以保存M的位数只有23位,这样舍去第一位后,可以保存24位有效数字。

对于指数E,情况比较复杂

首先,E是一个无符号整数

E如果是8位,那么取值范围是0——255,E如果是11位,取值范围是0——2047。但是科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值,必须要再加上一个中间数,对于8位的E,这个中间数是127,对于11位的E,这个中间数是1023.

例如E为10的时候,要先加上127,将137即1000 1001存入E中

然后,将指数E从内存中取出也可以分为3种情况:

1.E不全为0或不全为1

指数E要减去127(或1023),得到真实值,再在有效数字M前加上第一位的1.

2.E全为0

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

3.E全为1

这时,如果有效数字M全为0,表示±无穷大

关于浮点数的规则,就讲到这里了。

接下来看看开始的那道题目

#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;
}

 int n = 9 将9存入n中,是将整数按照整型的方式进行存储,然后float* pFloat = (float*)&n是以浮点数的类型对n这块空间进行访问,9存放在内存中的补码是00000000000000000000000000001001,按照浮点数存储规则,可以知道S = 0,E = 00000000, M = 00000000000000000001001.

对此,浮点数V可以写成:

V = (-1)^0*0.00000000000000000001001*2(-126) = 1.001*2^(-146),以%f的形式打印即为0.000000.

对于*pFloat = 9.0,是以浮点数的类型进行存储,所以要对9.0进行一个转换

9.0(十进制) --> 1001.0(二进制) --> (-1)^0*1.001*2^3 --> S = 0, M = 1.001, E = 3

而存入内存的时候M要舍去第一位,E要加上127,所以在内存中的形式为

0 1000 0010 001 0000 0000 0000 0000 0000

而后以%d的形式打印,所以第3个结果为1,091,567,616.

我是煎饼,今后也会不定期更新

你可能感兴趣的:(c#,c语言,开发语言,visual,studio)