【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储

数据的存储

  • 前言
  • ✨数据类型汇总
    • 整型家族
    • 浮点型家族
    • 自定义类型
    • ‍指针类型。
    • 空类型
  • 大小端字节序说明
    • 出现大小端字节序的原因
    • 字节序的概念
    • ✋大小端字节序
    • ‍百度系统工程师笔试题(通过编程判断该编译器为大端存储还是小端存储)
      • 问题分析
      • 代码演示
      • 代码分析
  • 整型数据在内存中的存储
    • 原码、反码、补码
    • 截断与整型提升
    • 整型数据存储练习
  • 浮点型数据在内存中的存储
    • 证明整数和浮点数的存取方式不同
    • IEEE标准形式
    • IEEE存储标准规定
    • IEEE读取标准规定
  • 总结

前言

我们在敲代码的时候总是会定义各种变量,对各种数据进行存储,比如int a = 10;就是将10这个数据存放进变量a中,而变量a,就是我们在内存中申请开辟的一块空间。
在内存中如何开辟空间给变量的问题博主已经在函数栈帧里用反汇编的方式将其原理剖析了,具体可看图解函数栈帧 - 函数栈帧的创建及销毁。
本文将进一步剖析在已经开辟好存储单元的情况下,各种数据是如何存储的。


在了解数据如何存储之前,应该先了解我们常见的数据类型。

✨数据类型汇总

在C99标准中,我们可将数据类型划分为以下几大类。

  1. 整型家族
  2. 浮点型家族(实型家族)
  3. 自定义类型(构造类型)
  4. 指针类型
  5. 空类型

下面一一介绍这五种类型的基本情况。

整型家族

char
		unsigned char
		signed char
short
		unsigned short [int]
		signed short [int]
int
		unsigned int
		signed int
long
		unsigned long [int]
		signed long [int]

注:在C99之后的标准规定,将char类型数据划分为整型家族,因为字符在内存中会将其转化为ASCII码值进行存储。

如上所示,所有的整型家族都被分为有符号整型和无符号整型,并且signed都是可以被省略的,换言之,signed int完全等价于int,其他以此类推,但其中有一个例外: char类型和signed char并不等价,只写一个char ch = 0;我们将无法分辨这个ch变量到底是有符号字符型还是无符号字符型,他完全取决于编译器,但经博主测试,大部分编译器下char类型都被编译器翻译为有符号的char类型。

在C99中还引入了long long - 长长整型,用法和long类型一致,但C语言语法规定,sizeof(long)<= sizeof(long long),而long类型所占内存大小为4/8字节,所以long long类型所占内存空间大小一定为8个字节。

浮点型家族

float
double

浮点型家族只有float和double这两种类型,float类型所占空间大小为4byte,double类型所占空间大小为8byte。

他们之间的区别除了所占空间大小不同之外还有精度的区别,float称为单精度浮点型,有效精度为小数点后6位,而double类型称为双精度浮点型,精确到小数点后15位,但其有效数字只有11位左右。

自定义类型

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

这里可能会有很多人无法李姐为什么数组类型也被划分为自定义类型,这里稍微做一些解释。

我们知道数组类型的变量定义形式:数据类型+数组名+[数组大小];

如:

int arr[10] = { 0 };

这里可能会让很多人产生误区,认为arr数组的类型是int类型,也就把这条语句理解为是int类型的、数组名为arr的数组大小为10的数组,其实不然,这个数组的数组名确实是arr,但其数据类型是int [10],这里可能让大部分人无法接受,

举个简单的例子即可解释:

我们知道,sizeof操作符是用来计算所占内存空间大小的,其操作数既可以是变量名,也可以是变量类型。

#define _CRT_SECURE_NO_WARNINGS 1

#include 

int main()
{
     
	int a = 10;

	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));

	return 0;
}

这两种写法都正确,打印结果为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第1张图片

而对于数组,操作数也同样可以是数组名或者数组类型:

#define _CRT_SECURE_NO_WARNINGS 1

#include 

int main()
{
     
	/*int a = 10;

	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));*/

	int arr[10] = {
      0 };

	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(int[10]));

	return 0;
}

其打印结果为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第2张图片
这么一来,就验证了int [10]是数组类型。

知道了这点,解释为什么数组类型是自定义类型就更清晰了,用上面解释的结论就可以知道,int arr[10]和int arr[9]的数组类型不同,并不都是int类型的,数组大小是我们程序员人为规定的,所以可以把他划分为自定义类型。

其他的自定义类型比较明显,这里就不一一解释。

‍指针类型。

指针类型很特殊。

我们常说的指针有两个含义:

  1. 某一个变量的地址,也就是其在内存中的编号,我们可称其为指针。
  2. 用于存放地址(编号)的变量,我们称其为指针变量,常简称指针。

指针类型的定义方式为:

数据类型+*(用于标识指针类型)+指针变量名

常见的指针类型有:

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

这里着重介绍一点,指针变量赋值大部分都是取出某变量地址存放进指针变量,如int pc = &c;

但有一个例外:

int main()
{
	char* pc = "hello world";

	printf("%c\n", *pc);

	return 0;
}

这里之间将一个字符串常量赋值给指针变量pc,我们知道,字符串常量时放在常量区的,他的值不可修改,并且这里的字符串加上隐藏的’\0’总共是12个字节,而我们的指针变量根据平台的不同只能是4/8个字节,怎么都不可能放的下这个字符串常量,所以这么理解是错误的。

我们将其打印看看结果:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第3张图片
打印结果为单字母h,这么一来其实就解释的通了,将整个常量字符串赋值给指针变量,其实并不会把整个字符串放进去,而是把整个字符串的首地址赋给指针变量,比较指针存放的就是地址,这和将字符数组名赋值给指针变量类似,存放的都是首元素地址。

空类型

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

下面举几空类型的例子帮助理解:

  • 返回类型:
void test(int x)
{
     
	printf("%d\n", x);
}

int main()
{
     
	int a = 10;
	test(a);

	return 0;
}

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第4张图片

这里test函数的返回类型就是void。

  • 函数的参数:
int test(void)
{
     
	return 1;
}

int main()
{
     
	int ret = test();

	printf("%d\n", ret);

	return 0;
}

这个代码就是将函数的参数置为空,表示不允许主调函数传参,如果非要传参,编译器将给出警告。

int test(void)
{
     
	return 1;
}

int main()
{
     
	int a = 10;
	int ret = test(a);

	printf("%d\n", ret);

	return 0;
}

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第5张图片

  • 指针类型:
void* pc;

表示定义一个指针pc,但他什么都不指向,作为一个空指针存在。


大小端字节序说明

我们知道不管是什么样的数据,最终都会被编译器编译为二进制机器码进行存储,并且我们的内存是以字节为最小存储单元划分而进行存储的,那么就存在了一个问题,数据以字节为单位进行存储的时候,是以怎样的顺序进行存储的呢?这就引出了大小端字节序的概念。

出现大小端字节序的原因

为什么会有大小端字节序模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit位。但是在C语言中除了8bit的char类型之外,还有16bit的short类型,32bit的long类型(要看具体的编译器,64位平台long类型为64位),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器的宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

例如:一个16bit位的short类型变量x ,在内存中的地址为0x0010,变量x 的值为0x1122 ,那么0x11为高字节,0x22为低字节。对于大端模式,就将 0x11放在低地址中,即0x0010中,0x22 放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86(32位平台)结构是小端模式,而KEILC51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

字节序的概念

字节序,即字节顺序,又称端序或尾序,在计算机科学领域中,指「存储器」中或者「数字通信链路」中,组成多字节的字节排列顺序 。在几乎所有的机器上,多字节对象都被存储为连续的字节序列 。例如在C语言中,一个 int类型的变量x地址为0x100,那么其对应的地址表达式&x的值为0x100 且 x 的4个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。字节的排列方式有2个通用规则。

  1. 顺序排列 - 大端字节序
  2. 逆序排列 - 小端字节序

上面的文字描述也许过于抽象,接下来用较为容易理解的方式分别简单的介绍大端字节序和小端字节序的概念。

✋大小端字节序

所谓大小端字节序,就是将多字节数据中的高低字节位按不同顺序存放在内存中的高低地址处,相当于顺(逆)序存放。接下来博主将把上述抽象概念划分逐一介绍:

  1. 首先理解什么叫做多字节数据。

我们知道一个数据根据大小不同被划分为不同的数据类型,各数据类型所占字节数不同,我们也就据此根据数据字节大小来将其存放于不同的数据类型中。

比如字符类型 - 其扩展之后的ASCII码值为0~255,我们知道一个字节是8位,按照无符号字符型的理解也就是从00000000 ~ 11111111,刚好是0 ~ 255,所以字符类型被称为单字符类型数据。

而十六进制数,如:0x11223344则为多字节数据,其中有4个字节,分别是0x11、0x22、0x33、0x44,像这样的数据则被称为多字节数据。


  1. 理解什么叫做多字节数据的高字节位。

在一个二进制序列中,

如:01010110101001011010100101101001

我们把前方高亮部分的0101称为高字节位,把后端加删除线的1001 部分称为低字节位,以此区分。

其实很好理解,因为最后一个1的的权重为20,也就是2的0次方,而第一个0的权重为231,也就是2的31次方,以此来区分高低字节位也是很不错的选择。


接下来介绍大小端字节序的存储方式:

大端字节序

所谓大端字节序,就是将处于高字节位的数据存放在内存的低地址处,将处于低字节位的数据存放在内存的高地址处

如今给一数据:0x11223344

在内存中的存放形式为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第6张图片
以这样的形式存放的模式,就称为大端存储模式,这样的存放顺序,也就被称为大端字节序。

小端字节序

所谓小端字节序,就是将处于高字节位的数据存放在内存的高地址处,将处于低字节位的数据存放在内存的低地址处

今给一数据:0x11223344

在内存中的存放形式为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第7张图片
以这样的形式存放的模式,就称为小端存储模式,这样的存放顺序,也就被称为小端字节序。

在博主使用的VS2019编译器上,采用的就是小端字节序:

例:

int main()
{
	int a = 0x0000ff40;

	return 0;
}

调试 - 内存窗口(&a):

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第8张图片
0x001DFEFC就是该代码中a变量的地址,存放情况为40 ff 00 00。

也就是小端存储模式。

‍百度系统工程师笔试题(通过编程判断该编译器为大端存储还是小端存储)

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

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

该题前半部分在上文其实已经解决了,这里博主将分析问题,并实现代码。

问题分析

要判断编译系统到底是大端存储还是小端存储,其实并不复杂。

如0x11223344

如果是在大端存储模式下:
存储方式为:11 22 33 44

如果是在小端存储模式下:
存储方式为:44 33 22 11

所以其实只需要知道第一个字节的内容到底是11还是44就可以判断了。
但这样的数据太过于复杂,不如换简单一点的数字,比如1。

1的高字节位就是00,低字节位就是01,比较好判断。

代码演示

int check_sys(int x)
{
     
	return *(char*)&x;
}

int main()
{
     
	int a = 1;

	//约定:
	//如果是大端,返回0
	//如果是小端,返回1
	int ret = check_sys(a);
	if (ret)
	{
     
		printf("是小端存储模式\n");
	}
	else
	{
     
		printf("是大端存储模式\n");
	}

	return 0;
}

运行结果:
【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第9张图片
之前也分析了,我的编译器VS2019是小端存储模式,所以代码的结果正确,下面分析代码。

代码分析

  1. 想要在4个字节中拿到第一个字节,只需要在取地址时将整型强制类型转换为字符型即可,拿到存放第一个字节的地址后对其解引用便可拿到第一个字节数据。

  2. 如果拿到的是01,说明存储方式是01 00 00 00,也就是小端存储模式,反之则为大端存储模式。

这里如果有没有讲清楚的地方,欢迎评论区留言或者私信博主解决嗷。


整型数据在内存中的存储

数据在内存中的存储遵循一定的法则,而整型数据和浮点型数据在内存中所遵循的法则是不同的,这里我们先介绍整型数据在内存中是如何存储的。

介绍整型数据的存储需要先引进一个概念:原反补码。

原码、反码、补码

计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位(或称有效位)两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。在计算机系统中,数值一律用补码来表示和存储。原因在于:使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。
而补码其实是针对负数存储设定的,对于无符号数来说,其反码和补码都和原码相等。

原码:

所谓原码,就是将数据直接翻译为二进制序列。

拿32位平台举例,最高位作为符号位,正数的符号位为0,负数的符号位为1,后面的31位称为有效位,以不同的权重计算出不同的数字,最低位的权重为20,其次为21,以此类推。

如:

13的原码为:00000000000000000000000000001101

-3的原码为:10000000000000000000000000000011

反码:

反码,顾名思义,就是将原码的二进制序列按位取反,但这里需要注意,并不是将所有的二进制位都按位取反,符号位是特殊独立出来的,他表示一个数的正负,随意取反可能会遭遇意想不到的结果。

所以反码应该通过原码除符号位,其他位按位取反获得。
(注:正数的反码和原码相等。)

如:

13的反码为:00000000000000000000000000001101

-3的反码为:11111111111111111111111111111100

补码:

整数在内存中的存储存的都是补码,所以要通过上面的反码求出补码,补码的获取规则是原码按位取反(除符号位)再加一。
(注:正数的补码和原码相等。)

如:

13的补码为:00000000000000000000000000001101

-3的补码为:11111111111111111111111111111101

因为整数在内存中的存储形式是补码,所以引出原反补的意义就是求出补码,而补码的计算公式为:补码 = 原码按位取反(除符号位)再加一

这里我们通过VS2019编译器进行验证内存中存储的是数据的补码:

int main()
{
     
	int a = 13;
	//原码:00000000 00000000 00000000 00001101
	//反码:01111111 11111111 11111111 11110010
	//补码:01111111 11111111 11111111 11110011

	int b = -3;
	//原码:10000000 00000000 00000000 00000011
	//反码:11111111 11111111 11111111 11111100
	//补码:11111111 11111111 11111111 11111101

	return 0;
}

编译器下调试 - 内存 - &a:

&a的结果
内存中存储的是:0d 00 00 00

为小端存储模式,00001101转换为十六进制就是0d。

编译器下调试 - 内存 - &b:

&b的结果
内存中存储的是:fd ff ff ff

为小端存储模式,1111 1111转换为十六进制就是ff,1111 1101转换为十六进制就是fd。

如此说来,在内存中真的存放的就是补码,所以为了弄清楚整型数据在内存中的存储,必须牢牢掌握原反补的概念。


截断与整型提升

我们知道int类型的变量所占空间大小是4个字节32个bit位(32位平台下),而char类型的变量所占空间大小是1个字节8个bit位,那我要怎么将一个整型的数据存放在一个char类型的变量里呢?这里教大家一个很有用的办法,那就是没办法,32个比特位是不可能放进8个小格子里的,所以就会发生所谓的截断

我们知道,一个char类型只能存放8个比特位,那如果我要将char类型的数据以%d的形式打印,也就是看做32位数据将其打印,那有要怎么做呢?再教大家一个办法,那依然是没办法,所以编译器只能对char类型的数据进行整型提升

接下来简单讲解截断和整型提升的原理。

截断

假设我有一个32位二进制序列:
01010011001000110001000100100011

这是一个非常大的数字:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第10张图片

有一个char类型的空间:
【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第11张图片

在把32位数字往里放的时候会发现放不下,便会发生截断,只保留低八位的数字,其他24位数字直接舍弃,

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第12张图片

最终存放的结果为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第13张图片

这就是截断的过程。

整型提升

当我要将char类型的数据以%d的形式打印时,我们知道,%d是打印有符号整型,打印的是32位0/1序列的最终结果,但我们的char类型里只存放了8位,这个时候就会发生整型提升。

整型提升规则:

  1. 如果对无符号数进行整型提升,则在前面补24位0。
  2. 如果对有符号数进行整型提升,则判断该数在当前的二进制0/1序列的首元素,相当于符号位。
    - 如果是0,则全补0
    - 如果是1,则全补1

如:

今有一8位无符号数。

unsigned char a = 148;

首先我们写出该数的二进制序列。

10010100 - 148

由于变量a是无符号类型的,所以不管该二进制序列首元素是0还是1,都将全部补0

获得:

00000000000000000000000010010100

最终打印的结果就是148

整型数据存储练习

对以下代码分析输出结果:

1.
//输出什么?
int main()
{
     
	char a = -1;
	signed char b = -1;
	
	unsigned char c = -1;

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

	return 0;
}

首先VS2019编译器对char类型的处理为默认认为是有符号的char,所以变量a和变量b属于同一类型。

先计算出-1的补码。

int main()
{
     
	//-1
	//原码:10000000000000000000000000000001
	//反码:11111111111111111111111111111110
	//补码:11111111111111111111111111111111
	
	char a = -1;
	signed char b = -1;
	

	unsigned char c = -1;
	

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

	return 0;
}

三个变量都是char类型,所以存储时都将发生截断

int main()
{
     
	//-1
	//原码:10000000000000000000000000000001
	//反码:11111111111111111111111111111110
	//补码:11111111111111111111111111111111

	char a = -1;
	//存储的补码:11111111
	signed char b = -1;
	//存储的补码:11111111

	unsigned char c = -1;
	//存储的补码:11111111

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

	return 0;
}

现在要将三个变量以%d形式打印,则会发生整型提升

  • 而对于变量a和变量b来说,存放的是有符号的char,根据第一个二进制位决定提升的数为1,所以

变量a和变量b整型提升后的结果为:

11111111111111111111111111111111
  • 而对于变量c来说,它是无符号的char,直接全部补0,所以

变量c整型提升后的结果为:

00000000000000000000000011111111

因为提升后的c符号位是0,所以原反补码均相等。

而按%d形式打印需要将补码转化为原码后转化为十进制进行打印,

所以:

int main()
{
     
	//-1
	//原码:10000000000000000000000000000001
	//反码:11111111111111111111111111111110
	//补码:11111111111111111111111111111111

	char a = -1;
	//存储的补码:11111111
	//提升后的补码:11111111111111111111111111111111
	//提升后的反码:10000000000000000000000000000000
	//提升后的原码:10000000000000000000000000000001
	signed char b = -1;
	//存储的补码:11111111
	//提升后的补码:11111111111111111111111111111111
	//提升后的反码:10000000000000000000000000000000
	//提升后的原码:10000000000000000000000000000001

	unsigned char c = -1;
	//存储的补码:11111111
	//提升后的补码:00000000000000000000000011111111
	//提升后的反码:00000000000000000000000011111111
	//提升后的原码:00000000000000000000000011111111

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

	return 0;
}

这么一来,打印的结果就应该是-1 -1 255

打印结果:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第14张图片

  1. 下面程序输出什么?
2.
int main()
{
     
	char a = -128;

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

	return 0;
}

这道题的变量a是有符号的char类型的。

首先计算出-128的原反补码。

int main()
{
     
	char a = -128;
	//-128
	//原码:10000000000000000000000010000000
	//反码:11111111111111111111111101111111
	//补码:11111111111111111111111110000000

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

	return 0;
}

将01111111111111111111111110000000这样一个二进制序列存放进a中将会发生截断

截断之后a中存放的结果为:10000000

这时以%u的形式打印,也就是以无符号整型的形式打印,要进行整型提升,而变量a是一个有符号的char类型,第一个元素是1,所以整型提升24个1。

int main()
{
     
	char a = -128;
	//-128
	//原码:10000000000000000000000010000000
	//反码:11111111111111111111111101111111
	//补码:11111111111111111111111110000000

	//截断的结果:10000000
	//整型提升后的结果:11111111111111111111111110000000

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

	return 0;
}

这时要将提升之后的补码转换为原码后以十进制的形式进行打印。

而%u的形式将把补码中的符号位看做是有效位,所以其原反补都是一样的。

int main()
{
     
	char a = -128;
	//-128
	//原码:10000000000000000000000010000000
	//反码:11111111111111111111111101111111
	//补码:11111111111111111111111110000000

	//截断的结果:10000000
	//整型提升后的结果:11111111111111111111111110000000
	
	//补码:11111111111111111111111110000000
	//反码:11111111111111111111111110000000
	//原码:11111111111111111111111110000000

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

	return 0;
}

而11111111111111111111111110000000的值应该是4,294,967,168

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第15张图片

所以输出结果:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第16张图片

3.
int main()
{
     
	char a = 128;

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

	return 0;
}

还是一样,先求出128的补码,由于128是正数,所以其原反补都是相同的为:

00000000000000000000000010000000

存放进变量a中将发生整型截断:

10000000

而变量a为有符号的char类型,所以整型提升为

11111111111111111111111110000000

变量a以%u形式打印,则把符号位看成有效位,则此时原码反码补码相同,直接进行计算,11111111111111111111111110000000的十进制形式为4,294,967,168

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第17张图片

所以打印结果为:【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第18张图片

4.
int mian()
{
     
	int i = -20;
	unsigned int j = 10;

	//按照补码的形式进行运算,最后格式化成为有符号整数
	printf("%d\n", i + j);
	
	return 0;
}

还是先把-20和10的补码计算出来,但是这里的i和j都是整型变量,所以不会发生截断和整型提升。

int mian()
{
     
	int i = -20;
	//-20
	//原码:10000000000000000000000000010100
	//反码:11111111111111111111111111101011
	//补码:11111111111111111111111111101100
	unsigned int j = 10;
	//10
	//补码:00000000000000000000000000001010


	//按照补码的形式进行运算,最后格式化成为有符号整数
	printf("%d\n", i + j);
	
	return 0;
}

数据的计算是按照二进制补码的形式进行计算的,最后的结果再根据打印要求或者存储要求进行调整更改。

计算的结果

int mian()
{
     
	int i = -20;
	//-20
	//原码:10000000000000000000000000010100
	//反码:11111111111111111111111111101011
	//补码:11111111111111111111111111101100
	unsigned int j = 10;
	//10
	//补码:00000000000000000000000000001010

	//计算:
	//11111111111111111111111111101100
	//00000000000000000000000000001010
	//11111111111111111111111111110110 - 补码相加的结果

	//按照补码的形式进行运算,最后格式化成为有符号整数
	printf("%d\n", i + j);
	
	return 0;
}

要求按%d的形式打印,则将计算的结果转化为原码以有符号十进制数打印。

补码:11111111111111111111111111110110
反码:10000000000000000000000000001001
原码:10000000000000000000000000001010

计算结果为-10

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第19张图片

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

	return 0;
}

程序分析:

变量i从9开始自减到0时,都可以正常进入程序打印的值就是

9 8 7 6 5 4 3 2 1 0

在打印完0之后,变量i再自减1,变成-1,按道理来说应该跳出循环,但我们注意,这里的变量i为无符号整型,而-1的补码为11111111111111111111111111111111,所以会被解析为一个特别大的正整数:4294967295。

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第20张图片
那么他也符合循环控制条件(i >= 0),所以循环会继续4294967295次,而一直自减到0的时候,再次自减又变成-1,有被解析为4294967295,所以该程序将无限循环下去。

这里博主随便截两张打印结果的图供大家参考。
【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第21张图片

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第22张图片

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

程序分析:

根据代码可知数组中第一个存放的数应该是-1,第二个是-2,以此类推。

内存中存放形式

但是这个数组是char类型的,我们知道char类型可存放的数据范围是-128~127,所以这些数据一直自减到-128之后,如果再自减就会放不下了,但是这里有一个小知识点。

通过画图给大家讲解。

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第23张图片

  1. 这个图中放的是char类型补码对应十进制的全部情况,二进制位从0开始补码加1,即十进制从0开始加1计算,最终计算到127。

  2. 11111111为-1的补码,往上减1计算得到-2,再减1就是-3,以此类推可计算到-127。

  3. 而10000000这个二进制序列是无法计算的,所以系统直接将其赋为-128。

综合以上三点可知,char类型的补码其实是以从-1,-2,…,-127,-128,127,126,…,2,1这样的方式连续的。

画成图的形式为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第24张图片
由以上两个图可知,内存中的数据存放为:

内存中的数据

其实这又是一个无限循环的存放,一直存放满1000个数据为止。

而打印的是字符串长度,使用的是strlen函数,strlen函数遇到\0就停止计算,所以计算的结果应该为128 + 127 = 255。

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第25张图片

7.
unsigned char i = 0;

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

	return 0;
}

程序分析:

首先定义了一个全局变量:无符号整型i。

无符号的char类型范围是0~255,所以代码前面会打印255个"hello world\n",这一点肯定没错。

而255作为无符号数在内存中的补码是:

00000000000000000000000011111111

自增1之后的结果是:

00000000000000000000000100000000

将这个数存放于变量i中必然是存不下,所以会发生截断

只保留低八位存储,所以变量i现在存储的是00000000,也就是0,是一个无符号数,原反补相同,并且符合循环条件,所以循环又开始了。

经过上述分析,该代码的结果应该是一个无限打印的死循环。

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第26张图片


浮点型数据在内存中的存储

首先我们先见一下常见的浮点型数据有哪些?

3.14159
1E10

浮点型数据类型:

  1. float
  2. double
  3. long double

long double是在C99标准中引入的,比较老旧的编译器都不支持这种写法。

浮点数表示的范围:在"float.h"文件中可以查看。

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第27张图片

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第28张图片

在此文档中即可查看浮点型数据的范围大小。

接下来介绍浮点型数据在内存中的存储方式。

证明整数和浮点数的存取方式不同

浮点数存储的例子:

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

打印结果为:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第29张图片

  • 将9存放进int类型的变量i中,所以第一个打印是9,这很容易理解,而用float类型的指针对其解引用得到的值却是0;

  • 用float指针类型将内存中的值覆盖为9.0,以整型的方式打印出来是我们不知道的值,而用float类型指针解引用得到了9.0。

以上例子证明了整型数据和浮点型数据的存储方式是截然不同的,接下来就
开始研究浮点型数据在内存中到底是以怎样的形式进行存储的。


IEEE标准形式

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

  • (-1)S ✖ M ✖ 2E
  • (-1)s表示符号位,当s = 0时,V为正数;当s = 1时,V为负数。
  • M表示有效数字,M必须大于等于1,且小于2。
  • 2E表示指数位。

举两个例子:

  1. 十进制数3.75,先将其转化为二进制数011.11

二进制数011就是十进制数3,小数点后面的第一个1表示1.0 / 21,第二个1表示1.0 / 22
转换为IEEE标准形式为(-1)0 ✖ 1.111 ✖ 21
此时S = 0,M = 1.111,E = 1

  1. 十进制数-0.5,现将其转化为二进制数-0.1

二进制数0就是十进制数0,小数点后面的1表示1.0 / 21
转换为IEEE标准形式为(-1)1 ✖ 1.0 ✖2-1
此时S = 1,M = 1.0,E = -1

==注意:==小数点后面的数都是按照1.0 / 2n的形式相加得到的,所以很多数其实是得不到准确值的。

IEEE存储标准规定

  1. IEEE 754规定:
  1. 对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
  2. 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

画图说明

  1. 对于单精度浮点数:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第30张图片

  1. 对于双精度浮点数:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第31张图片

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

对于M(有效数字)的规定:

前面说过,1 ≤ M < 2 ,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再自动把第一位的1给加上去。这样做的目的是节省1位有效数字,以增加M的精度。
以32位浮点数为例,留给M的空间只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

对于E(指数部分)的规定:

至于指数E,情况就比较复杂。

首先,E为一个无符号整数(unsigned int),这意味着,如果E为8位,它的取值范围为0 ~ 255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,210的E是10,所以保存为32位浮点数时,必须保存为10 + 127 = 137,即10001001。

加上127或者1023进行存储的原因是取出该数的时候就需要减去127或者1023,这样E就可以得到负数的情况。


IEEE读取标准规定

指数E从内存中取出还可以再分成三种情况:

  1. E不全为0或不全为1
    这时,浮点数就采用下面的规则表示:
    即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
    比如:
    十进制数0.5的二进制形式为0.1,由于规定整数部分必须为1,即将小数点右移1位,则为(-1)0 ✖ 1.0 ✖ 2(-1),其阶码(指数部分)为-1 + 127 = 126,表示为01111110,而有效位部分1.0去掉整数部分为0,补齐0到23
    位00000000000000000000000,则其二进制表示形式为:
    0 01111110 00000000000000000000000
  1. E全为0
    可以理解为E为全0时,该数被解析为0。
    因为,当E为全0时,说明以IEEE标准形式写出的式子的指数部分是-127或者-1023,也就是说符号位和有效位要乘以1.0 / 2127或者乘以1.0 / 21023的数,而这个数非常小,近乎为0,所以在内存中取出该数时通过一些办法直接将其翻译为0。
  1. E全为1
    这时,表示±无穷大(正负取决于符号位S);
    原因是,如果E全为1,则指数位计算的是128,2128次方是一个非常大的数字,所以这里我们可以认为他是正负无穷大。

举个简单的例子,如十进制数-12.75,转换为二进制数为:-1100.11,转换为IEEE标准形式为(-1)1 ✖ 1.10011 ✖ 2 3,此时的S = -1, M = 1.10011,E = 3

以单精度浮点型为例,将其存入内存的方式为:把S放在第一位作为符号位,E加上127,即3 + 127 = 130转化为二进制数10000010,把M的整数部分去掉,将小数部分存储,E和M不够的位全部补0。

1 10000010 10011000000000000000000

在VS2019编译器上测试:

int main()
{
     
	float f = -12.75;

	return 0;
}

调试 - 内存 - &f:

调试结果

编译器的形式为十六进制

将其翻译为二进制为:

00000000 00000000 01001100 11000001

而我们刚才的计算结果是:

11000001 01001100 00000000 00000000

可以发现,和我们写的正号相反,这说明浮点型数据在内存中存储也遵循大小端字节序规则,且这里遵循的是小端字节序。

最后,我们在来看最开始给出的那道例题:

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

程序分析:

  1. 第一次赋值时,将9赋值给n,属于整型数据存储。

其二进制序列为:

000000000000000000000000000001001

第一次打印为整型打印,输出为9
第二次打印为浮点型打印,就要以浮点型数据的方式取出:

0 00000000 000000000000000000001001

第一部分为S(符号位),第二部分为E(指数位(需要减去127/1023)),第三部分为M(有效位(小数部分))

符号位为0,说明是正数,指数位为全0,减去127后得到-127,放在指数部分是2-127,即1.0 / 2127,是一个非常小的数,无论M(有效位)为多少,这里都将翻译为0,所以第二次打印结果输出为0.0。

  1. 第二次赋值时,是以浮点型存储方式进行赋值。

十进制数9.0,转换为二进制数为1001.0,转换为IEEE标准格式为(-1)0 ✖ 1.001 ✖ 23
其中S = 0,M = 1.001,E = 3
进行二进制存储时,第一位放符号位,后8位放E+127的二进制序列,其余位放M的小数部分。

0 10000010 00100000000000000000000

第三次打印结果为将这个二进制数翻译为十进制。

即1091567616‬
【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第32张图片

所以,第三次打印结果为1091567616‬

第四次打印为按浮点数打印,即按浮点数形式取出数据,所以第四次从打印结果为9.0。

打印结果:

【数据类型存储原理】数据的存储 - 深度剖析数据在内存中的存储_第33张图片

总结

本文内容较多,首先介绍了各个数据类型,又介绍了编译器中的大小端存储模式,接着讲解了2015年百度系统工程师的一道笔试题,在整型数据内存存储中介绍了原反补、截断和整型提升的概念,并进行了7道题目的训练,最后根据IEEE协会讲述了浮点型数据在内存中的数据,可谓干货慢慢,建议大家收藏下来慢慢看。

最后我是Aaron,希望今天的博文对各位有帮助,别忘了三连支持哇~

点赞 + 关注 + ✔收藏✔

如果以上内容有任何不懂的地方欢迎评论区留言或者私信博主哦~

你可能感兴趣的:(C重点突破,c语言)