一、数据类型
1.构造类型
构造类型的成员可以是基本类型,指针类型,构造类型。
构造类型的成员是构造类型的例子:
1> 二维数值的元素是一维数组。
2> 结构体类型的成员也可以是结构体,但不能是同一类型的结构体,可以是同一类型结构体的指针(单链表的节点)
构造类型的存储实际上就是将各个成员分别保存到内存中。
2.指针类型
基于基本类型和构造类型可以衍生出各种各样的指针类型
还有指向指针的指针即二级指针类型
甚至有多级指针(不常用)
指针类型的存储方式类似于无符号整型。
3.空类型
1.Void 类型在VS环境下大小为0;在linux环境下大小为1
2.Void 作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待。既然无法开辟空间正常使用,编译器干脆强制的不允许定义void变量。
3.空类型的指针,不能进行解引用和加减操作(移动指针)。要先进行强制类型转换,明确指针的类型后才能进行解引用(具体原因参考指针类型的意义)
Void 类型的作用
1.作为函数的返回值:起占位符的作用,明确函数没有返回值;编译过程中,这个函数的返回值无法被接收
2.作为函数的参数:起占位符的作用,告知程序员or编译器,该函数不需要传参。
3.空类型的指针(void*)可以被任意类型的指针接受;也可以接受任意类型的指针(常用于设计通用接口)
4.空类型的指针,一般用于函数参数,作用是可以接受任意类型的指针,使函数能处理各种类型的数据。(举例:库函数 qsort)
4.结论
C语言数据的存储实际上就是整型家族和浮点型家族(基本类型)数据的存储。
二、自动类型转换
自动类型转换就是编译器默默地、隐式地、偷偷地进行的数据类型转换,这种转换不需要程序员干预,会自动发生。
1.在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低;所以说,自动类型转换并不一定是安全的。对于不安全的类型转换,编译器一般会给出警告。
2.在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
整型提升:char 和 short 参与运算时,必须先转换成 int 类型。整型提升是按照变量数据类型的符号位来提升的,即正数和无符号数高位补0,负数高位补1。
扩容转换:转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
整型与浮点型混合运算:全部转换为浮点型
有符号数和无符号数混合运算:全部转换为无符号数
下图对这种转换规则进行了更加形象地描述:
举例说明:
signed int a ;
unsigned long b ;
a+b;(a转换为unsigned long后进行计算)
具体验证过程请参考”C语言中的类型转换“https://zhuanlan.zhihu.com/p/138546274
3.自动类型转换可能造成数据丢失
1.整型与整型运算,是不会出现浮点类型的。也就是说,运算结果将丢失小数部分。
2.赋值造成的类型转换:将级别高的类型赋给级别低的类型时,可能造成数据丢失。
将浮点型赋给整型,运算结果将丢失小数部分。
将long型数据赋给int型数据,可能造成数据失真。
4.自动类型转换改变内存中的数据吗?
1.同为整型家族之间的运算,在进行自动类型转换时只改变类型,不改变内存中的数据。
2.整型与浮点型的混合运算,在进行自动类型转换时不尽改变类型还改变内存中的数据。
三、大小端模式
1.如何理解大小端?
地址有高地址和低地址之分(以字节为单位)
数据按照字节为单位划分也有高权值位,低权值位之别
大小端就是解决数据的高权值位是存放在低地址处还是高地址处的问题(数据和空间的对应关系)
2.大小端的基本概念
大端:按照字节为单位,高权值位数据存储在低地址处,就叫做大端
小端:按照字节为单位,低权值位数据存储在低地址处,就叫做小端(口诀:小小小)
3.大小端是如何影响数据存储的?
大小端存储方案的本质是:数据和空间按照字节为单位的一种映射关系。
数据等大小端存储由系统完成,以哪种方案存就需要用那种方案取。
注意:浮点型数据的存储同样使用大小端的存储方案
4.如何验证电脑是大端机还是小端机?
思路:只要将值为1的整型变量的第一个字节(低地址)取出来判断即可
#include<iostream>
using namespace std;
int main()
{
int a = 1;
if ((*(char*)&a) == 1)
cout << "小端" << endl; //低权值位存放在低地址处
else
cout << "大端" << endl; //低权值位存放在高地址处
return 0;
}
四、整型数据的存储
4.1 原码反码补码
1.原反补是有符号整型二进制的三种形式。无符号数,浮点数没有原反补的概念。
2..原码:直接根据数值写出的二进制序列,就是原码
3.原反补码的相互转换
原码---->反码:原码的符号位不变,其他位按位取反就是反码
反码---->补码:反码+1,就是补码。整数在内存中就是以补码形式存储的
补码---->原码:符号位不变,其他位按位取反再+1(与原码---->补码相同)
4.正整数的原码反码补码相同
5.在计算机系统中,整数一律用补码来表示和存储,原因有三:
1.将符号位和数值域统一处理
2.加法和减法也可以统一处理(CPU只有加法器)
3.原码与补码的转换方式其过程是相同的,不需要额外的硬件电路
6.举例说明
我们知道二进制左边的第一位叫“符号位”,用“0”表示正数,用“1”表示负数,也就是说十进制数 3 的二进制表示方式是 00000011(1个字节),而 -3 则可以表示为 10000011,对吗?那请问 3 - 3 的值用二进制数应该如何表示?
3 - 3 == 3 + (-3) == 0。二进制就是 00000011 + 10000011 == 10000110,并不等于 00000000。补码的发明就是为了解决这个问题!
正数的补码是其本身的二进制形式;
负数的补码需要先将其绝对值按位取反,再 + 1。
比如 -3,就是先将 10000011 按位取反(除了符号位),得到 11111100,再 +1 得到 11111101。这样就可以解决 3 + (-3) == 0 的问题了。
00000011 + 11111101 == 100000000,这里的结果是 9 位,对于一个字节单元来说,这左边的 1 是“溢出”的了,会被自动舍弃,因此结果就变成了 00000000。
4.2 整型存取的过程
如何存?
1.根据数据类型开辟相应大小的空间
2.将数据转为二进制补码
3.根据大端或小端存储方案存储数据
如何取?
1.根据大端或小端存储方案将数据取出,然后:
2.一看变量本身的类型:
3.如果是无符号类型,不需要进行转换,直接取用数据即可(输出,计算,比较等)
4.如果是有符号类型,二看符号位:
5.符号位为0(正数),由于原反补相同,不需要进行转换,直接取用数据即可。
6.符号位为1(负数),需要将补码转为原码后才可取用数据。
变量类型的作用
整型存储的时候,空间不关心是不关心数据类型的。会将待存储的数值转换为二进制后直接放入内存。(例子:unsigned int a = -10)
那么变量类型什么时候起作用呢?
1.在定义变量的时,类型决定了开辟空间的大小
2.在读取变量数据时,类型决定了计算机如何理解空间内部保存的二进制序列
五、整型数据的取值范围
特定的数据类型能表示多少个数据,取决于多个比特位对应排列组合的个数。计算机不会浪费任何一种排列组合的情况,下面的说明以char型为例:
8个比特位能表示256个数据其中:
如果1 1111111表示-127;0 1111111表示+127;也就是说除符号位,其他位从全0变为全1,表示的数都为[0-127]的话,0就被表示了两次(1000 0000 和 0000 0000)。
所以规定只有0000 0000表示0;而1000 0000由于符号位为1所以表示-128。
存数据的角度
char c = -128;
先转为补码:1 1000 0000
由于char类型只能存放8个比特位,所以就会发生截断---->1000 0000
此处的计算结果与上述的规定一致。
取数据的角度
由于在存数据时发生过截断(实际上是一种错误,使得数据不完整)所以在取数据时可能无法完成正确的原反补转换
如:1000 0000转换后的原码为:0000 0000(为0不正确)
因此当内存中的数据为 "1000 0000" 时,不用通过计算,直接规定为-128;
由此我们可以得出结论:1000 0000 ---> -128 是一种半计算半规定的方式
整型数据的取值范围
Char [-2^7, 2^7-1] ([-128, 127])
Short [-2^15, 2^15-1]
Int [-2^31, 2^31-1]
整型数据的“循环”现象
(-128)-1=+127
1000 0000
+ 1111 1111
1 0111 1111 --->+127
就好像把数轴的两端系在一起形成了一个环!
六、浮点数在内存中的存储
6.1 浮点数的二进制形式
注意:在将浮点数的小数部分转换为2的负指数幂相加的过程中,可能会出现计算不尽的情况,这种情况下就会发生精度损失(如3.6)。因此,浮点数之间的比较不能使用”==“或”!=“,正确的比较方法请参考 【初级C语言】表达式和基本语句(布尔型与0比较,浮点型与0比较,switch语句,提高循环语句的效率)
6.2 浮点数的存储模型
6.3 举例说明
以5.5为例(二进制:101.1):
S | E | M |
0 | 2 | 1.011 |
0 | 2 + 127 = 129 | 011 |
0 | 10000001 | 011 |
5.5的二进制形式:01000000 10110000 00000000 00000000(十六进制:40 b0 00 00)
小端模式下的结果:
6.4 浮点数的取用
E不全为0或不全为1
指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。最后根据S,E,M计算得出浮点数的值。
E全为0
无限接近与0
E全为1
表示正负无穷大
七、内存对齐
7.1 什么是内存对齐
平台原因(移植原因)
有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了
性能原因
CPU每次寻址都是要消费时间的,并且CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐后可以提升性能。
注意:字长=机器字长=CPU总线的宽度(地址总线和数据总线)=CPU的位数(32/64)=通用寄存器的宽度。
举例说明:(以四个字节为一个字长)
Short a;(起始地址:0x01)
Char b;(起始地址:0x03)
Short c;(起始地址:0x04)
不进行内存对齐,每个字长的数据
如图,要读取short c中的数据,要先访问前后两个字的内存,再将多余的数据剔除,最后将两部分数据合并起来,这样的访存效率低。
进行内存对齐后,每个字长的数据
0x04这个字节内存闲置。
内存对齐后,只需要访问一次即可。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
建议:让占空间小的成员尽量集中在一起,这样既能满足对齐,又能尽量节省空间。
7.2 结构体的内存对齐规则
一提到内存对齐,大家都喜欢拿结构体的内存对齐来举例子。这里要提醒大家一下,内存对齐是一个统一的、整体的内存布局规则,程序中所有类型的数据都要经过内存对齐。只不过拿结构体来举例子范围较小能更好的理解内存对齐,并且结构体中的成员变量对齐有自己的规则,我们需要搞清这个对齐规则。
对于结构体的各个成员,每个成员都要进行对齐
1. 第一个成员从结构体的首地址处开始存储(偏移量为0)
2. 其他成员的偏移量要对齐到对齐数的整数倍处
3. 对齐数=编译器的默认对齐数(VS中默认对齐数尾8)和该成员类型大小的较小值
对于整个结构体,整体也要在内存中进行对齐
4. 嵌套结构体的请况,内层结构体也要对齐到自己对齐数的整数倍处。
5. 结构体的对齐数通常是结构体成员中的最大对齐数。
6. 结构体的总大小是各结构体成员最大对齐数的整数倍。
7. 外层结构体的整体大小也是所有成员(包括内层结构体的对齐数)最大对齐数的整数倍。
7.3 如何修改默认对齐数
#pragma pack(2)//设置默认对齐数为2
#pragma pack()//取消设置,还原为默认
默认对齐数为1时数据紧挨着存放,结构体的大小就是各个成员变量大小的和
offsetof宏:计算结构体中某变量相对于首地址的偏移量。
八、数字记忆
8.1 熟记2的1~10次方
2^5=32 2^8=256
2^6=64 2^9=512
2^7=128 2^10=1024
8.2 十进制二进制快速转换
1.口诀:1后面有几个比特位就是2的几次方
2.十进制转二进制
将十进制数拆分成几个2^n相加
利用口诀转换
3.二进制转十进制
利用口诀先将每位二进制1转换成十进制数
再将各个十进制数相加
4.从低位到高位n个连续的1:2^n-1