C语言进阶-数据在内存中的存储(1)

目录

1.数据类型介绍

1.1类型的基本归类:

2.整型在内存中的存储

2.1原码、反码、补码

2.2大小端介绍

2.3练习


1.数据类型介绍

前面我们已经学习了基本的内置类型:

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

以及它们所占存储空间的大小依次为:1,2,4,4/8,8,4,8(单位是字节),其中的长整型(long),C语言中只规定了sizeof(long)>sizeof(int),但是具体是4个字节还是8个字节由编译器决定。

那为什么整型又要分为长整型、短整型等呢?

我们说每个类型开辟的内存空间的大小不同,大小决定了使用范围,例如:要定义一个年龄的变量,年龄最大就只能到3位数了,而一个short类型的大小在-32768~32767之间,远远足够了。

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]

因为字符在存储时是ASCIl值,所以把字符型也归类于整型家族。 

浮点数家族:

float
double

构造类型:

> 数据类型

> 结构体类型 struct

> 枚举类型  enum

> 联合类型  union

构造类型可以自己创建,数组类型也算是构造类型,比如我们构造如下三个数组:

    int arr1[10];
	int arr2[5];
	char arr3[10];

 它们的类型都不相同,分别是:int [10]、int [5]、char[10]。

指针类型:

int *pi

char *pc

float *pf

void *pv

空类型:

void表示空类型(无类型)

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

2.整型在内存中的存储

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

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

比如:

    int a = 20;
	int b = -10;

创建两个整型变量,内存为它们分别开辟4个字节的空间。

那到底是如何存储的呢?

下面来了解一下:

2.1原码、反码、补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码。

三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”

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

负整数的三种表示方法各不相同:

原码:

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

反码:

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

补码:

反码+1就得到补码。

关于原码、反码、补码,之前的章节中讲过,下面我们再来举两个例子:

    int num1 = 10;//创建一个整型变量,在内存中开辟4个字节
	//4个字节--32个bit位
	//00000000000000000000000000001010 - 原码、反码、补码
	int num2 = -10;
	//负数
	//10000000000000000000000000001010 - 原码
	//11111111111111111111111111110101 - 反码
	//11111111111111111111111111110110 - 补码

对于整型来说,数据存放在内存中其实存放的是补码。

下面我们可以打开监视窗口观察一下:

本质上来说数据在内存中是以二进制的形式存储,但是VS中为了方便展示,显示的是16进制。学过计算机组成原理,大家应该都知道,4位二进制数可以化为1位十六进制数,而十六进制中的10~15用字母a~f表示。

其实我们将补码11111111111111111111111111110110化为16进制数就是:ff ff ff f8.

这时我们就可以发现内存中存储的是补码,但是内存存储的是f8 ff ff ff ,与我们转换出来的刚好是倒着存储的,这又是为什么呢?

后文我们会讲到。

那为什么数据在内存中存放的是补码呢?

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

例如我们要算 1 - (-1)

如果我们用原码计算就会发现,算出来的结果是-2,显然不对。

而用补码计算就能算出正确的值:

	//计算 1 -(-1)
	//原码计算:
	//00000000000000000000000000000001   1的原码
	//10000000000000000000000000000001   -1的原码
	//10000000000000000000000000000010  相加结果是-2
	//补码计算:
	//00000000000000000000000000000001   1的补码
	//11111111111111111111111111111111   -1的补码
	//00000000000000000000000000000000   相加结果是0

接下来,我们就讲一讲为什么会倒着存储。

2.2大小端介绍

什么是大端小端:

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

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

如下图所示:

注意上图中我们讨论的是字节序存储,即以字节为单位讨论存储顺序。两位十六进制数就是一个字节。

 有人对数据的高位和低位不太明白,其实很简单,就像十进制中的123,个位的3就是其低位,百位的1就是其高位,类比一下,上面的十六进制数0x11223344,44就是低位的字节,11就是高位的字节。

那这里有个问题,一个char类型的数据它有存储顺序吗?

答案是没有的,因为char类型的数据只有一个字节,不管怎么存储,顺序都是一样的,它不需要存储顺序。

学了大端小端的概念后,我们来设计一个小程序来判断当前的机器的字节序是大端还是小端。

我们先来分析一下设计思路,在这里我们可以定义一个变量a,令其初始化为1,它的十六进制应该是:0x00 00 00 01,如果是大端字节序存储,存储顺序应该是00 00 00 01,而小端字节序存储,存储顺序应该是01 00 00 00 ,我们只要看它的在内存中第一个字节是不是等于1,如果等于1就是小端字节序存储,如果等于0就是大端字节序存储。

那要判断第一个字节是不是等于1,就要先将其取出来,前面我们学过,可以将&a赋给一个指针,然后解引用指针就可以得到数据,但是因为该指针是int*型,通过解引用,一次取出的是4个字节,我们只需要判断一个字节,这时可以将它强制类型转换为char*,就取出第一个字节了。然后进行解引用并判断,即*(char*)&a == 1

代码实现如下:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int check_sys()
{
	int a = 1;
	int* p = &a;
	if (*(char*)p == 1)
		return 1;
	else
		return 0;
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端");
	else
		printf("大端");
	return 0;
}

当然,我们也可以简单优化一下:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int check_sys()
{
	int a = 1;
	return *(char*)&a;
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端");
	else
		printf("大端");
	return 0;
}

2.3练习

上文讲到整型家族,对于整型家族的类型来说,有:有符号和无符号的区分。

例如:short == signed short(有符号短整型),而无符号短整型就是 unsigned short。

int == signed int (有符号整型),而无符号整型就是 unsigned int。

但是char到底是 signed char 还是 unsigned char 不确定,C语言并没有明确规定,但是通常我们见到的VS编译器上是signed char。

而有符号数和无符号数的区别就是:有符号数的第一位二进制位会被看成符号位,其他的位是数值位。而无符号数没有符号位,全部的二进制位都是数值位。(如下图所示)

以上是char和unsigned char 的取值范围,以此类推,我们也可以知道short的取值范围是:-32768~32767,unsigned short的取值范围是:0~65535。

这就是有符号和无符号的区别,下面我们来做几道练习:

练习1:

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

运行结果:

为什么会输出这样一个结果呢?

下面我们来分析一下:

a在内存中的补码形式是:11111111111111111111111111111111,由于是char类型所以会发生截断:11111111,而打印时的%d打印的是十进制的有符号整型整数,此时要进行整型提升(有符号数根据符号位补足32bit位),11111111111111111111111111111111,接着将补码化为原码是:10000000000000000000000000000001(即十进制的-1)。

上文讲过,char就是signed char,所以b输出的也是-1。而c是unsigned char型,整型提升时无符号数直接补0,00000000000000000000000011111111,此时输出就是255。

整型提升前面讲过,再来总结一下:

整型提升根据数据本身的类型,有符号型按符号位补足32个bit位,无符号型直接补0

练习2:

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

运行结果:

下面我们也来分析一下:

a的补码形式是:11111111111111111111111110000000 。char型发生截断:10000000。因为%u输出的是十进制的无符号整型整数,所以这里要整型提升,11111111111111111111111110000000 此时虽然依然是补码形式,但是我们要打印的是无符号数,这里就可以直接将其当做无符号数的原码,那此时输出的无符号整数就是4394967168。

练习3:

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

这次将a由-128换成了128,那么打印出的结果是什么呢?

分析一下会发现,在128的补码发生截断之后,保留的8位和-128截断之后的一样,都是10000000,那么它们最终的结果应该相同,也是4394967168

练习4:

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

运行结果:-10

i的补码形式是:11111111111111111111111111101100

j的补码形式是:00000000000000000000000000001010

相加后的补码:11111111111111111111111111110110

化为原码:10000000000000000000000000001010,即十进制的-10

练习5:

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

运行结果:

我们可以发现上述代码打印完1~0之后,开始陷入死循环,为什么会陷入死循环呢?为什么后面打印的数字这么大?

因为i是unsigned int 型,无符号数永远不可能小于0,它一直满足循环条件,所以会一直循环下去。而当循环到 i = -1的时候,-1的补码是11111111111111111111111111111111,它会被当做一个无符号整数看待,32bit的1化为十进制数就是4294967295

练习6:

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

运行结果:

为什么是这个结果呢?

首先来看循环体,本来循环1000次后应该会将-1 ~ -1000的数字赋给数组a[1000],但是此时的数组是char型的,上文讲过,char的范围是-128~127,所以不可能将所有的数都赋给数组,-1截断后应该是11111111,第一次循环a[1]的值是11111110,而每次循环相当于在上次的基础上减一操作,直到a[127] = -128时,它的补码是10000000,再次减一后会变成01111111,即127,接着循环,126 125 124......直到a[255] = 0,减一之后又变成 -1,接着-2 -3 -4......-127 -128 127 126......1 0

而strlen是计算 \0 之前的字符串的长度,\0 和 0 的ASCII值都是0,所以在上述代码中计算的只是循环第一次到0之前的长度。

下图给出char类型数值的轮回过程,以及上述代码的解析图。

练习7:

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

运行结果应该是hello world 死循环打印。

原因也很简单,上文我们说过unsigned char 的取值范围是0~255,恒满足循环条件,所以陷入了死循环。

今天就学到这里,未完待续。。。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

成屿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值