Linux系统编程 --- 【2、3】文件IO与标准IO

news2024/11/16 6:45:42

一、文件IO

1.1 文件描述符 

1.1.1 学习前的疑问?

1. 什么是文件描述符?
2. 文件描述符的作用是什么?
3. 文件描述符是怎样进行使用的?

1.1.2 文件描述符是什么以及作用是什么? 

文件描述符(File Descriptor,简称 FD)是 Linux 和其他类 Unix 操作系统中一种抽象概念,用于表示一个打开的文件、管道、网络套接字或其他输入输出资源。文件描述符是非负整数,系统通过文件描述符来访问这些资源。

1.1.3 文件描述符是怎么进行使用的? 

      文件描述符的使用可以概括为以下几个方面:
1)打开文件:在 Linux 中,可以使用 open()系统调用打开一个文件,该系统调用返回一个文件描述符,该文件描述符唯一地标识该文件,同时也可以用于访问和操作该文件。
2)读写文件:一旦文件已经打开,可以使用 read()和 write()等系统调用读取和写入文件的内容。这些系统调用需要一个文件描述符作为参数,以指定要读取或写入的文件。
3)关闭文件:在不再需要使用文件时,应该使用 close()系统调用来关闭文件描述符,样可以释放文件描述符所占用的系统资源。

      在 Linux 系统中,系统预定义了三个标准的文件描述符:stdin(标准输入 0)、stdout(标准输出 1)和 stderr(标准错误输出 2)。一般情况下,标准输入 0 对应终端的输入设备(通常是用户的键盘),标准输出 1 和标准错误 2 对应终端输出设备(通常指的是 LCD 显示器)。

标准文件描述符: Linux 系统默认为每个进程分配了三个标准文件描述符:

  • 标准输入(stdin): 文件描述符为 0,用于接收输入数据,通常是键盘输入。
  • 标准输出(stdout): 文件描述符为 1,用于输出数据,通常是显示器或终端。
  • 标准错误(stderr): 文件描述符为 2,用于输出错误信息,通常也是显示器或终端。

1.2 文件IO之打开(open())、关闭(close())、读(read())、写(write())等操作 

1.2.1 打开文件【open系统调用】 

1.2.1.1 基础知识介绍 

      在 Linux 系统中,open()是一个非常重要的系统调用,其作用是打开一个文件,并返回该文件对应的文件描述符(File Descriptor)。关于文件描述符已经在1.1小节进行了介绍,通过文件描述符,可以实现读取、写入和修改文件等操作。 open()函数所使用的头文件和函数原型,如下所示:

      open()函数执行成功之后会返回 int 型文件描述符,出错时返回-1,并设置 error 值(关 于 error 值在之后的小节会进行讲解)。 open()函数三个参数含义如下所示:


      常用的 flags 标志位参数分为主参数和副参数,主参数有三个分别为 O_RDONLY、 O_WRONLY、O_RDWR,三个主参数在使用的时候只能使用一个(即为互斥关系),具体用途如下图所示:

      副参数有很多,可以使用“|”符号与主参数进行同时使用(副参数没有互斥关系,可以多个同时使用),副参数的具体用途如下图所示:

      mode: 权限掩码,对不同用户和组设置可执行、读、写权限,使用八进制数表示,此参数可不写。只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)mode 的参数类型为 mode_t,是一个 32 位的无符号整形数据, 使用低 12 位每三位一组来进行权限的表示。
      1-3 位表示 O---用于表示其他用户的权限;
      4-6 位表示 G---用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有 用户;
      7-9 位表示 U---用于表示文件所属用户的权限,即文件或目录的所属者;
      10-12 位表示 S---用于表示文件的特殊权限,一般情况下用不到(设置为 0)。
      对应的权限表格如下所示:


mode参数取值及对应关系列举如下:

举例:

int fd = open("test", O_RDWR | O_CREAT, 0666); 这行代码中,0666 是用于指定文件的权限,表示当使用 open() 函数创建文件时,应赋予该文件的访问权限。

权限的组成

0666 是一个八进制数,其中每个数字代表文件的不同用户类别的权限。这些权限分为三类:

  • 第一位:文件所有者的权限(user, u
  • 第二位:同组用户的权限(group, g
  • 第三位:其他用户的权限(others, o

每一位的权限可以由以下权限标志组合而成:

  • 4:读权限(r,read)
  • 2:写权限(w,write)
  • 1:执行权限(x,execute)

这些数字可以相加来组合权限。例如:

  • 7 表示 读、写、执行 权限(4 + 2 + 1 = 7
  • 6 表示 读和写 权限(4 + 2 = 6
  • 5 表示 读和执行 权限(4 + 1 = 5

0666 的含义

0666 可以拆解为:

  • 文件所有者的权限6(读和写,即 rw-
  • 同组用户的权限6(读和写,即 rw-
  • 其他用户的权限6(读和写,即 rw-

所以,0666 赋予的权限是:

  • 文件所有者可以 读和写
  • 同组用户可以 读和写
  • 其他用户可以 读和写

权限示例

如果 open() 创建了 test 文件,那么文件的权限将会是:rw-rw-rw-

这意味着所有用户(文件所有者、同组用户以及其他用户)都可以读取和写入文件,但没有执行权限。

总结

0666 权限意味着文件的所有者、同组用户和其他用户都拥有读和写权限,但没有执行权限。

1.2.1.2 实验  

      目标:通过系统调用 open()函数,创建一个可读可写名称为 test 的文件,并打印其文件描述符。
实验步骤:
      首先进入到 ubuntu 的终端界面输入命令 vim 01_open_learning.c 来创建 01_open_learning.c 文件,如下图所示:
 
然后向该文件中添加以下代码:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>

int main(){
    int fd = open("test",O_RDWR | O_CREAT,0666);
    if(fd<0){// 对open函数返回的文件描述符进行判断
        perror("open file errir \n");
        return -1;
    }
    printf("fd = %d\n",fd);
    return 0;
}

 

  在上述代码中,我们使用 open()函数创建了一个名为 test 的文件,并指定了 O_RDWR 和 O_CREAT 标志,表示文件可读可写并且在文件不存在时创建它。此外,我们还使用了 0666 权限位来设置文件的读、写和执行权限,这意味着所有用户都可以读取和写入该文件。 保存退出之后,使用以下命令对 01_open_learning.c 进行编译,编译完成如下图所示: 

可以看到当前文件夹中并没有 test 文件,然后使用命令“./01_open_learning”来运行,运行成功如下图所示:

      可以看到程序运行成功之后,打印的文件描述符为 3,在 1.1 小节中也讲解过在一个进程中至少包含三个文件描述符,0 表示标准输入 stdin,1 表示标准输出 stdout,2 表示标准错误 stderr,所以分配的文件描述符一般都是从 3 开始,然后使用“ls”命令查看当前文件夹的文件信息,可以看到 test 文件已经被创建成功了。 至此,关于 open()函数的实验就完成了。
我们使用 ls -l 来查看当前目录下的文件信息:

      在上图中,我们看到test文件的权限是(rw-rw-r--),这表示对于其他用户是没有写的权限的,这与我们在上面引用块部分分析所得结果不一致,引用块部分分析时test文件应具有的权限为:rw-rw-rw-,对于其他用户是有写的权限的,那经过 ls -l 查询后为什么对其他用户没有写权限呢?莫非是被ls -l 吃了?
这就不得不提到 umask 这个概念了,如下块引用部分 所示:

当使用 open("test", O_RDWR | O_CREAT, 0666); 创建文件时,虽然指定了 0666 作为权限(即 rw-rw-rw-),但是文件的最终权限可能受到了用户掩码(umask) 的影响。

什么是 umask

  umask 是一个系统设置,用于限制新创建文件的默认权限。它通过屏蔽(去掉)一些权限位来控制文件和目录的最终权限。umask 值会被从指定的权限中减去,导致最终的文件权限与预期不同。

umask 的工作原理

umask 是一个四位的八进制数,它与文件的默认权限通过按位操作来计算最终权限。umask 中的每一位代表相应用户类别的权限:

  • 第一位:文件所有者权限(u
  • 第二位:同组用户权限(g
  • 第三位:其他用户权限(o

通常,umask 会从新文件的默认权限中去掉相应的位。常见的 umask 值有:

  • 022:去掉“写入”权限,通常的默认值。
  • 002:去掉“其他用户”的写权限。
  • 077:去掉组和其他用户的所有权限,只有所有者拥有读写权限。

解释为什么看到的权限是 rw-rw-r--

  1. 你指定的权限是 0666,即文件所有者、同组用户和其他用户都有读写权限(rw-rw-rw-)。

  2. 系统默认的 umask002,如下图所示(在大多数 Linux 系统上是 022),这会去掉其他用户的写权限

    • 默认权限:rw-rw-rw- (0666)
    • umask002 (去掉其他用户的写权限)
  3. 计算最终权限:

    • 0666(默认权限)减去 002(umask)后,得到 0664,即:
      • 文件所有者:读写权限(rw-
      • 同组用户:读写权限(rw-
      • 其他用户:权限(r--

因此,当你运行 ls -l 时,看到的文件权限是 rw-rw-r--,表示文件所有者和同组用户有读写权限,而其他用户只有读权限。

如何查看和修改 umask

  • 查看当前 umask:在终端中输入 umask,可以查看当前系统的 umask 值。

    umask

    典型输出为 0022,表示去掉组和其他用户的写权限。

  • 修改 umask:可以通过在 shell 中使用 umask 命令来临时更改 umask,例如:

    umask 000

    这将不去掉任何权限,使得新创建的文件具有完全的 0666 权限。

总结

当你看到 test 文件的权限是 rw-rw-r-- 时,这是因为系统的 umask 值(002)去掉了其他用户的写权限。因此,最终权限是 0664,即文件所有者和同组用户有读写权限,其他用户只有读权限。


 

1.2.2 关闭一个打开的文件描述符【close系统调用】 

1.2.2.1 基础知识介绍

      在上一小节中学习了 open()函数用来打开文件,那有没有系统调用用来关闭文件呢,答案是肯定的,将在本小节讲解关闭文件的系统调用。

学习前的疑问:
1. 关闭文件要使用哪个系统调用 API?
2. close()函数要怎样进行使用?
      上面的描述其实并不准确,本小节要讲解的 close()函数的功能是关闭一个打开的文件描述 符,并不是关闭文件。在程序打开一个文件之后,操作系统会为该文件分配一个文件描述符,通过该文件描述符我们可以对文件进行读写等操作。当不再需要访问该文件时,应该及时关闭文件描述符以释放系统资源。 close()函数所使用的头文件和函数原型,如下所示:

      close()函数执行成功时会返回 0,出错时返回-1,并设置 error 值(关于 error的值会在之后的小节进行讲解)。
      close()函数只有一个参数,含义如下:

1.2.2.2 实验

      目标:创建并打开一个可读可写“test”,打印文件描述符后关闭文件描述符。
实验步骤:
首先进入到 ubuntu 的终端界面输入以下命令vim demo03_close.c 来创建 demo03_close.c 文件,如下图所示:

然后向该文件中添加以下内容:

保存退出之后,使用以下命令对 demo03_close.c 进行编译,编译完成如下图所示:

然后使用命令“./demo03_close”来运行,运行成功如下图所示:

可以看到实验现象和上一小节相同,其中 close()函数的作用只是关闭已经打开的文件描述 符,释放系统资源。 至此关于 close 函数的实验就完成了。

1.2.3 读文件 【read系统调用】

1.2.3.1 基础知识介绍

      read()函数用于对文件或者设备进行读取,函数功能较为单一,所使用的头文件和函数原 型,如下所示:

返回值:

函数的返回值表示从文件描述符 fd 中成功读取的字节数,或者在出现错误时返回错误码。

具体来说,返回值可以是以下几种情况:

1. 成功读取的字节数

  • 如果读取成功,返回值为成功读取的字节数。这个值可以是 0 到 count 之间的任意值。
  • 如果返回值为 0,表示已经到达文件的末尾(EOF,End Of File)。对于非阻塞设备,例如管道或终端,返回 0 也可能表示暂时没有数据可读取。

2. 错误

  • 如果发生错误,read() 返回 -1,并设置 errno 以指示错误的具体类型。常见的错误包括:
    • EAGAIN:文件描述符是非阻塞的,且没有数据可读取。
    • EBADFfd 不是一个有效的文件描述符,或者文件描述符没有打开为读模式。
    • EFAULTbuf 指向的内存地址是无效的。
    • EINTR:读取操作被信号中断,需要重试。
    • EINVAL:无效的参数,例如 fd 无效,或 count 值太大。

3. 到达文件末尾

  • 当返回值为 0 时,表示已经读取到了文件的末尾,这时不会有更多的数据可读取。

read()函数三个参数含义如下所示:

1.2.3.2 实验

首先进入到 ubuntu 的终端界面输入以下命令来创建 demo04_read.c 文件,如下图所示: 

然后输入以下代码:

ssize_t 是 C 和 C++ 中的一个数据类型,用于表示系统调用中涉及的字节数或错误码。它的定义通常在 <unistd.h> 头文件中。ssize_t 是一种有符号整数类型,用于处理可能的负值,以表示错误。

特点

  • 有符号ssize_t 是有符号的整数类型,因此可以表示正数和负数。这允许它用于返回读取或写入的字节数(正值),也可以用于表示错误(负值)。
  • 适应性ssize_t 的大小通常与系统架构的 size_t 类型大小相匹配,即在 32 位系统上通常为 32 位,在 64 位系统上通常为 64 位。

定义

ssize_t 的定义通常是依赖于系统的实现,常见的实现方式是将其定义为 longint 的别名,但具体取决于系统和编译器。以下是其常见的定义方式之一:

typedef __ssize_t ssize_t;

在 GNU C 库中,__ssize_t 通常定义为 long 类型,而在其他系统中可能有所不同。

使用场景

  • 系统调用:许多系统调用函数(如 read(), write(), pread(), pwrite())返回 ssize_t 类型的值。对于这些函数,返回正值表示操作成功,返回 -1 表示出错(错误信息由 errno 变量给出)。
  • 错误处理ssize_t 能够表示负值,用于指示错误情况,因此它非常适合处理系统调用可能遇到的各种错误。

 保存退出之后,使用以下命令对 demo04_read.c 进行编译,编译完成如下图所示:
gcc demo04_read.c -o demo04_read

然后直接使用命令“./demo04_read”来运行,运行成功如下图所示:

由于 test 文件是我们刚刚创建的,文件内容为空,所以 buf 为空,ret 为 0,接下来对 test 文件进行编辑,添加完成如下图所示:


保存退出之后,然后再使用命令“./demo04_read”来运行,运行成功如下图所示,成功读取到 test 文件中的 hello world!,返回的字节数为 13。

C 字符串在内存中存储时,实际存储的字节数是字符串的长度加上一个额外的字节来表示字符串的结束标志(空字符 '\0')。所以,"hello world!" 在内存中占用的空间是 12 个字符 + 1 个空字符 = 13 字节。
      接下来,修改test文件的内容,使字节数超过32 ,下面我们有四个 hello world! ,因此共有12×4+1=49个字节,那么我们来看程序会得到什么结果呢?

执行程序,得到:

1.2.4 写文件 【write系统调用】

1.2.4.1 基础知识介绍

      write()函数用于对文件或者设备进行数据写入,函数功能较为单一,所使用的头文件和函 数原型,如下所示:

返回值:
      大于或等于 0 表示执行成功,返回写入的字节数,返回-1 代表出错,并设置 error 值(关 于 error 值在之后的小节会进行讲解)。
write()函数三个参数含义如下所示:

1.2.4.2 实验

实验1: 将“hello”打印到输出设备上(即将写入的文件描述符设置为 1)
      首先进入到 ubuntu 的终端界面输入以下命令来创建 demo05_write_01.c 文件,如下图所示: vim demo05_write_01.c

      上述内容的 8 行调用了 write()函数,设备描述符参数被设置为了 1 ,1 代表标准输出(在这里为显示屏终端),第二个参数为要写入的字符串,而最后一个参数表示写入数据字节数为 6。 保存退出之后,使用命令 gcc demo05_write_01.c -o demo05_write_01 对 demo05_write_01.c 进行编译,编译完成如下图所示:

然后使用命令“./demo05_write_01”来运行,运行成功如下图所示:

可以看到“hello”就被输出到了终端界面。
实验2:创建一个可读可写的文件名称为“test”的文件,并将“hello”字符串内容写入到 test 文件中。
实验步骤: 首先进入到 ubuntu 的终端界面输入以下命令来创建 demo05_write_02.c

添加以下代码:


使用指令 gcc demo05_write_02.c -o demo05_write_02 对 demo05_write_02.c 进行编译,编译完成如下图所示:

然后使用命令“./demo05_write_02”来运行,运行成功如下图所示:

程序运行成功之后,test 文件被创建,然后使用 cat 命令对文件内容进行查看,可以看到 “hello”已经被写入了。

至此,关于 write()函数的实验就结束了。

1.3 综合练习

通过命令行操作,将a.c文件里面的内容写到b.c里面。

a.c文件中的内容如下所示:

b.c文件没有被创建。
思路:
      使用read函数读取a.c文件中的内容,然后存放在缓冲区中,然后使用write函数将缓冲区的内容写入到b.c文件中。代码如下所示:

打开当前目录,只有两个文件,如下图所示: 

 
编译上述ab.c文件,得到ab可执行程序:

执行该可执行程序,如下所示:


查看b.c中的内容:

发现a.c的内容被写入到b.c中。

1.4 lseek

1.4.1 基础知识介绍 

  lseek 是一个在 Unix 和 Linux 系统中用于文件操作的系统调用。它的作用是将文件读写指针移动到文件的指定位置,便于在文件的不同部分进行读写操作。通过使用 lseek,你可以在文件中以随机访问的方式读写数据,而不需要顺序地从文件开始进行读取或写入。 

      文件读写指针是一个指针变量,它用于标识文件读写时的当前位置。在进行文件读写操作时,文件读写指针会随着读写操作的进行而移动。使用 open()函数打开一个文件时读写指针默认在文件头,那如果想要读取文件尾的数据要怎样做呢,这时候就轮到 lseek 函数出场了(当然 lseek 的功能不仅仅有读取文件尾的数据)。

lseek() 用于设置文件指针位置。所使用的头文件和函数原型,如下所示:

      lseek()函数执行成功会返回当前位移大小,失败返回-1,并设置 error 值(关于 error 值在之后的小节会进行讲解)。
      lseek()函数三个参数含义如下所示:

案例:
 把文件位置指针设置为 5             lseek(fd,5,SEEK_SET);
 把文件位置设置成文件末尾         lseek(fd,0,SEEK_END); 对应读书时:你将一本书翻到最后一页,然后你向前翻了0页,那么你最终停留的位置是这本书的最后一页。
 确定当前的文件位置                    lseek(fd,0,SEEK_CUR);
至此关于 lseek()函数的相关讲解就完成了,下面进行相应的实验。 

1.4.2 实验 

对比使用 lseek 函数使用前后,read()函数返回值的变化。 

实验步骤:
首先进入到 ubuntu 的终端界面输入命令 vim test 来创建 test 文件,如下图所示:

然后向 test 文件中写入以下内容,写入完成如下图所示:
 
保存退出,输入命令 vim demo06_lseek.c 来创建 demo06_lseek.c 文件,并给文件添加如下代码,如下图所示:


保存退出之后,使用命令 gcc demo06_lseek.c -o demo06_lseek 对 demo06_lseek.c 进行编译,编译完成如下图所示:

然后使用命令“./demo06_lseek”来运行,运行成功如下图所示:

      可以看到程序运行成功之后,会将 test 文件中的内容打印出来,且第一次返回的字符数量为 25,在使用 lseek 进行字符偏移 5 之后,就只会打印 test 文件中 hello 之后的部分,返回的字符数量变为了 20。

结果分析:

  • 第一次读取:读取到了整个文件内容 "hello world!hello world~",并成功读取 25 个字节,ret 返回 25。
  • 第二次读取:由于文件指针移动到偏移量 5,因此从第6个字节开始读取内容,即 " world!hello world~",但由于 buf 中之前的内容没有被完全清除,导致 buf 中还有部分旧内容,因此显示为 "world!hello world~rld~"ret 返回 20。

若我们在第二次读取之前将buf中的内容清空,会得到什么结果呢?
修改代码如下:
为了避免 buf 中的旧内容影响输出结果,可以在每次 read() 之前清空 buf,例如通过 memset() 来初始化 buf

注意还要引入该函数所需的头文件,如下所示:

然后重新编译程序,得到运行结果:

我们发现这个时候就可以正常输出了!!
      至此关于 lseek 函数的实验就完成了。

二、标准IO

      标准 IO(Standard I/O)是一种抽象层,用于在程序和底层操作系统 I/O 接口之间提供一 个标准化的、可移植的 I/O 接口。标准 IO 提供了对文件、终端、套接字等不同类型的 I/O 设备的统一访问接口。
      标准 IO 主要包括以下三个文件流:
stdin:标准输入流,通常关联着键盘输入。
stdout:标准输出流,通常关联着控制台显示器。
stderr:标准错误流,通常关联着控制台显示器,用于输出错误信息。
      标准 IO 提供了一组函数来读写这三个文件流,包括 fopen()、fclose()、fread()、fwrite()、 fseek()等。
标准 IO 的主要优点是:
1. 与底层的系统调用相比,标准 IO 函数更加容易使用和掌握,可以大大降低编程的难度。
2. 标准 IO 函数可以自动进行缓冲,从而提高 IO 效率。缓冲可以是全缓冲、行缓冲或无缓冲,可以通过 setvbuf()函数进行设置。
3. 标准 IO 函数是可移植的,可以在不同的操作系统上使用相同的代码进行编译和运行。
标准 IO 也有一些缺点:
1. 标准 IO 函数的效率相对较低,因为需要进行多次函数调用和缓冲区的复制。
2. 标准 IO 函数有时不能提供足够的控制力,比如无法直接控制文件描述符或进行底层的操作。
下面就跟随我一起进入标准 IO 的学习吧。

2.1 FILE指针

      FILE 指针是 C 语言中用来处理文件的重要概念,它指向文件中的某个位置,可以用来进行文件的读取和写入操作。在 C 语言中,所有的文件 I/O 操作都通过 FILE 结构体来实现,FILE 指针则是指向这个结构体的指针。
      使用 FILE 指针进行文件操作的基本流程如下:
1. 打开文件:使用 fopen 函数打开文件,并返回一个 FILE 指针,该指针指向打开的文件。
2. 对文件进行读写操作:可以使用 fscanf、fprintf 等函数对文件进行读写操作。
3. 关闭文件:使用 fclose 函数关闭文件,释放资源,并将 FILE 指针设置为 NULL。
会在接下来的几个小节中对打开文件、关闭文件、文件的读写等相关 C 语言库函数进行讲解。

FILE指针详解

      在 C 语言中,`FILE` 指针是标准库定义的一个指向文件流的指针,通常用于处理文件操作。它是 `FILE` 类型的指针,`FILE` 类型是标准输入输出库(stdio.h)中的一个结构体,包含了文件的相关信息(如文件描述符、缓冲区、文件位置指针等)。通过 `FILE` 指针,程序可以进行文件的打开、读写、关闭等操作。

 `FILE` 指针的定义

FILE *fp;

这里的 `fp` 是一个 `FILE` 类型的指针,表示它指向一个打开的文件。

常见的 `FILE` 指针操作函数

1. `fopen()`:打开文件,并返回一个 `FILE *` 类型的指针。

   FILE *fp = fopen("file.txt", "r");
   if (fp == NULL) {
       perror("Error opening file");
       return -1;
   }

   - `"r"` 表示以只读模式打开文件。
   - `"w"` 表示以写模式打开文件,文件不存在时创建文件,存在时清空文件内容。
   - `"a"` 表示以追加模式打开文件,写入数据时不会清空文件内容。

2. `fclose()`:关闭文件,并释放 `FILE` 指针相关的资源。

   fclose(fp);

3. `fread()`:从文件中读取数据。

   char buffer[100];
   size_t bytesRead = fread(buffer, sizeof(char), 100, fp);

- 从 `fp` 指向的文件中读取最多 100 个字节的数据到 `buffer` 中。

4. `fwrite()`:将数据写入文件。

 char data[] = "Hello, World!";
   fwrite(data, sizeof(char), strlen(data), fp);

5. `fscanf()` 和 `fprintf()`:分别用于从文件中格式化读取数据和将格式化数据写入文件。
 

int num;
   fscanf(fp, "%d", &num);  // 从文件读取一个整数
   fprintf(fp, "%d", num);  // 将整数写入文件

6. `fseek()` 和 `ftell()`:用于在文件中定位和获取文件指针的位置。
 

 fseek(fp, 0, SEEK_END);  // 将文件指针移动到文件末尾
   long size = ftell(fp);   // 获取文件大小(字节数)

7. `feof()`:检查文件是否到达末尾(EOF)。

   if (feof(fp)) {
       printf("End of file reached.\n");
   }

8. `fflush()`:刷新文件缓冲区,确保缓冲区中的数据被实际写入文件。

   fflush(fp);

9. fget() :用于从文件或标准输入中读取一行字符,直到遇到换行符、文件结尾(EOF),或达到指定的字符数为止。

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

参数说明

  • str:指向存储读取内容的字符数组(缓冲区)的指针。
  • n:要读取的最大字符数。读取的字符数最多为 n-1 个字符,最后一个位置会存储空字符 '\0',以便将读取的内容作为字符串处理。
  • stream:文件指针,指向一个打开的文件或输入流(如 stdin)。

返回值

  • 成功时:返回 str,即读取的字符串缓冲区指针。
  • 失败时:返回 NULL,表示遇到文件结尾(EOF)或发生错误。

工作机制

  • fgets() 会读取输入或文件中的字符,并将其存储在 str 缓冲区中,直到遇到以下三种情况之一:
    1. 读取了 n-1 个字符。
    2. 读取到换行符 \n(换行符也会被包含在返回的字符串中)。
    3. 遇到文件结尾(EOF)。

综合示例:文件读取和写入

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[100];

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return -1;
    }

    // 读取文件内容
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    // 关闭文件
    fclose(fp);
    return 0;
}

总结
- `FILE` 指针用于引用文件流,C 语言通过它提供高效的文件读写操作。
- 使用 `fopen()` 函数打开文件,操作完成后应使用 `fclose()` 关闭文件。
- `FILE` 指针可以结合其他标准库函数执行文件的读取、写入、定位等操作。

2.2 打开文件函数 fopen()

2.2.1 基础知识介绍 

fopen()是 C 库函数中用来打开文件的函数,所使用的头文件和函数原型,如下所示:

fopen 的返回值是 FILE 类型的文件流,当它的值不为 NULL 时表示正常,后续的 fread、 fwrite 等函数可通过文件流访问对应的文件。 fopen()函数两个参数含义如下所示:

常用的 mode 参数如下:

2.2.2 实验

通过 C 语言库函数 fopen(),创建一个可读可写名称为 test 的文件。
      首先进入到 ubuntu 的终端界面输入命令 vim demo08_fopen.c 来创建 demo08_fopen.c 文件,如下图所示:

然后在该文件中添加如下代码:


上述内容中,第 8 行调用了标准 IO 库函数 fopen(),第一个参数为要打开或者创建的文件名称,第二个参数为 w+,代表如果文件不存在就创建,存在或者创建成功之后以可读可写的方式打开文件。 保存退出之后,使用命令gcc demo08_fopen.c -o demo08_fopen 对 demo08_fopen.c 进行编译,编译完成如下图所示:

然后使用命令“./demo08_fopen”来运行,运行成功如下图所示:

可以看到程序运行成功之后,test 文件就被创建成功了,然后再使用“ls -l”命令查看文件属性,如下图所示:

可以看到 test 文件的属性为可读可写,至此关于 fopen 函数的实验就完成了。

2.3 关闭文件 fclose()

2.3.1 基础知识介绍

fclose()函数用于关闭已经打开的文件指针,所使用的头文件和函数原型,如下所示: 


      fclose 函数返回 0 表示成功关闭文件,返回 EOF 表示关闭失败。fclose 函数会将所有的缓冲 区中的数据写入文件中,关闭文件并释放相应的资源。如果在写入数据时发生错误,fclose 函数会返回 EOF 并设置相应的错误标志。 fclose()函数参数含义如下所示:

2.3.2 实验

      创建一个可读可写名为 test 的文件,使用 fopen 函数打开文件之后使用 fclose()函数对打开的文件流进行关闭。

      首先进入到 ubuntu 的终端界面输入命令 vim demo09_fclose.c 来创建 demo09_fclose.c 文件,如下图所示:

添入以下代码:


编译并运行:

2.4读文件 fread()

2.4.1 基础知识介绍

fread()函数从文件中读取数据,所使用的头文件和函数原型,如下所示:

      调用成功时返回读取到的数据项数目(数据项数目并不等于实际读取的字节数,除非参数 size 等于 1);如果发生错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb,那么到底发生了错误还是到达了文件末尾,fread()并不能对此进行区分,具体是哪一种情况,此时可以使用 ferror()或 feof()函数来判断(这两个函数会在 2.7 小节进行讲解)。
      fread()函数参数含义如下所示:

2.4.2 实验部分

读取 test 文件中的内容,并打印出来。

      首先进入到 ubuntu 的终端界面输入命令 vim demo10_fread.c 来创建 demo10_fread.c 文件,并添入以下代码,如下图所示:       

编译并运行,如下所示:

然后使用命令“vim test”创建 test 文件,并添加以下内容:

然后使用命令“./demo10_fread”来运行,运行成功如下图所示:

  可以看到已经将 test 文件中的“hello world!”数据就被打印了出来,数据读取完成之后,文件流被关闭了。

关于fread()的返回值的分析

  fread() 返回的是读取的项(元素)数,而不是字符数或字节数。这里的“项”是指你想读取的“元素”,而每个元素的大小由 fread() 的第二个参数 size 指定。

是什么意思呢?我们且看如下实验:

1. size=1,全英文字符

对代码进行修改,如下:

test 文件中的内容为 hello world! ,fread()函数的返回值为13,如下图所示:


      返回13是因为 hello world!有十二个字符,占用十二个字节,再加上空字符"\0"占用一个字节,所以共13个字节,又由于size参数取值为sizeof(char)=1,因此num为13。

2. size =1,含有中文字符

将test文件中的内容修改为如下:


fread()函数的返回值变为了20,如下图所示:

同理,num = 12(hello world!) + 6(你好【UTF-8编码格式下】)+1(!) +("\0") = 20。

3. size=2,含中文字符(总字节数与size构成整除关系)

在上面的基础上,我们将size取值设置为2,看看num会返回什么呢?

编译后执行程序,得到结果如下:

我们发现num变为了10。这也即是说,返回的“项”数为10,这个项数计算方法就是总共的字节数除以size的尺寸得来的。

4. 总字节数与size不构成整除关系

上例给出的总字节数与size的尺寸大小构成整除的关系,那要是不整除呢?又会出现什么情况呢?

test文件内容不变,size设置为3,如下图所示:

test文件内容:


编译程序并执行,得到:

返回的num值为6。

2.5 写文件 fwrite()

2.5.1  基础知识介绍

fwrite()函数从文件中读取数据,所使用的头文件和函数原型,如下所示:

      调用成功时返回写入的数据项的数目(数据项数目并不等于实际写入的字节数,除非参数 size 等于 1);如果发生错误,则 fwrite()返回的值将小于参数 nmemb(或者等于 0)。由此可知,库函数 fread()、fwrite()中指定读取或写入数据大小的方式与系统调用 read()、write() 不同,前者通过 nmemb(数据项个数)*size(每个数据项的大小)的方式来指定数据大小, 而后者则直接通过一个 size 参数指定数据大小。
      fwrite()函数参数含义如下所示:

2.5.2 实验

目标:创建一个可以读写名为 test 的文件,并写入字符“hello world”。


然后添加如下代码:

保存退出之后,使用命令 gcc demo11_fwrite.c -o demo11_fwrite 对 demo11_fwrite.c 进行编译,编译完成如下图所示:

然后使用命令“./demo11_fwrite”来运行,运行成功如下图所示:

观察程序运行后项目文件夹的情况:

2.6 fseek

2.6.1 基础知识介绍

      和1.4小节lseek系统调用的作用相同,在 C 语言库函数中使用 fseek 函数设置文件指针的位置,在本小节将对 fseek 函数进行讲解。

      fseek()函数用于设置文件读写位置偏移量,所使用的头文件和函数原型,如下所示:

fseek 函数返回 0 表示设置成功,返回非 0 值表示设置失败。
      fseek()函数参数含义如下所示:

2.6.2 实验

目标:测试 fseek 函数移动文件读写位置的功能。
      首先进入到 ubuntu 的终端界面输入命令 vim demo12_fseek.c 来创建 demo12_fseek.c 文件,并添入以下代码,如下图所示:


保存退出之后,使用命令 gcc demo12_fseek.c -o demo12_fseek 对 demo12_fseek.c 进行编译,编译完成如下图所示: 

然后使用命令“vim test”创建 test 文件,并添加以下内容:


然后使用命令“./demo12_fseek”来运行,运行成功如下图所示: 

      可以看到程序运行成功之后,只会打印第五个字节之后的字符“ world!”,证明文件读写指针发生了变化,至此关于 fseek() 函数相关的实验就完成了。

2.7 perror

2.7.1 基础知识介绍

      经过了前面许多个实验,大家会发现每个实验代码中都会有 perror 函数,用来进行错误信息的输出。perror 是一个标准 C 库函数,其作用是将当前 errno 的值作为参数,输出对应的错误信息到标准错误输出(stderr)上。perror 函数可以帮助我们快速定位程序运行时出现的错误,便于进行调试和排错。
      perror 函数的函数原型如下:

      其中,参数 s 是一个字符串,表示我们希望在输出错误信息前输出的一段文本,通常是一些提示信息或者函数名。如果 s 为 NULL,则只输出错误信息而不添加前缀。

参数

  • s: 这是一个用户提供的字符串,通常是错误发生的上下文描述信息。perror 会先输出这个字符串,然后跟随一个冒号和空格,再输出与 errno 相关的错误消息。
  • errno: perror 根据全局变量 errno 的值来决定打印哪种错误消息。errno 是一个由系统调用或库函数设置的全局错误代码,用来标识错误类型。

工作原理

  • 当一个系统调用或库函数发生错误时,它通常会返回一个特殊值(如 -1 表示失败)。与此同时,操作系统会将错误码保存在 errno 中。
  • perror 函数会根据 errno 的值,打印出错误的文本描述。

常见错误代码与错误消息

perror 输出的错误信息是根据 errno 的值映射来的。以下是一些常见的 errno 值及其对应的错误消息:

  • ENOENT (2): No such file or directory
  • EACCES (13): Permission denied
  • EBADF (9): Bad file descriptor
  • ENOMEM (12): Out of memory
  • EEXIST (17): File exists

这些错误信息在不同的系统中可能会有所不同,但通常保持一致。

使用场景

  • 调试: 在系统调用失败后立即使用 perror 输出错误信息,以便快速了解问题的原因。
  • 错误处理: 可以通过 perror 直接将详细的错误信息输出给用户或记录日志。

2.8 检查和复位状态

      在调用 fread()进行文件数据读取时,如果返回值小于参数 nmemb 所指定的值,表示发生了错误或者已经到了文件末尾(文件结束 end-of-file),但 fread()无法具体确定是哪一种情况,在本小节将学习两个函数对该问题进行解决。

2.8.1 feof()函数

2.8.1.1 基础知识介绍

      feof()函数是 C 标准库中的一个函数,用于判断文件指针所指向的文件是否已经到达文件结尾。所使用的头文件和函数原型,如下所示:

      如果 end-of-file 标志被设置了,则调用 feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。当文件的读写位置移动到了文件末尾时,end-of-file 标志将会被设置。
feof()函数参数含义如下所示:

2.8.1.2 实验

      使用 feof()函数,分别测试读写位置移动到文件末尾和没有移动到文件末尾两种情况。

首先进入到 ubuntu 的终端界面输入命令 vim demo13_feof.c 来创建 demo13_feof.c 文件,并添入如下代码,如下图所示:

      保存退出之后,使用命令gcc demo13_feof.c -o demo13_feof 对 demo13_feof.c 进行编译,编译完成如下图所示:

然后使用命令“vim test”创建 test 文件,并添加以下内容: hello world!
然后使用命令“./demo13_feof”来运行,运行成功如下图所示:

      可以看到程序运行成功之后,会打印“Moved to end of file”证明读写指针已经移动到了文件的末尾,如果我们将第 6 行的 buffer 值进行修改,修改为 10,再次运行对应的可执行文件之后,打印信息如下图所示:


      可以看到上图的打印信息为“Not moved to end of file or error occurred”,证明读写指针并没有到达文件末尾,至此 feof()函数就测试成功了。

2.8.2 ferror()函数

2.8.2.1 基础知识

ferror()函数是 C 标准库中的一个函数,如果错误标志被设置了,则 ferror()函数会被调用, 用于检查文件流的错误状态。所使用的头文件和函数原型,如下所示:

如果错误标志被设置了则返回 1,否则返回 0。 ferror()函数参数含义如下所示: 

2.8.2.2 实验


      保存退出之后,使用命令 gcc demo14_ferror.c -o demo14_ferror 对 demo14_ferror.c 进行编译,编译完成如下图所示:

然后使用命令“vim test”创建 test 文件,并添加以下内容: hello world!
然后使用命令“./demo14_ferror”来运行,运行成功如下图所示:

可以看到程序运行成功之后,会打印“No error in reading”证明读写没有错误,至此我们的 ferror()函数就测试成功了。

2.8.3 clearerr()函数

  clearerr() 是 C 标准库中的一个函数,用于重置指定文件流的错误和结束标志。它不会影响文件指针的当前位置或数据内容,但会清除由先前的读写操作导致的错误标志或到达文件末尾的标志。     

      当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。 clearerr()函数所使用的头文件和函数原型,如下所示:

此函数类型为 void,没有返回值,所以函数调用总是会成功。 clearerr()函数参数含义如下所示:

参数:

  • stream:要清除错误标志和文件结束标志的文件流指针(FILE*)。

作用:

  • 清除由函数 ferror() 设置的错误标志。
  • 清除由函数 feof() 设置的文件结束标志。

      当文件操作(如 fread()fwrite() 等)发生错误或遇到文件结束时,ferror()feof() 函数将分别返回非零值。通过 clearerr(),可以重置这些标志,以便再次进行文件操作。

三、文件IO与标准IO的区别

文件 I/O(Input/Output) 是指在程序中通过函数或系统调用与文件进行交互的操作,主要涉及读取(Input)写入(Output)数据到文件的过程。通过文件 I/O,程序可以存储数据、读取外部数据、持久化操作结果等。

标准 I/O(Standard Input/Output,标准输入输出)是指程序在运行时与外部设备(如键盘、显示器、文件)进行数据输入和输出的接口。标准 I/O 是通过系统提供的标准输入标准输出、和标准错误输出实现的,通常对应于终端或控制台上的输入输出操作。

1.什么是文件IO?
文件lO就是直接调用内核提供的系统调用函数。
2.什么是标准IO?
标准IO就是间接调用系统调用函数,是C库函数。
 

文件IO和标准lO的区别?
文件IO是直接调用内核提供的系统调用函数,头文件是unistd.h,标准IO是间接调用系统调用函数,头文件是stdio.h,文件lO是依赖于Linux操作系统的,标准IO是不依赖操作系统的,所以在任何的操作系统下,使用标准IO,也就是C库函数操作文件的方法都是相同的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2110393.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深圳市专精特新预计9月初申报!申报成功后,有哪些好处?

广东省专精特新中小企业已开放申报&#xff0c;9月30日申报截止&#xff01;深圳市专精特新中小企业预计9月初开放&#xff08;请以官方通知为准&#xff0c;推荐使用深科信项目申报通知订阅功能~&#xff09; 我们整理专精特新中小企业认定有哪些好处&#xff1f;认定流程如何…

视频监控管理平台LntonAIServer视频智能分析噪声检测应用场景

在视频监控系统中&#xff0c;噪声问题常常影响到视频画面的清晰度和可用性。噪声可能由多种因素引起&#xff0c;包括但不限于低光环境、摄像机传感器灵敏度过高、编码压缩失真等。LntonAIServer通过引入噪声检测功能&#xff0c;旨在帮助用户及时发现并解决视频流中的噪声问题…

推荐一个小巧的截屏软件,无需安装,双击使用,功能强大

今天分享一款强大、小巧的截屏软件——ScreenCapture&#xff0c;一共不到4M。 ScreenCapture无需安装&#xff0c;在使用的时候双击执行即可&#xff0c;有时候存放在U盘中&#xff0c;直接双击启动截屏即可。 同时在截屏的时候&#xff0c;会显示截屏区域的大小。 支持跨屏幕…

【神经网络系列(中级)】小数据学习中的“特征提取+推理”模型【通俗理解】

【通俗理解】小数据学习中的“特征提取推理”模型 关键词提炼 #小数据学习 #特征提取 #推理模型 #机器学习 #数据效率 第一节&#xff1a;小数据学习与“特征提取推理”模型概述 1.1 小数据学习的挑战 在小数据场景下&#xff0c;模型训练面临数据稀缺的挑战&#xff0c;难…

C,C++ 宏定义## 的使用

宏定义## 的使用 C语言中宏定义## 的使用 宏定义## 的使用 #define LV_color(x,y,z) x##y##z 文章目录 宏定义## 的使用宏定义## 的使用 宏定义## 的使用 #include<stdio.h> #include<stdlib.h>#define LV_color(x,y,z) x##y##z int main() {printf("LV_col…

.net core接口项目中的国产神器SqlSugar

SQLSugar 是一款基于 .NET Core 平台的 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;其轻量级、易用性和强大的功能&#xff0c;使其在 .NET 社区中备受欢迎。结合其在接口项目中的应用&#xff0c;以下几点可以证明 SQLSugar 的学习和使用具有必要性和技术上的先…

70%的人都答错了的面试题,vue3的ref是如何实现响应式的?

本文将通过debug的方式带你搞清楚当ref接收的是对象和原始类型时&#xff0c;分别是如何实现响应式的。注&#xff1a;本文中使用的vue版本为3.4.19。 看个demo 还是老套路&#xff0c;我们来搞个demo&#xff0c;index.vue文件代码如下&#xff1a; <template><div&g…

顶升搬运小车与激光叉车在卡板物流及仓储效率提升中的应用

一、项目概述 本项目旨在实现卡板的自动化运输与仓储管理&#xff0c;通过引入顶升搬运小车&#xff08;AGV&#xff09;与激光叉车两种自动化设备&#xff0c;提高运输效率&#xff0c;减少人工干预&#xff0c;并确保流程的顺畅与安全。 二、产品特性与需求分析 考虑到产品…

fastadmin 清除插件缓存报错

Argument 1 passed to Symfony\Component\VarExporter\VarExporter::export() must be an instance of Symfony\Component\VarExporter\mixed, array given, called in F:\work\awebsite\oeob\vendor\karsonzhang\fastadmin-addons\src\addons\Service.php on line 404 我用的…

在DevEco Studio中安装通义灵码

下载插件离线包 离线压缩包 打开设置 打开插件 安装 5.安装后找到右侧图标&#xff0c;登录阿里账号。

Qt TabWidget添加多个窗口,实现分页窗体布局

Qt TabWidget添加多个窗口窗体&#xff0c;可关闭与打开 点击按钮可判断是否打开&#xff0c;避免重复打开 使用Qt中的TabWidget组件创建一个简单的分页窗体布局。点击按钮时&#xff0c;会新增一个窗体并添加到TabWidget中。每个子窗体能动态获取父窗体指针以进行操作 分别…

SpringBoot2:请求处理原理分析-接口参数解析原理

一、知识回顾 我们知道&#xff0c;接口的参数&#xff0c;一般都要配上注解来一起使用。 不同的参数注解&#xff0c;决定了传参的方式不同。 为什么会这样&#xff1f; 如果让你设计接口参数解析&#xff0c;你会怎么做&#xff1f; 本篇就来探究springboot底层是如何通过参…

又发现一个国内超好用的 AI 开放平台!

首先&#xff0c;我认为一个好的人工智能开放平台应该具备以下关键特性&#xff1a;提供多种AI模型和工具&#xff0c;涵盖自然语言处理、计算机视觉、语音识别、数据分析等多个领域。 满足不同应用领域的需求&#xff1b;需具备强大的计算资源&#xff0c;以支持大规模模型的…

K线图新玩法:利用Pin Bar精准捕捉市场反转

对于交易者来说&#xff0c;K线图是必备工具之一&#xff0c;所谓K线图就是由一系列的蜡烛形状组成&#xff0c;每个蜡烛代表一定时间周期内的价格变动情况。而有一种交易策略就是依据蜡烛形态预测市场趋势情况&#xff0c;这种交易策略被叫做Pin Bar交易法。 Pin Bar是什么&am…

高效智能 | 客户运营与知识库管理系统的融合策略

在当今快速变化的商业环境中&#xff0c;企业不仅要关注产品的创新和市场的拓展&#xff0c;更需要深耕客户运营&#xff0c;以提升客户满意度和忠诚度。而知识库管理系统作为支撑客户运营的重要工具&#xff0c;其与企业客户运营策略的深度融合&#xff0c;正成为企业提升核心…

畅捷通如何远程访问

畅捷通如何远程访问 越来越多的企业选择了畅捷通ERP来提升管理效率与资源整合能力。然而&#xff0c;随之而来的远程访问问题却成为了不少用户的困扰。作为一名畅捷通ERP的使用者&#xff0c;我深刻体会到&#xff0c;如何高效、便捷地进行远程访问是实现企业数字化管理的关键。…

软件测试之UI自动化测试

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、about自动化测试 定义&#xff1a;把人为驱动的测试转化为机器执行的一种过程&#xff0c;重点在于持续集成这个概念&#xff1b; 优势&#xff1a;节约人力和…

DDoS对策是什么?详细解说DDoS攻击难以防御的理由和对策方法

攻击规模逐年增加的DDoS攻击。据相关调查介绍&#xff0c;2023年最大的攻击甚至达到了700Gbps。 为了抑制DDoS攻击的危害&#xff0c;采取适当的对策是很重要的。 特别是在网站显示花费时间或频繁出现504错误的情况下&#xff0c;可能已经受到了DDoS攻击&#xff0c;需要尽早采…

代理IP中的API接口:解锁高效与自动化的网络访问新方式

“在当今数字化时代&#xff0c;网络数据的获取与分析已成为各行各业不可或缺的一部分。随着网络环境的日益复杂和网站反爬机制的升级&#xff0c;直接使用原始IP地址进行数据采集或网络访问变得越来越困难。此时&#xff0c;代理IP作为一种有效的解决方案&#xff0c;成为了众…

网络编程 0905作业

作业 1、流式域套接字敲一遍。 服务器 server.c 代码 #include <myhead.h> #define BACKLOG 10int main(int argc, const char *argv[]) {//1、创建流式域套接字int oldfd socket(AF_UNIX,SOCK_STREAM,0);if(oldfd -1){perror("socket");return -1;}//2、…