C语言中数据在内存中的存储

前言:


博客主页: 干脆面la的主页

弄清楚数据再内存中的存储,这篇就够了!

本章节我们将以以下几个重点来深入刨析数据在内存中的存储:

  • 数据类型详细介绍
  • 整型在内存中的存储:原码、反码、补码
  • 大小端字节序介绍与判断
  • 浮点数在内存中的存储

目录

前言:

 1. 数据类型介绍

1.1 类型的基本归类

整型家族:

浮点数家族:

构造类型(自定义类型):

指针类型:

空类型:

 2. 整型在内存中的存储

 2.1 原码、反码、补码

2.2 大小端介绍

2.3 练习

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

3.1 一个浮点数存储的例子

 3.2 浮点数存储规则

4. 小结


 1. 数据类型介绍

char                字符整型类型        1字节

short               短整型                   2字节

int                   整型                       4字节

long                长整型                    4字节

long long        更长的整型             8字节

float                单精度浮点数         4字节

double            双精度浮点数         8字节

1.1 类型的基本归类

整型家族:

char:

        unsigned char

        signed char                char != signed char

short:

        unsigned short [int]    ([int]可省略)

        signed short [int]        short == signed short

int:

        unsigned int        无符号整型

        signed int            有符号整型(通常省略signed) --> int == signed int

long:

        unsigned long [int]

        signed long [int]        long == signed long


  1. 每一个char型字符都对应这一个Ascll码值,而Ascll码值是整数,因此归类为整型。
  2.  char到底时signed char还是unsigned char取决于编译器时如何实现的,常见情况下char就是signed char。
  3. 我们知道无符号整型不能存放负数,如果输入负数也会转化为对应的正数(但不是单纯地去掉符号),为什么会出现这种情况呢?——后面我们介绍整型的原、反、补码时就能解决。

    ps:观察下面两张图,心中是否有个疑问:为什么有时负数能被正常打印,有时却不可以呢?同样是整型数据在内存中存储的问题,接下来我们将会了解。

浮点数家族:

float        4字节

double    8字节

构造类型(自定义类型):

>数组类型

>结构体类型        struct

>enum类型          enum

>联合类型            union


顾名思义:构造类型是可以自己创造的类型,如数组类型:我们可以通过改变元素个数元素类型来改变数组类型:int a[10],实际上int [10]是数组类型,而a是数组名;因此int a[5]的数组类型和int a[10]的数组类型其实是不一样的。

ps:剩下的三种构造类型本文暂时不做展开介绍 struct类型和enum类型在初识C语言 基础篇有简单介绍,如有兴趣欢迎了解~

指针类型:

int* pa

char* pb

float* pc

double* pd

空类型:

void表示空类型(无类型)

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

void/*函数类型*/ test(void/*表示不传参*/)
{
    void* p;//无具体类型指针
}

int main()
{
    return 0;
}

 2. 整型在内存中的存储

问:假设我们定义一个变量int a = 10,这个整型变量是如何放到内存中去的呢?


我们可以用vs2019的调试窗口中的内存来观察。内存中存放的实际上是二进制,但在窗口中用十六进制表示,一个十六进制数可以表示四位二进制数,因此用十六进制展示更加简洁:

2个十六进制数--->8个二进制数--->1个字节

我们知道a分配四个字节的空间,那么该如何存储?

 —— 接下来的概念非常重要

 2.1 原码、反码、补码

 计算机的整数的二进制有三种表示方式,即原码、反码、补码

而内存中存储的是二进制的补码,具体上补码是怎么得到的呢?

下图是一个正数和负数的原码:


正整数:           原码、反码、补码相同

              原码 :按照一个数直接写出来的二进制就是原码

负整数:反码符号位不变,其他位按位取反(0-->1,1-->0)

              补码 :反码的二进制序列+1,就是补码

我们再回到先前提到的10的存放:>

观察下图: 我们发现内存中实际上是按每个字节倒着存放的,这个地方其实就是我们后面要讲到的一个点:内存中存放的补码其实不是直接将补码形式放进去的,而是有一个顺序的问题。

接下来我们再来看 -10的存放:>

我们发现两种都是倒着放的,为什么要这样放呢?

是一会儿我们将提到的“大小端字节序”问题。

问:为什么要计算机要用补码来存放呢? 

原因是计算机中只有加法器:1-1其实就是1+(-1)

下面我们用补码进行计算 我们可以发现结果是完全正确的,并且符号位和数值位可以统一进行处理,这就是补码存储的意义

2.2 大小端介绍

如下图定义一个变量 int a = 0x11223344 从11到44是从高字节数据到低字节数据:

大端字节序存储:当一个数据的低字节数据存放在高地址处高字节数据存放在低地址处,这种存储方式就叫大端字节序存储

小端字节序存储:当一个数据的低字节数据存放在低地址处高字节数据存放在高地址处,这种存储方式就叫小端字节序存储

如下图编译器中的存储方式是小端字节序存储,也就是低对低,高对高。

 百度2015年系统工程师面试题

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


 其实思路很简单:定义一个变量int a = 1,那么它转化为16进制的话就是0x00 00 00 01设法拿到四个字节的第一个字节的内容就可以判断,如果第一个字节是0那么就是大端第一个字节是1那么就是大端。可以用char* p = (char*)&a;对p解引用就可以访问第一个字节。

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

 


想一想:如果用以下代码是否可行?
char ch = (char) a;

答:这段代码是不可行的,因为无论大端存储还是小端存储它只会将最低字节数据赋值给ch


上面的代码虽说可以实现目的但不是一种很好的方式:我们应该用一个函数分装起来

int check_sis()
{
    int a = 1;
    return *(char*)&a;//1就是小端,0就是大端
}

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

2.3 练习

1.下面代码输出什么?

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

解析:上题中a b c均为char型,存储-1时都会从最低字节数据进行截断,在打印的时候又以%d形式输出,会得到整型提升 而在vs中char == signed char,也就是有符号整型,因此a和b进行截断的时候最高位是符号位,整型提升时补符号位1;而unsigned char是无符号整型,因此c对-1进行截断时最高位不是符号位,因此整型提升时补0。


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

 解析:截断和整型提升与上题道理一样,然后此次以%u输出,也就是无符号位,因此11111111 11111111 11111111 10000000就是实际值。

通过计算机换算得出结果为4294967168 

运行起来的结果也相同,所以推理正确。


3.

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


4.

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

解析:由于unsigned int为无符号数,里面不会存放负数,当因此i从9开始打印到0之后,再i--就会把-1放到一个无符号的整型里面去,对应的补码也就是11111111 11111111 11111111 11111111而每一位都是有效位,对应的正数4294967295,然后一直死循环下去...


5.

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

解析:首先我们先弄明白一个char型变量的取值范围(如下图)

无符号char:每一位都是真实数值,取值范围就是0~255

有符号char:第一位(红色框内)为是符号位,从最底下的11111111对应的原码为-1依次向上递减到-127都可以计算出其对应的原码,也就是-1 ~ -127直到10000000的时候是不需要计算的:只要二进制序列中是符号位为1,后面的都为0的时候(无法先-1后取反得到原码)会直接解析,此时10000000直接被解析为-128因此取值范围是-128~127

其实我们可以把char型变量不断+1这个过程看成一个循环(其他类型的变量也是一个道理,大家可以自己推推short类型的取值范围),如下图:

 为大家了解完这么一个道理,我们便可以愉快地开始解题啦:

 由题可知a[i] = -1 - i;也就是从a[0] = -1,a[1] = -2,a[3] = -3......直到a[127]=-128之后再-1,根据上面的思想a[128] = 127,然后再依次向后-1,直到a[255] = 0,在a[255]前面有255个元素,而strlen(a)返回的数值就是第一次遇到\0之前的元素个数。因此答案就是255


6.

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

解析:unsigned char的取值范围是0~255,然而在i++到255的时候,仍然满足i<=255的条件,再++一次,i就循环变成了0,然后陷入死循环。


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

常见的浮点数:

3.14(字面浮点型)

1E10(科学计数法的形式)

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

在浮点数后+f:如3.14f为float类型,否则默认为double型

浮点数表示的范围:float.h中定义

3.1 一个浮点数存储的例子

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) (2) 9.000000 (3) (4) 9.000000

ps:用%f或者%lf打印的时候,默认小数点后面有六位


 实际上运行的结果是这样的:num和*pFloat在内存中存储的明明是同一个数,为什么解读的差别这么大呢?

我们可以肯定浮点型和整型在内存中的存储一定是有差别的。

接下来我们就要了解:浮点型在内存中到底是怎么存储的呢?

 3.2 浮点数存储规则

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

  • (-1)^S * M * 2^E
  • (-1)^S表示符号位:当S=0,V为正数;当S=1,V为负数
  • M表示有效数字,大于等于1,小于2
  • 2^E表示指数位

1、首先我们要了解一个十进制数该怎么转化为一个二进制数

任何一个十进制数都可以转化为下面的形式,如下图:


2、而101.1又可以把小数点向左移动两位写成科学计数法的形式

解析:这与十进制的数转化为科学计数法形式的道理是一样的,如下图:


3、这时候我们又可以把上述形式写成IEEE 754规定的形式

解析:当我们能够把一个浮点数V转化为以下这种形式,我们只要把S M E三个值存储起来就可以了,这个时候思路似乎就通了:


4、IEEE 754规定:

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

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

  • 对于浮点数:没有原码、反码、补码的概念
  • 对于符号位S:1表示负数,0表示正数
  • 对于有效数字M的修正:前面说过1<=M<2,也就是说可以写成1.xxxxxxxx的形式,其中xxxxxxxx是小数部分,在计算机中保留M的时候1可以被舍去,只保留小数部分(xxxxxxxx)就可以了,意义是:精度更高一位
  • 对于指数E的修正:(1)首先E为一个无符号整型数,如果E是8位,它的取值范围是0~255;如果E是11位,它的取值范围是0~2047(2)但是,科学计数法中其实是可以出现负数的,所以IEEE 754规定,存入内存是E的真实值必须再加一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023(3)比如:2^10的E是10,所以保存位32位浮点数时必须为10+127=137,即10001001。

上图:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移一位变成1.0*2^-1的形式,其E为-1+127=126,表示为01111110;其M为1.0去掉整数部分即为0,补齐0到23位000000000000000000000000;因此其二进制表示形式为:

0 01111110 00000000000000000000000

浮点数就是以这样的方式存入的......


5、E的取出

E从内存中取出又可以分为三种情况:

  • E不为全0又不为全1:E的值直接减去127(或1023)
  • E为全0:浮点数的指数E=1-127(或1023),并且M不还原隐藏位'1'。
  • E为全1:如果M为全0,那么其表示的真值为无限大

接下来我们便可以对前面这道题进行相应的解释了~

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的结果毋庸置疑

第二个输出:以整型存放,以浮点型拿出,拿出数据的时候就会以浮点型的形式解析内存中的数据,因此实际数值非常小,打印出来就是0.000000

第三个输出:由于pFloat时float*定义的指针,因此通过对pFloat解引用来赋值可以以浮点型的数据存入n,再以整型取出,以整形的形式解析内存中的数据也就是1091567616 

第四个输出:以浮点型存放,以浮点型拿出,答案是9.000000也毋庸置疑

再来看看结果,是不是很神奇呢?


4. 小结

弄明白了数据在内存中的存储,目的是为了修炼内功,比如当我们在未来写代码的时候,要是遇到自己输出的值并不是自己想要的,我们便可以联想到是不是类型选错了等等...


 如果觉得本篇博客对你有所帮助,欢迎三连加关注,我将持续更新相关知识~~~

如有讲解不准确的地方,请轰炸博主

本章完......

  • 13
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

干脆面la

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值