Linux系统下C语言中的标准IO总结

 更新时间:2024年01月05日 10:35:34   作者:coolhuhu~  
最近用到了C语言的标准IO库,由于对其中的一些细节不是非常清楚,导致了许多Bug,花了好长时间来调试,所以在此做个笔记,这篇文章主要给大家介绍了关于Linux系统下C语言中标准IO的相关资料,需要的朋友可以参考下

本文对 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 */
}

格式化读写

格式化输出常用的有 printffprintfdprintfsprintfsnprintf 五个函数,下面介绍最常用的 printf 和 fprintf 函数。

#include <stdio.h>
int printf(const char* format, ...);
int fprintf(FILE* fp, const char* format, ...);
/*
以上两个函数,成功,返回输出的字符数;出错,返回复制。
format 表示格式化字符串;
... 为C语言的可变参数,需要与 format 中的格式化进行匹配。
*/

格式化输入常用的有如 scanffscanf 和 sscanf 。

对于标准库中格式化读写的更多细节,太过琐碎,可参考:https://en.cppreference.com/w/cpp/io/c/fscanf

多线程的安全性

上述的章节中介绍的文件流的读写函数都是线程安全的,会在正常进行磁盘文件读写时进行加锁操作。标准库中也提供非线程安全的版本,它们都以 _unlocked 后缀结尾。例如对于 fread 和 fwrite 的非线程安全版本为 fread_unlocked 和 fwrite_unlocked。

总结

到此这篇关于Linux系统下C语言中的标准IO的文章就介绍到这了,更多相关C语言标准IO内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言实现扫雷附完整代码

    C语言实现扫雷附完整代码

    本文详细讲解了C语言实现扫雷并附完整代码,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-11-11
  • C++深入探究哈希表如何封装出unordered_set和unordered_map

    C++深入探究哈希表如何封装出unordered_set和unordered_map

    哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过把关键码映射的位置去寻找存放值的地方,说起来可能感觉有点复杂,我想我举个例子你就会明白了,最典型的的例子就是字典
    2022-06-06
  • C++程序的五大内存分区实例详解

    C++程序的五大内存分区实例详解

    C++内存区域,一般可分为栈内存区、堆内存区、全局/静态内存区、文字常量内存区及程序代码区5大分区,本文就带大家深刻的理解这5大内存分区,感兴趣的可以了解一下
    2021-10-10
  • C语言静态动态两版本通讯录实战源码

    C语言静态动态两版本通讯录实战源码

    这篇文章主要为大家带来了C语言实现静态动态两版本的通讯录实战源码,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-02-02
  • vscode 配置 C/C++编译环境(完整教程)

    vscode 配置 C/C++编译环境(完整教程)

    这篇文章主要介绍了vscode 配置 C/C++编译环境(完整教程),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • C语言实现扫雷游戏(含注释详解)

    C语言实现扫雷游戏(含注释详解)

    这篇文章主要为大家详细介绍了C语言实现扫雷游戏,含注释,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • C语言中各类指针的用法(小结)

    C语言中各类指针的用法(小结)

    这篇文章主要介绍了C语言中各类指针的用法(小结),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • C++实现双向链表代码分析

    C++实现双向链表代码分析

    这篇文章主要介绍了C++实现双向链表代码分析,前面文章分析了单向链表,这篇文章就来给大家分享双链表的实现吧,需要的朋友可以参考一下
    2022-03-03
  • C语言实现BMP图像处理(直方图均衡化)

    C语言实现BMP图像处理(直方图均衡化)

    这篇文章主要为大家详细介绍了C语言实现BMP图像直方图均衡化处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-10-10
  • C++ 继承的范例讲解

    C++ 继承的范例讲解

    继承是C++面向对象编程中的一门。继承是子类继承父类的特征和行为,或者是继承父类得方法,使的子类具有父类得的特性和行为。重写是子类对父类的允许访问的方法实行的过程进行重新编写,返回值和形参都不能改变。就是对原本的父类进行重新编写,但是外部接口不能被重写
    2022-06-06

最新评论