一、前言
在之前,我们学习了有关C语言中的各种数据类型以及它们的存储空间大小,如下图所示
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)
- 如何看待内存空间的视角
二、类型的基本归类
接下去我将上面的这些类型做一个分类,大致分为以下5类
1、整型家族
首先看到的是【整型家族】,分别有char
、short
、int
、long
可能有些同学看到上面的这些很多类型有点懵,什么signed
、unsigned
,下面我就为你来先做一个解答:mag:
👉char为何归到整型家族?
- 因为char在字符存储的时候存的是一个ASCLL码值,而ASCLL码值是一个整数
👉为什么有unsigned和signed两个不同的类型呢
- 因为数值有正数和负数之分
- 有些数值只有正数,没有负数(身高)—— unsigned
- 有些数值,有正数也有负数(温度)—— signed
👉子分类后面的[int]
是什么?
- 因为像
short
、long
这些都是属于整型的范畴,其实应该写成【signed short int】和【unsigned short int】这样,只是为了简写忽略了后面的int
👉像[char]、[signed char]、[unsigned char]
这些该如何区分?
- char 分为【char】、【signed char】、【unsigned char】
- short 分为【short == signed short】、【unsigned short】
- int 分为【int == signed int】
- long 分为【long == signed long】
2、浮点数家族
浮点数只分为两类,一个是【float】,一个则是【double】,这里只是做介绍,下文会专门介绍浮点数在内存中的存储
3、构造类型
有关构造类型的话就分为以下这四种,对于【结构体】、【枚举】、【联合】这里不再细说,会专门开章节叙述
- 主要的话是要提一嘴这个数组类型。例如看到下面的这三个数组,它们都是互不相同的,只要你修改了它的元素类型或者是元素个数,那这就是个不同的数组
4、指针类型
接下去是指针类型,对于int
、char
、float
这三种类型的指针我们之前都见到过,但是可能有同学没有遇见过这个void
类型的指针
- 它叫做【空指针】
- 对于int类型的指针可以用来接收int类型的数据的地址
- 对于char类型的指针可以用来接收char类型的数据的地址
- 对于float类型的指针可以用来接收float类型的数据的地址
- 对于
void
类型的指针可以用来接收任何类型数据的地址【它就像一个垃圾桶一样,起到临时存放的作用】
5、空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
三、整型在内存中的存储【⭐】
接下去我们来聊聊有关整型的数据在内存中的存储形式
1、原码、反码、补码
对于原码、反码、补码来说我们之前在学习【操作符】的时候有遇到过,这么我们再来正式地介绍一下
① 概念介绍
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
- 三种表示方法均有符号位和数值位两部分,符号位都是用
0
表示“正”,用1
表示“负”,而数值位
- 正数的原、反、补码都相同
- 负整数的三种表示方法各不相同
接下去就来分别讲讲正数和负数的原、反、补码有什么不同
int a = 10;
- 对于正数说,因为原、反、补都是相同的,所以当我们写出其原码的时候,其实就可以得出它的反码和补码了
int a = -10;
- 对于负数来说就不太一样了,要得到反码就将原码==除符号位外其余各位取反==,要得到补码的话就在==反码的基础上 + 1==
其实除了这三种之外,还有一种叫做【移码】,如果你学习过《计算机组成原理》这门课应该就可以知道移码就是符号位与补码相反,数值位与补码相同。本文不过过多细究
② 原码与补码的转换形式总结
学习了概念后,我们来总结一下有关原码与补码的之间的转换
- 原码到补码 —— 1种方式
- 原码取反,+1得到补码
- 补码到原码 —— 2种方式
- 补码 - 1,取反得到原码
- 补码取反,+1得到原码
- 第1种很直观,我们主要来说说第二种,也就是将补码取反,+1得到原码,回想原码是怎么到补码的,其实你也就学会了补码怎么转换回原码的,只是这一种转换方式大家可能没有怎听说过
③ 探究计算机内部的存储编码
上面说到了三种整型编码方式,但是真正到了计算机内部使用的是哪个呢?
==对于整形来说:数据存放内存中其实存放的是补码。==
- 通过去VS中进行调试观察【调试】- 【窗口】- 【内存】就可以看到其实在内存中是以补码的形式存放的
- 但是有同学说:这个
f6 ff ff ff
是啥呀,怎么就补码了?通过看前面的内存地址可以发现这其实是16进制的表示方式,若是以32位2进制来进行存放的话就太长了,所以采取十六进制的形式 - 在【进制转换】中,4位二进制表示1位16进制。通过将补码4位4位进行一个划分就可以得出8个16进制的数字为
ff ff ff f6
,但是仔细一看却可以发现这和VS中我们所观察的结果有所不同,感觉倒了一下【这就要涉及到我们下面所要将的大小端存储】
但是你有疑惑过在计算机内部要以【补码】的形式进行存放,而不是以原码的形式存放呢?
- 因为其实很简单,虽然原码的表示形式简单易懂【只需要将真值的+ - 号转换为01即可】,但是原码的加法却异常复杂,需要考虑到两数是同号还是异号以及其他复杂的问题,所以为了解决这些矛盾,人们找到了==补码表示法==
- 在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理
- 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
- 其运算过程是相同的其实也就印证了我上面介绍的补码转换为原码的第二种方式
- 虽然在计算机内部是以补码的形式进行存储,但是当其与我们进行交互的时候使用的却是原码的形式。
- 可能也有同学会疑惑上面的第二点讲【加法和减法也可以统一处理】,我们通过一个最简单的例子就是两数相加
+
来看看
int a = 1; int b = -1; int c = a + b; printf("c = %d\n", c);
- 首先,我们写出a与b的补码,因为在内存中要以补码的形式进行存放和计算
int a = 1; 00000000 00000000 00000000 00000001 - 原/反/补码 int b = -1; 10000000 00000000 00000000 00000001 - 原码 11111111 11111111 11111111 11111110 - 反码 11111111 11111111 11111111 11111111 - 补码
- 接下去对这两个补码进行相加,因为二进制逢二进一,所以可以看到最后进位开头多出了一位
int c = a + b; 00000000 00000000 00000000 00000001 11111111 11111111 11111111 11111111 --------------------------------------------- 100000000 00000000 00000000 00000000
- 但是呢,因为c为int类型的整数,所以只能存的下4个字节,也就是32个比特位的数据,所以将最高位【截断】之后剩下的32位全为0
100000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 - 整型只能存放4B,32b
- 所以可以得出最后的答案为0。在内存中
1 - 1 = 0
是这样计算的,你明白了吗?
【总结一下】:
- 内存中存放的都是补码
- 整型表达式计算使用的内存中补码计算的
- 打印和我们看到的都是原码
2、大小端介绍【补码存储的顺序】
① 大小端的由来
我们在开始可以先看这样一个故事
有两个特别强大的国家在过去进行了36个月的战争,在这期间发生了件事情,就是吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家Blefuscu的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有11000余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。Danny Cohen一位网络协议的开创者,第一次使用这两个术语指代字节顺序,后来就被大家广泛接受,这个就是关于大端小端名词的由来
- 看完后可以发现,原来大小端的由来就是因为鸡蛋🥚要从哪头剥引起的
② 为什么要有大端和小端之分?
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit
- 上面我们介绍过很多的数据类型,有【char】【int】【double】等等,不过除了8bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),这些数据类型所定义的数值在内存中存放的都超过了1个字节了,要存储到内存中,就有导致一个顺序问题
- 因为在内存中我们是以字节为单位来讨论数据的存放,就好像下面这个
0x12345678
在内存中12为1个字节,34为一个字节,56为一个字节,78为一个字节,所以通过右侧的【内存】我们就可以看出虽然呈现的是一个倒着存放样子,但是呢并不是完全倒着,像87 65 43 21
,而是78 56 34 12
。这就是因为它们整体作为一个字节,讨论的是每个字节顺序,而不是每个字节内部的顺序
这,也就导致了【大端】和【小端】的由来,接下去呢就正式地来给读者介绍一下这种倒着存放的方式
③ 大(小)端字节序存储
首先来看一下它们的概念,这至关重要⭐
- 【大端(存储)模式】:是指数据的
低位
保存在内存的高地址
中,而数据的高位
,保存在内存的低地址
中; - 【小端(存储)模式】:是指数据的
低位
保存在内存的低地址
中,而数据的高位
,,保存在内存的高地址
中;
- 可以看到,对于下面这一个十六进制数
0x11223344
,以进制的权重来看的话右边的权重低【0】,左边的权重高【3】,所以11为高位,44为低位。所以若是对其进行小端字节存储的话就要将44存放到低位,11存放到高位,这也就印证了为什么我们最后在看到内存中的存放是倒着的原因👈
- 讲完了小端,我们再来说说【大端字节序存储】,因为要将高位存放到低地址,低位存放到高地址,因此11要放在左边,44要放在右边,所以若是以【大端...】的形式进行存放的话最后看到的便是一个正序的样子
✍一道百度系统工程师笔试题
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
那有同学看到这个就懵逼了😐如何去判断一个机器的大端和小端呢?
- 如果在我上面的讲解中你有仔细观察的话应该可以知道在我当前的VS下采用的就是【小端字节序存储】。其实我们通过存储完后最前面的这个数就可以看出是大端还是小端,但是要怎么获取到这第一个数呢?
下面我通过一个简单的数作为案例来进行一个分析
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*)# }
- 最后再来展示下运行结果【我的机器只能是小端,可以放到其他机器上测试】