进阶C语言(1)-带你修炼数据结构存储

本文详细介绍了数据类型在内存中的存储方式,包括整型的原码、反码、补码表示以及大小端模式,并通过实例分析了补码在存储和计算中的应用。此外,还探讨了浮点数的IEEE 754标准存储规则,解释了浮点数转换为整型时可能出现的误解。通过对这些基础知识的深入理解,有助于提升编程技能和问题排查能力。
摘要由CSDN通过智能技术生成

目录

一.数据类型介绍

类型的基本归类

二.整形在内存中的存储

原码、反码、补码

大小端介绍

典例分析

三.浮点数在内存中的存储

引子

浮点数存储规则

总结


一.数据类型介绍

类型的基本归类

整形家族

char   
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
  • char虽然是字符类型,但是字符类型存储的时候,存储的字符是assic码
  • char有没有符号取决于编译器,C标准没有明确规定

浮点数家族

float
double

构造类型

>数组类型
>结构体类型 struct
>枚举类型 enum
>联合类型 union

指针类型

int *pi;
char *pc;
float* pf;
void* pv;
空类型

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

返回类型 void test();
函数的参数 test(void);
指针类型 void* p;

 

二.整形在内存中的存储

变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。那接下来我们谈谈数据在所开辟内存中到底是如何存储的?

原码、反码、补码

计算机中的整数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而对于数值位,负整数的三种表示方法各不相同。

引入这样几个概念:

  • 原码:直接将二进制按照正负数的形式翻译成二进制就可以。
  • 反码:原码的符号位不变,其他位依次按位取反就可以得到了。
  • 补码:反码+1就得到了补码

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

那么负数呢?将-10作为例子演示一次:

原码:1000 0000 0000 0000 0000 0000 0000 1010
反码:1111 1111 1111 1111 1111 1111 1111 0101
补码:1111 1111 1111 1111 1111 1111 1111 0110

将补码换算成16进制,4个二进制位为1个16进制位
于是得到FFFFFFF6
观察-10在内存中的情况,发现储存的是f6 ff ff ff。刚好反过来存的。
所以可以得出结论:
对于整形来说:数据存放内存中其实存放的是补码。

为什么存放补码呢?

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

举例1:1-1会自动转化为1+(-1),通过加法进行模拟减法

1的补码0000 0000 0000 0000 0000 0000 0000 0001
-1的补码1111 1111 1111 1111 1111 1111 1111 1111
相加后得到:1000 0000 0000 0000 0000 0000 0000 0000 0
多了一位,怎么办?-高位溢出
于是得到0000 0000 0000 0000 0000 0000 0000 0000

举例2:对补码取反然后+1能够得到原码

-1的原码1000 0000 0000 0000 0000 0000 0000 0001
-1的补码1111 1111 1111 1111 1111 1111 1111 1111
补码取反—>1000 0000 0000 0000 0000 0000 0000 0000
取反后+1—>1000 0000 0000 0000 0000 0000 0000 0001

还有一处疑问。为什么数据在内存中存储,补码是倒着存的呢?
这里就要引入一个新的概念-大小端了

大小端介绍

什么是大端小端

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

大小端储存模式如图所示

 以VS2019为例,来判断它是大端还是小端存储模式

我们发现,地址是从上往下递增的,且从左往右是增大的,所以左边是低地址,右边是高地址。而f6是数据的低位,低位储存在低地址,说明VS2019是采用的小端存储模式。

为什么有大端和小端

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

百度2015年系统工程师笔试题:写一个程序判断当前编译器是大端还是小端存储模式

思路:根据大小端存储模式的定义,我们可以通过存储的数据的低位来判断大小端。定义一个x=1,它的十六进制表达为00 00 00 01,假设地址从左往右依次变高。如果是大端,低位储存在高地址,高位储存在低地址,则1的储存形 00 00 01;如果是小端,低位储存在低地址,高位储存在高地址。则1的储存形式为01 00 00 00。那么就只需要写一个程序,来判断1在内存中储存的第一个字节是否为00了。那要如何访问整形的第一个字节呢?用char*即可,于是源码如下:

#include <stdio.h>
int main()
{
     int x=1;
     char*px=(char*)&x;
     if (*px == 0)
     {
        printf("这是大端");
     }
     else
     {
        printf("这是小端");
     }
     return 0;
}

典例分析

接下来我们看几个典例来加深对数据存储的理解,很重要!且不能混淆。

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

分析:-1的补码是1111 1111 1111 1111 1111 1111 1111 1111
存入char类型的a,由于char只有一个字节的大小,就发生截断,存进去的就是1111 1111,由于编译器的char默认是有符号的(但c标准没有规定,因编译器而定)此时认为高位是符号位
将它按整形视角打印,从char类型到int,会发生整形提升。左边自动补符号位1111 1111 1111 1111 1111 1111 1111 1111,这时得到的是补码,将它转为原码表达,所以打印出的结果是-1
b是有符号整形,a也是有符号整形,所以和a打印结果一样,此时认为高位是符号位
c是无符号整形,-1存进去的时候发生了截断
存的也是1111 1111,但由于c是无符号char类型数据,此时认为高位不是符号位
按整数打印,发生整形提升。由于没有符号位,左边自动补0。
0000 0000 0000 0000 0000 0000 1111 1111,由于符号位是0,所以原反补相等,直接把补码看成原码表达。程序运行结果如下

补充

char到底是signed char 还是 unsigned char,C语言标准并没有规定,取决于编译器,但C标准规定了,int 是 signed int,short是signed short

例2:

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

分析:
-128原码:1000 0000 0000 0000 0000 0000 1000 0000
反码:1111 1111 1111 1111 1111 1111 0111 1111
补码:1111 1111 1111 1111 1111 1111 1000 0000
存入char,则发生截断,实际只能存8个bit位,即存入1000 0000
以无符号整形形式输出,发生整形提升
1111 1111 1111 1111 1111 1111 1000 0000
然后此时高位的1不看作符号位了,所以补码直接看作原码来计算。所以程序运行结果为:

例3:

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

分析:
128的原反补码相同:0000 0000 0000 0000 0000 0000 1000 0000
发生截断后,存入a的是1000 0000
按无符号整形打印,则按原来的char类型的规则发生整形提升,因为a的符号位为1,所以左边补1,得到下面的结果。
1111 1111 1111 1111 1111 1111 1000 0000
而由于是以%u形式打印,此时首位的1不当作符号位,直接看作原码,直接输出结果,所以运行结果和T2结果相同。
这个程序说明,
数据发生整形提升的补位由数据的类型决定,而数据怎么表达则由%d,%u这些格式符决定。

我们来探讨char类型的取值范围:

char类型的取值范围

上图可以明显的看出char的取值范围情况,即-128~127。对于1000 0000这个特殊值,它的反码是1111 1111,原码为1 0000 0000,高位溢出,按理来说是0的,为什么这里是-128呢?
这里就要引入一个新的东西了:
我们先来算算,有符号的char占一个字节,最高位表示正负,其余的七位表示数值,七位二进制可以表示128个数(2^7 = 128),即0~127;加上符号位后,就变成了-127 ~ -0,0 ~127,这时,就会出现-0这样一朵奇葩。从数学角度上,0和-0是没区别的,可是用两种形式表示一个数,明显是浪费了。

于是计算机存储就约定,当符号位为0,即正零时才是0。符号位为1时,让它去表示另外一个数好了。
-0的原码是:1000 0000
而-128被截断后存入char,得到的原码和-0的原码相同(如上图)。
还有一种说法是:定义char n= -128,-128的原码为1 1000 0000,反码为原码符号位不变,其它位按位取反1 0111 1111,补码为反码+1,1 1000 0000,将其放入n中时发生截断即 1000 0000,恰好与-0吻合。
所以今后看到一个有符号的char,它的原码用二进制表示为1000 0000的时候,我们就把它当做-128就可以了。

同理可推出其余整形类型的范围。

例4:

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

-20的原码为1000 0000 0000 0000 0000 0000 0001 0100
反码为1111 1111 1111 1111 1111 1111 1110 1011
补码为1111 1111 1111 1111 1111 1111 1110 1100
10的原码为0000 0000 0000 0000 0000 0000 0000 1010
i+j= 1111 1111 1111 1111 1111 1111 1111 0110
按%d的形式打印,即按%d的形式表达
此时为i+j的符号位为-,则应把补码转为原码
补码-1得1111 1111 1111 1111 1111 1111 1111 0101
取反,得1000 0000 0000 0000 0000 0000 0000 1010
转为十进制,所以打印结果应为-10

例5:

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

我们分析,i是无符号整形,所以i恒>=0,即for循环的判断部分恒为真,则程序必然会死循环。然后我们来分析程序内部数据存储是怎么样的:
首先i是无符号整型,将初值9存入i,打印出9,这样依次打印…..打印9876543210,
此时i再–,i变成了-1
-1的二进制原码为1000 0000 0000 0000 0000 0000 0000 0001
反码为1111 1111 1111 1111 1111 1111 1111 1110
补码为1111 1111 1111 1111 1111 1111 1111 1111
-1存入i的时候,由于i是无符号整形,就把-1的补码当成原码,以%u形式表达,忽略符号位,则输出的就是1111 1111 1111 1111 1111 1111 1111 1111这个无符号的二进制数
于是此时的i又大于0了,输出的就是超级大的数,然后这个超级大的数再-1,直到减到0,又变成超级大的数。
于是就这样陷入死循环了,程序跑起来的结果是这样的:

 例6

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

这题看起来是不是有点晕乎乎的,这啥玩意嘛。谋定而后动吧。
观察这个程序的大概流程:
a[0]=-1,a[1]=-2,a[2]=-3…a[126]=-127,a[127]=-128,a[128]=127,a[129]=126…大概按这个规律存满1000个,然后程序的要求是打印字符串长度,那么我们要做的就是找0出现在哪里。由我们前面提到的char的范围-0~127~-128~0,逆过来同样适用:0~-128~127~0,这里就是从-1递减到-128,总共128个元素,然后从127递减到0,总共127个元素。128+127找到了0,即255个元素,所以程序运行结果为255。本题考察的是对char范围的理解。运行结果如下图:

例7

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

分析:
由于i是无符号整形,最小值为0000 0000,最大值为1111 1111。所以它的取值范围是0-255。然后分析条件部分,i<=255,这显然恒成立,所以这个程序的运行结果是死循环打印hello world。
我们对内部数据存储进行具体分析:
i一开始是正常打印的,执行到i=255时,i再++,就是256了。
256的原码是0000 0000 0000 0000 0000 0001 0000 0000
存入char的时候,发生截断,变成了0,即i回到0,于是这样反反复复陷入了死循环。

 看完了这几个例子,是不是对数据存储理解更深刻了呢(晕乎乎了)?
没关系,接下来还有更晕乎乎的。

三.浮点数在内存中的存储

引子

#include <stdio.h>
int main()
{
    int n = 9;//4byte 以整形方式存储n
    float* pFloat = (float*)&n;//将n的地址强制转换为float*存入pFloat。
    printf("num的值为%d\n", n);//以整形方式取出n
    printf("*pFloat的值为:%f\n",*pFloat);//是以浮点数的方式去看的
    *pFloat = 9.0;//以浮点数的方式,存储9.0
    printf("num的值为%d\n",n);//以整形方式取出9.0
    printf("*pFloat的值为:%f\n",*pFloat);//以浮点数的方式取出9.0
    return 0;
}

程序运行结果如上,你会感叹,这是什么寄玩意?我们以整形方式存入9,整形方式取出9的时候,是一切正常的,以浮点数形式存入9.0,浮点数形式取出的时候,也是一切正常的。然而,以整形存入,浮点数形式取出,出来的却是0,以浮点数形式存入,整形形式取出,出来的却是1091567616。这说明:浮点数和整数在内存中存储方式一定有差异,我们就此展开对浮点数的研究:

浮点数存储规则

num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法

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

V=(-1)^S * M * 2^E
●(-1)^S表示符号位,当s=0,V为正数;当s=1,V为负数。
●M表示有效数字,大于等于1,小于2。二进制是恒<2的
●E表示指数位

举例:

浮点数:5.5 -10进制
二进制:101.1 ——>1.011*2^2——>(-1)^0*1.011*2^2
s=0,M=1.01,E=2

浮点数:-5.0-10进制
二进制 :-101.0 ——> -1.01*2^2 ——>(-1)^1*1.01*2^2
s=1,M=1.01,E=2

IEEE 754规定:

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

IEEE 754对有效数字M和指数E,还有一些特别规定。
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

对于M,IEEE 754规定

在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的 xxxxxx部分。

比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字,提高了精度。

至于指数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;
}

5.5f的二进制形式为——>101.1
小数点左移两位——>1.011*2^2
此时的——>s=0,M=1.011,E=2
实际存储——>s=0,M=011(位不够后面自动补0),E=2+127
二进制存储——> 0 10000001 01100000000000000000000
四个二进制位可以看成一个十六进制位——>
0100 0000 1011 0000 0000 0000 0000 0000
转为16进制——>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),其阶码E为-1+127=126,表示为 01111110,而尾数M1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000

E全为0

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

E全为1

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

有了这层理解后,解释前面的引子就轻松了

#include <stdio.h>
int main()
{
    int n = 9;
    float* pFloat = (float*)&n;
    printf("num的值为%d\n", n);//按十进制打印9
    printf("*pFloat的值为:%f\n",*pFloat);//把9以浮点数形式取出
    *pFloat = 9.0;
    printf("num的值为%d\n",n);//以整形形式取出浮点数
    printf("*pFloat的值为:%f\n",*pFloat);//按浮点形式取出浮点数
    return 0;
}

9的二进制序列为00000000000000000000000000001001
①将9以浮点数形式打印
符号位S=0,E=00000000,M=00000000000000000001001
E全为0,直接把M取出来,前面加个小数点0,同时将E的真实值认定为-126。s为0,所以是正数,标上正号。于是得到:
+0.00000000000000000001001*2^-126
由于2^-126极小,float打印位数默认是六位,所以认定为0
②以float形式存储9.0的时候,二进制表达形式为:
1001.01.001*2^3
S=0,E=3+127=1000 0010,M=00100 00000 00000 00000 000
所以实际存储进去的就是0 10000010 00100000000000000000000
以整形形式取出浮点数9.0,计算机认为它是正数,补码和原码相同,所以直接二进制转十进制打印1091567616
以浮点数形式打印浮点数9.0,即正常还原。所以打印出来也是9.0

总结

学习这些数据存储,是为了提升我们的内功。这和修仙一样,如果一味的只修炼境界,那么境界必然就不稳定,但如果我们如果学习了足够优秀的修炼功法,修炼起来,就事半功倍。学习数据存储,能让我们对数据的认识更为深刻,知识了解到这个深度以后,未来去了解其他东西,也就更容易上手了。

  • 14
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

叶轻衣。

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

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

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

打赏作者

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

抵扣说明:

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

余额充值