Linux篇:基础IO

小明 2025-04-29 12:57:14 7

一 预备知识

1. ���件=内容+属性,内容与属性都是数据,都要在磁盘中保存。

2. 文件分为打开的文件和没打开的文件。

3. 进程在访问一个文件的时候,都是要先打开这个文件。打开文件之前,文件在磁盘,打开文件之后,文件在内存。

4. 一个进程可以打开多个文件,多个进程可以打开多个文件,被打开的文件要加载都内存,打开文件实际是操作系统去执行,操作系统也要将打开的文件管理起来,如何管理呢?先描述,再组织,一个文件被加载到内存就是一个进程。进程就要有自己的PCB,操作系统用双链表将PCB管理起来,这在之前就讲过了。

二 C语言文件I/O函数

2.1 文件打开关闭函数

2.1.1文件打开函数fopen

头文件stdio.h中包含了一系列的文件操作函数,以便我们对特定的文件进行相应的操作。学会对文件进行操作后,我们就可以将代码与文件联系起来,这样能让编程变得更有意思不是吗?比如你可以用代码写一个游戏,当游戏结束后可以将游戏当前的数据保存到一个文件中,那么当你下一次运行代码时只要先读取该文件中的数据,就可以接着上一次的游戏进度继续玩,而不至于从头再来。

下面介绍了打开以及关闭文件的操作函数,以及其中的一些细节。

FILE *fopen( const char *filename, const char *mode );

该函数的功能就是打开一个文件,函数的第一个参数是你要打开的文件的文件名,第二个参数是打开这个文件的形式。

打开一个文件时,系统会为该文件创建一个文件信息区,该函数调用完毕后,如果打开该文件成功,那么返回指向该文件信息区的指针(FILE*类型);如果打开文件失败,那么返回一个空指针(NULL)。

在C语言中,FILE其实是结构体封装的,FILE 结构体的具体实现取决于不同的编译器和操作系统,因此它的内部变量可能会有所不同。一般来说,FILE 结构体可能包含以下一些常见的成员变量:

  1. _IO_FILE * _flags: 用于存储文件状态标志,以什么模式打开的此文件。

  2. _IO_FILE * _IO_read_ptr: 用于存储读取缓冲区的当前位置。

  3. _IO_FILE * _IO_read_end: 用于存储读取缓冲区的结束位置。

  4. _IO_FILE * _IO_read_base: 用于存储读取缓冲区的起始位置。

  5. _IO_FILE * _IO_write_base: 用于存储写入缓冲区的起始位置。

  6. _IO_FILE * _IO_write_ptr: 用于存储写入缓冲区的当前位置。

  7. _IO_FILE * _IO_write_end: 用于存储写入缓冲区的结束位置。

  8. fpos_t _IO_save_base: 用于存储文件位置指示器的位置。

实际的 FILE 结构体内部成员变量可能远不止以上列出的内容,而且在不同的系统和编译器下可能会有所不同。

FILE封装变量大多数都是对缓冲区操作那这个缓冲区是什么?为什么?

Linux篇小知识点:进程被创建时,操作系统到底为它做了什么工作?

 使用上面的模式说明符,文件将作为文本文件打开。为了将文件作为二进制文件打开,模式字符串中必须包含“b”字符。这个附加的“b”字符可以附加在字符串的末尾(从而形成以下复合模式:“rb”、“wb”、“ab”、“r+b”、“w+b”、“a+b”),也可以插入在“+”符号之前(“rb+”、“wb+”、“ab+”)。

如果序列后面有其他字符,则行为取决于库实现:一些实现可能会忽略其他字符,例如,接受额外的“t”(有时用于显式表示文本文件)。

在某些库实现中,使用更新模式打开或创建文本文件可能会视为二进制文件。

举个几个例子:

1.若我们要以文本形式打开一个名叫data.txt的文件,将要对其进行输入操作,那么打开文件时应该这样写:

FILE* pf = fopen("data.txt", "r");

注:data.txt文件必须存在,不然打开文件失败,fopen函数会返回一个空指针。

2.若我们要以二进制打开一个名叫data.bin的文件,将要对其进行输出操作,那么打开文件时应该这样写:

FILE* pf = fopen("data.bin", "wb");

注:data.bin文件若存在,将销毁文件原有内容,再对其进行输出;data.bin文件若不存在,系统将主动创建一个名叫data.bin的文件。

前面说到,如果文件打开成功,fopen函数会返回指向文件信息区的指针,否则fopen函数会返回一个空指针。所以当使用接收fopen函数的返回值的指针前,我们必须检测其有效性,否则可能非法访问内存。

检测有效性:

	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;//失败返回
	}
	//使用...

2.1.2 文件关闭函数fclose

当我们打开文件时,会在内存中开辟一块空间,文件被打开就是进程了,不知道看这个链接内容。如果我们打开该文件后不关闭,那么这个空间会一直存在,一直占用那块内存空间,所以当我们对一个文件的操作结束时,一定要记住将该文件关闭。这就需要用到fclose函数来关闭文件。

int fclose( FILE *stream );

我们如果要关闭一个文件,那么直接将该文件的文件指针传入fclose函数即可,fclose函数如果关闭文件成功会返回0。与free函数一样,当我们调用完fclose函数将文件关闭后,我们也要将指向文件信息区的指针置空,避免该指针变成野指针。

	fclose(pf);//关闭pf指向的文件
	pf = NULL;//及时置空

2.2 文件读写函数

2.2.1 字符输入输出函数 - fgetc和fputc

2.2.2.1 fputc函数
int fputc( int c, FILE *stream );

fputc函数的第一个参数是待输出的字符, 参数是要写入文件的字符,通常是一个 int 类型的字符值。第二个参数该字符输出的位置,即fputc函数的功能是将一个字符输出到指定的位置。该函数调用完毕会返回用户传入的字符。

例子:

#include 
int main() {
    FILE *file;
    file = fopen("output.txt", "w"); // 以写入模式打开文件
    if (file) {
        char text[] = "Hello, world!";
        int i;
        for (i = 0; text[i] != '
2.2.2.2 fgetc函数
'; i++) { fputc(text[i], file); } fclose(file); // 关闭文件 } else { printf("无法打开文件\n"); } return 0; }
int fgetc( FILE *stream );

2.2.2 文本行输入输出函数 - fgets和fputs

fgetc函数只有一个参数,即你要读取的文件的文件指针。fgets函数的功能就是从指定位置读取一个字符。该函数调用成功会返回读取到的的字符;若读取文件时发生错误,或是已经读取到文件末尾,则返回EOF。

为什么返回值是int?

fgetc() 函数会从指定文件流中读取下一个字符,成功读取下一个字符时,fgetc() 将返回读取的字符作为 unsigned char 转换为的 int 类型值。如果到达文件末尾或者发生错误,fgetc() 将返回 EOF,这通常被定义为 -

2.2.2.1 fputs函数
int fputs( const char *string, FILE *stream );
2.2.2.2 fgets函数

puts函数的第一个参数是待输出的字符串,第二个参数该字符串输出的位置,即fputs函数的功能是将一个字符串输出到指定的位置(有没有发现fputs函数的参数设计和fputc函数参数的设计非常类似)。该函数调用成功会返回一个非负值;若输出时发生错误,则返回EOF。

char *fgets( char *string, int n, FILE *stream );

2.2.3 格式化输入输出函数 - fscanf和fprintf

fgets函数的第三个参数是你要读取的文件的文件指针,第二个参数是你要读取的字符个数(也可以说是字节个数),第一个参数是你所读取到的数据的储存位置。fgets函数的功能就是从指定位置读取指定字符个数的数据储存到指定位置。该函数调用成功会返回用于储存数据的位置的地址,如果读取过程中发生错误,或是读取到了文件末尾,则返回一个空指针(NULL)。

fgets函数读取字符的过程中会出现两种情况:

在fgets函数读取到指定字符数之前,若读取到换行符(’\n’),则停止读取,读取带回的字符包含换行符。

直到fgets函数读取到第n-1个字符时都没有遇到换行符(’\n’),则返回读取到的n-1个字符,并在末尾加上一个空字符一同返回(共n个字符)。

2.2.3.1 fprintf函数
int fprintf( FILE *stream, const char *format [, argument ]...);
  • stream 是一个指向已打开文件的指针,指定了要写入数据的文件流。
    • format 是一个格式化字符串,包含了要写入的内容以及格式说明符,类似于 printf() 函数中的格式化字符串。
    • 2.2.3.2 fscanf函数
    • ... 是可变数量的参数,用于填充到 format 字符串中的格式说明符中。

      fprintf() 函数根据格式化字符串 format 的内容,将格式化后的数据写入到指定的文件流中。如果成功写入数据,则返回写入的字节数;如果发生错误,则返回一个负数。

      可变参数可能有人不太懂,插入可变参数列表的讲解

      int fscanf( FILE *stream, const char *format [, argument ]... );
      
    • stream 是一个指向已打开文件的指针,指定了要从中读取数据的文件流。
      • format 是一个格式化字符串,包含了要读取的数据的格式以及格式说明符,类似于 scanf() 函数中的格式化字符串。
      • 2.2.4 二进制输入输出函数 - fread和fwrite

      • ... 是可变数量的参数,用于保存从文件中读取的数据。

        fscanf() 函数根据格式化字符串 format 中的格式说明符,从指定的文件流中读取数据并存储到相应的变量中。如果成功读取数据,则返回成功匹配和赋值的参数个数;如果读取到文件末尾或发生错误,则返回EOF。

        2.2.4.1 fwrite函数
        size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
        
        size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
        

        fwrite函数的第一个参数是输出数据的位置,第二个参数是要输出数据的元素个数,第三个参数是每个元素的大小,第四个参数是数据输出的目标位置。该函数调用完后,会返回实际写入目标位置的元素个数,当输出时发生错误或是待输出数据元素个数小于要求输出的元素个数时,会返回一个小于count的数。

        fwrite函数的功能就是将buffer位置的,每个元素大小为size的,count个元素,以二进制的形式输出到stream位置。

         2.2.4.2 fread函数

        三 系统文件IO 

        fread函数的第一个参数是接收数据的位置,第二个参数是要读取的每个元素的大小,第三个参数是要读取的元素个数,第四个参数是读取数据的位置。函数调用完会返回实际读取的元素个数,若在读取过程中发生错误或是在未读取到指定元素个数时读取到文件末尾,则返回一个小于count的数。

        概括一下,fread函数的功能就是从stream位置,以二进制的形式读取count个每个元素大小为size的数据,到buffer位置。

        3.1 默认打开的三个流

        extern FILE *stdin;
        extern FILE *stdout;
        extern FILE *stderr;
        

        都说Linux下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么显示器和键盘当然也可以看作是文件。我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据。

        为什么我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作?

        标准输入流(stdin)和标准输出流(stdout)分别代表键盘和显示器。这些文件流在程序开始执行时已经自动打开,并且无需显式打开或关闭它们。这是因为它们是标准的输入和输出设备,在程序执行期间一直可用。

        stdin、stdout以及stderr这三个家伙实际上都是FILE*类型的。

        #include 
        int main()
        {
        	fputs("hello stdin\n", stdout);
        	fputs("hello stdout\n", stdout);
        	fputs("hello stderr\n", stdout);
        	return 0;
        }
        

        stdin、stdout以及stderr与我们打开某一文件时获取到的文件指针是同一个概念,试想我们使用fputs函数时,将其第二个参数设置为stdout,此时fputs函数会不会之间将数据显示到显示器上。

        注意: 不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。

        3.2 系统调用接口

        操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。

        相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。

        我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。

        3.2.1 open

        int open(const char *pathname, int flags, mode_t mode);
        
        O_WRONLY | O_CREAT
        

         open的第一个参数:open函数的第一个参数是pathname,表示要打开或创建的目标文件。

         open的第二个参数:open函数的第二个参数是flags,表示打开文件的方式。

        打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。

        例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:

        #include 
        #include 
        #include 
        #include 
        int main()
        {
        	umask(0);
        	int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
        	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
        	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
        	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
        	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
        	printf("fd1:%d\n", fd1);
        	printf("fd2:%d\n", fd2);
        	printf("fd3:%d\n", fd3);
        	printf("fd4:%d\n", fd4);
        	printf("fd5:%d\n", fd5);
        	return 0;
        }
        

        open的第三个参数

        open函数的第三个参数是mode,表示创建文件的默认权限。八进制。

        创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0 若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

        注意: 当不需要创建文件时,open的第三个参数可以不必设置。

        open的返回值:open函数的返回值是新打开文件的文件描述符。文件描述符不懂的看这里。

        #include                                         
        #include 
        #include 
        #include 
        int main()
        {
            int fd = open("test.txt", O_RDONLY);
            printf("%d\n", fd);
            return 0;
        }
        

        运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的。

        我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败:

        3.2.2 close

         打开文件失败时获取到的文件描述符是-

        实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。

        当使用open函数打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的。

        而Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。这里有详细讲解。

        int close(int fd);
        

        系统接口中使用close函数关闭文件,close函数的函数原型如下:

        3.2.3 write

        使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-

        ssize_t write(int fd, const void *buf, size_t count);
        

        系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

      • 如果数据写入成功,实际写入数据的字节个数被返回。
      • 我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

        • 如果数据写入失败,-1被返回。
          #include 
          #include 
          #include 
          #include 
          #include 
          #include 
          int main()
          {
          	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
          	if (fd  
          

          运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容。

           3.2.4 read

          ssize_t read(int fd, void *buf, size_t count);
          
        • 如果数据读取成功,实际读取数据的字节个数被返回。
        • 我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

            #include 
            #include 
            #include 
            #include 
            #include 
            #include 
            int main()
            {
            	int fd = open("log.txt", O_RDONLY);
                if (fd 
          • 如果数据读取失败,-1被返回。
    The End
    微信