9. C语言之数据在计算机内部的存储
文章目录
- 一、前言
- 二、类型的基本归类
- 1、整型家族
- 2、浮点数家族
- 3、构造类型
- 4、指针类型
- 三、整型在内存中的存储
- 1、原码、反码、补码
- 1.1 概念
- 1.2 原码与补码的转换形式
- 1.3 计算机内部的存储编码
- 2、大小端介绍~~
- 2.1 为什么要有大端和小端之分?
- 2.2 大(小)端字节序存储
- 2.3 一道百度系统工程师笔试题
- 3、数据范围的介绍
- 3.1 char与signed char数据范围
- 3.2 unsigned char数据范围
- 4、笔试题
- 四、浮点型在内存中的存储
- 1、引入
- 2、浮点数存储规则
- 2.1 概念
- 3、开局疑难解答
一、前言
前面我们已经学习了基本的内置类型,以及他们所占存储空间的大小~~
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)
- 如何看待内存空间的视角
二、类型的基本归类
1、整型家族
- 下面是整形家族
-
char在字符存储的时候存的是一个ASCLL码值,而ASCLL码值是一个整数
-
数值有正数和负数之分
- 有些数值只有正数,没有负数(身高)—— unsigned
- 有些数值,有正数也有负数(温度)—— signed
2、浮点数家族
- 浮点数只分为两类,一个是【float】,一个是【double】
3、构造类型
4、指针类型
-下面我们来介绍一下这个空指针
- void*叫做【空指针】
- 对于int类型的指针可以用来接收int类型的数据的地址
- 对于char类型的指针可以用来接收char类型的数据的地址
- 对于float类型的指针可以用来接收float类型的数据的地址
- 对于void类型的指针可以用来接收任何类型数据的地址【它就像一个垃圾桶一样,起到临时存放的作用】
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
三、整型在内存中的存储
1、原码、反码、补码
1.1 概念
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
- 三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位正数的原、反、补码都相同,负整数的三种表示方法各不相同
int a = 10;
- 对于正数说,因为原、反、补都是相同的~~
int a = -10;
- 对于负数来说就不太一样了,要得到反码就将原码除符号位外其余各位取反,要得到补码的话就在反码的基础上 + 1
1.2 原码与补码的转换形式
- 原码到补码 —— 1种方式
- 原码取反,+1得到补码
- 补码到原码 —— 2种方式
- 补码 - 1,取反得到原码
- 补码取反,+1得到原码
1.3 计算机内部的存储编码
- 对于整形来说:数据存放内存中存放的是补码。
- 这里存储的是16进制类型的补码~~
- 在【进制转换】中,4位二进制表示1位16进制。通过将补码4位4位进行一个划分就可以得出8个16进制的数字为ff ff ff f6,这里发现是倒着存的,那么就要涉及到大小端存储的~~
2、大小端介绍~~
2.1 为什么要有大端和小端之分?
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为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处理器还可以由硬件来选择是大端模式 还是小端模式。大端】和【小端】的由来,接下去呢就正式地来给读者介绍一下这种倒着存放的方式
2.2 大(小)端字节序存储
- 【大端(存储)模式】:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
- 【小端(存储)模式】:是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中;
- 对于下面这一个十六进制数0x11223344,以进制的权重来看的话右边的权重低【0】,左边的权重高【3】,所以11为高位,44为低位。若是对其进行小端字节存储的话就要将44存放到低位,11存放到高位
- 【大端字节序存储】是要将高位存放到低地址,低位存放到高地址,因此11要放在左边,44要放在右边
2.3 一道百度系统工程师笔试题
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
- 下面我通过一个简单的数作为案例来进行一个分析
int a = 1;
- 对于a以【小端字节序存储】会将01放在低位;而以【大端字节序存储】会将01放在高位,那么此时我们只需要获取到内存中规定最低位即可
- 因为01在内存中表示一个字节,而一个【char】类型的数据就为1个字节,所以此时我们可以使用到一个字符型指针接受要存放到内存中的这个数,然后对其进行一个解引用,就可以获取到低位的第一个字节了
char* p = &a;
- 若直接使用一个字符型指针去接收一个整型数值的地址,就会出现问题,因为一个字符型的指针只能放得下一个字节的数据,所以我们要对这个整型的数值去进行一个强制类型转换为字符型的地址
- 通过强制类型转换后,再对这个字符型指针进行解引用,就可以取到一个字节的数据,继而对其进行一个判断,如果为1的话那就是【小端】,反之则是【大端】
char* p = (char *)&a; if (*p == 1){ printf("小端\n"); } else { printf("大端\n"); }
- 通过学习了函数章节,我们可以对其进行一个分装~~
int check_sys(int num) { char* p = (char*)# if (*p == 1) { return 1; } else { return 0; } } int ret = check_sys(1);
- 或者,对于这个if判断我们可以就直接写成解引用的形式,然后对其去进行一个判断
if (*(char*)&num == 1)
- 那既然是我们要return 1或者0的时候,其实在解引用获取到低地址的第一个字节时直接return即可~~
int check_sys(int num) { return *(char*)# }
3、数据范围的介绍
3.1 char与signed char数据范围
-
首先我们通过下面这幅图来看一看对于有符号的char和无符号的char
在内存中所能表示的范围各自是多少:
- 【signed char】:-128 ~ 127
- 【unsigned char】:0 ~ 255
-
char数据类型在内存中占1个字节,也就是8个比特位。若是从00000000开始存放,每次+1上去然后逢二进一,之后你就可以得出最大能表示的正整数为【127】,可是在继续+1后又会进行进位然后变为10000000,符号位为1,这是-128这是为什么呢?
-
在内存中都是以【补码】的形式进行存放,所以我们看到的1 1111111只不过是补码的形式
-
但是对于10000000我们直接将其记作【-128】,它就对应的【-128】在内存中的补码,通过去写出【-128】的原、反、补码可以发现是需要9个比特位来进行存放,对于char类型的数值而言只能存放8个比特位,因此在转换为补码之后会进行一个截断
-
最后剩下的就是10000000,即为有符号char的最小负数为【-128】
3.2 unsigned char数据范围
- 因为是无符号char,所以第一位不作为符号位,就是从0 ~ 255
4、笔试题
- 有符号的数在整型提升的时候补符号位,无符号的数在整型提升的时候补0
- %u 是打印无符号整型,认为内存中存放的补码对应的是一个无符号数
- %d 是打印有符号整型,认为内存中存放的补码对应的是一个有符号数
第一道
#include 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】和【b】都是有符号位的char类型,那它们就是一样的,现在将-1存放到这两个数据中去,首先你应该要考虑到的是一个数据放到内存中去是以补码的形式
- 所以我们首先将-1转换为补码的形式
1 0000000 00000000 00000000 00000001 1 1111111 11111111 11111111 11111110 1 1111111 11111111 11111111 11111111
- 可是呢,需要存放的地方又是char类型的变量,只能存放8个字节,无法放得下这32个字节,因此便需要进行一个截断的操作,放到变量a和变量b中都只剩下11111111这8个字节。
- 对于变量c来说,它是一个无符号的char类型变量,不过-1存放到它里面还是11111111这8个字节不会改变,只不过在内存中的变化与有符号char不太一样~~
printf("a = %d, b = %d, c = %d", a, b, c);
- 以%d的形式进行一个打印,但是呢三个变量所存放的都是char类型的变量,因此会进行一个整型提升,只是有符号数的整型提升和无符号数不太一样
//a b - 有符号数 11111111111111111111111111111111 - 补符号位 //c - 无符号数 00000000000000000000000011111111 - 补0
- 在进行整型提升之后,这些二进制数据还是存放在内存中的,可是要输出打印在屏幕上的话还要转换为【原码】的形式
11111111111111111111111111111111 10000000000000000000000000000000 10000000000000000000000000000001 ——> 【-1】 00000000000000000000000011111111 ——> 【255】
第二道
#include int main() { char a = -128; printf("%u\n",a); return 0; }
- 同理,一个整数存放到内存中,首先要将其转换为【补码】的方式
10000000 00000000 00000000 10000000 11111111 11111111 11111111 01111111 11111111 11111111 11111111 10000000
- 接着因为这32个二进制位要存放到一个char类型的变量中,因为进行截断为10000000
- 然后在内存中需要进行一个整型提升,char类型的变量将会填充符号位11111111111111111111111110000000
- 执行打印语句,可以看到这里是以%u的形式进行打印,认为在内存中存放的是一个无符号整数。我们知道,对于无符号整数来说,不存在负数,所以其原、反、补码都是一样的,因此在打印的时候就直接将其转换为十进制进行输出
printf("%u\n",a);
输出:
- 不信的话还可以使用计算器来算一下
第三道
#include int main() { char a = 128; printf("%u\n",a); return 0; }
- 接下去我们来看第三道题,可以看出和上面那题基本基本一样,只是把-128变成了128而已
- 如果是【128】的话放到内存中就不需要像负数那样还要进行很多的转化了,因为正数的原、反、补码都一致,当我们内存中真正存的是10000000又因为是%u的形式打印,然后需要整形提升
00000000 00000000 00000000 10000000
- 然后我们可以看到时10000000符号位是1,所以整形提升的时候补1
11111111 11111111 11111111 10000000
- 所以还是和上题一样,这里就不多赘述~~
第四道
int main() { int i = -20; unsigned int j = 10; printf("%d\n", i + j); return 0; }
下面是解法:
int main() { int i = -20; //1 0000000 00000000 00000000 00010100 //1 1111111 11111111 11111111 11101011 //1 1111111 11111111 11111111 11101100 unsigned int j = 10; //0 0000000 00000000 00000000 00001010 printf("%d\n", i + j); //1 1111111 11111111 11111111 11101100 //0 0000000 00000000 00000000 00001010 //------------------------------------------ //1 1111111 11111111 11111111 11110110 //1 1111111 11111111 11111111 11110110 //1 0000000 00000000 00000000 00001001 //1 0000000 00000000 00000000 00001010 —— 【-10】 //按照补码的形式进行运算,最后格式化成为有符号整数 return 0; }
- 本次我们用到的是两个int类型的数据,一个是有符号的,一个是无符号的。但无论是有符号还是无符号,放到内存中都是要转换为补码的形式
- 就是对算出来的两个补码一个二进制数的相加运算,注意这里是将整数存放到int类型的变量中去,所以不需要进行【截断】和【整型提升】
1 1111111 11111111 11111111 11101100 0 0000000 00000000 00000000 00001010 ------------------------------------------ 1 1111111 11111111 11111111 11110110
- 在运算之后要以%d的形式进行打印输出,那就会将内部中存放的补码看做是一个有符号数,既然是有符号数的话就存正负,可以很明显地看到最前面的一个数字是1,所以是负数,要转换为原码的形式进行输出
1 1111111 11111111 11111111 11110110 1 0000000 00000000 00000000 00001001 1 0000000 00000000 00000000 00001010 —— 【-10】
第五道
- 接下去第五道,是一个for循环的打印
int main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%u\n", i); } return 0; }
运行结果:>死循环
-
有同学就很诧异为什么会陷入死循环呢?这不是就是一个正常的打印过程吗?
-
其实,问题就出在这个unsigned,把它去掉之后就可以正常打印了
-
回忆一下我们在将无符号整数的时它的数据范围是多少呢
- 对于char类型来说是0 ~ 255;
- 对于short来说是0 ~ 65536;
- 对于int类型来说是0 ~ 16,777,215;
-
对比进行观察其实可以发现它们的数值范围都是 > 0的,所以对于无符号整数来说就不会存在负数的情况。因此这个for循环的条件【i >= 0】其实是恒成立的,若是当i == 0再去--,此时就会变成【-1】
-
对于【-1】我们有看过它在内存中的补码形式为11...11是全部都是1,而此时这这个变量i又是个无符号的整型,所以不存在符号位这一说,那么在计算机看来它就是一个很大的无符号整数。此时当i以这个数值再次进入循环的时候,继续进行打印,然后执行--i,最后知道其为0的时候又变成了-1,然后继续进入循环。。。
光是这么说说太抽象了,我们可以通过Sleep()函数在打印完每个数之后停一会,来观察一下
#include int main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%u\n", i); Sleep(200); } return 0; }
- 接着你便可以发现,当i循环到0的时候,突然就变成了一个很大的数字,这也就是印证了我上面的说法
第六道
- 本题和四五道的原理是一样的,对于unsigned char来说,最大的整数范围不能超过255,所以当这里的【i】加到255之后又会再+1就会变成00000000,此时又会进入循环从0开始,也就造成了死循环的结果
unsigned char i = 0; int main() { for (i = 0; i printf("hello world\n"); } return 0; } char a[1000]; int i; for (i = 0; i 0 10000010 00100000000000000000000
- 然后去执行打印语句,那我们以浮点数的形式放进去,但是以%d的形式打印n,那么这一串二进制就会被编译器看做是补码,既然是打印就得是原码的形式,不过看到这个符号位为0,那我们也不需要去做一个转换,它就是原码
printf("num的值为:%d\n", n);
- 那么最后机器就会将二进制形式的原码转换为十进制的形式然后打印。一样,我们可以将它放到【程序员】计计算器进行运行,然后找到十进制的形式,便是最后打印输出在屏幕上的结果
01000001000100000000000000000000 —— 1,091,567,616
- 那么最后机器就会将二进制形式的原码转换为十进制的形式然后打印。一样,我们可以将它放到【程序员】计计算器进行运行,然后找到十进制的形式,便是最后打印输出在屏幕上的结果
- 然后去执行打印语句,那我们以浮点数的形式放进去,但是以%d的形式打印n,那么这一串二进制就会被编译器看做是补码,既然是打印就得是原码的形式,不过看到这个符号位为0,那我们也不需要去做一个转换,它就是原码
- 本题和四五道的原理是一样的,对于unsigned char来说,最大的整数范围不能超过255,所以当这里的【i】加到255之后又会再+1就会变成00000000,此时又会进入循环从0开始,也就造成了死循环的结果
- 接着你便可以发现,当i循环到0的时候,突然就变成了一个很大的数字,这也就是印证了我上面的说法
-
- 接下去第五道,是一个for循环的打印
- 在运算之后要以%d的形式进行打印输出,那就会将内部中存放的补码看做是一个有符号数,既然是有符号数的话就存正负,可以很明显地看到最前面的一个数字是1,所以是负数,要转换为原码的形式进行输出
- 所以还是和上题一样,这里就不多赘述~~
- 然后我们可以看到时10000000符号位是1,所以整形提升的时候补1
- 不信的话还可以使用计算器来算一下
- 同理,一个整数存放到内存中,首先要将其转换为【补码】的方式
- 在进行整型提升之后,这些二进制数据还是存放在内存中的,可是要输出打印在屏幕上的话还要转换为【原码】的形式
- 以%d的形式进行一个打印,但是呢三个变量所存放的都是char类型的变量,因此会进行一个整型提升,只是有符号数的整型提升和无符号数不太一样
- 有符号的数在整型提升的时候补符号位,无符号的数在整型提升的时候补0
- 因为是无符号char,所以第一位不作为符号位,就是从0 ~ 255
-
-
- 那既然是我们要return 1或者0的时候,其实在解引用获取到低地址的第一个字节时直接return即可~~
- 或者,对于这个if判断我们可以就直接写成解引用的形式,然后对其去进行一个判断
- 通过学习了函数章节,我们可以对其进行一个分装~~
- 通过强制类型转换后,再对这个字符型指针进行解引用,就可以取到一个字节的数据,继而对其进行一个判断,如果为1的话那就是【小端】,反之则是【大端】
- 若直接使用一个字符型指针去接收一个整型数值的地址,就会出现问题,因为一个字符型的指针只能放得下一个字节的数据,所以我们要对这个整型的数值去进行一个强制类型转换为字符型的地址
- 下面我通过一个简单的数作为案例来进行一个分析
- 【大端字节序存储】是要将高位存放到低地址,低位存放到高地址,因此11要放在左边,44要放在右边
- 对于下面这一个十六进制数0x11223344,以进制的权重来看的话右边的权重低【0】,左边的权重高【3】,所以11为高位,44为低位。若是对其进行小端字节存储的话就要将44存放到低位,11存放到高位
- 原码到补码 —— 1种方式
- 对于负数来说就不太一样了,要得到反码就将原码除符号位外其余各位取反,要得到补码的话就在反码的基础上 + 1
- 对于正数说,因为原、反、补都是相同的~~
- 三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位正数的原、反、补码都相同,负整数的三种表示方法各不相同
- void*叫做【空指针】
- 浮点数只分为两类,一个是【float】,一个是【double】
-