目录
文本文件和二进制文件
一个数据在内存中是怎么存储的呢?
答:字符一律用ASCII形式存储,数值型数据既可以用ASCII,也可以使用二进制。
例如,如有整数10000,如果以ASCII码的形式存储,则磁盘中占用5个字节,假如以二进位制的形式存储,因为一个整型占四个字节,所以在磁盘上只占4个字节。
这里的话,ASCII码形式存储的明显多了一个字节,是不是对于所有的整数,ASCII码形式的存储占用的空间都更多呢?
答:不,例如1,我们用ASCII码形式的存储,只占一个字节,我们用二进位制的形式,却需要占四个字节。
有人会说,以ASCII的形式和以二进位制的形式在内存中的存储形式是什么呢?
答:我们画图进行解析:整型10000
其中,我们以二进位制的形式的存储的形式和这个是相同的。
那么,以ASCII码的形式呢?
因为我们的10000有五个字符,一个字符占一个字节,1对应的ASCII值为49,对应的二进位制序列就是00110001,0对应的二进位制序列就是00110000。
测试代码:
#include<stdio.h>
int main()
{
int a = 10000;
FILE*pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
我们对代码进行解析:创建一个变量a=10000,以二进位制的形式写入test.txt的文件,假如没有,我们对该文件进行创建,返回文件指针,用pf来接受。
调用fwrite函数
函数的意义是:把一块数据以二进位制的形式写进流中(流是内存和文件之间的媒介)
第一个参数:这一块数据的地址。
第二个参数:这一块数据所占空间的大小。
第三个参数:我们要写入几个这样的块。
第四个参数:指向文件的指针pf
这里表示我们把整型a以二进位制的形式写进文件test.txt中。
我们操作文件结束后需要关闭文件,fclose实现关闭文件,fclose类似与free,关闭之后需要把文件指针置为NULL。
我们打开计算机中的文件test.txt
这个信息我们看不懂
能不能把他转化为我们能看懂的文本数据呢?
答:可以,我们在vs中以二进制编译器的形式打开文件。
打印的结果是10 27 00 00,这是为什么呢?
答:这是10000在内存中对应的二进位制的形式
对应的16进位制为00 00 27 10
又因为我们是小端字节序存储,小端字节序是低位放在低地址,所以对应的结果为
10 27 00 00
文件读取结束的判定
被错误理解的feof
牢记:不能用feof的返回值来判断文件是否读取结束。
应用的场景是:文件已经结束,用来判断文件是因为读取错误而结束的,而是遇到文件尾结束的。
feof是用来判断遇到文件尾是否结束的标志。
假如feof的返回值为真,则表示遇到文件尾结束。
1:文本文件读取结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
fgetc判断是否为EOF
fgets判断是否为NULL
总结:当文件结束时,我们可以进行判断:
int main()
{
if (feof(fp))
{
printf("遇到文件尾结束");
}
else if (ferror(fp))
{
printf("遇到错误结束");
}
fclose(fp);
}
当feof(fp)为真时,遇到文件尾而结束。
而当ferror(fp)为真时,则表示我们是因为读取错误结束的。
那么二进位制的读取结束的判定呢?
二进位制的读取结束的判断,判断返回值是否小于要读的个数。
例如:fread判断返回值是否小于要读的个数。
fread的返回值是一个无符号整型。
返回值是成功被读取的元素的个数。
我们对fread的判定进行解析
假如我们要读的文件中的内容是1234567.
我们的参数size_t count设置为6时,我们读取的就是123456,返回值就是6
假如我们的参数size_t count设置为9的时候,我们读取的结果就是1234567,返回值是7,小于我们的参数9,所以 二进位制读取结束。
文件缓冲区:
我们举一个例子:
#include<stdio.h>
int main()
{
int a = 10000;
FILE*pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
这里的代码表示我们将整型a以二进位制的形式写到文件”test.txt",其中的细节在哪里呢?我们进行画图展示:
我们的步骤首先是把内存中我们要输出的数据存储到输出缓冲区,直到输出缓冲区被填满后,再输出到文件中去。
假如我们要进行输入的话,我们首先把为文件存储到输入缓冲区,直到输入缓冲区被填满后,再输入到程序数据区。
这样的作用是什么?
答:能够提升我们的效率。
我们可以用代码证明缓冲区的存在。
#include<stdio.h>
#include<windows.h>
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);
printf("睡眠10秒,打开文件\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);
printf("再睡眠10秒,打开文件\n");
Sleep(10000);
fclose(pf);
pf = NULL;
return 0;
}
我们进行解析:我们首先以写的形式打开文件"test.txt",把指针传递给pf。fputs表示把“abcdef”传递到pf所指向的文件中去。然后我们sleep,休息10s,在这10秒间,我们pf所指向的文件,可以发现,文件中什么都没有
接下来,我们刷新缓冲区,引入fflush函数:
fflush能够把输出缓冲区的数据刷新到文件中。
我们调用该函数,刷新过后,我们再休眠10s,在这10s之间,我们再次打开文件,可以发现文件中有abcdef
我们再休眠10s,因为我们的fclose函数也是能够有刷新缓冲区的作用,这个Sleep能够区分fclose函数和fflush函数。
经证明,的确存在有缓冲区
得出一个结论:因为有缓冲区的存在,c语言在操作文件的时候,需要刷新缓冲区或者关闭文件
如果不做,会导致数据的丢失问题。
程序环境和预处理
在标准c中,存在两个不同的环境:
1:翻译环境:源代码被转换为可执行的机器指令。
2:执行环境:用于执行代码。
每一个源文件都会经编译器生成自己对应的目标文件。
我们写一串简单的代码
我们打开对应的test.c的文件的地址
可以发现一个test.obj,这就是我们所说的目标文件。
假如我们再添加一个源文件
我们再进行编译
再次打开对应的文件地址
我们可以发现,这一次产生了两个obj文件,也就是目标文件。
证明:每一个源文件都会经编译器形成对应的目标文件。
在linux系统上,目标文件的后缀是.o
我们的多个目标文件和链接库一起经过编译器处理形成了对应的可执行程序,图如下
编译和链接的具体的过程
两个阶段的分析的话:
源文件经过编译和链接形成对应的exe文件,也就是可执行程序。
四个阶段的分析的话:
源文件经过预处理(预编译),编译,汇编,链接形成对应的可执行程序。
接下来,我们实验代码在预处理,编译,汇编,链接都会发生什么?
预处理阶段要进行1:头文件的包含
也就是#include的指令
2:#define定义符号的替换。
并删除该定义的符号。
3:注释的删除
编译阶段:1:把c语言代码转化成汇编代码(语法分析,词法分析,符号汇总,语义分析)
我们单独说一下符号汇总:汇总全局的变量名和函数名
汇编:把汇编代码转换成对应的二进位制指令。
形成符号表
链接:合并段表
符号表的合并和重定义
解析:我们经过链接形成了两个目标文件,两个目标文件的格式都是elf格式,所以,对应文件的elf格式,我们可以进行合并,相同项目合并在一起,画图
这就是对应的段表,1和1‘,2和2’,3和3‘中的文件都是相同的,所以可以进行合并
统一合成到一个文件里,与链接库一起形成对应的可执行程序。
我们对符号表的合并和重定位进行解析:
我们在编译阶段会进行符号汇总,我们的函数中,汇总的符号分别是add.c中的Add,test.c中的Add和main
我们在汇编阶段会形成符号表
add.c中的Add,test.c中的Add,test.c中的main都会单独拥有一个地址
假想的地址0x11 0x00 0x10
因为我们的test.c中的Add只是声明,并不是真正的Add函数,所以我们给的地址也不是正常的地址。
我们将地址与符号一一对应形成符号表。
在链接阶段我们进行符号表的合并和重定位,我们将对应的符号表合并起来,合并之前,我们要先进行重定位,对那些不合格的符号除去。
运行环境
预处理:
我们进行尝试打印文件名
#include<stdio.h>
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("file:%s\n %d\n", __FILE__, i);
}
return 0;
}
进行编译:
形成文件名。
#define定义标识符和宏
#define定义标识符不要加分号,因为在替换时,分号也会加上。