2 标准IO
该节对应APUE(UNIX环境高级编程)的第五章---标准IO库
2.1 简介
IO分为标准IO(stdio)和系统调用IO(sysio);
系统调用IO根据操作系统的实现方式而定,对于程序员来说会造成很大困扰(例如打开文件,Linux的系统调用为open,而Windows的系统调用为opendir),于是又有了标准IO,提供了一套标准的IO实现的库函数(例如pringtf,fopen等),它实际上也是调用了系统IO进行操作,但是屏蔽了系统调用IO,方便程序员调用。
常用的标准IO库函数如下:
|----------|------------------|----------|----------|
| 打开关闭文件 | 输入输出流 | 文件指针操作 | 缓存相关 |
| fopen() | fgetc(),fputc() | fseek() | fflush() |
| fclose() | fgets(),fputs() | ftell() | |
| | fread(),fwrite() | rewind() | |
| | printf族,scanf族 | | |
注意:FILE类型贯穿始终
2.2 fopen
C 库函数 fopen 使用给定的模式 mode 打开 filename 所指向的文件。
cs
FILE *fopen(const char *filename, const char *mode)
filename--字符串,表示要打开的文件名称
mode--字符串,表示文件的访问模式,该指针指向以下面字符开头的字符串
|------|----------------------------------|
| 模式 | 描述 |
| "r" | 打开一个用于读取的文件,该文件必须存在,否则报错 |
| "r+" | 打开一个用于更新的文件,可读取也可写入,该文件必须存在 |
| "w" | 创建一个用于写入的空文件,有则清空,无则创建。 |
| "w+" | 创建一个用于读写的空文件,有则清空,无则创建。 |
| "a" | 追加到一个文件,写操作向文件末尾追加数据。如果文件不存在,则创建 |
| "a+" | 打开一个用于读取和追加的文件,无则创建。 |
只有模式r和r+要求文件必须存在,其他模式都是有则清空,无则创建;
mode也可以包含字母b,放在最后或者中间,表示二进制流。例如"rb","r+b";
代码示例
cs
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
FILE *fp;
fp = fopen("tmp", "r");
if(fp == NULL) {
fprintf(stderr, "fopen() failed! errno = %d.\n", errno);
exit(1);
}
puts("OK!");
exit(0);
}
编译执行后打印结果:
cs
fopen() failed! errno = 2.
可知errno为2,为No such file or directory;
在C标准中定义了两个函数帮助打印输出errno的对应错误原因,一个是strerror,另一个是perror;
perror包含在stdio.h中:
cs
//函数原型
/*
*功能:根据error打印对应的错误信息
*参数:s: 用户自定义信息字符串,一般是出错的函数名
*/
void perror(const char *s);
修改后的程序为:
cs
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
FILE *fp;
fp = fopen("tmp", "r");
if(fp == NULL) {
// fprintf(stderr, "fopen() failed! errno = %d.\n", errno);
perror("fopen()");
exit(1);
}
puts("OK!");
exit(0);
}
打印结果:
cs
fopen(): No such file or directory
fopen函数解析:
有函数原型可知,fopen函数返回的是一个FILE类型的指针,FILE是一个结构体,由typedef进行了重命名,而指针实际上是指向结构体的指针。
问题:指针指向的内存空间是哪一块?(或者说FILE结构体放在内存的哪一块?)栈,堆,静态区?
假设在栈上
cs
// 简单的fopen源码分析
FILE *fopen(const char *filename, const char *mode) {
FILE tmp;
// 给结构体成员赋值初始化
tmp.xxx = xxx;
tmp.yyy = yyy;
...
return &tmp;
}
分析:tmp变量的存储类别是自动类型(块作用域,自动存储期),当程序退出这个块时,释放刚才为变量tmp匹配的内存,因此,指针指向的地址实际上没有tmp,是一个没有被分配的内存;
在栈上是错误的!
假设在静态区上
cs
// 简单的fopen源码分析
FILE *fopen(const char *filename, const char *mode) {
static FILE tmp;
// 给结构体成员赋值初始化
tmp.xxx = xxx;
tmp.yyy = yyy;
...
return &tmp;
}
加上static,将tmp保存在静态区(静态无链接),但是只能存在一个FILE实例(因为只有这一个内存区供指针指向);例如:
cs
fp1 = fopen("a", "r");
fp2 = fopen("b", "r");
// 此时fp1实际指向了b,第二次的结果会把第一次的结果覆盖掉
堆(正解)
cs
// 简单的fopen源码分析
FILE *fopen(const char *filename, const char *mode) {
FILE *tmp = NULL;
tmp = malloc(sizeof(FILE));
// 给结构体成员赋值初始化
tmp->xxx = xxx;
tmp->yyy = yyy;
...
return tmp;
}
此时变量tmp具有动态存储期,从调用malloc分配内存到调用free释放内存为止,而free就在fclose函数中被调用。
2.3 fclose
C库函数fclose关闭流stream。刷新所有的缓冲区。
cs
int fclose(FILE *stream)
stream--这是指向FILE对象的指针,该FILE对象指定了要被关闭的流。
返回值为整型,流成功关闭返回0,不成功返回EOF
代码示例
cs
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
FILE *fp;
fp = fopen("tmp", "r");
if(fp == NULL) {
perror("fopen()");
exit(1);
}
puts("OK!");
fclose(fp); // 释放内存
exit(0);
}
2.4 fgetc和fputc
getchar和putchar
cs
int getchar(void); // 从标准输入 stdin 获取一个字符(一个无符号字符)。
这等同于 getc 带有 stdin 作为参数
cs
int putchar(int char); // 把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中。
这等同于 putc 带有 stdout 作为参数
getc和putc
cs
int getc(FILE *stream); // 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
cs
int putc(int char, FILE *stream); // 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
fgetc和fputc
cs
int fgetc(FILE *stream); // 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
// 该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
cs
int fputc(int char, FILE *stream); // 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
// 如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。
getc,putc和fgetc,fputc的区别
两者的使用完全相同,只是实现不同。这里的f指的是function,而不是file。
getc,putc是通过宏定义实现,而fgetc,fputc是通过函数来实现。
宏只占用编译时间,不占用调用时间,而函数相反,因此内核的实现通常使用宏来定义函数,减少调用时间。
代码示例
需求:实现拷贝文件
cs
./mycpy src dest
cs
#include <stdio.h>
#include <stdlib.h>
// 命令行传参
int main(int argc, char **argv) {
FILE *fps, *fpd;
int ch; // 存储读入的字符
if(argc < 3) {
fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
exit(1);
}
fps = fopen(argv[1], "r");
if(fps == NULL) {
perror("fopen()");
exit(1);
}
fpd = fopen(argv[2], "w");
if(fpd == NULL) {
fclose(fps);
perror("fopen()");
exit(1);
}
while(1) {
ch = fgetc(fps);
if(ch == EOF) { // 读到文件末尾结束循环
break;
}
fputc(ch, fpd);
}
// 释放内存,后开的先关
fclose(fpd);
fclose(fps);
exit(0);
}
使用:
cs
./mycpy /usr/local/test /temp/out
代码示例:
需求:统计一个文件中的字符个数
cs
#include <stdio.h>
#include <stdlib.h>
// 命令行传参
int main(int argc, char **argv) {
FILE *fps;
int count=0; // 存储读入的字符个数
if(argc < 2) {
fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
exit(1);
}
fps = fopen(argv[1], "r");
if(fps == NULL) {
perror("fopen()");
exit(1);
}
while(fgetc(fps)!=EOF) {
count++;
}
printf("count = %d\n",count);//假设文件中字符个数不超过int最大值
fclose(fps);
exit(0);
}
使用:
cs
./fgetc tmp
2.5 fgets和fputs
gets和puts
cs
char *gets(char *str); // 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
// 如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
cs
int puts(const char *str); // 把一个字符串写入到标准输出 stdout,直到空字符,但不包括空字符。换行符会被追加到输出中。
// 如果成功,该函数返回一个非负值为字符串长度(包括末尾的 \0),如果发生错误则返回 EOF。
fgets和fputs
cs
// 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。
当读取 (n-1) 个字符时,或者读取到换行符时,
或者到达文件末尾时,它会停止,具体视情况而定。
char *fgets(char *str, int n, FILE *stream);
// 如果成功,该函数返回相同的 str 参数。
如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,
并返回一个空指针。
// 如果发生错误,返回一个空指针。
cs
// 把字符串写入到指定的流 stream 中,但不包括空字符。
int fputs(const char *str, FILE *stream);
// 该函数返回一个非负值,如果发生错误则返回 EOF。
区别
fgets比gets安全,使用gets编译时会警告。所以不要使用gets!
原因:函数 gets 可以无限读取,不会判断上限,所以程序员应该确保 buffer 的空间足够大,以便在执行读操作时不发生溢出。也就是说,gets 函数并不检查缓冲区 buffer 的空间大小,事实上它也无法检查缓冲区的空间。
如果函数的调用者提供了一个指向堆栈的指针,并且 gets 函数读入的字符数量超过了缓冲区的空间(即发生溢出),gets 函数会将多出来的字符继续写入堆栈中,这样就覆盖了堆栈中原来的内容,破坏一个或多个不相关变量的值。
fgets读取结束的条件,满足其一即可:
读到size-1个字符时停止,size位置存放\0
读到换行符'\n'时停止
读到文件末尾EOF
简单的实例
cs
#define SIZE 5
char buf[SIZE]; // 栈上的动态内存
fgets(buf, SIZE, stream);
如果stream = "abcde"
则buf = "abcd\0"(读到size-1),文件指针指向e
如果stream = "ab"
则buf = "ab\n\0"(读到换行符),文件指针指向EOF
极端的情况:
如果stream = "abcd"
则需要fgets读取两次才能读完
第一次读取的为"abcd\0"(读到SIZE-1),指针指向'\n'
第二次读取的为"\n\0"(读到换行符),指针指向EOF
代码示例:
重构之前的mycpy代码。用fgets和fputs代替fgtec和fputc:
cs
#include <stdio.h>
#include <stdlib.h>
#define SIZE 1024
int main(int argc, char **argv) {
FILE *fps, *fpd;
char buf[SIZE];
if(argc < 3) {
fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
exit(1);
}
fps = fopen(argv[1], "r");
if(fps == NULL) {
perror("fopen()");
exit(1);
}
fpd = fopen(argv[2], "w");
if(fpd == NULL) {
fclose(fps);
perror("fopen()");
exit(1);
}
while(fgets(buf, SIZE, fps) != NULL)
fputs(buf, fpd);
fclose(fpd);
fclose(fps);
exit(0);
}
2.6 fread和fwrite
fread从给定流 stream 读取数据到 ptr 所指向的数组中。
cs
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
ptr --- 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
size --- 这是要读取的每个元素的大小,以字节为单位。
nmemb --- 这是元素的个数,每个元素的大小为 size 字节。
stream --- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。
成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。
fwrite把 ptr 所指向的数组中的数据写入到给定流 stream 中。
cs
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
ptr --- 这是指向要被写入的元素数组的指针。
size --- 这是要被写入的每个元素的大小,以字节为单位。
nmemb --- 这是元素的个数,每个元素的大小为 size 字节。
stream --- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。
简单的实例
cs
fread(buf, size, nmemb, fp);
// 情况1:数据量足够
// 情况2:文件只有5个字节
// 读10个对象,每个对象1个字节
fread(buf, 1, 10, fp);
// 情况1:
// 第一次读:返回10(读到10个对象),读到10个字节
// 情况2:
// 第一次读:返回5(读到5个对象),读到5个字节
//--------------------------------
// 读1个对象,每个对象10个字节
fread(buf, 10, 1, fp);
// 情况1:
// 第一次读:返回1(读到1个对象),也读到10个字节
// 情况2:
// 第一次读:返回0(读不到1个对象,因为1个对象要10字节,而文件只有5个字节)
代码示例
用fread和fwrite代替fgtec和fputc:
cs
#include <stdio.h>
#include <stdlib.h>
#define SIZE 1024
int main(int argc, char **argv) {
FILE *fps, *fpd;
char buf[SIZE];
int n;
if(argc < 3) {
fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
exit(1);
}
fps = fopen(argv[1], "r");
if(fps == NULL) {
perror("fopen()");
exit(1);
}
fpd = fopen(argv[2], "w");
if(fpd == NULL) {
fclose(fps);
perror("fopen()");
exit(1);
}
// 如果成功读到n(n>0)个对象,则返回n
// 将这n个对象写入流中
while((n = fread(buf, 1, SIZE, fps)) > 0)
fwrite(buf, 1, n, fpd);
fclose(fpd);
fclose(fps);
exit(0);
}
2.7 printf和scanf
printf一族函数
printf:发送格式化输出到标准输出 stdout。
cs
int printf(const char *format, ...);
fprintf:发送格式化输出到流 stream 中。可以实现格式化输出的重定向,例如重定向至文件中。
cs
int fprintf(FILE *stream, const char *format, ...);
sprintf:发送格式化输出到 str 所指向的字符串。它能够将多种数据类型(整型、字符型)的数据综合为字符串类型。有溢出风险,可以使用snprintf来防止。
cs
int sprintf(char *str, const char *format, ...)
atoi:把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。包含在stdlib.h中。
cs
int atoi(const char *str)
代码示例:
cs
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char str[] = "123456";
printf("%d\n", atoi(str)); // 123456
exit(0);
}
cs
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char str[] = "123a456";
// 遇到字符就停止
printf("%d\n", atoi(str)); // 123
exit(0);
}
cs
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char buf[1024];
int year = 2022, month = 11, day = 28;
// 将格式化输出重定向为字符串
sprintf(buf, "%d-%d-%d", year, month, day);
puts(buf);
exit(0);
}
scanf一族函数
scanf
fscanf
sscanf
2.8 fseek和ftell
fseek:设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。
cs
int fseek(FILE *stream, long int offset, int whence)
stream --- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
offset --- 这是相对 whence 的偏移量,以字节为单位。
whence --- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
|----------|-----------|
| 常量 | 描述 |
| SEEK_SET | 文件的开头 |
| SEEK_CUR | 文件指针的当前位置 |
| SEEK_END | 文件的末尾 |
如果成功,则该函数返回零,否则返回非零值。
ftell:返回给定流 stream 的当前文件位置。
cs
long int ftell(FILE *stream)
stream --- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。
程序实例------求程序的有效字节
cs
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
FILE *fp;
if(argc < 2) {
fprintf(stderr, "Usage...\n");
exit(1);
}
fp = fopen(argv[1], "r");
if(fp == NULL) {
perror("fopen()");
exit(1);
}
// 将指针定位在文件末尾
fseek(fp, 0, SEEK_END);
printf("%d\n", ftell(fp));
exit(0);
}
rewind:设置文件位置为给定流 stream 的文件的开头。
cs
void rewind(FILE *stream)
相当于(void) fseek(stream, 0, SEEK_SET);
注意
fseek和ftell中偏移offset的修饰类型是long,因此只能对2G左右大小的文件进行操作,否则会超出long的范围;
fseeko和ftello则将偏移的修饰类型使用typedef定义为offset_t,具体类型交由系统决定,因此不存在文件大小的限制。但是这两个函数不是C标准库函数,而是隶属于POSIX标准(POSIX是标准C库的超集,或者说,C库是普通话,而POSIX是方言)。
2.9 fflush
fflush:刷新流 stream 的输出缓冲区。刷新,指的是将缓冲区(内存上的一片区域)的内容写入到磁盘(外存)中,或者输出到终端上显示。
cs
int fflush(FILE *stream)
如果参数为NULL,则刷新所有的已打开的流
如果成功,该函数返回零值。如果发生错误,则返回 EOF,且设置错误标识符(即 feof)。
代码示例
cs
#include <stdio.h>
int main() {
printf("Before while(1)");
while(1);
printf("After while(1)");
exit(0);
}
打印结果:
// 什么都不打印
原因:
对于标准输出,输出缓冲区刷新的时机:
输出缓冲区满
或者遇到换行符\n
强制刷新,或者进程结束
因此,可以修改为:
cs
#include <stdio.h>
#include <stdlib.h>
int main() {
// 遇到\n刷新
printf("Before while(1)\n");
while(1);
printf("After while(1)\n");
exit(0);
}
或者修改为:
cs
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Before while(1)");
// 强制刷新
fflush(stdout);
// 或者 fflush(NULL);
while(1);
printf("After while(1)");
exit(0);
}
缓冲区的作用:大多数情况下是好事,合并系统调用,增加程序的吞吐量。
缓冲的分类:
行缓冲line buffered:针对标准输出(终端设备),有换行刷新,缓冲满刷新,强制刷新三种,后两个和全缓冲一致;
全缓冲fully buffered:默认缓冲机制(除标准输出【终端设备】,例如重定向到文件),有缓冲满刷新,强制刷新两种,强制刷新例如调用fflush函数,或者进程结束时也会强制刷新;此时换行符仅仅只是个换行符,没有刷新功能;
无缓冲unbuffered:例如stderr,需要立即输出,数据会立即读入内存或者输出到外存文件和设备上;
setvbuf:定义流 stream 应如何缓冲。理解即可。
cs
int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
stream --- 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
buffer --- 这是分配给用户的缓冲。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲。
mode --- 这指定了文件缓冲的模式:
模式 描述
_IOFBF 全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。
_IOLBF 行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。
_IONBF 无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
2.10 getline
之前介绍的函数,都不能获得完整的一整行(有缓冲区大小的限制),而下面介绍的getline函数则可以动态分配内存,当装不下完整一行时,又会申请额外的内存来存储。
getline是C++标准库函数,但不是C标准库函数,而是POSIX所定义的标准库函数(在POSIX IEEE Std 1003.1-2008标准出来之前,则只是GNU扩展库里的函数)。在gcc编译器中,对标准库stdio进行了扩展,加入了一个getline函数。
getline会生成一个包含一串从输入流读入的字符的字符串,直到以下情况发生会导致生成的此字符串结束:
到文件结束
遇到函数的定界符
输入达到最大限度
函数原型:
cs
#define _GNU_SOURCE // 通常将这种宏写在makefile中,现在的编译器没有了该宏,直接使用即可
#include <stdio.h>
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
lineptr:指向存放该行字符的指针,如果是NULL,则有系统帮助malloc,请在使用完成后free释放。该参数是一个二级指针,因此传参需要一级指针的地址。即函数会把读取到的字符串的首地址存放在一级指针中。
cs
// 传参:
char *ptr;
// 函数内的实际操作:
// 假设读取到的字符串Hello的首地址为0x000
&ptr = 0x000; // 此时ptr就指向了Hello
n:如果是由系统malloc的指针填0;
stream:函数需要读取的FILE流
返回值:成功返回读取的字节数,失败或读完返回-1。
代码示例
cs
int main(int argc, char **argv) {
FILE *fp;
// 一定要初始化,否则指针会指向内存中的随机位置
char *linebuf = NULL;
size_t linesize = 0;
if(argc < 2) {
fprintf(stderr, "Usage...\n");
}
fp = fopen(argv[1], "r");
if(fp == NULL) {
perror("fopen()");
exit(1);
}
while(1) {
// 当返回-1时则读完
if(getline(&linebuf, &linesize, fp) < 0)
break;
printf("%ld\n", strlen(linebuf));
}
fclose(fp);
exit(0);
}
2.11 临时文件
临时文件产生的问题:
1.如何命名不冲突
2.如何保证及时销毁
tmpnam:生成并返回一个有效的临时文件名,该文件名之前是不存在的。如果 str 为空,则只会返回临时文件名。
存在并发问题,可能会产生两个或多个名字相同的临时文件。
cs
char *tmpnam(char *str)
str --- 这是一个指向字符数组的指针,其中,临时文件名将被存储为 C 字符串。
返回一个指向 C 字符串的指针,该字符串存储了临时文件名。如果 str 是一个空指针,则该指针指向一个内部缓冲区,缓冲区在下一次调用函数时被覆盖。
如果 str 不是一个空指针,则返回 str。如果函数未能成功创建可用的文件名,则返回一个空指针。
tmpfile:以二进制更新模式(wb+)创建临时文件。被创建的临时文件会在流关闭的时候或者在程序终止的时候自动删除。
该文件没有名字(匿名文件)只返回指向FILE的指针,因此不存在命名冲突的问题,同时会自动删除,因此可以及时销毁。
cs
FILE *tmpfile(void)
如果成功,该函数返回一个指向被创建的临时文件的流指针。如果文件未被创建,则返回 NULL。