👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、数据类型介绍
- 1.1 基本的内置类型
- 1.2 类型的基本归类
- 二、整型在内存中的存储
- 2.1 原码、反码、补码
- 2.2 探讨:为什么整型内存中存的是补码,而不是反码和原码?
- 三、 大小端介绍
- 3.1 经典大小端面试题
- 四、练习
- 4.1 例1
- 4.2 例2
- 4.3 例3
- 4.4 例4
- 4.5 例5
- 4.6 例6
- 4.7 例7
- 4.8 例8
- 五、浮点数在内存中的存储
- 5.1 浮点数存储规则
- 5.2 浮点数存储模型
- 5.3 特别规定
- 5.4 浮点数存储的例题
- 5.4.1 例1
- 5.4.2 例2
一、数据类型介绍
1.1 基本的内置类型
char //字符数据类型
short //短整型
int //整型
long //长整型
long long //更长的整型
float //单进度浮点型
double //双精度浮点型
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了其使用范围)
- 看待内存空间的视角
1.2 类型的基本归类
- 整型家族
char
unsigned char
signed char
//后面带括号的可省略
short
unsigned short (int)
signed short (int)
int
unsigned int
signed int
long
unsigned long (int)
signed long (int)
long long
unsigned long long (int)
signed long long (int)
char
属于整型并不奇怪,因为字符在存储的时候在内存存储的是ASCII
值,因为ASCII
是整数,所以在归类的时候,字符就属于整型家族。- 不管是
long long
/long
/short
/int
+ 变量都等价于signed long long
/long /short /int
+ 变量,但注意:char
到底是signed char
还是unsigned char
完全取决于编译器,常见的char
是有符号的
- 浮点数家族:
float
double
- 构造类型(又称自定义类型):
数组类型 int[]、char[]...
结构体类型 struct
枚举类型 enum
联合类型 union
- 指针类型
int *p;
char *p;
float* p;
void* p; //无具体类型的指针
- 空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
二、整型在内存中的存储
我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。那么数据在所开辟的内存空间中到底是如何存储的?
比如:
int a = 20
int b = -10;
我们知道int
需要开辟4个字节的空间,那么这4个字节的空间到底该如何使用呢?要知道这些首先必须知道什么是原码、反码、补码:
2.1 原码、反码、补码
我们再回头讨论整型在所开辟的空间中到底是如何存储的?
对于整形来说:数据在内存中存储的是二进制序列的补码。
#include <stdio.h>
int main()
{
int a = 20;
//整数的原码、反码、补码相同
//原码:00000000 00000000 00000000 00010100
//反码:00000000 00000000 00000000 00010100
//补码:00000000 00000000 00000000 00010100
int b = -10;
//原码:10000000 00000000 00000000 00001010
//反码:11111111 11111111 11111111 11110101 //符号位不变,其他位取反
//补码:11111111 11111111 11111111 11110110 //反码+1
return 0;
}
接着我们可以通过调试分别查看变量a
的内存和变量b
的内存:
我们发现它们是按十六进制数存储的,这是因为如果是二进制的话,显得过于太长了
接下来分别写出a和b的十六进制,我们发现它们是倒着存放的(后面大小端介绍为什么是倒着放):
#include <stdio.h>
int main()
{
int a = 20;
//整数的原码、反码、补码相同
//原码:00000000 00000000 00000000 00010100
//反码:00000000 00000000 00000000 00010100
//补码:00000000 00000000 00000000 00010100
//十六进制:00 00 00 14
int b = -10;
//原码:10000000 00000000 00000000 00001010
//反码:11111111 11111111 11111111 11110101 //符号位不变,其他位取反
//补码:11111111 11111111 11111111 11110110 //反码+1
//十六进制:ff ff ff f6
return 0;
}
2.2 探讨:为什么整型内存中存的是补码,而不是反码和原码?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
这里可以举个例子帮助大家理解:
三、 大小端介绍
- 大端:又称大端字节序存储,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
- 小端:又称小端字节序存储,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
文字有点干巴,我画图来帮助大家理解:
假设有一个十六进制位:0x 00 11 22 33 44
,怎么知道数据的低位和高位呢?举个例子123,个位数的3就是低位,1就是高位,在上面的数据中,44就是低位,00就是高位。
【小端模式 - x86环境】
【大端模式 - x64环境】
3.1 经典大小端面试题
问:如何设计一个程序去判断当前的系统是大端还是小端呢?(请用编程实现)
思路:这里我们只要拿
1
就非常好判断,因为1
的十六进制为0x00 00 00 01
,在小端的存储模式是0x 01 00 00 00
,大端则是0x 00 00 00 01
,所以只需要判断第一个字节即可,是1
就是小端,是0
就是大端。
【代码实现】
#include <stdio.h>
int main()
{
int a = 1;
// char类型的指针一次只访问一个字节
char* p = (char*)&a;
if (*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
四、练习
4.1 例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;
}
【解析】
整型提升:点击跳转
4.2 例2
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
【解析】
4.3 例3
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
【解析】
4.4 例4
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
【解析】
4.5 例5
#include <stdio.h>
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
return 0;
}
【解析】
4.6 例6
#include <stdio.h>
int main()
{
unsigned int i;
for (int i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
【解析】
i
的类型是unsigned int
,是无符号整型,说明i
不可能为负数,因此以上代码发生死循环。
4.7 例7
#include <stdio.h>
#include <string.h>
int main()
{
char a[1000];
for (int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
【解析】
strlen
只需计算'\0'
之前的所有字符,所以只需要找到'\0'
即可,其本质就是0
。注意:有符号的char
的取值范围:-128~127
。则-1、-2、-3...-128、127、126、125...1、0
。因此一共有127 + 128 = 225
4.8 例8
#include <stdio.h>
unsigned char i = 0;
//0~255
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
i
的类型是无符号char
,因此范围:i
的范围是0~255
,永远都不可能超过225
。所以循环里的内容恒成立,所以结果为死循环。
五、浮点数在内存中的存储
5.1 浮点数存储规则
注意:整型和浮点数在内存中的存储是截然不同的!
浮点数在计算机内部的表示方法:
任意一个二进制浮点数可以表示成下面的形式:(-1)S * M * 2E
- (-1)S表示符号位,当S = 0,浮点数为正数;当S = 1,浮点数为负数。
- M表示有效数字,其范围:大于等于1,小于2。
- 2E表示指数位
举个例子来说:
十进制的
5.0
,写成二进制是101.0
,就相当1.01×2²
。那么,按照上面的格式,就可以得出S = 0
(浮点数为正数),M = 1.01
,E
= 2。
有了S、M、E
,那浮点数在内存中又怎么表示呢?
5.2 浮点数存储模型
IEEE 754规定:
- 对于
32
位的浮点数,最高的1
位是符号位S
,接着的8
位是指数E
,剩下的23
位为有效数字M
- 对于
64
位的浮点数,最高的1
位是符号位S
,接着的11
位是指数E
,剩下的52
位为有效数字M
5.3 特别规定
注意:对于有效数字M
和指数E
,还有一些特别规定:
- 前面说过,
1≤M<2
,也就是说,M可以写成1.xxxxxx
的形式,在计算机内部保存M
时,默认这个数的第一位总是1
,因此可以被舍去,只保存后面的xxxxxx
部分。也就是说,浮点数存入内存时1.xxxxxx
中的1可以省略。比如保存1.01
的时候,只保存01
,剩下位补0。最后等到读取的时候,再把第一位的1
加上去。- 对于
E
,规定:存入内存时E
的真实值必须再加上一个中间数,对于8
位的E
,这个中间数是127
;对于11
位的E
,这个中间数是1023
。比如,210的E
是10
,所以保存成32
位浮点数时,必须保存成10+127=137
,即10001001
。等到读取的时候再减去对应的中间数。
然后,指数E
从内存中==取出==还可以再分成三种情况:
- E不全为0或不全为1
规定:指数E的计算值减去对应的中间值(127或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:
0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为
1.0 × 2-1,其阶码为-1+127=126
,表示为01111110
,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:00111111000000000000000000000000- E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。- E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
5.4 浮点数存储的例题
5.4.1 例1
#include <stdio.h>
int main()
{
float f = 5.5f;
return 0;
}
【图解】
5.4.2 例2
#include <stdio.h>
int main()
{
int n = 9;
float* p = (float*)&n;
printf("n的值为:%d\n", n);
printf("*p的值为:%f\n", *p);
*p = 9.0;
printf("num的值为:%d\n", n);
printf("*p的值为:%f\n", *p);
return 0;
}
【程序结果】
【图解】