前瞻

文件IO要求我们熟悉各种库函数。能够根据需求和数据类型,选择使用不同的库函数来操作数据。
因此,深刻理解这些函数之间的区别和具体实现原理非常重要。

这里我将这些函数按操作对象上分为:C内部和流之间、C内部和C内部的函数。
根据操作的数据类型上分为,操作字节流、二进制数据、操作格式化数据的函数。
两种分类更加利于在看到需求时精确定位到应该使用什么函数

用户应该如何操作文件
API(系统调用)中有一部分是用于操作文件的,即系统IO函数。
标准C库通过编程集成并简化了方便的对系统IO的复杂化调用,因此我们也可以用标准C库中的文件IO函数。

文件描述符 file description :fd

即文件被打开时内核返回的一个整数,标志着文件在内核中的入口,内核会分配一个结构体来记录这个文件的所有权限,光标指针等。这个整数就是结构体数组的下标。因此它是耗资源的,不用的时候尽量将其释放。

必要时调整光标指针 lseek fseek ftell rewind

无论使用哪一个IO,同一个文件描述符或FILE都共享同一个文件光标。
也就是说,只要文件描述符相同,同时读写将会出现不确定行为,因为光标在两个函数的操作下同时移动

系统IO(字节流) : open close write read

特点

  1. 系统IO虽然可以操作大部份情况,但是基本是被用来操作字节流数据,比如将颜色按字节写入屏幕显示文件。
  2. 所有的读写操作都直接通过文件描述符进行,进入内核空间
  3. 劣势:代码复杂,特别是在处理结构化数据时。错误处理机制不如标准IO。没有缓冲机制,会比较消耗资源
  4. 优势:更底层,可以精准控制和精细操作

技巧
write的返回值可能因为某些原因,达不到给出的第三个参数的量,可能需要再次写。读则因为有文件指针,无需调整

1
2
3
4
5
6
7
8
9
10
11
12
int* p = buf
while(nread>0)
{
nwrite = write( fd , p , nread);
while( nwrite == -1)
{
perror("wriete...");
return -1;
}
p += nwrite;
nread -= nwrite;
}

dup和dup2重定向fd

dup将最小未用的文件描述符指向参数,dup2将第二个参数指向第一个参数的指向文件。

ioctl和fcntl

可以使用驱动提供的具体操作,涵盖了除了读写意外的绝大部分API接口,比如上面的重定向,比如管道设置非阻塞。

mmap内存映射文件

通过mmap可以将内存空间和文件空间一一对应起来。
从此就可以通过修改内存(memcpy)来修改文件

特别用法
mmap的最后一个参数可以指定偏移量,也就是说修改第一个内存空间将修改偏移量的文件空间。在屏幕的刷新上可以通过预先将内容放到大内存中,通过调整偏移量显示不同内存中的内容

较为常用的标准IO : stdio.h

常见的数据类型:二进制数据(结构体、数组等),结构化数据(json、xml或者以逗号,空格分开的基础格式)

对于二进制数据:fopen fclose fread fwrite fleek

操作二进制数据非常方便,成块的操作内容。操作结构化的数据读写的一般为二进制文件。

fread(buff,size4,nmemb3,FILE * stream);

比如文件中有10个字符
按4个字符的单位来读,读3次。返回值是读4个的次数。如果某一次发现不够4个字符,就读剩余的,且设置文件流结束标志EOF,且本次不算一次完整读写。因此返回值为3.
但是一般不会使用fread来操作字符数据。而是操作块数据,以字符为块只是为了理解原理,常见用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//遍历所有
Student student;
while (fread(&student, sizeof(Student), 1, file)) {
printf("ID: %s, Name: %s, Age: %s, Sex: %s\n", student.id, student.name, student.age, student.sex);
}

//遍历+查找
while (fread(student, sizeof(Student), 1, file)) {
if (strcmp(student->id, id) == 0) {
fclose(file);
return student;
}
}


fwrite(buff,size4,nmemb3,FILE * stream);

发现不够4个字符时,函数也会继续读满4个包括后面未定义在内的字符,将其强制写入文件流。

也就是说没有其它错误的情况下,即便buff值不够,也会正确返回nmenb3?????????????好像不是,但是编译器确实给我返回了3

1
2
3
4
5
6
7
8
9
10
//fwrite函数写入未定义字符到文件,会发生什么?是否会算一次完整的写入?
int example5() {
FILE *fp = fopen("test.txt", "w");
char buffer[13]="123456789012";
size_t result = fwrite(buffer, 3,4 , fp);
printf("%ld times write.\n", result);
printf("buffer: %s\n", buffer);
fclose(fp);
return 0;
}

大佬帮我看看,万分感谢
当然,大部分情况下我们读写都是成块操作,并不会遇到块不完整的情况。
常见用法如下:

1
fwrite(student, sizeof(Student), 1, file);

对格式化数据简单处理(C与流之间数据交换)

fgets(缓冲区,最多读取数+1,流指针)

最大特点是遇到换行提前结束,因此他适合按行读取文件内容到用户缓冲区。
当然也可以从stdin中读。

以下是遇到\n提前结束读取:
流内容:abc\ndef\n
fgets一次后缓存区:abc\n\0
fgets一次后流内容:def\n

以下是没遇到\n,按第二个参数读取:
fgets(buffer, sizeof(buffer), stdin)//buffer[3]
流内容:abcdef\nghijkl\n
fgets一次后缓存区:ab\0
fgets一次后流内容:cdef\nghijkl\n
当我们char buffer[10]时,如果希望它为字符串,那么最多写9个字符
fgets(buffer, sizeof(buffer), stdin)最多读取数为9,第二个参数为10

fputs

最大特点是按\0来提前结束写入,因此它适合写入一个完整的字符串内容到流中
从缓冲区读取字符到文件,遇到\0会停止,\0不会读到文件中,字符串中\n不会打断读取,因此可以在缓冲区中写\n来分行存入。

一般来说,不会遇到提前结束,因为字符串中出现\0就意味着结束,而且由于是放入文件中,因此不用像fgets一样设置第二参数防止内容溢出,因此fputs使用起来就相当简单了。当然,如果缓冲区中没有\0,将会越界读取,可能导致文件崩溃。

对格式化数据格式化处理 之: C与流之间的数据交换

%s格式说明符,表示读取字符,直到空白符停止,空白符包括’ ‘’\t’’\n’’\r’’\v’’\f’

fscanf(fgets的格式化)

从流中,格式化地读取内容到用户缓存区,如果不完全能匹配上,则返回值为前面匹配上的数量,且文件光标将停止在不匹配的内容前面。进化版的scanf。

而且,fscanf将不会跳过任何一个字符,比如fscanf(fp, “:%s”,str);只要fp的开头没有冒号也不是空白符(\n \t 空格将会被跳过),将直接返回-1,这点和scanf一样。
也就是说,三种情况:完全匹配上、匹配到一半、一开始就没匹配上

提高匹配的格式:使用%[^ ]作为格式化以空白结尾的字符串,%[^,]格式化以逗号分隔的字符串。
如果只是在匹配格式中使用逗号等比如scanf(“%s,%s”)是很有可能出现种种问题的。

为什么选择fscanf而不是fgets:可以将流中的数据格式化到缓冲区,fgets无法格式化
什么情况下选择fgets:当需要读取包括空白符的内容时,比如代码,句子,路径这种没什么格式,很容易出现空白符的数据。

fprintf(fputs的格式化)

格式化将内容送到流,进化版的printf。

对格式化数据格式化处理 之:C内部的数据交换

sscanf(标准拆分)

将用户缓存区的内容,参考格式,提取到变量中

sprintf (标准拼接)

将变量中的内容,加上格式,放到用户缓存区中。
如果需要控制截断,或者设置多一个防止缓冲区溢出的参数,可以使用snprintf

错误处理机制 : ferror、feof、perror和clearerr

优秀的错误处理机制也是标准IO的优势,必须用

perror(参数)会获取最近一次发生错误的错误信息,打印在参数后面,最为常用。

ferror(fp)会判断fp的错误并且返回非零值,因此适合作为可能出错的地方的if的判断值,一般配合perror使用,会比单独perror更精准,例如

1
2
3
写文件代码;
if (ferror(fp)) {
perror("Erro

feof(fd) 返回文件是否有结束标志,常用于fread和fget等函数,当文件流结束时,置非零数

1
2
3
4
5
6
7
FILE *fp = fopen("example.txt", "r");
char buffer[10];
size_t result = fread(buffer, 1, 10, fp);
if (result < 10 && feof(fp)) {
printf("End of file reached.\n");
}
fclose(fp);

clearerr,当有可能出现错误,或EOF被置位时,我们还需要操作文件但又不希望关闭又开启。
此时,我们应该清除文件流的错误和文件流结束标志。clearerr(fp);
如果还需要移动光标到开头,使用rewind(fp),一次达成两个愿望!

与stdin、stdout、stderr等流的交互:scanf fgets fgetc

stdin

标准输入流,一般连接的是键盘,类似一个队列,内容先进先出。比如输入abc,那么先出去的是a

scanf(从stdin格式化读取数据到地址参数去)

初学时我们就常用scanf来从stdin中输入内容。
scanf可以标准化输入的长度, scanf(“%9s”, str);只接收九个字符,且在第十个位置加一个\0.如果遇到\n则提前结束,且把\n换成\0
也就是说,scanf可以防止输入内容越界,但是会将\n留在stdin中。

以下是常见解决方法

1
2
scanf("%9s", str);
while (getchar() != '\n'); // 清除缓冲区中的所有字符,直到换行符

fgets(str, sizeof(str), stdin) 将流(文件流也可以)的内容get到用户缓冲区去

利用fgets的整行读取,我们可以获取包括\n在内的整行内容,且fgets会在\n后面补一个\0.如果不需要\n,可以这样操作:

1
2
3
4
5
6
if (fgets(str, sizeof(str), stdin) != NULL) {
size_t len = strlen(str);
if (len > 0 && str[len-1] == '\n') {
str[len-1] = '\0'; // 替换换行符为字符串终止符
}
}

文件的属性

stat fstat istat

我们man 2 stat可以看到stat函数以及结构体stat的内容。从stat

设备号 stat.st_dev

每个文件都会有设备号,一般的文件会归属于一个设备,比如在桌面上创建一个文件,这个文件就归属于硬盘设备,stat.st_dev可以获得文件所属的的设备的设备号,也就是文件系统的设备号
特殊的文件自己会有一个设备号,例如块设备,字符设备。比如说一个键盘,那么我们可以通过stat.st_rdev获取设备号

其次,设备号又分为主设备号和次设备号,通过major和minor函数获取。
主设备号规范了设备类型,次设备号即序号,类似前面的fd

1
2
3
4
5
6
7
8
void print_dev_no(char* dev_name) {
int major, minor;
struct stat st;
stat(dev_name,&st);
printf("major number: %d\n",major(st.st_dev));
printf("minor number: %d\n",minor(st.st_dev));

}

类型与权限 stat.st_mode

stat.st_mode是一个16位二进制数
通过宏定义可以拿到文件类型

目录文件(遇到的第一种特殊文件)

通过操作目录文件,我们可以在Linux中的结构中随意移动,去到需要去的地方,找到需要的文件,创建文件到特定的地方。深刻理解目录不仅仅是一个字符串,而是一个有特定内容的文件。

目录文件是如何被解析的

目录文件中包括目录文件名索引号
当我们需要拿目录下内容的地址时,
vi /root/usr/hello.c
路径解析:

  1. / 中找 root,找到 root 的i-node号100。
  2. 通过i-node号100,访问 root 目录文件。
  3. 在 root 目录文件中找 usr,找到 usr 的i-node号200。
  4. 通过i-node号200,访问 usr 目录文件。
  5. 在 usr 目录文件中找 hello.c,找到 hello.c 的i-node号300。
  6. 通过i-node号300,访问 hello.c 文件的数据块。

C中如何与目录文件交互

创建和删除目录 mkdir和rmdir

打开和读取关闭目录:opendir readdir closedir

三个需要配合使用。和普通文件一样,需要打开,需要读取内容,也需要关闭文件
每次读取目录下的一个文件

1
2
3
4
5
6
DIR* dir = opendir(path);
while((dirent = readdir(dir))!= NULL){
printf("文件名:%s\n",dirent->d_name);
printf("文件类型:%uud\n",dirent->type);
printf("文件i-node索引:%d\n",dirent->d_ino);
}