现代C语言程序设计之数据存储

现代C语言程序设计之数据存储
C语言?2.1 计算机信息数据存储
2.1.1 计算机信息数据存储单位
在计算机最底层,数据都是以二进制(01010)的方式存储,而计算机中最小的存储单位是位(bit),用来表示0或者1。计算机中最基本的存储单位是字节(Byte),1个字节对应8个位(Bit)。
而日常应用中常使用的基本存储单位包括KB,MB,GB,TB,PB,
- KB,MB:使用迅雷下载某些资源时的网速就是KB或者MB,它们之间的换算关系如下
但是网络运营提供商(例如长城宽带、移)声称的百兆带宽实际上是100Mb,但是网络下载速度是以字节为单位的,因此真实的网速理论上只有100Mb/8=12.5MB
- GB:在买内存或者买移动硬盘时,通常使用的存储单位就是GB
但是在买4T的移动硬盘时,实际的可用容量却只有3T多,因为计算机的存储单位是以2的10次方(即1024)换算,而硬盘厂商们是以1000为换算单位。
4T的硬盘换算成位如下所示
而硬盘厂商的实际容量
因此实际的可用容量是
而在一些互联网巨头(例如国内的BAT,国外的亚马逊、苹果、微软、谷歌)公司中,可能使用到比TB更大的海量数据,也就是PB或者EB。
2.1.2 计算机内存
为什么说32位系统只能使用4G内存?下面是4G的内存换算
因为4G只能够寻址到2^32,使用16进制表示就是0xFFFFFFFF,这里可以借助Visual Studio的调试功能查看内存的寻址,如下图所示
2.2 变量
2.2.1 变量概述
内存在程序看来就是有地址编号的一块连续空间,当数据放到内存中后,为了方便的找到和操作这个数据,需要给这个位置起名字,编程语言通过变量来表示这个过程。
2.2.2 变量的声明和初始化赋值
在使用变量前必须先要声明变量并初始化赋值,并且要遵守变量的命名规范
- 变量名由字母数字下划线组成,不能以数字开头
- 变量名区分大小写。
- 变量名不能是C语言的关键字(Visual Studio中的关键字都是蓝色的)
- 考虑到软件的可维护性,建议变量见名知意
如下应用案例所示展示了C语言的变量命名案例
在声明变量后,一定要给变量赋初始值,否者无法编译通过,如下应用案例所示
2.2.2 变量存储
如下应用程序所示,通过"="可以给变量赋值,同时可以通过printf()函数传递%p参数来获取变量在内存中的地址。
如下图所示,还可以通过Visual Studio 提供的调试功能通过断点查看变量在内存的存储,通过输入变量的内存地址便可以观察变量对应的值。
在同一时刻,内存地址对应的值只能存储一份,如果修改地址对应的值,之前的值会被覆盖,这个就是变量的特点,变量名是固定的,但是变量值在内存中是随着业务逻辑在变化的,例如最常见的游戏场景中,游戏人物生命值的变化。
2.2.3 编译器对变量的处理
当在程序中声明变量并赋值时,编译器会创建变量表维护变量的信息,包括变量的地址,变量的类型以及变量的名称。
而在内存中变量的内存地址和变量值是一一对应的,编译器正是通过变量表的内存地址和内存中的变量地址关联。因此在使用变量进行相关操作之前必须先声明并赋值,否则程序会发生编译错误,如下代码片段所示。
2.2.4 变量运算的原理
当两个变量在执行相关运算(例如加法)时,系统会将把两个变量地址对应的变量值移动到CPU内部的寄存器中执行运算后将运算结果返回给内存,如下应用程序所示
如下图所示,可以借助VisualStudio的调试功能来观察EAX寄存器的变化的值。
为了能够更加直接的理解寄存器的作用,这里使用C语言嵌套汇编语言来完成变量的赋值运算和加法运算。
2.2.5 变量交换的实现
如下应用案例所示,实现了三种变量交换的算法,同时也比较了每种算法的时空复杂度,变量交换的应用场景主要在使用在排序算法中。
1.通过使用中间变量实现交换
- 使用算术运算实现变量交换
- 使用异或运算实现变量交换
2.2.6 自动变量与静态变量
在函数中的形式参数和代码块中的局部变量都是自动变量,它们的特点是只有在定义的时候才会被创建(即系统自动开辟内存空间),在定义它们的函数返回时系统自动回收变量占据的内存空间,为了考虑到代码的可读性,通常使用auto关键字来修饰自动变量,应用案例如下所示
可以通过下断点来调试该程序,观察当执行auto_varriable()函数完成以后,局部变量data将会被回收,如下图所示
同时可以通过观察内存地址,发现当调用auto_varriable()函数时,num=20
然后当执行完auto_varriable()函数后,num的值变量一个系统分配的垃圾值
而静态变量不会发生变化,即使函数执行完成也不会被操作系统回收,应用案例如下所示
调试以上应用程序,会发现直到main函数执行完成,静态整数变量x都不会被操作系统回收。
2.3 常量
常量表示一旦初始化之后便不能再次直接改变的变量,例如人的身份证编号一旦确定之后就不会再次改变。C语言支持使用const关键字和#define CONST_NAME CONST_VALUE 两种方式来定义和使用常量。
2.3.1 const常量
如果想要使一个变量变成常量,只需要在变量前面使用const关键字即可,const常量虽然不能直接修改,但是可以通过C语言的指针来修改,因此不是真正意义上的常量。,应用案例如下所示。
2.3.3 #define常量
在C语言中使用const定义的变量不能直接修改,但是可以通过指针来修改,因此不是真正意义上的常量。
如果想要使用真正意义上的常量,可以使用#define CONSTA_NAME VALUE 来实现,应用案例如下所示
使用#define定义常量的好处:
- 通过有意义的常量名,可以指定该常量的意思,使得开发人员在越多代码时减少迷惑
- 常量可以在多个方法中使用,如果需要修改常量,只需要修改一次便可实现批量修改,效率高而且准确。
#define的应用场景: 实现代码混淆
首先在define.h头文件中定义如下常量
然后定义define.c源文件,内容如下
运行程序后,可以打开计算器。
2.4 数据类型
2.4.1 sizeof()运算符
数据类型即对数据进行分类,数据在计算机底层是二进制的,不太方便操作,因此编程语言引入了数据类型将其进行分类处理。
不同的数据类型占据不同的内存大小,这里可以使用C语言提供的sizeof()运算符来获取指定数据类型占据的字节数量,应用案例如下所示
当然sizeof()还可以求表达式的数据类型,应用案例如下所示
3.4.2 数据的解析
同样的数据,按照不同的解析方式会得到不同的结果,如下应用案例所示
启动程序调试,通过查看控制台输出num变量的地址,然后在内存中分别以1字节带符号整数查看结果为-1,8字节整数查看结果为14757395259826634751,1字节不带符号显示(结果为255),如下图所示,不同的方式查看通过鼠标右键获取。
而如果数据使用了错误的解析方式,则结果也会发生错误,这里以printf()函数为例子,应用案例如下所示。
2.4.3 数据类型的极限
每种数据类型都有自己的极限值(即最大值和最小值),如果在参与运算时超过了极限值,则运算结果是错误的,应用案例如下所示
整数的极限值定义在<limits.h>
头文件中,
浮点数的极限值定义在<float.h>
头文件中,
如下应用案例所示展示了整数以及浮点数的极限值使用。
2.4.4 数据的正负
在最底层,计算机的数据都是以二进制的形式表示的,那么如何区分正负数呢?
最高位(左边第一位)是符号位,如果是1,则表示为负数,如果是0则表示正数。
如下应用案例所示
如下图所示,可以通过Visual Studio的调试功能查看两个变量在内存中的存储
2.4.5 数据在内存中的排列
PC、手机的内存排列是低位在低字节,高位在高字节,节省寻址时间。
如下应用程序所示
可以通过Visual Studio 下断点调试程序,使用1字节查看整数1在内存中的排列,如下图所示:

数据在内存中的排列
而Unix等大型服务器的内存排列都是低位在高字节。
2.5 原码、反码、补码的计算
原码 | 反码 | 补码 | |
---|---|---|---|
+7 | 00000111 | 00000111 | 00000111 |
-7 | 10000111 | 11111000 | 11111001 |
+0 | 00000000 | 00000000 | 00000000 |
-0 | 10000000 | 11111111 | 00000000 |
数的取值范围 | -127-127 | -127-127 | -128-127 |
从上面的表格可以看出,正数的原码、反码和补码都相同,而负数的补码就是原码取反(最高位不变,其他位取反)后加1的结果。
而实际数据在计算机(手机、电脑、服务器)的内存中也是以补码的形式存储数据的,如下应用案例所示
首先需要计算出-7的补码,然后转换为16进制的结果为F9,然后通过Visual Studio的调试功能查看内存的存储结果,如下图所示
2.6 整数
2.6.1 整数常量
C语言整数常量可以使用八进制,十进制和十六进制表示。它们在计算时遵循逢R进1,借1当R。如下表格所示展示了它们的组成部分和应用场景。
进制类型 | 组成部分 | 应用场景 |
---|---|---|
二进制 | 0或者1 | 底层数据存储 |
八进制 | 0-7之间的8个整数 | Linux权限系统 |
十进制 | 0-9之间的10个整数 | 整数的默认进制类型 |
十六进制 | 0-9,a-f 之间的十个整数加上六个字母 | 内存地址 |
同时可以使用u后缀表示位无符号整数,使用l后缀表示long类型的整数,使用ll后缀表示为long long类型的整数,应用案例如下所示
2.6.2 整数极限
而且整数按照占据不同的字节大小可以分为short,int,long和long long 四种类型,它们默认是有符号(signed)类型用于存储正负数,而对应的无符号类型则用来存储非负数的整数,关于它们能够存储数据的极限以及占据内存的大小如下应用程序所示。
2.6.3 long long类型的整数
在应用开发时需要主要使用数据类型的极限,如果超越数据存储范围的极限,程序会出现Bug,例如想要存储QQ或者手机号就应该使用无符号的long long 类型,应用案例如下所示
2.6.4 整数的越界
在使用整数参与运算时,需要考虑到数据范围对应的极限,否则会发生错误的结果,应用案例如下所示
2.6.5 跨平台的整数
C语言是在使用标准库的前提下是可移植的,但是C语言的整数在不同的平台上,同样的数据类型占用的字节大小是不一样的。例如int在16位系统占据2个字节,在32位及其以上系统占据四个字节,long在Windows平台上,无论是32位还是64位都是占四个字节,而在64位ubuntu下却占据8个字节,应用案例如下所示
Linux版
Windows版
为了解决不同平台,相同的类型占据的大小不一致的问题,C语言标准委员会在C99标准中提出了跨平台的整数,在<stdint.h>头文件中定义,意味着同样的类型在不同的系统下的大小是一致的,应用案例如下所示
linux版
windows版
2.7 浮点数
2.7.1 浮点数常量
浮点数就是数学意义上的小数,C语言中分别使用float,double和long double表示,默认类型是double,浮点数的常量可以使用十进制的小数和科学计数法表示,科学计数法可以存储特大或者特小的数字,应用案例如下所示
2.7.2 浮点数极限
C语言在limits.h的头文件中使用常量定义了float和double的极限值,我们可以尝试使用printf函数输出该结果,分别保留 800和1500位小数。
2.7.3 赋值时自动类型转换
在进行赋值运算时会发生自动类型转换,例如把一个double类型的常量10.5赋值给float类型的变量,它们占据的字节数量不同,但是能够赋值成功,因为发生了自动类型转换,应用案例如下所示。
2.7.4 浮点数相等性判断
float占据四个字节,提供的有效位是6-7位,而double占据八个字节,提供的有效位数是15-16位,如果在使用float或者double表示实数时超过有效数字,若拿来进行关系运算(例如等于)的话,会得到一个错误的结果,应用案例如下所示
2.7.5 浮点数内存存储原理
int和float同样占据四个字节的内存,但是float所能表示的最大值比int大得多,其根本原因是浮点数在内存中是以指数的方式存储。
我们都知道在内存中,一个float类型的实数变量是占据32位,即32个二进制的0或者1组成
如上代码片段所示,从低位依次到高位叫第0位和第31位,这32位可以由三部分组成:
- 符号位:第31位数表示符号位,如果为0表示整数,如果为1表示负数
- 阶码:第23位到第30位,这8个二进制表示该实数转化为规格化的二进制实数后的指数与127(127即所谓的偏移量)之和所谓阶码,规格化的二进制实数只能在-127-127之间。
- 尾数:第0位到第22位,最多可以表示23位二进制小数,否则超过了就会产生误差。
应用案例如下所示
2.7.6 浮点数应用案例
使用math.h头文件中的sqrt函数实现给定三角形三边的面积计算
使用math.h的pow函数实现中美GDP计算,并计算出中国GDP超过美国GDP的年份
2.8 字符与字符串
2.8.1 字符
字符和字符串是日常开发中经常打交道的数据类型,使用一对单引号('')包含起来的内容就是字符,C语言提供了putchar()和printf()函数输出字符(英文),应用案例如下所示
而字符常量通常为了考虑兼容和扩展宽字符(即中文),通常会占据4个字节,英文占据一个字节,中文占据两个字节,应用案例如下所示。
如果要想输出中文字符,可以参考以下方式
字符在内存中是以数字的方式存储,而ASC||码表规定了字符对应的数字编号,当使用printf()函数以数字的输出方式打印字符时,便输出了字符对应的ASC||码表的数字编号,应用案例如下所示
字符1和整数1的区别:
字符0,'\0'和整数0的区别
字符应用:实现大写转小写
2.8.2 字符串
字符串用于表示字符序列,也就是一串使用""
包含起来的内容,接下来使用system函数调用系统命令理解下什么是字符串,应用案例如下所示
C语言中的字符串以/0
结尾,这也就是意味着即使双引号""
中什么都没有也会占据一个字节,而中文字符串中的每个字符同样会占据两个字节,应用案例如下所示
字符串加密解密的实现
使用sprintf函数实现整合字符串
通过sprintf函数实现整合字符串
2.9 布尔类型
bool类型只有两个值,即true和fasle,它们在内存中分别使用1和0表示,这样一个字节便可以存储bool类型的变量,应用案例如下所示
bool的应用场景就是用来判断条件表达式是否成立,应用案例如下所示
2.10 类型转换及其内存原理
2.10.1 printf与强制类型转换
printf()函数在输出数据时,不会进行数据类型转换,如果想要获取预期的结果,就需要进行强转实现,应用案例如下所示
2.10.2 自动类型转换
表示范围小的数据和表示范围大的数据在参与运算时,运算结果的类型会自动转换为表示范围大的类型,应用案例如下所示。
2.10.3 强制类型转换
在某些应用场景下需要使用到强制类型转换,例如银行账户的取整等等,强制类型转换的应用案例如下所示
而需要注意的是强制类型转换则会损失原有数据的精度,应用案例如下所示
但是由于强制类型转换是由CPU的寄存器完成的,强制转换后不会影响原来的变量值,应用案例如下所示
在进行强制类型转换时要考虑数据的极限问题,不然会引发数据溢出,应用案例如下所示。
2.10.4 数据类型转换的内存原理
当在进行数据类型转换时,如果该数据是有符号的,在进行数据类型转换时按照符号位数来填充,如果是无符号则按照0来填充,应用案例如下所示
2.11 应用案例
- 使用强制数据类型转换实现偷钱程序
- 小数点后三位实现四舍五入