笔记18(C语言进阶 数据的存储)

目录

注:

数据类型详细介绍

类型的基本归类

整型家族

 浮点型家族

构造类型(自定义类型)

指针类型

空类型

整型在内存中的存储

大小端介绍(大端字节序和小端字节序)

例1:设计一个小程序来判断当前机器的字节序

例2:求输出结果(char、signed char、unsigned char)

例3:下面程序输出结果是什么?(char a = -128 → %u)

例4:求打印结果(char a = 128 → %u)

补充:char类型变量的取值范围

例5:代码结果是什么?(int、unsigned int → %d)

例6:代码执行的结果是什么?(unsigned int → for循环)

例7:求最终打印的结果(char a[1000])

例8:求代码执行结果(unsigned char → for循环)

浮点型在内存中的存储

浮点数存储的例子(引例)

浮点数在内存中的存储方式

解释引例


注:

 本笔记参考B站up鹏哥C语言的视频


数据类型详细介绍

基本的内置类型:

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

 类型的意义:

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

类型的基本归类

整型家族

(因为 char 存储的是字符的ASCII值,所以分类时,通常把char类型归为整型)

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

整型分为有符号和无符号,其范围定义在 limits.h 中。

---

 浮点型家族

float
double

其范围定义在 float.h 中。

---

构造类型(自定义类型)

数组类型如 {int arr1[10]},类型是 {int [10]};{int arr2[5]},类型是{int [5]}
结构体类型struct
枚举类型enum
联合类型union

---

指针类型

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

---

空类型

void 表示空类型(无类型)

通常应用于函数的返回类型[void test()]、函数的参数[void test(void)]、指针类型[void *p]

整型在内存中的存储

||| 复习:变量的创建是要在内存空间中开辟空间的。空间的大小是根据不同的类型决定的。

那么数据在所开辟内存中到底是如何存储的?

如:

int main()
{
	int a = -10;
	int b = 10;
	return 0;
}

通过调试可以看见a在内存中的存储:

b在内存中的存储:

 这里的表示形式是十六进制。(4个二进制位 → 1个十六进制)

注意:

(数据在内存中是以二进制的形式存储。)

  • 但是整数的二进制有3种表示形式:原码、反码 和 补码
  • 对于正整数而言,原码、反码、补码相同
  • 对于负整数而言,原码、反码、补码是要进行计算的。

比如在内存中放入 -10:

  1. -10的原码:10000000 00000000 00000000 00001010
  2. -10的反码:11111111 11111111 11111111 11110101  -  按照数据的数值直接写出的二进制序列,就是原码,原码的符号位不变,其他位按位取反,得到反码
  3. -10的补码:11111111 11111111 11111111 11110110  -  +1
  4. -10的补码转换为十六进制之后,是 ff ff ff f6,倒置之后,就是f6 ff ff ff。可得结论:整型在内存中存储的是它的补码

那为什么存储补码呢?

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

ps:如果负整数的补码符号位不变,其他位按位取反,再+1,可以得到原码

例子:1 - 1

由于CPU只有加法器,使用实际上是模拟减法计算。所以 [1 - 1] 实际上是 [1 + (-1)] 。对于-1:

原码:10000000 00000000 00000000 00000001
反码:11111111 11111111 11111111 11111110
原码:11111111 11111111 11111111 11111111

如果原码相加,得:

10000000 00000000 00000000 00000010 即 -2 --- 计算错误

如果反码相加,得:

11111111 11111111 11111111 11111111 ,明显不是正确结果

如果补码相加,得:

00000000 00000000 00000000 00000000 即0 --- 计算正确

大小端介绍(大端字节序和小端字节序)

int main()
{
	int a = 0x11223344;
	return 0;
}

对a:

0x12345678
说明数字是十六进制该数位的权重较高,是数据高位数据的权重较低,是数据低位

在内存中如果有不同的存储方式(此处数字代表十六进制):

11 22 33 44(大端字节序内存存储数据保存下来的形式1
44 33 22 11(小端字节序内存存储数据保存下来的形式2
低地址                                      高地址
44 11 22 33(不方便、复杂)
11 44 33 22(不方便、复杂

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

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

此时在通过调试窗口观察 a 在内存内的存储:

可以发现当前编译器采取的是小端存储的模式。

为什么会有大小端模式之分:

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

例1:设计一个小程序来判断当前机器的字节序

  • 选定一个简单数字,如:1 - 00 00 00 01;
  • 则小端存储的第一个字节就是 01
  • 大端存储的第一个字节就是 00
  • char* 的指针就是访问一个字节
#include<stdio.h>
int main()
{
	int a = 1;
	char* p = (char*)&a;//把int类型强行转换
	if (*p == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

或者使用函数:

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

例2:求输出结果(char、signed char、unsigned char)

#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=-1,b=-1,c=255

解析:

-1放入内存中,放入的是补码 --- 11111111 11111111 11111111 11111111

但是char类型大小是一个字节 - 8bit:

  • 所以 a 中存入的是 11111111  ---  认为最高位是符号位
  • b 有符号,所以放入 b 中的是 11111111  ---  认为最高位是符号位
  • c 是无符号的,c 中存入的也是 11111111  ---  认为没有符号位

而在执行打印时,是要求[%d],所以需要整型提升,a 和 b 有符号,要按照符号位提升,所以提升后,成为:11111111 11111111 11111111 11111111;

但是 c 无符号位,高位默认补0,整型提升后,成为:00000000 00000000 00000000 11111111,最高位是0,认为是正数,补码就是原码,打印 11111111 对于的十进制数 - 255。

补充:

  1. char  到底是[signed char]还是[unsigned char],C语言并没有规定,取决于编译器;
  2. 但是C语言有规定:int 就是[signed int]、short 就是[signed short]。

例3:下面程序输出结果是什么?(char a = -128 → %u)

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

(%u - 打印一个无符号数)

打印结果:4294967168

解析:

  • -128的原码:10000000 00000000 00000000 1000000
  • -128的反码:11111111 11111111 11111111 01111111
  • -128的补码:11111111 11111111 11111111 10000000

由于char类型,所以 a 中存入的是 10000000。a 是有符号的,所以在打印(整型提升)时,最高位是补1:11111111 11111111 11111111 10000000。

但是,%u 认为最高位不是符号位,此时认为补码就是原码,于是[11111111 11111111 11111111 10000000]未经转换,直接被当中原码打印,直接转换为十进制:4294967168。

ps:

  • 如果通过 %d 打印,结果是:-128。
  • 129的原码:00000000 00000000 00000000 10000001
  • 129如果存入char类型中,截断为:10000001
  • 此时129的反码:10000000
  • 129的补码:11111111  ---  结果是-127

例4:求打印结果(char a = 128 → %u)

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

打印结果:4294967168

解析:

128的补码和原码相同:00000000 00000000 00000000 1000000

a 中存放的是:10000000,和上题一样,不再赘述。

---

补充:char类型变量的取值范围

8个bit内存放的二进制数:

正数00000000(0)

(一直+1)→

01111111(127)
负数10000000(无法计算,认为是-128)

(一直+1)→

11111111(-1)

 01111111(127) +1 → 10000000(-128)

得出结论:有符号的char的取值范围是:-128 ~ 127(无符号的char类型取值范围是0~255,有符号数和无符号数的区间长度是相同的)

所以有符号的char内放入的127以上的数字就会被解读成其他数字。


例5:代码结果是什么?(int、unsigned int → %d)

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

	return 0;
}

打印结果为:-10

解析:

-20的原码:10000000 00000000 00000000 00010100

-20的补码:11111111 11111111 11111111 11101100

10的补码:00000000 00000000 00000000 00001010

-20和10的补码相加:11111111 11111111 11111111 11110110 --- 结果的补码,按照[%d]打印,认为有符号,需要转换为原码:10000000 00000000 00000000 00001010


例6:代码执行的结果是什么?(unsigned int → for循环)

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

结果:陷入死循环。

解析:

i 是无符号数,i 在任何情况下都不可能小于0。

||| ps:在打印时,变量的符号有无取决于[%u]、[%d]这些格式。


例7:求最终打印的结果(char a[1000])

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

打印结果:255

解析:

1000次循环后,arr内存放:

-1 -2 -3 …… -127 -128 127 126 125 …… 3 2 1 0 -1 -2 …… -127 -128 127 ……

[strlen(a)] → 求a的字符串长度。strlen在寻找到'\0'(即0)后结束:-1 -2 -3 …… -127 -128 127 126 125 …… 3 2 1 0


例8:求代码执行结果(unsigned char → for循环)

#include<stdio.h>
unsigned char i = 0;

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

结果:死循环

解析:

[unsigned char] - 无符号,这里的char的取值范围是:0~255,i 永远满足循环条件。

故使用无符号数时容易死循环。

浮点型在内存中的存储

常见的浮点数:

|||   3.12149 1E10 浮点数家族包括:float、double、long double 类型。浮点数表示的范围:float.h 中定义。(1E10:1.0 * 10^10)

浮点数存储的例子(引例)

int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);

    printf("\n");

	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);

	return 0;
}

代码执行结果:

解析:

(强制类型转换是不会改变变量的值的,发生改变的是变量的地址类型)

[int n = 9]开辟了4个字节(4byte)的空间。n的小端存储模式:

首先:

  • [printf("n的值为:%d\n", n)],通过整型的视角存入,通过整型的视角取出,没有问题。
  • 上面的[printf("*pFloat的值为:%f\n", *pFloat)],通过浮点型的视角存入,通过浮点型的视角取出,也没问题。

再看:

float类型的指针解引用访问4个字节,恰好可以取出 09 00 00 00

  • [int n = 9]在定义变量时,是通过整型的视角定义的。
  • 而下面的[printf("*pFloat的值为:%f\n", *pFloat)]则是通过整型的视角存入数值,通过浮点数的视角取出数值(pFloat是浮点型指针,*pFloat认为pFloat指向的空间存放的是浮点数(但是存入n时是存入整数),引用的也是[%f],而打印结果却是:0.000000)

---

  • [*pFloat = 9.0],改变变量时,是通过浮点型的视角存入[9.0]。
  • 而[printf("num的值为:%d\n", n)]是通过整数的视角取出 n 中的数值。

小结:浮点型和整型在内存中的存储方式(和解读方式)是有区别的。

浮点数在内存中的存储方式

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

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

浮点数十进制转换为二进制时,要根据小数点的存在,往前的整数部分和往后的小数部分分开判断。

举例1:

  • 浮点数5.5,转换为二进制就是101.1,其中小数点后面是0.5,也就是1/2^1。
  • 而101.1再转换为科学计数法的形式,就是1.011*2^2(注意:这里是二进制)。
  • 再表示符号位:(-1)^0 * 1.011*2^2。

总结:浮点数5.5 - 十进制 → 二进制 → 101.1 → 1.011*2^2 → (-1)^0 * 1.011*2^2 → s=0、M=1.011、E=2。

举例2:

  1. 十进制的5.0,写成二进制是101.0,相当于1.01×2^2。那么,按照上面V的格式,可以得出s = 0,M = 1.01,E = 2。
  2. 十进制的-5.0,写成二进制是-101.0,相当于-101×2^2。那么,s = 1,M = 1.01,E = 2。

IEEE 754规定

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

---

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

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

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

IEEE 754规定,在计算机内部保存M时:

默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如:

保存1.01时,只保存01,等到读取时,再把第一位的1加上去,这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M的只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。(如果出现23个bit过多的情况,如 M=1.011,则将空位补齐)

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

首先,E为一个无符号整数(unsigned int)这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。

但是,科学计数法中的E是可以出现负数的。所以IEEE 754 规定,存入内存E的真实值必须再加上一个中间数:对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如:2^10的E是10,所以保存成32位浮点数时,必须保存为10 + 127 = 137,即10001001。

例子

int main()
{
	float f = 5.5f;
	return 0;
}

解析

5.5的二进制数字为:101.1
科学计数法:1.011 * 2^2
s=0 M=1.011 E=2
存入内存:s=0 M=011 E=2+127
    
对应内存的二进制:
   0 10000001 01100000000000000000000
→ 0100 0000 1011 0000 0000 0000 0000 0000
    
对应内存的十六进制:40 b0 00 00

开启调试(浮点数的存储也有大小端问题):

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

  • E不全为0或不全为1

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

比如:0.5(1/2)的二进制形式为0.1,由于规定正数的部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1 + 127 = 126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制的表示形式为:

0 011111110 00000000000000000000000                                                                                        

  • E全为0

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

  • E全为1

||| 这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。

解释引例

int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);

        printf("\n");

	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);

	return 0;
}

解析:

9的二进制序列:00000000 00000000 00000000 00001001 (赋值)→ n

第一个[printf("*pFloat的值为:%f\n", *pFloat)],此时整型输入,浮点型输出:

  • 9的二进制序列被认为是浮点数:0(s)  00000000(E)  00000000000000000001001(M) --- 这是存在内存里的。
  • 此时E为全0,不补0:0.00000000000000000001001;
  • 再乘以E = -126(1-127):0.00000000000000000001001 * 2^(-126) --- 几乎为0,数字太小,没有打印到1出现的位置。

---

[printf("num的值为:%d\n", n)],此时浮点型输入,整型输出:

  • 9.0以浮点数的形式写入:1.001 * 2^3   此时E = 3,3 + 127 = 130
  • 所以此时9.0的二进制表达是:0(s)  10000010(E)  00100000000000000000000(M)
  • 而打印时认为上面的二进制是整数的二进制,此时符号位是0,原码就是补码。原码即:01000001 00010000 00000000 00000000
  • 打印结果:

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值