1. C标准IO库函数
1.1 打开/关闭文件--fopen
• Makefile
# 解释
#有时编译器不只是 gcc,我们将编译器定义为变量 CC,当切换编译器时只需要更改该变量的定义,而无须更改整个 Makefile。
# $@相当于当前 target 目标文件的名称,此处为 fopen_test。
# $^相当于当前 target 所有依赖文件列表,此处为 fopen_test.c
# ./$@的作用是执行目标文件
# rm ./$@的作用是在执行完毕后删除目标文件,如果没有这个操作,当源文件
#fopen_test.c 未更改时就无法重复执行,会提示:make:"fopen_test"已是最新。
#此处删除目标文件,使得我们在不更改源文件的情况下可以多次执行。
# 所有命令前都添加了"-"符号以忽略错误,确保即便上面的命令执行失败,仍然
#会向下执行。这样做是为了在发生错误时,确保删除目标文件,使得再次执行相同 target
#时不会提示:make:"fopen_test"已是最新,可以重新执行 target 下的命令。
CC := gcc
fopen_test: fopen_test.c
-$(CC) $^ -o $@
-./$@
-rm ./$@
• 例子
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
/* code */
/* 打开文件
char *__restrict __filename: 字符串表示要打开文件的路径和名称
char *__restrict __modes: 字符串表示访问模式
(1)"r": 只读模式 没有文件打开失败
(2)"w": 只写模式 存在文件写入会清空文件,不存在文件则创建新文件
(3)"a": 只追加写模式 不会覆盖原有内容 新内容写到末尾,如果文件不存在
则创建
(4)"r+": 读写模式 文件必须存在 写入是从头一个一个覆盖
(5)"w+": 读写模式 可读取,写入同样会清空文件内容,不存在则创建新文件
(6)"a+": 读写追加模式 可读取,写入从文件末尾开始,如果文件不存在则创建
return: FILE * 结构体指针 表示指向一个文件
FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
FILE * fp = fopen("a.txt","w");
if(fp == NULL){
printf("打开失败\n");
}else{
printf("打开成功\n");
}
return 0;
1.2 fclose:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("a.txt","w");
if(fp == NULL){
printf("打开失败\n");
}else{
printf("打开成功\n");
}
/*
FILE *__stream: 需要关闭的文件
return: 成功返回 0 失败返回 EOF(负数) 通常关闭文件失败会直接报错
int fclose (FILE *__stream)
*/
int result = fclose(fp);
if(result == EOF)
printf("关闭失败,%d\n",EOF);
else if(result == 0)
printf("关闭成功\n");
return 0;
}
1.3 向文件写入数据的函数
1.3.1 fputc:一次只能写入一个字符。例子:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("a.txt","a+");
if(fp == NULL){
printf("打开失败\n");
}else{
printf("打开成功\n");
}
/*写入文件一个字符
int __c: 写入的 char 按照 AICII 值写入 可提前声明一个 char
FILE *__stream: 要写入的文件,写在哪里取决于访问权限,在fopen参数那里
return: 成功返回 char 的值 失败返回 EOF
int fputc (int __c, FILE *__stream)
*/
int put_result = fputc('A',fp);
if(put_result == EOF){
printf("写入失败,%d\n",EOF);
}else {
printf("写入%d成功\n",put_result);
}
int result = fclose(fp);
if(result == EOF)
printf("关闭失败,%d\n",EOF);
else if(result == 0)
printf("关闭成功\n");
return 0;
}
1.3.2 fputs:一次写入一个字符串,例子:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("a.txt","a+");
if(fp == NULL){
printf("打开失败\n");
}else{
printf("打开成功\n");
}
int putc_result = fputc('A',fp);
if(putc_result == EOF){
printf("写入失败,%d\n",EOF);
}else {
printf("写入%d成功\n",putc_result);
}
/*写入文件一个字符串
char *__restrict __s: 需要写入的字符串
FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问权限
return: 成功返回非负整数(一般是 0,1) 失败返回 EOF
int fputs (const char *__restrict __s, FILE *__restrict __stream)
*/
int puts_result = fputs(" love B\n",fp);
if(puts_result == EOF){
printf("写入失败,%d\n",EOF);
}else {
printf("写入%d成功\n",puts_result);
}
int result = fclose(fp);
if(result == EOF)
printf("关闭失败,%d\n",EOF);
else if(result == 0)
printf("关闭成功\n");
return 0;
}
1.3.3 fprintf:一次可以写很长的字符串,通过格式化输出的方式,输出一个长字符串,直接写入到文件,例子:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("a.txt","a+");
if(fp == NULL){
printf("打开失败\n");
}else{
printf("打开成功\n");
}
int putc_result = fputc('A',fp);
if(putc_result == EOF){
printf("写入失败,%d\n",EOF);
}else {
printf("写入%d成功\n",putc_result);
}
int puts_result = fputs(" love B\n",fp);
if(puts_result == EOF){
printf("写入失败,%d\n",EOF);
}else {
printf("写入%d成功\n",puts_result);
}
/*
FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式
char *__restrict __fmt: 格式化字符串
...: 变长参数列表
return: 成功返回正整数(写入字符总数不包含换行符) 失败返回 EOF
fprintf (FILE *__restrict __stream, const char *__restrict
__fmt, ...)
*/
int s_len = fprintf(fp,"床前明月光,\n疑是地上霜。\n举头望明月,\n低头思故乡 \n\t\t%s\n","李白");
if(s_len == EOF){
printf("写入失败\n");
}else {
printf("写入成功\n");
}
int result = fclose(fp);
if(result == EOF)
printf("关闭失败,%d\n",EOF);
else if(result == 0)
printf("关闭成功\n");
return 0;
}
1.4 从文件中读取数据的函数
1.4.1 fgetc:一次读一个字符(字节),例子:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("a.txt","r");
if(fp == NULL)
printf("文件打开失败\n");
/*
FILE *__stream: 需要读取数据的那个文件
return: 读取的一个字节 到文件结尾或出错返回 EOF
int fgetc (FILE *__stream)
*/
//假如要读取的是中文,在UTF-8中普通的中文一般占3个字节,一些生僻字可能占4个
//那么如果用fgetc读取,一次只能读取一个字节,就是说读不完整。需要连续读取(中间不能有换行)
//char c = fgetc(fp);
//printf("%c",c);
char c = fgetc(fp);
while (c != EOF)
{
printf("%c",c);
c = fgetc(fp);
}
int result = fclose(fp);
if(result == EOF)
printf("关闭文件失败\n");
return 0;
}
1.4.2 fgets:一次读取一行字符串,并且读取一行的数据长度是有限制的,例子:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("a.txt","r");
if(fp == NULL)
printf("文件打开失败\n");
/*
char *__restrict __s: 接收读取的数据字符串
int __n: 能够接收数据的长度
FILE *__restrict __stream: 需要读取的文件
return: 成功返回字符串 失败返回 NULL(可以直接用于 while)
fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
*/
char buff[128];
//char * readBuf = fgets(buff,sizeof(buff),fp);
while (fgets(buff,sizeof(buff),fp) != NULL)
{
//printf("读取出来的数据是:%s\n",buff);
printf("%s",buff);
}
//printf("读取出来的数据是:%s\n",readBuf);
//printf("读取出来的数据是:%s\n",buff);
int result = fclose(fp);
if(result == EOF)
printf("关闭文件失败\n");
return 0;
}
1.4.3 fscanf:按指定格式从文件读取数据,例子:
cpp
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("user.txt","r");
if(fp == NULL)
printf("文件打开失败\n");
/*
FILE *__restrict __stream: 读取的文件
char *__restrict __format: 读取的字符串
...: 变长参数列表 用于接收匹配的数据,
return: 成功返回参数的个数 失败 返回 0报错 或结束返回 EOF
int fscanf (FILE *__restrict __stream, const char *__restrict
__format, ...)
*/
char name[128];
int age;
char wife[128];
// int count = fscanf(fp,"%s %d %s",name,&age,wife);//出现空行会自动跳过
// if(count != EOF){
// printf("成功配对的个数是%d\n",count);
// printf("%s %d %s\n",name,age,wife);
// }
while (fscanf(fp,"%s %d %s",name,&age,wife) != EOF)
{
printf("%s %d %s\n",name,age,wife);
}
int result = fclose(fp);
if(result == EOF)
printf("关闭文件失败\n");
return 0;
}
1.5 标准输入,输出,错误
cpp
#include<stdio.h>
#include<stdlib.h>
int main(){
//从标准输入里面读取数据
char * buf = malloc(10);
//stdin: 标准输入 FILE *
fgets(buf,10,stdin);
printf("从标准输入读到的数据是%s\n",buf);
//从标准输出 里面 输出数据
//stdout: 标准输出 FILE * 这个文件流会将数据输出到控制台
//printf 底层就是使用的这个
fputs("baiziyan zhen shuai!",stdout);
printf("\n");
//错误输出
//错误输出 FILE * 一般输出到 错误日志
fputs("baiziyan zhen shuai!",stderr);
return 0;
}
1.6 例子
cpp
配置文件的修改
如:
SPEED=5
LENG=100
SCORE=90
LEVEL=95
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
FILE * fp = fopen("config.txt","r");
FILE * fp1 = fopen("config1.txt","w");
char readbuf[100];
char res[100];
int i = 0;
while (fgets(readbuf,sizeof(readbuf),fp) != NULL){
if(strstr(readbuf,argv[1]) != NULL){
printf("读取到的数据为:%s\n",readbuf);
char * rres = strtok(readbuf,"=");//分割字符串,获取键名(如"LEVEL")
sprintf(res,"%s=%s\n",rres,argv[2]);//拼接新字符串
printf("拼接之后的数据为%s\n",res);
fputs(res,fp1);
}else{
fputs(readbuf,fp1);//一行字符串,原样写入
}
}
fclose(fp);
fclose(fp1);
system("mv config1.txt config.txt");//覆盖
return 0;
}
//./xxx.exe LEVEL 50
2. 系统调用
• 系统调用 是 操作系统内核 提供 给应用程序,使其可以间接访问硬件资源的接口。
2.1 open:系统调用用于打开一个标准的文件描述符。
cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
/*
const char *__path: 文件路径
int __oflag: 用于指定打开文件的方式,可以是以下选项的组合:
(1) O_RDONLY: 以只读方式打开文件
(2) O_WRONLY: 以只写方式打开文件
(3) O_RDWR: 以读写方式打开文件
(4) O_CREAT: 如果文件不存在,则创建一个新文件
(5) O_APPEND: 将所有写入操作追加到文件的末尾
(6) O_TRUNC: 如果文件存在并且以写入模式打开,则截断文件长度为 0
还有其他标志,如 O_EXCL(当与 O_CREAT 一起使用时,只有当文件不存在时才创建新文件)
可选参数: mode -> 仅在使用了 O_CREAT 标志且文件尚不存在的情况下生效,用于
指定新创建文件的权限位 权限位通常由三位八进制数字组成,分别代表文件所有者、同组
用户和其他用户的读写执行权限
return: (1) 成功时返回非负的文件描述符。
(2) 失败时返回-1,并设置全局变量 errno 以指示错误原因。
*/
//linux操作系统有文件权限的保护,就是其他用户没有写权限。
int fd = open("b.txt",O_RDWR | O_CREAT,0757);
if(fd == -1){
printf("文件打开失败\n");
}
return 0;
}
2.2 read: 系统调用 用于读取已经打开的文件描述符
cpp
#include <unistd.h>
/*
int __fd:一个整数,表示要从中读取数据的文件描述符
void *__buf:一个指向存数据的缓冲区的指针,读取的数据将被存放到这个缓冲区中
size_t __nbytes:一个 size_t(long类型) 类型的整数,表示要读取的最大字节数 系统调用将
尝试读取最多这么多字节的数据,但实际读取的字节数可能会少于请求的数量
return: (1) 成功时,read()返回实际读取的字节数 这个值可能小于__nbytes,如果遇到了文件结尾(EOF)或者因为网络读取等原因提前结束读取
(2) 失败时,read()将返回-1
*/
2.3 write:系统调用用于 对打开的文件描述符写入数据
cpp
#include <unistd.h>
/*
int __fd:一个整数,表示要写入数据的文件描述符
void *__buf:一个指向缓冲区的指针,写入的数据需要先存放到这个缓冲区中
size_t __n:一个 size_t 类型的整数,表示要写入的字节数 write()函数会尝试写
入n 个字节的数据,但实际写入的字节数可能会少于请求的数量
return: (1) 成功时,write()返回实际写入的字节数 这个值可能小于__n,如果写
入操作因故提前结束,例如: 磁盘满、网络阻塞等情况
(2) 失败时,write()将返回-1
*/
2.4 close:系统调用用于在使用完成之后,关闭对文件描述符的引用
cpp
#include <unistd.h>
/*
int __fd:一个整数,表示要关闭的文件描述符
return: (1) 成功关闭时 返回 0
(2) 失败时 返回-1
*/
2.5 exit和_exit
2.5.1 系统调用_exit
• _exit()是由 POSIX 标准定义的系统调用,用于立即终止一个进程,定义在unistd.h 中。这个调用确保进程立即退出,不执行任何清理操作。
• _exit()在子进程终止时特别有用,这可以防止子进程的终止影响到父进程(比如,防止子进程意外地刷新了父进程未写入的输出缓冲区)。
• _exit 和_Exit 功能一样。
cpp
#include <unistd.h>
/**
* 立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。这个函数
通常在程序遇到严重错误需要立即退出时使用,或者在某些情况下希望避免清理工作时调用。
*
* int status: 父进程可接收到的退出状态码 0 表示成功 非 0 表示各种不同的错误
*/
void _exit(int status);
void _Exit (int __status) ;
2.5.2 库函数exit
cpp
#include <stdlib.h>
/**
* 终止当前进程,但是在此之前会执行 3 种清理操作
* (1) 调用所有通过 atexit()注册 的 终止处理函数(自定义)
* (2) 刷新所有标准 I/O 缓冲区(刷写缓存到文件)
* (3) 关闭所有打开的标准 I/O 流(比如通过 fopen 打开的文件)
*
* int status: 父进程可接收到的退出状态码 0 表示成功 非 0 表示各种不同的错误
*/
2.6 例子
cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int fd = open("a.txt",O_RDONLY);
if(fd == -1){
perror("open");
exit(EXIT_FAILURE);
}
char buf[1024];
ssize_t n_read ;
n_read = read(fd,buf,sizeof(buf));
if(n_read == -1){
perror("read");
close(fd);
exit(-1);
}
write(STDOUT_FILENO,buf,n_read);
close(fd);
return 0;
}
3. 文件描述符
3.1 定义
• 在 Linux 系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符,这是一个非负整数,我们可以通过它来进行读写等作。
• 文件描述符本身 只是 操作系统给应用程序 操作 底层资源(如文件、套接字等)所提供的一个引用或"句柄 ",其实也可以说文件描述符是一个索引。
• 在 Linux 中,文件描述符 0、1、2 是有特殊含义的。
• 0 是标准输入(stdin)的文件描述符。
• 1 是标准输出(stdout)的文件描述符。
• 2 是标准错误(stderr)的文件描述符。
3.2 文件描述符引用的图解,每个进程都只存自己的文件描述符表。如图:

3.3 每个文件描述符 都关联到 内核一个 struct file 类型的结构体数据。
• f_count:多少个 fd/进程在引用这份 file(这个文件),降到 0 时释放对象。
• f_pos:当前文件位置(读写位置)。
• f_path:记录文件的路径。
• f_inode:指向与文件相关联的 inode 对象的指针,该对象用于维护文件元数据,如文件类型、访问权限等。
•const struct file_operations *f_op; // 指向文件操作函数表的指针,定义了文件支持的操作,如读、写、锁定等。
• private_data:存储别的数据。
• 小结:
当我们执行 open()等系统调用时,内核会创建一个新的 struct file(文件描述的结构体),这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将 struct file 维护在文件描述符表中,最后将文件描述符返回给应用程序。 我们可以通过文件描述符 对文件执行它所支持的各种函数进行操作,而这些函数的函数指针都维护在struct file_operations 数据结构中。文件描述符实质上是底层数据结构 struct file 的一个引用或者句柄,提供了操作底层文件的入口。
• 补充,如图:


3.4 例子,实现linux cp命令的代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int fd = open(argv[1],O_RDONLY);
if(fd == -1){
perror("open");
close(fd);
exit(EXIT_FAILURE);
}
off_t move = lseek(fd,0,SEEK_END);
printf("偏移量为%ld\n",move);
char * buf = malloc(sizeof(char) * move + 8);
lseek(fd,0,SEEK_SET);
ssize_t n_read = read(fd,buf,move);
if(n_read < 0){
perror("read");
close(fd);
exit(EXIT_FAILURE);
}else{
int fd1 = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC);
ssize_t n_write = write(fd1,buf,n_read);
close(fd1);
}
close(fd);
return 0;
}
4. 系统调用和fopen等这些的区别
• 来源的不同:
• fopen是来自是ANSIC标准中的C语言库函数,在不同的系统中应该调用不同的内核api。返回的是一个指向文件结构的指针。
• 系统调用的open :是由UNIX系统调用函数(包括LINUX等),返回的是文件描述符(File Descriptor),它是文件在文件描述符表里的索引。
• 移植性:
• fopen是C标准函数,因此拥有良好的移植性。
• open是由UNIX系统调用,移植性有限。如windows下相似的功能使用API函数CreateFile。
• 使用范围:
• open 返回的是文件描述符 ,而文件描述符是UNIX系统下的一个重要概念,UNIX下的一切设备都是以文件的形式操作。如网络套接字、硬件设备等。当然包括操作普通正规文件(Regular File)。
• fopen是用来操纵普通正规文件(Regular File)的。
• 文件IO层次:如果从文件IO的角度来看,前者属于低级IO函数,后者属于高级IO函数。低级和高级的简单区分标准是:谁离系统内核更近。低级文件IO运行在内核态,高级文件IO运行在用户态。
• 缓冲区:
• 使用fopen函数,由于在用户态下就有了缓冲,因此进行文件读写操作的时候就减少了用户态和内核态的切换(切换到内核态调用还是需要调用系统调用API:read,write)。
• 使用open函数,在文件读写时则每次都需要进行内核态和用户态的切换。