C语言进阶1:数据的存储

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

本节重点

  1. 数据类型详细介绍
  2. 整形在内存中的存储:原码、反码、补码
  3. 大小端字节序介绍及判断
  4. 浮点型在内存中的存储解析

1.数据类型介绍

1.1 基本内置类型(C语言本身库有函数)

char字符数据类型
short短整型
int整形
long长整型
long long更长的整形
float单精度浮点数
double双精度浮点数

1.2 构造类型(自定义函数)

自定义函数是系统不自带的,通过自己的编写后可以使用的函数。一般的编程语言、工作表等都可以编写自定义函数使用。

编写自定义函数可以简化主程序,让程序的检查调试更方便

1.3 类型的意义

a.使用这个类型开辟内存空间的大小(大小决定了使用范围)

比如说使用char类型创建的变量,开辟的内存空间是1个字节,使用int类型创建的变量,开辟的内存空间是4个字节。

b.如何看待内存空间的视角

int main()
{
    int a = 10;
    int b = 10;
    float c = 10.0;
    return 0;
}


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

虽然 a, b都是占用4个字节的空间,但是我们在看待a的时候,因为其类型是int,所以我们会把a当做整型来看待,在看待b的时候,因为其类型是float,所以我们会把b当做小数(而非整型)来看待。

2.数据类型细分类

2.1 整形家族(基本内置类型)

char(char在存储时存储的是ASCLL码值)unsigned char
signed char
shortunsigned short
signed short
intunsigned int
signed int
longunsigned long
signed long

unsigned:无符号,signed:有符号



2.2 浮点型家族(基本内置类型)

float

double


2.3 构造类型(自定义函数类型)

数组类型去掉数组名称,剩下的就是数组类
如: int arr[10]的类型是int [10]
结构体类型struct
枚举类型enum
联合类型union

2.4 指针类型

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

2.5 空类型

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型。

3.整型在内存中的存储

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

那接下来我们谈谈数据在所开辟内存中到底是如何存储的?

3.1 如何储存

看下面这个例子:

#include<stdio.h>
int main()
{
	int a = 3;
	int b = -1;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

数据在内存中存储时是按二进制的补码存储的

展示内存的时候,为了方便展示,显示的是16进制数据。

3.2 原码、反码、补码

下面先来了解几个概念︰原码、反码、补码

计算机中的有符号数(整型)有三种表示方法,即原码、反码和补码。

三种表示方法均有符号位数值位两部分,符号位都是用0表示′正”,用1表示"负”,而数值位三种表示方法各不相同。

(注:无符号数也有三种表示方法,即原码、反码和补码;但三者相同)

有符号数中:

正数 :原码反码补码 三码合一;

负数的原反补 按照下面的规则进行转换:

##blue##
原码:直接将二进制按照正负数的形式翻译成二进制就可以。

反码:将原码的符号位不变,其他位依次按位取反就可以得到了。

补码:反码 + 1就得到补码

整数有两种,有符号数和无符号数

有符号数:符号位 + 数值位,即:

正数 : 0 + 数值位

负数 : 1 + 数值位

int a = 3;//4个字节---32比特位
//十进制显示形式
//00000000 00000000 00000000 00000011 - 原码、反码、补码
//0000 0000 0000 0000 0000 0000 0000 0000 0000 0011
//十六进制显示形式
//0x00 00 00 03 

int b = -1;
//十进制显示形式
//10000000 00000000 00000000 00000001 - 原码
//11111111 11111111 11111111 11111110 - 反码
//11111111 11111111 11111111 11111111 - 补码
//十六进制显示形式
//0xff ff ff ff (补码)

3.3 为什么内存中要存储补码?

我们首先来看一下 1 - 1 这个例子:

①先按照原码的方式去计算:


在这里插入图片描述

②接下来用补码来进行计算:

在这里插入图片描述

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

怎么理解补码与原码相互转换,其运算过程是相同的?(以下运算,符号位均不变)

原码->取反 + 1->补码

补码->取反 + 1->原码

当然补码到原码也可以是:补码 -> - 1 取反->原码

4.大小端介绍

4.1 什么是大小端

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

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


在这里插入图片描述

4.2 为什么有大小端

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。

但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器)。

另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。

因此就导致了大端存储模式和小端存储模式。

例如一个16bit的short型x,在内存中的地址为ox0010),x的值为0×1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反

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

4.3 一道笔试题

请简述大端字节序和小端字节序的概念,并设计一个小程序来判断当前机器的字节序
int a = 20; //大端0x 00 00 00 14
//小端0x 14 00 00 00

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
int check_sys()
{
  	int a=1;
      return   	char* p=(char*)&a;;
}

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

4.4 练习与理解

练习1

下面这段代码的结果是什么?

#include<stdio.h>
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;
}

以下是解析:

#include<stdio.h>
int main()
{
	char a = -1;
	//10000000 00000000 00000000 00000001 原码
	//11111111 11111111 11111111 11111110 反码
	//11111111 11111111 11111111 11111111 补码
	//真正存储的补码: 11111111(左边)
	//整型提升(根据真补码左边第一位补充):11111111 11111111 11111111 111111111
	//转换成原码输出打印

	signed char b = -1;
	//真正存储的补码: 11111111(过程与a相同)

	unsigned char c = -1;
	//真正存储的补码: 11111111
	//无符号数整型提升时,将符号位当0处理
	//整型提升结果(无符号数 原反补相同) : 00000000 00000000 00000000 11111111

	printf("a = %d,b = %d,c = %d\n", a, b, c);		// %d表示打印整型
	return 0;

结果 :  -1, -1, 255
}

练习2

下面这段代码的结果是什么?

#include<stdio.h>
int main()
{
	char a = -128;       //128二进制: 10000000
	printf("%u\n", a);   
    // %u打印十进制的无符号数字  
    // %d打印十进制有符号
	return 0;
}

以下是解析:

在这里插入图片描述

在这里插入图片描述

注:%d与%u的整型提升是一样的,但随后

%d :若符号位为负,则进行原反补

%u :直接打印

#include<stdio.h>
int main()
{
	char a = -128;
	printf("%u\n", a);   
    // %u打印十进制的无符号数字  
    // %d打印十进制有符号
	return 0;
}

练习3

下面这段代码的结果是什么?

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

过程与结果 同上…

练习4

下面这段代码的结果是什么?

#include<stdio.h>
int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	return 0;
}

以下是解析:

#include<stdio.h>
int main()
{
	int i = -20;
	//原码	 10000000 00000000 00000000 00010100
	//反码	 11111111 11111111 11111111 11101011
	//补码	 11111111 11111111 11111111 11101100

	unsigned int j = 10;
	//原反补00000000 00000000 00000000 00001010

	//结果
	//补码:11111111 11111111 11111111 11110110
	//原码:
	//即 -10
	printf("%d\n", i + j);
	return 0;
}

练习5

下面这段代码的结果是什么?

#include<stdio.h>
int main()
{
	unsigned int i = 0;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
	}
	return 0;
}

以下是解析:

#include<stdio.h>
#include<Windows.h>		//用于引用Sleep函数
int main()
{
	unsigned int i = 0;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
		Sleep(1000);
	}
	return 0;
}
\\结果:9,8,7,6,5,4,3,2,1,0,4294967295,......
\\00000000 00000000 00000000 00000000 减1
\\得: 11111111 11111111 11111111 11111111
\\即4294967295

练习6

下面这段代码的结果是什么?

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		arr[i] = -1 - i;
	}
	printf("%d", strlen(arr));
	return 0;
}

以下是解析:

在这里插入图片描述

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[1000];			//0-999
	int i;
	for (i = 0; i < 1000; i++)
	{
		arr[i] = -1 - i;			//char arr数组内储存的值 : -1-2......-128,127(-129),......0 原因如上图
	}
	printf("%d", strlen(arr));
	return 0;
}
char arr数组内储存的值:-1

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

练习7

下面这段代码的结果是什么?

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

以下是解析:

#include<stdio.h>
unsigned char i = 0;		//unsigned char的范围: 0-255
int main()
{
	for (i = 0; i <= 255; i++)		//当i=255是,i+1=0,陷入死循环
	{
		printf("hello world\n");
	}
	return 0;
}
结果:死循环打印hello world

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

5.1浮点型数据基础知识

①常见的浮点数∶3.14159 ,1E10(即 1*10^10)

②浮点数家族包括:float、double、long double类型。

③浮点数表示的范围 : float.h 中定义,整型家族表示的范围:limits.h 中定义

5.2下面这段代码的结果是什么?

#include<stdio.h>
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;
}

在这里插入图片描述

通过对以上结果的直观分析,我们可以察觉到浮点型存储的形式跟整型存储的形式是不一样的,如果两者一样,那么结果应该相等。

思考:n和 * pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?

要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。

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

5.3储存方式:(−1)𝑆∗𝑀∗2E

其中

  • (-1) s表示符号位,当s = 0,V为正数﹔当s = 1,V为负数.

    即(-1)s =1时,V为正数 ; 即(-1)s =0时,V为负数
  • M表示有效数字,大于等于1, 小于2(科学计数法)
  • 2 E 表示指数位

举例来说:

9.0 —> 1001.0 —>1.002*2^3 —> (-1)^0 * 1.002 * 23(注:此处2x代表小数点位置,此处1,002不是逢十进一而是逢二进一)

(-1)0 * 1.002 * 23

(-1) 𝑆 ∗ 𝑀 ∗2 E

s = 0 ; M = 1.001 ; E = 3

IEEE754规定 : 对于32位的浮点数(float),最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。下面是图解:

在这里插入图片描述

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

在这里插入图片描述

5.4存储时的特别规定

IEEE754对有效数字M和指数E,还有一些特别规定:

5.4.1对M

前面说过,1 < M < 2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。

因此

IEEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

5.4.1对E

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

在介绍之前,举几个例子以巩固浮点数计算 : 5.0 , 3.0 , 0.5 , 3.5写出这几个数的二进制储存方式(公式)

过程及结果:101.0 , 11.0 , 0.1(0.5即1/2,即2-1) , 11.1 ----->>> 1.01* 22 , 1.1* 22

首先,E为一个无符号整数(unsigned int)(为了高效,兼容与准确)这意味着,如果E为8位,它的取值范围为0 - 255;

如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;

对于11位的E,这个中间数是1023。

比如,2 ^ 10的E是10,所以保存成32位浮点数时,必须保存成10 + 127 = 137(类似开氏度与摄氏度的转换),即10001001。

尝试将float f = 5.5; 完全转化为二进制

结果 : 0 10000001 01100000000000000000000(注意,M储存时不足部分向右补0)

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

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。

比如∶0.5的二进制形式为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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值