Linux系统下C语言中的标准IO总结
作者:coolhuhu~
本文对 Linux 下C语言的标准IO进行总结,所有代码示例均在 Ubuntu-20.04、GCC 11.3.0 环境下运行通过。
标准IO中的一些概念
流和FILE对象
在 Linux 操作系统中,提供给用户操作文件的接口是“文件描述符”以及对应的函数,例如 read,write等。而在C语言中,提供给用户的文件操作的接口是“流(stream)”,当使用C语言中的标准I/O库打开或创建一个文件时,就使得一个流与一个文件关联起来。而这个 “流” 的概念,在程序上,使用 FILE 对象来表示,例如:
#include <stdio.h>
int main()
{
	// 打开或者创建一个文件,使用 FILE 对象与该文件进行绑定。
	// 也常把 fp 叫为 文件流
	FILE* fp = fopen("example.txt", "a");
	// ...
}
注意:一个进程预定了三个流,标准输入、标准输出和标准错误。而本文的重点讨论对象是 文件流,也即 FILE 对象以及标准I/O中提供的操作 FILE 对象的一系列函数。
流的定向(stream’s orientation)
对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节(也称“宽”字符)字符集。流的定向(stream’s orientation)决定所读、写的字符是单字节还是多字节的。
下面给出一个宽字符集输出到标准输出的示例:
#include <iostream>
#include <cstring>
int main() {
    const char* charString = "你好,世界!";
    std::cout << "Length of charString: " << std::strlen(charString) << std::endl;
    const char* multibyteString = u8"你好,世界!";  // UTF-8编码的多字节字符串
    std::cout << "Length of multibyteString: " << std::strlen(multibyteString) << std::endl;
    const wchar_t* wideString = L"你好,世界!";  // UTF-16编码的宽字符字符串
    std::wcout << L"Length of wideString: " << std::wcslen(wideString) << std::endl;
    return 0;
}
/*
运行结果为:
Length of charString: 18
Length of multibyteString: 18
Length of wideString: 6
*/
注意:下文讨论的都是单字节的流向。
缓冲(buffer)
在 Linux 中,使用 read、write 函数对文件描述符进行读写操作属于系统调用,用于直接读写磁盘文件。(其实也不是直接读写读写磁盘文件,Linux内核中会维护高速缓冲区用于提高磁盘文件的读写效率,这部分内容超出的本文的讨论范畴,略过。)而C语言I/O标准库提供的I/O操作在用户态,通常带有缓冲(当然,也可以没有缓冲),使用标准I/O库提供的I/O操作先将数据写入缓冲中,然后等待某个条件达成,在将缓冲中的数据写入磁盘文件(调用 write 函数)。标准I/O库提供缓冲的目的是减少 read 和 write 调用的次数,提高 I/O 效率。(而在实际的开发中,I/O缓冲的利用需要根据实际的场景来使用,并不是说有了缓冲,I/O效率就提高了。)
标准 I/O 提供了三种缓冲类型:
- 全缓冲。对于写操作,当缓冲区写满后,才将缓冲中的数据写入文件;读操作同理。对于写操作,可以调用 flush 函数主动将缓冲中的数据写入文件而不论缓冲是否被写满。下文将调用 flush 函数的操作称为 ”刷新缓冲“。
 - 行缓冲。当读或写数据遇到换行符时,将缓冲中的数据进行输入或输出。行缓冲的一个限制是:当缓冲已满,即使未遇到换行符,也将其进行输入输出。
 - 不带缓冲。即I/O操作直接写入文件。
 
I/O标准库中常用的函数
文件流的打开和关闭
下面三个函数可用于文件流的打开,其中 fopen 最为常用,先重点介绍该函数,剩余两个当遇到具体的使用场景时再来补充。在 Linux 中是可以使用 man 命令查看详情。
#include <stdio.h> /* pathname参数表示打开的文件路径名; type参数指定对文件流的读、写方式。 若打开出错,返回 NULL。 */ FILE* fopen(const char* pathname, const char* type); FILE* freopen(const char* pathname, const char* type, FILE* fp); FILE* fdopen(int fd, const char* type);
对于 fopen 函数,type 参数的值及表示的读、写方式如下所示:
r或rb,以读的方式打开。w或wb,以写的方式打开;若指定文件名存在,则将该文件内容清空;不存在,创建新文件。a或ab,以追加写的方式打开文件。若指定文件名不存在,创建新文件。r或r+b或rb+,以读写的方式打开文件;指定文件名不存在,则出错。w+或w+b或wb+,以读写的方式打开文件;若指定文件名存在,则将该文件内容清空;不存在,创建新文件。a+或a+b或ab+,以读写的方式打开文件,读写操作在文件尾开始进行,若指定文件名不存在,创建新文件。
使用字符b作为type的一部分,使得标准I/O系统可以区分为文本文件和二进制文件。UNIX不对这两种文件进行区分。
– 《UNIX 高级环境编程》
使用 fopen 函数开打的文件流默认是自带缓冲的,缓冲模式为全缓冲。
fclose 函数用于关闭一个打开的文件流。注意,对于一个已经关闭了的文件流调用 fclose 函数,行为是未定义的。
#include <stdio.h> /* 若成功,返回0;若出错,返回 EOF */ int fclose(FILE* fp);
当调用 fclose 函数或者当一个进程正常终止(调用 exit 函数或从 main 函数返回),会先刷新缓冲。若是使用标准IO默认的缓冲,则会释放缓冲。
给文件流设置自定义的缓冲
若希望自己掌控文件流的缓冲,可以自定义一个缓冲,将其于打开的文件流的进行绑定。主要有如下三个函数可以绑定自定义的缓冲:
#include <stdio.h> void setbuf(FILE* fp, char* buf); /* buf参数为指定缓冲区,mode表示缓冲类型,size指定了缓冲的大小。 成功返回0;出错返回非0。 */ void setvbuf(FILE* fp, char* buf, int mode, size_t size); void setbuffer(FILE* fp, char* buf, size_t size);
mode 参数的可选值如下:
_IOFBF,全缓冲。_IOLBF,行缓冲。_IONBF,无缓冲。
对上面三个函数进行如下补充说明:
setbuf等价于setvbuf(fp, buf, buf ? _IOFBF : _IONBF, BUFFSIX);其中, BUFZSIZE 在标准库的默认值,我的环境下为 8096。setbuffer等价于setvbuf(fp, buf, buf ? _IOFBF : _IONBF, size)。- 若 buf 参数为空,则文件流为无缓冲。
 
在使用文件流的进行文件操作时,全缓冲的缓冲模式用得最多,因此推荐使用 setbuffer ,不用费力去记缓冲模式的参数值。
文件流的读写
打开文件流后,有三种类型的非格式化I/O操作:
- 每次读写一个字符。一次读写一个字符。
 - 每次读写一行。一次读写一行,每一行以换行符终止。
 - 直接读写,即指定读写的字节数。每次IO操作读写某种类型的对象,每个对象具有指定的长度。
 
读写一个字符
对于读一个字符,有如下三个函数可供选择:
#include <stdio.h> int getc(FILE* fp); int fgetc(FILE* fp); int getchar(void); /* 以上三个函数,成功返回读取的字符,返回前将 unsigned char 类型转换为 int 类型;若已到达文件尾端或出错,返回 EOF。 */
对上面三个函数进行补充说明:
getchar(void)等价于getc(stdin)。- 在《UNIX 环境高级编程》中,” 
getc函数可能被实现为宏,而fgetc一定为函数“。因此推荐使用fget函数,因为宏定义的参数存在副作用。 
对于上述三个函数的出错,在文件流 FILE 对象中,每个 FILE 对象维护了两个标志:
- 出错标志;
 - 文件结束标志;
 
可以使用 ferror 和 feof 函数进行检查:
#include <stdio.h> /* 检查 fp 指定的流是否发生了错误。若为真,则返回非0;否则,返回0 */ int ferror(FILE* fp); /* 检查 fp 指定的流是否到达文件尾。若为真,则返回非0;否则,返回0 */ int feof(FILE* fp); /* 清楚上述两个标志 */ void clearerr(FILE* fp);
在使用文件流进行文件操作时,一个好的编码习惯是,使用 ferror 函数检测读写后的文件流状态。
对于写一个字符,有如下三个函数可供选择:
#include <stdio.h> int putc(int c, FILE* fp); int fputc(int c, FILE* fp); // putchar(c) 等价于 putc(c, stdout); int putchar(int c); /* 以上三个函数,成功,返回c;若出错,返回EOF。 */
和 getc、fgetc 类似,putc 可能实现为宏,fputc 被定义为一个函数,因此推荐使用 fputc。
下面给出几个编码示例:
假设文件中的内容为 python,一个字符一个字符的把文件中的内容输出到标准输出。
FILE* fp = fopen("example.txt", "a+");
int c;
while ((c = fgetc(fp)) != EOF) {
	printf("%c", (unsigned char)c);
}
fclose(fp); 
/*
输出为:python
*/假设文件中的内容为 python,一个字符一个字符写入文件。
FILE* fp = fopen("example.txt", "a+");
char buf[7] = "golang";
for (int i = 0; i < 6; ++i) {
	fputc(buf[i], fp);
}
fclose(fp);  
/*
文件中的内容为:pythongolang
*/(以上只是简单的编码示例,更复杂的操作可以结合文件流的定位来进行,碰到实际的场景在来补充。)
读写一行
对于读一行,有以下两个函数可供选择:
#include <stdio.h> // n 为指定的缓冲区大小 // 将 fp 文件中的内容写入 buf 中,直至遇到换行符或者buf写满。 char* fgets(char* buf, int n, FILE* fp); // gets 从标准输入进行读 char* gets(char* buf); /* 以上两个函数,若成功,返回buf;若已达到文件末尾或出错,返回NULL。 */
gets 函数不能指定缓冲区大小,建议只使用 fgets 函数。
对于写一行,有以下两个函数可供选择:
#include <stdio.h> // fputs 不会将换行符写入到文件流中 int fputs(const char* str, FILE* fp); // puts 将字符串输出到标准输出,会将换行符作为输出。 int puts(const char* str); /* 以上两个函数,若成功,返回非负责;若出错,返回 EOF。 */
建议只是用 fputs 函数。
直接读写
在《UNIX环境高级编程》一书中,直接读写也即二进制读写,指一次读写一个完整的结构,例如一个结构体对象。通常用来读写指定的字节大小的数据。
常用的直接读写的函数如下:
#include <stdio.h> // 若出错或到达文件末尾,返回值可以小于 nobj;需要调用 ferror 或 feof 来判断是哪一种情况。 size_t fread(void* ptr, size_t size, size_t nobj, FILE* fp); // 若出错,返回值小于 nobj size_t fwrite(const void* ptr, size_t size, size_t nobj, FILE* fp); /* 以上两个函数,返回读写的对象数量。 ptr 指向待写入的对象 size 表示对象的大小 nobj 表示写入的对象数量 */
下面给出几个编码示例:
读写char数组形式的字符串。
char buffer[16] = "python";
FILE* wfp = fopen("test2.txt", "w");
fwrite(buffer, 1, strlen(buffer), wfp);
fclose(wfp);
printf("%s\n", buffer);
char buffer2[16];
FILE *rfp = fopen("test2.txt", "r");
fread(buffer2, 1, strlen(buffer), rfp);
fclose(rfp);
printf("%s\n", buffer2);使用 fread 和 fwrite 读写一个类的示例对象(有bug)。(无意中测试出来的一个bug,暂未解决。一个初步的思路为,需要去学习了解 C++ 的对象模型,即一个C++的对象在内存中是如何布局的,然后在深入 fread 和 fwrite 的源码中,去了解,其底层是如何读写的。在此文中先留个坑,后面再来填补)
void test1()
{
    Person p1("Jack", 25);
    FILE* wfp = fopen("Person", "wb");
    fwrite((void*)&p1, sizeof(Person), 1, wfp);
    fclose(wfp);
    Person* p2 = new Person("Lisa", 19);
    std::cout << p2->name() << std::endl;
    FILE* rfp = fopen("Person", "rb");
    fread(p2, sizeof(Person), 1, rfp);
    fclose(rfp);
    std::cout << p2->name() << std::endl;
    // delete p2;   /* 若在这行执行此语句,出现 Segmentation fault */
    Person p3;
    FILE* rfp2 = fopen("Person", "rb+");
    fread(&p3, sizeof(Person), 1, rfp2);
    fclose(rfp2);
    std::cout << p3.name() << std::endl;
    /* 程序结束,Segmentation fault */
}格式化读写
格式化输出常用的有 printf, fprintf, dprintf, sprintf, snprintf 五个函数,下面介绍最常用的 printf 和 fprintf 函数。
#include <stdio.h> int printf(const char* format, ...); int fprintf(FILE* fp, const char* format, ...); /* 以上两个函数,成功,返回输出的字符数;出错,返回复制。 format 表示格式化字符串; ... 为C语言的可变参数,需要与 format 中的格式化进行匹配。 */
格式化输入常用的有如 scanf, fscanf 和 sscanf 。
对于标准库中格式化读写的更多细节,太过琐碎,可参考:https://en.cppreference.com/w/cpp/io/c/fscanf
多线程的安全性
上述的章节中介绍的文件流的读写函数都是线程安全的,会在正常进行磁盘文件读写时进行加锁操作。标准库中也提供非线程安全的版本,它们都以 _unlocked 后缀结尾。例如对于 fread 和 fwrite 的非线程安全版本为 fread_unlocked 和 fwrite_unlocked。
总结
到此这篇关于Linux系统下C语言中的标准IO的文章就介绍到这了,更多相关C语言标准IO内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
