☃️内容专栏:【C语言】进阶部分
☃️本文概括: C语言中的数据类型及其存储方式。
☃️本文作者:花香碟自来_
☃️发布时间:2023.2.24
目录
一、数据类型详细介绍
1.1 基本的数据类型
1.2 整型家族
1.3 构造类型
1.4 指针类型
1.5 空类型
二、整型在内存中的存储
2.1 原码、反码、补码
三、大小端字节序
3.1概念
3.2 笔试题
四、 练习——内存存储
4.1.
4.2.补充(char类型存放数值的范围)
1. signed char 范围划分
2. unsigned char 范围划分
4.3
五、浮点型在内存中的存储
5.1浮点数存储的规则
5.2浮点数存储的例子
以下为C语言的几种常见类型
char | 字符数据类型 |
short | 短整型 |
int | 整型 |
long | 长整型 |
long long | 更长的整型 |
float | 单精度浮点数 |
double | 双精度浮点数 |
char存储的是字符,但本质存储和表示的是其字符所对应的ASCII码值,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) |
我们知道对于数值,如“10”、“-10”,数值有正负,数值是有符号的,比如说温度,是有正数也是有负数的,我们可以用unsigned int 来存储数值,比如说年龄,没有负数的概念,我们就可以用signed int来存储。
我们平时默认写的short 、int 、long,前面其实省略了signed,这个signed可添加可不添加,表示的都是有符号。需要注意的是,对于char类型来说,不一定省略的都是signed,需要根据编译器确定,一般情况下,属于signed的。
也称自定义类型
数组类型 | int [],char [],float []…… |
结构体类型 | 由struct关键字构造的结构体 |
枚举类型 | enum |
联合类型 | union |
整型指针 | int * |
字符指针 | char * |
浮点数指针 | float * / double * |
空类型指针 | void * |
void 表示空类型(无类型),通常应用于函数的返回类型、函数的参数、指针类型。
我们在初阶部分就已经知道,变量的创建是需要在内存中开辟空间的。空间的大小是根据不同的类型来决定的。内存中存放的数据是以二进制的形式表示。
那么整数在内存中是如何存储的,我们就需要引入原码、反码、补码的概念,
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位(最高位)都是用0表示“正”,用1表示“负”。其余位数均为数值位。
正数的原、反、补码都相同。负整数的三种表示方法各不相同。
#include
int main()
{
int a = 10;
// 0000 0000 0000 0000 0000 0000 0000 1010 原码
// 0000 0000 0000 0000 0000 0000 0000 1010 反码
// 0000 0000 0000 0000 0000 0000 0000 1010 补码
int b = -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 补码
return 0;
}
那么整型存放在内存中是以什么码的形式进行存储的呢?
正数并不好观察,所以我们考虑观察一个负数,我们取出变量b的地址,观察是以小端字节序的形式显示,什么是小端,后面会讲到,这里暂且理解为倒着存放,即将补码化为十六进制后的数据ff ff ff f6与内存中的数据吻合,所以,我们有结论,整数数据在内存中存放的是补码。
为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
为何叫字节序?是因为将内存的数据以字节为单位进行存储。
- 大端字节序(Big-Endian)将数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。这种排列方式与数据用字节表示时的书写顺序一致,符合人类的阅读习惯。
- 小端字节序(Little-Endian),将一个多位数的低位放在较小的地址处,高位放在较大的地址处。小端字节序与人类的阅读习惯相反,但更符合计算机读取内存的方式,因为CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。
简单理解,
大端字节序(存储模式),是指数据的低位保存在内存的高地址处,而数据的高位保存在内存的低地址处;
小端字节序(存储模式),是指数据的低位保存在内存的低地址处,而数据的高位保存在内存的高地址处。
为什么会有大端小端呢?
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元
都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为
高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高
地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则
为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式
还是小端模式。
百度2015年系统工程师笔试题
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
如何设计一段代码来判断机器的大小端呢?
思路:我们拿最简单的数字1进行判断,数字1存放在内存中转换为十六进制位0x000001,若在小端模式中,则以“01 00 00 00”的形式进行存储,若在大端模式中,则以“00 00 00 01”的形式进行存储。通过观察,我们会发现,其区别是第一个字节为1还是0,我们就可以确定机器是大端存储还是小端存储。于是我们可以想到通过指针来访问,取地址访问的是四个字节,那么什么类型的指针可以访问1个字节的数据呢,当然是char*类型的指针,将拿到的地址强制转换成char*。再通过指针解引用结果是1那么断定是小端,结果是0就是大端了。
代码1:
#include
int main()
{
int a = 1;
char* p = (char*)&a;
if (*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
优化代码,封装成函数
#include
//大端返回0
//小端返回1
int check_sys()
{
int i = 1;
return (*(char*)&i);//强制类型转换后解引用
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
思考以下代码,输出什么?
#include
int main()
{
char a = -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 补码
// 1111 1111 (截断)
signed char b = -1;
// -1
unsigned char c = -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 补码
// 1111 1111 (截断,因为char里面只能存放8个bit位)
printf("a=%d,b=%d,c=%d", a, b, c);// -1 -1 255
//打印%d的a(%d为有符号整数)
编译器默认写char 是有符号的char
// 按符号位填充
// 1111 1111 整型提升为
// 1111 1111 1111 1111 1111 1111 1111 1111(补码)
// %d为有符号的数,所以按内存的角度,高位为1,为负数
// ,所以需要转换为原码
// 1111 1111 1111 1111 1111 1111 1111 1110 (反码)
// 1000 0000 0000 0000 0000 0000 0000 0001 (原码)
// a = -1
//打印b同理 分析过程和a一样
//打印%d的c
//将无符号的char c打印,首先进行整型提升
//无符号的char高位补充0
// 1111 1111 整型提升为
// 0000 0000 0000 0000 0000 0000 1111 1111(补码)
// %d为有符号的数,所以按内存的角度,高位为0,为正数
//正数的原码反码补码相同
// c = 255
return 0;
}
打印代码观察:
char类型数值的存储范围是按照有符号和无符号区分的。char类型的大小为1个字节,1个字节为8个bit位。
推断如下
需要注意的是,内存中 10000000 这个补码表示的是原码(十进制)-128这个值;
我们不妨先将 -128用二进制形式表示为110000000(原码),101111111(反码),110000000(补码),但是存放在char类型空间里,只有8bit的空间,也就是蓝色底纹的部分,剩余的部分将会被丢弃。故以上的 10000000 会被直接翻译成128
推断如下:
对于unsigned来说,就没有符号位的概念了,存在内存中的补码也是原码,数值均为正数,所以unsigned的取值范围是0~255
下列程序输出什么?
//2.
#include
int main()
{
char a = -128;
//将-128转换成二进制
// 1000 0000 0000 0000 0000 0000 1000 0000 (-128的原码)
// 1111 1111 1111 1111 1111 1111 0111 1111 (反码)
// 1111 1111 1111 1111 1111 1111 1000 0000 (补码)
// 1000 0000(截断)
printf("%u\n", a);
//整型提升
// char为signed 高位按符号位补充
// 1111 1111 1111 1111 1111 1111 1000 0000
//%u打印无符号的数 站在内存的角度会视为无符号的数
//所以为正数,直接打印
//计算得到4,294,967,168
return 0;
}
//3.
#include
int main()
{
char a = 128;
//将128转换成二进制
// 0000 0000 0000 0000 0000 0000 1000 0000 (128的原码)
// 1000 0000(截断)
printf("%u\n", a);
//整型提升
// char为signed 高位按符号位补充
// 1111 1111 1111 1111 1111 1111 1000 0000
//%u打印无符号的数 站在内存的角度会视为无符号的数
//所以为正数,直接打印
//计算还是得到4,294,967,168
return 0;
}
//4.
#include
int main()
{
int i = -20;
// -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 (补码)
unsigned int j = 10;
// 1000 0000 0000 0000 0000 0000 0000 1010 (补码)
//i+j
// 1111 1111 1111 1111 1111 1111 1111 0110(补码)
//按照补码的形式进行运算,最后格式化成为有符号整数
// 1111 1111 1111 1111 1111 1111 1111 0101(反码)
// 1000 0000 0000 0000 0000 0000 0000 1010 (原码) -> -10
printf("%d\n", i + j); //发生了算术转换,但并不影响值的计算
return 0;
}
该代码的结果是个死循环,因为i为unsigned的int类型,循环条件大于等于0是恒成立的。
解释:当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
以%u的形式打印,直接将补码打印,结果就为4,294,967,295
//6.
#include
#include
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
//char a[1000]里面的值存放的是 -1 -2 -3 …… -999 -1000 ?
//我们知道signed char类型的取值范围是 -128~127
//具体怎么运算的呢,我们可以观察代码段以下图形
//
//其实是一个轮回的过程
// -129不是真正意义上的-129了,而是站在char内存的角度,截断计算为127了
//我们就知道 -1 -2 -3 … -127 -128 127 126 … 2 1 0 -1
printf("%d", strlen(a));// 128 + 127 = 255
//求字符串长度 找到'\0'(0)即为字符串的结束标志
return 0;
}
该代码的运行结果是一个死循环,unsigned char 的范围是 0 ~ 255,当i++得到i = 256的时候,化为二进制序列为1000 0000 0000 0000 0000 0001 0000 0000,但是存放在char里面的时候,存储的是0000 0000,高位丢掉了。所以循环条件恒小于等于255。
常见的浮点数
3.14159
1E10(科学计数法)
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.h中定义
将一个十进制的浮点数转换为一个二进制浮点数的规则如下:
根据国际标准IEEE 754 (电气和电子工程协会)规定,任意一个二进制浮点数V可以表示成下面的形式:
V = (-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字, 1≤M<2。
2^E表示指数位。
举例来说: 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出S=0,M=1.01,E=2。 十进制的-5.0,写成二进制是 -101.0 ,相当于 -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。
对于有效数字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是可以出现负数的(比如说0.5),所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间 数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即 10001001。
然后,指数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 01111110 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); //code1
printf("*pFloat的值为:%f\n",*pFloat); //code2
*pFloat = 9.0;
printf("num的值为:%d\n",n); //code3
printf("*pFloat的值为:%f\n",*pFloat); //code4
return 0;
}
将代码打印得到以下结果
n的值为9 //code1
*pFloat的值为:0.000000 //code2
num的值为:1091567616 //code3
*pFloat的值为:9.000000 //code4
code1与code2我们能够表面地看出来,但是对于code2 和code3就需要对浮点数进行内存存储的计算了。
对于code2,为什么 0x00000009 还原成浮点数,就成了 0.000000 ? 首先,将 0x00000009 拆分,得到第一位符号位s=0,后面8位的指数 E=00000000 , 最后23位的有效数字M=000 0000 0000 0000 0000 1001。
9 -> 0000 0000 0000 0000 0000 0000 0000 1001
由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。
再次观察code3
问浮点数9.0,如何用二进制表示?还原成十进制又是多少? 首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。
9.0 -> 1001.0 ->(-1)^0*1.001*2^3 -> s=0, M=1.001,E=3+127=130
那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130, 即10000010。 所以,写成二进制形式,应该是S+E+M,即
0 10000010 001 0000 0000 0000 0000 0000
将其二进制换算成十进制打印就是 1091567616
❤ 好啦,数据的存储内容到此为止,创作不易,还请多多三连支持我哦~