从零开始C语言精讲篇7:数据的存储


前言

本章重点:

  1. 数据类型详细介绍
  2. 整形在内存中的存储:原码、反码、补码
  3. 大小端字节序介绍及判断
  4. 浮点型在内存中的存储解析

一、数据类型详细介绍

char        //字符数据类型 1字节
short       //短整型 2字节
int         //整形 4字节
long        //长整型 32位下是4字节,64位下是8字节
long long   //更长的整形 8字节

float       //单精度浮点数 4字节
double      //双精度浮点数 8字节
//C语言有没有字符串类型?

ps:字符类型char在底层存储的是ASCII码值,所以我们往往把char划分到整形家族里

类型的意义:

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

二、类型的基本归类:

2.1整形家族:

char
 unsigned char//无符号字符型
 signed char//有符号字符型
 //char 类型默认是有符号还是无符号是取决于编译器的,大部分编译器默认是有符号的

short
 unsigned short [int]//[int]表示这个int可以不写, unsigned short= unsigned short int
 signed short [int] //默认情况都是有符号的,signed short=short

int
 unsigned int
 signed int//默认情况都是有符号的,signed int=int

long
 unsigned long [int]
 signed long [int]//默认情况都是有符号的,signed long=long

long long
 unsigned long long [int]
 signed long long [int]//默认情况都是有符号的,signed long long=long long

关于unsigned:

#include<stdio.h>
int main()
{
	unsigned char c1 = 255;
	printf("%d\n", c1);

	char c2 = 255;
	printf("%d\n", c2);

	return 0;
}

在这里插入图片描述
解释如下:
我们知道一个char类型是1字节,也就是8比特位,255转换成2进制是1111 1111

如果是unsigned char,就说明是无符号的char,也就是最高位是有效位,那么这个值就不存在原反补的概念了,类似非负数,原反补一样,内存里存的1111 1111打印出来也是1111 111这个值(我们转换成10进制是变成了255)

如果是char,也就是有符号的char,最高位是符号位,这个就是有原反补码的
补:1111 1111
反:1111 1110 (反码=补码-1)
原:1000 0001(原码=反码符号位不变,其他位全部改变)
我们存储的是补码,打印的是原码,1000 0001(有符号)转换成10进制就是-1

注:无符号char取值范围是0~255
有符号char取值范围是-128~127

2.2浮点数家族:

float
double

构造类型:

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

结构体类型详解链接
枚举类型详解链接
联合体类型详解链接

关于数组类型,这个知识点很简单,
我以int型数组给大家举个例子,推广到其他类型数组也是一样的

int main()
{
	int arr[10] = { 0 };//数组类型就是把数组名去掉,剩下的类型int [10]
	printf("%d",sizeof(int[10]));//打印40,因为整形int大小为4,一共有10个整形,4*10=40
	return 0;
}

在这里插入图片描述

2.3指针类型

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

指针详解链接

2.4空类型:

void//一般用于函数返回值类型,也就是不需要返回值的情况

ps:如果你哪天突发奇想想测一下sizeof(void),你会发现,会报错

三、整形在内存中的存储

3.1引子

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

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

我们对上面代码进行调试,然后监视内存,发现a的地址上存储的是8个f
在这里插入图片描述
我们这里一个整形是4字节,然后对应32位比特位(二进制)

f是十六进制的表示,4个二进制才能表示1个十六进制数,
比如1111表示f(他们都对应十进制的15)

那么32个二进制数就可以用8个十六进制表示
ff ff ff ff也就是11111111 11111111 11111111 11111111
也就是我们常说的补码

3.2原码、反码、补码

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

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


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

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

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

正数的原、反、补码都相同。
对于整形来说:数据存放内存中其实存放的是补码。
我们以%d打印出来的,那个是原码

还是看这段代码:

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

a=-1
-1的原码:10000000 00000000 00000000 00000001
-1的反码:111111111 111111111 111111111 111111110(反码=原码符号位不变,其他按位取反)
-1的补码:111111111 111111111 111111111 111111111(补码=反码+1)
这个补码也就是我们上面说的ff ff ff ff

到这块,我们知道了,计算机里整形存的是补码,但是为啥是补码呢?我直接存原码不行吗?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

下面是原反补互相得到的方式
在这里插入图片描述
除了上面这种方式,补码想得到原码,也可以直接按位取反,然后再加一
在这里插入图片描述

举个例子:
-1的补码为 11111111 11111111 11111111 11111111
符号位不变,其他位按位取反 1000000 0000000 0000000 0000000
最后+1得 1000000 0000000 0000000 0000001,也就是-1的原码

3.3大小端问题

int main()
{
    //8个16进制数字正好占32位比特位
	int a = 0x11223344;//0x表示这是一个16进制数字
	//11是该数字的高位,44是该数字的低位!
	return 0;
}

在这里插入图片描述
可以看到,我们这个编译器是把低地址内容放到了低地址处,高地址内容放在高地址处
我们称它为:小端字节序

在这里插入图片描述
还有一种放法是:低地址内容放到了高地址处,高地址内容放在低地址处
我们称它为:大端字节序
在这里插入图片描述

快速记忆:低位地址放低地址就是小端,否则就是大端。你记住“低-低-小”即可

百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

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

3.4整形提升与有/无符号数

下列代码输出什么?
例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;
}

在这里插入图片描述
大部分编译器char都是默认signed char的,笔者这个编译器也是。
所以我们这里只要解释一下为啥char a=-1和unsigned char c=-1即可

-1是一个整形,它的原反补如下:
原码:00000000 00000000 00000000 00000001
反码:011111111 111111111 111111111 111111110
补码:011111111 111111111 111111111 111111111

但是把-1赋值给a时,由于a是char类型,所以存储时会截断变成111111111
同理,c是unsigned char类型,存储时也会截断变成111111111

我们打印时,是以%d的形式打印,是打印有符号整形

所以char类型a在打印的时候,会整形提升,会由1字节(8比特位)变成4字节(32位比特位)
整形提升:
如果是无符号数,则高位直接补0;
如果是有符号数,则高位全补符号位。

a是有符号数,高位补符号位
a的补码:111111111 111111111 111111111 11111111
a的反码:111111111 111111111 111111111 11111110
a的原码:10000000 00000000 00000000 0000001
转换成10进制也就是-1

由于c是无符号数,高位补0
c的补码:00000000 00000000 00000000 11111111
然后我们无符号数,是认为和非负数一样的,也就是原反补码相同
c的原码:00000000 00000000 00000000 11111111
转换成10进制也就是255

例2:

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

-128是一个整形,它的原反补如下:
原码:10000000 00000000 00000000 10000000
反码:111111111 111111111 111111111 011111111
补码:111111111 111111111 111111111 10000000

-128赋值给char类型的a,存储时会发生截断变成10000000

我们的a是char类型,是有符号的数,发生整形提升,高位补符号位
补码:111111111 111111111 111111111 10000000

我们打印时,是以%u的形式进行打印,是打印无符号整形(非负数原反补相同)
原码:111111111 111111111 111111111 10000000
转换成10进制是下面这个数字
在这里插入图片描述

例3:

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

128是一个整形,它的原反补如下:
原码:00000000 00000000 00000000 10000000
反码:01111111 111111111 111111111 011111111
补码:01111111 111111111 111111111 10000000

-128赋值给char类型的a,存储时会发生截断变成10000000

我们的a是char类型,是有符号的数,发生整形提升,高位补符号位
补码:111111111 111111111 111111111 10000000

我们打印时,是以%u的形式进行打印,是打印无符号整形
原码:111111111 111111111 111111111 10000000
转换成十进制和例2的数字一样
在这里插入图片描述

小结:
整形提升的时候,看的是你原先是char a还是unsigned char a
但是打印的时候%d则视最高位为符号位,是否把补码转换成原码你得考虑正负。
%u则视最高位为有效位,补码也就是原码,直接打印补码即可。

例4:

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

计算机中加减操作都是对补码进行的
-20的原反补如下:
原:10000000 00000000 00000000 00010100
反:111111111 111111111 111111111 11101011
补:111111111 111111111 111111111 11101100

10原反补相同
补:00000000 00000000 00000000 00001010

10+20补码
111111111 111111111 111111111 11110110
ps:这里进行计算时会进行算术转换,整形变成无符号整形
总而言之,你是什么类型的数据存储在内存里不重要,我打印的时候再决定你最后是什么类型

我们打印时,是以%d的形式进行打印,是打印有符号整形
这里就要把补码转换成原码

反码111111111 111111111 111111111 11110101
原码10000000 00000000 00000000 00001010
转换成10进制就是-10
在这里插入图片描述

例5:

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

在这里插入图片描述
解释如下图:
在这里插入图片描述

上图也可以画成一个圈来帮助记忆,如下图
在这里插入图片描述
知道了signed char 的存储,该题基本上就迎刃而解了。
我们把数组a的内容画出来:
在这里插入图片描述

知道了数组a里面放的内容,还要知道strlen这个函数的特性,它是检测到\0停止,\0不计算
而我们的0,存放在内存里也就是\0,所以第一次检测到0就结束了。

0前面一共有-1 ~ -128还有127 ~ 1,一共是128+127=255个字符,所以打印255

例6:

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

在这里插入图片描述
解释如下:
在这里插入图片描述
unsigned char也是0000 0000 ~ 1111 1111,只不过这里最高位是有效位(你可以理解为全是非负数)

最高的数也就是1111 1111,转换成10进制是255,然后我们这里1111 1111再+1会变成1 0000 0000
但是unsigned char只有8位,会发生截断,又会变成0000 0000

所以我们这里会变成死循环,永远无法大于255

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

常见的浮点数:
3.1415926
1E10
ps:1E10这种是科学计数法,表示1.0*10^10
浮点数家族包括: float、double、long double 类型。

浮点数存储的实例:

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

在这里插入图片描述
对于第一个值9,因为我们&n得到了n的地址,
然后强制类型转换变成了float类型的指针(和int类型指针一样,大小都是4字节),
把这个指针赋给float*类型 pFloat,虽然指针类型变了,但是指针里面存储的地址还是n的地址,
所以可以通过pFloat找到n,然后以%d形式打印n的值9

而第一个*pFloat的值,我们知道pFloat是存储n的指针,
然后 *pFloat就是对这个指针解引用,得到该地址里面的值,
但是为啥打印的是0.000000呢?
我们n是以整形的形式放进去的,但是我们要拿出来,也就是以%f形式打印,
发现是0.000000,就说明浮点型的存储方式和整形不一样,如果一样,那拿出来应该也是9

对于num的值,我们*pFloat = 9.0,也就是找到n的那块地址,然后把该地址上的值用浮点数9.0赋值,也就是说,我们是用浮点型的形式放进去了。然后我们%d打印该值,发现也不是9.0,
再次验证浮点型的存储方式和整形不一样

第二个*pFloat的值,因为我们是以浮点型的形式放进去,
然后再以浮点型的形式拿出来(以%f的形式进行打印),所以我们这里可以正常打印9.0

num 和 * pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
在这里插入图片描述
在这里插入图片描述
十进制5.5转换成二进制101.1后,再使用科学计数法,就是1.011*2^2

所以我们计算机中的5.5表示如下
(-1)^0 * 1.011 * 2^2
S=0
M=1.011
E=2

然后类比其他的浮点数,我们也是可以通过SME这三个数很快的还原出来
那么我们内存里也不用存那么多数字了啊,只要存S、M、E即可
ps:对于M和E实际上是存的是和它们相关的值,不是直接存M和E
在这里插入图片描述
在这里插入图片描述
IEEE 754对有效数字M和指数E,还有一些特别规定。

关于有效数字M
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
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,即1000 1001。
再比如,2^-1的E是-1,所以保存成32位浮点数时,必须保存成-1+127=126,即0111 1110

在这里插入图片描述

解释前面的题目:

#include<stdio.h>
int main()
{
	float f = 5.5f;
	//5.5转换成二进制是101.1
	
	//(-1)^0 * 1.011 * 2^2 
	//S = 0
	//M = 1.011
	//E = 2

	//E=2,根据IEEE754,存储在内存中是2+127=129,转换成二进制是1000 0001
	//0 10000001 01100000000000000000000
	//S E        M
	
	//上面的01000000101100000000000000000000转换成十进制就是1,085,276,160
	return 0;
}

五、总结

本文介绍了整形和浮点型的数据类型,对于整形又细讲了原码、反码、补码和整形提升相关知识点,整形提升的例题也在文章着重讲解了。对于浮点型则是细讲了IEEE754规定的存储。本文相对而言并不复杂,相信耐心学习的你一定有所收获!

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

劲夫学编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值