REGRET
blog

C语言进阶之数据的存储

前言

分享这么一句话,‘所有的曾经都是背景,衬托你到来时破晓的黎明’。

一、数据类型介绍

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

C语言类型分为两类:

  1. 内置类型(char、int、float...)
  2. 自定义类型(构造类型)

类型的意义:

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)
  2. 如何看待内存空间的视角

二、类型的基本归类

整型家族

char //以ascll码值存储,ascll码值是整数
    unsigned char //无符号
    signed char //有符号
short
    unsigned [int]
    signed [int]
int
    unsigned int
    signed int
long
    unsigned [int]
    signed [int]

浮点型家族

float
double
long double

构造类型

数组类型            int arr[]
结构体类型           struct
枚举类型             enum
联合类型             union

指针类型

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

空类型

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

#include <stdio.h>
#include <stdlib.h>

void test() //什么都不写的时候,传不传参都可以
{
    printf("123\n");
}

int main()
{
    test();
    return 0;
}

三、整型在内存中的存储

一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的,下面让我们看一下数据在所开辟的内存中到底是如何存储的。

原码、反码、补码

数据在内存中是以二进制的形式进行存储的

计算机中的有符号数有三种表示方法:原码、反码、补码

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

原码:
直接将二进制按照正负数的形式翻译成二进制就行

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

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

例1:
原码:10001010
反码:11110101
补码:11110110

例2:
原码:00000101
反码:00000101
补码:00000101

对于有符号数,正数的原、反、补码都相同

对于整形来说,数据存放内存中其实存放的是补码:

原因:使用补码,可以将符号位和数值域统一处理,同时,加法和减法也可以统一处理,此外补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

四、大小端介绍

思考:为什么这里的变量是倒着存的呢?

int main()
{
    int a = 20;
    //二进制:000000000000000000010100
    //十六进制:0x00 00 00 14
    // 20 在内存中的存储: 14 00 00 00
    return 0;
}

什么是大端小端

大端(存储)模式,
是指将数据的高字节内容放到内存的低地址处。
而数据的低字节内容放到内存的高地址处

小端(存储)模式,
是指将数据的高字节内容放到内存的高地址处。
而数据的低字节内容放到内存的低地址处

例:假设有一个数:0x11223344

为什么会有大端小端

为什么会有大端小段之分呢?

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

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

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

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

例题

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

代码:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    //写一段代码告诉我们当前机器的字节是什么
    int a = 1;
    char *p = (char*) &a;
    if(*p == 1)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
}

修改后:

#include <stdio.h>
#include <stdlib.h>

int check_sys()
{
    int a = 1;
    char *p = (char*) &a;
    return *p;
    //返回1表示小端,返回0表示大端
}
int main()
{
    int ret = check_sys();
    if(ret == 1)
        printf("小端\n");
    else
        printf("大端\n");
    return 0;
}

五、一些代码的输出

1.

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char a = -1;
	//原:10000000 00000000 00000000 00000001
	//反:11111111 11111111 11111111 11111110
	//补:11111111 11111111 11111111 11111111
	//只能存8个bit位:11111111
	//整型提升:11111111 11111111 11111111 11111111(补码)
	signed char b = -1;
	//11111111
	unsigned char c = -1;
	//11111111
	//无符号char,整型提升,高位补0
	//00000000 00000000 00000000 11111111
	printf("a=%d, b=%d, c=%d",a, b, c);
	return 0;
}

2.

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char a = -128;
	//10000000 00000000 00000000 10000000
	//11111111 11111111 11111111 01111111
	//11111111 11111111 11111111 10000000   补码
	//10000000
	//11111111 11111111 11111111 10000000   整型提升之后(还是补码)
	//%u打印无符号数,将整型提升之后的数直接打印出来
    printf("%u\n", a);
    //%d,打印十进制的有符号数字
    //%u,打印十进制的无符号数字
	return 0;
}

char类型变量的取值范围

char类型范围的定义:
signed char
unsigned char
一个字节(8个bit位)

00000000
00000001
00000010
......
01111111
10000000
10000001
......
11111111

总共有2的8次方(256)种这样的二进制序列
假设是有符号数(第一位是符号位):
范围:-128~127
正数:
00000000
00000001
00000010
......
01111111(127)
负数:
10000000(直接被翻译成-128)
10000001(-127)
......
11111111(-1)
假设是无符号数(第一位是有效位):
范围:0~255
00000000(0)
00000001(1)
00000010
......
01111111(127)
10000000
10000001
......
11111111(255)

image

3.

#include <stdio.h>
#include <stdlib.h>

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

4.

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	//按照补码的形式进行运算,最后格式化为有符号整数
	return 0;
}
/*
10000000 00000000 00000000 00010100
11111111 11111111 11111111 11101011
11111111 11111111 11111111 11101100  补码
00000000 00000000 00000000 00001010  补码
11111111 11111111 11111111 11110110  结果
11111111 11111111 11111111 11110101
10000000 00000000 00000000 00001010  -10
*/

5.

#include <stdio.h>
#include <stdlib.h>

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

i是个无符号类型的,也就是i只能>=0,所以就造成了死循环。

6.

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char a[1000];
	int i;
	for(i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	return 0;
}
//一个char的范围是127~-128
//圈逆着走,-1 --> -128 -> 127 -> 3 -> 2 -> 1 -> 0

7.

#include <stdio.h>
#include <stdlib.h>

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

结果发生了死循环,这是因为对于无符号的char类型的取值范围是0 ~ 255。而当i == 255时,再往后加,又会变成0。所以这个循环的条件就恒成立。

六、浮点型在内存中的存储

常见的浮点数:3.14159、1E10

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

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

浮点型存储的例子:

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

通过这个例子,我们发现:

以整型的视角放,以整型的视角拿或以浮点型的视角放,以浮点型的视角拿没有问题

以整型的视角放,以浮点型的视角拿或以浮点型的视角放,以整型的视角拿出现了问题

这说明整型和浮点型在内存中的存储方式是有区别的

详细解读

1.标准规定

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

  • (-1)^S * M * 2^E

  • (-1)^S表示符号位,当S = 0,V为正数;当S = 1,V为负数

  • M表示有效数字,大于等于1,小于2

  • 2^E表示指数位

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

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

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

如何放进去?

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

对于E:
情况比较复杂,首先,E是一个无符号整数(unsigned int)这意味着,如果E为8位,它的取值范围就为0 ~ 255;如果E为11位,它的取值范围就是0 ~ 2047。但是我们知道,科学计数法的E是可以出现负数的(比如说:十进制的0.5要转换为二进制是0.1,再写成科学计数法就是1.0 * 2^-1。这里S = 0;M = 1.0;E = -1 ),所以IEEE 754规定,存入内存时E的真实值必须加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如:2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001

如何取出来?

对于E从内存中取出还可以分为三种情况:
1、E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127或1023,得到真实值,再将有效数字M前加上第一位的1。比如:0.5的二进制形式是0.1,由于规定正数部分必须为1,即将小数点右移一位,则为1.0 *2^(-1),其阶码为-1 + 127 = 126,表示01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为
0 01111110 00000000000000000000000

2、E为全0
这时,浮点数的指数E等于1 - 127或1 - 1023,即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxxx的小数。这样做是为了表示 ±0,以及接近于0的很小的数字

3、E全为1
这时,如果有效数字M全为0,表示 ±无穷大(正负取决于符号位S)

2.再回首

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int n = 9;
	//0(S) 00000000(E) 00000000000000000001001  补码

	float *pFloat = (float*)&n;
	printf("n的值为:%d\n", n);//9
	printf("*pFloat的值为:%f\n", *pFloat);//0.000000 无限接近于0
    //(-1)^0*0.00000000000000000001001*2^(-126)

	*pFloat = 9.0;
	//1001.0
	//1.001*2^3
	//(-1)^0*1.001*2^3
	//0 10000010 00100000000000000000000  9.0的内存形式

	printf("num的值为:%d\n", n);
	printf("pFloat的值为:%f\n", *pFloat);//9.0
	return 0;
}

结尾

种下一颗树最好的时间是十年前,其次,是现在。当你觉得为时已晚的时候,恰恰是最早的时候。

posted on 2022-08-09 17:51  REGRET。  阅读(224)  评论(0编辑  收藏  举报