一、文件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--
你指定的权限是
0666
,即文件所有者、同组用户和其他用户都有读写权限(rw-rw-rw-
)。系统默认的
umask
是002,如下图所示
(在大多数 Linux 系统上是022
),这会去掉其他用户的写权限。
- 默认权限:
rw-rw-rw-
(0666
)umask
:002
(去掉其他用户的写权限)计算最终权限:
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
:文件描述符是非阻塞的,且没有数据可读取。EBADF
:fd
不是一个有效的文件描述符,或者文件描述符没有打开为读模式。EFAULT
:buf
指向的内存地址是无效的。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
的定义通常是依赖于系统的实现,常见的实现方式是将其定义为long
或int
的别名,但具体取决于系统和编译器。以下是其常见的定义方式之一:
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
缓冲区中,直到遇到以下三种情况之一:
- 读取了
n-1
个字符。- 读取到换行符
\n
(换行符也会被包含在返回的字符串中)。- 遇到文件结尾(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 directoryEACCES
(13
): Permission deniedEBADF
(9
): Bad file descriptorENOMEM
(12
): Out of memoryEEXIST
(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库函数操作文件的方法都是相同的。