目录
一、IO 简介
I/O 是一切实现的基础:
- 标准 IO(stdio);
- 系统调用 IO(sysio,文件IO);
不同系统上的系统调用 IO 的使用方式可能不一样,为了隐藏不同系统上的细节,提出了标准 IO 给程序员调用,标准 IO 底层其实还是调用了系统调用 IO,但是标准 IO 的可移植性更好:
二、标准 IO(stdio)
部分标准 IO 的接口如下(FILE 类型贯穿始终):
- fopen()、fclose()
- fgetc()、fputc()
- fgets()、fputs()
- fread()、fwrite()
- printf()、scanf()
- fseek()、ftell()、rewind()
- fflush()
- getline()
- tmpfile()
2.1 fopen()、fclose()
打开和关闭文件流。
man fopen
#include <stdio.h>
/* 打开文件流 */
FILE *fopen(const char *pathname, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *pathname, const char *mode, FILE *stream);
/* 关闭打开的流 */
int fclose(FILE *stream);
1. 是否能通过指针对字符常量 "abc" 进行修改?
答:看不同环境下,字符常量的存储区域。
2.2 fgetc()、fputc()
从打开的文件流中读写字符。
#include <stdio.h>
/* 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动 */
int fgetc(FILE *stream);
/* 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动 */
int fputc(int c, FILE *stream);
/* putc() 等同于 fputc(),一般使用宏实现。 */
int putc(int c, FILE *stream);
/* 和 putc 一样 */
int putchar(int c);
使用 fget 和 fput 实现的拷贝函数,可以实现文件的拷贝:
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "Usage : %s file1_path file2_path\n", argv[0]);
exit(1);
}
FILE *fp1 = NULL;
FILE *fp2 = NULL;
int count = 0;
if ((fp1 = fopen(argv[1], "r")) == NULL) {
perror("fopen()");
exit(1);
}
if ((fp2 = fopen(argv[2], "w")) == NULL) {
fclose(fp1);
perror("fopen()");
exit(1);
}
int c;
while ((c = fgetc(fp1)) != EOF) {
fputc(c, fp2);
count++;
}
fclose(fp2);
fclose(fp1);
printf("The num of char in %s is %d\n", argv[1], count);
exit(0);
}
2.3 fgets()、fputs()
从打开的文件流中读写字符串。
/* 从指定的流 stream 读取 size 大小的字符串,并把它存储在 str 中。读取到换行符时,或者到达文件末尾时,它也会停止 */
char *fgets(char *s, int size, FILE *stream);
/* 把字符串写入到指定的流 stream 中,但不包括空字符 */
int fputs(const char *s, FILE *stream);
不要使用 gets()!这个函数不会检查溢出。
2.4 fread()、fwrite()
从打开的文件流中读写块数据。
/* 从给定流 stream 读取 nmemb 个大小为 size 的数据到 ptr 所指向的数组中 */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/* 把 ptr 所指向的数组中的 nmemb 个大小为 size 的数据写入到给定流 stream 中 */
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fread() 和 fwrite() 最好对单字节进行操作(即 size = 1),除非你能确定要读写的文件是对齐的:
使用 fread() 和 fwrite() 来实现拷贝操作, 注意 fwrite(buf, 1, num, fp2) 中的 nmemb 不能是 BUF_SIZE,而是读到的字节数(因为最后一次读到的字节数可能会小于 BUF_SIZE):
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "Usage : %s file1_path file2_path\n", argv[0]);
exit(1);
}
FILE *fp1 = NULL;
FILE *fp2 = NULL;
int count = 0;
if ((fp1 = fopen(argv[1], "r")) == NULL) {
perror("fopen()");
exit(1);
}
if ((fp2 = fopen(argv[2], "w")) == NULL) {
fclose(fp1);
perror("fopen()");
exit(1);
}
char buf[BUF_SIZE];
int num;
while ((num = fread(buf, 1, BUF_SIZE, fp1)) <= 0) {
fwrite(buf, 1, num, fp2); // important!! the number of byte to write is num not BUF_SIZE
count++;
}
fclose(fp2);
fclose(fp1);
printf("The num of char in %s is %d\n", argv[1], count);
exit(0);
}
2.5 printf()、scanf()
#include <stdio.h>
int printf(const char *format, ...);
/* 发送格式化输出到流 stream 中 */
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
/* 可以使用 sprintf 将多种不同类型的变量以 format 格式转变成字符串并且写入 str 中 */
int sprintf(char *str, const char *format, ...);
/* 增加了 size 防止溢出 */
int snprintf(char *str, size_t size, const char *format, ...);
int scanf(const char *format, ...);
/* 从流 stream 读取格式化输入 */
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
2.6 fseek()、ftell()、rewind()
操作文件位置指针。
#include <stdio.h>
/* 设置流 stream 的文件位置为给定的 whence 位置的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数 */
int fseek(FILE *stream, long offset, int whence);
/* 返回给定流 stream 的当前文件位置 */
long ftell(FILE *stream);
/* 设置文件位置为给定流 stream 的文件的开头,和 fseek(stream, 0L, SEEK_SET) 效果相同 */
void rewind(FILE *stream);
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);
为什么需要这些函数来操作文件位置指针呢?想象一个场景:你打开一个文件后,使用 fputc() 写入了 10 个字符到文件中,然后再执行 10 次 fgetc() 将这 10 个字符读出来。。。很明显,这是不行的,因为 fputc() 和 fgetc() 在读写文件的时候都会把文件位置往前移动,所以后面的 10 次 fget() 读到不是前面的 10 个写入的字符,而是 10 个字符后面的内容!
通过 fseek() 和 ftell() 两个函数可以获得文件的大小。fseek() 函数还可以制造空洞文件。
fseek() 和 ftell() 中的 offset 都是使用的 long,这意味着文件位置的设置最大不超过 2^31B,即 2GB,这也限制了文件的上限。
在与其相同功能的函数 fseeko() 和 ftello() 中,long 被替换成了 off_t,off_t 和 long 在某些架构上都是 32 位,但 off_t 可以通过编译时指定宏 #define _FILE_OFFSET_BITS 64 来使其变成 64 位。
2.7 fflush()
刷新流 stream 的输出缓冲区。
#include <stdio.h>
/* 刷新流 stream 的输出缓冲区 */
int fflush(FILE *stream);
先举一个简单的例子,先猜一下下面程序的输出是什么:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("before");
while(1);
printf("after");
exit(0);
}
答案是,在运行后本应该打印的 before 没有被打印出来:
为什么会这样呢?这是因为 printf() 不会立即调用设备去打印输出内容,而是先将输出内容放到缓冲区中,这样做的好处就是可以合并系统调用,即将多个要输出的内容合并起来,然后调用设备一次打印完。
缓冲区有如下几种:
- 行缓冲(line buffered):换行时候刷新缓冲区,满了的时候刷新缓冲区,强制刷新(fflush),标准输出 stdout 就是行缓冲;
- 全缓冲(fully buffered):满了的时候刷新缓冲区,强制刷新(fflush),除了终端设备外,默认都是全缓冲模式;
- 无缓冲(unbuffered):如 stderr 等,需要立即输出的内容;
上面例子中的标准输出就是行缓冲,所以如果在 printf("before\n") 的 before 后面加上换行符 '\n',或者在 printf() 后面使用 fflush() 函数强制刷新,before 就可以正常打印出来了。
另外,通过 setvbuf() 函数可以修改打开流的缓冲模式,具体内容可以查看 linux 的 man 手册。
2.8 getline()
从流中读取一行。
#include <stdio.h>
/* 从流中读取一行,并且将存放该行的地址放到 *lineptr,将该行的长度放到 *n 中 */
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
下面是使用 getline() 读取文件的内容的程序,可以看到我们不需要事先为 linebuf 分配空间,getline 会自动帮我们分配空间,最后读到的内容和内容的长度都会由 getline 返回:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage %s <file_path>\n", argv[0]);
exit(1);
}
FILE *fp;
if ((fp = fopen(argv[1], "r")) == NULL) {
perror("fopen()");
exit(1);
}
// below is important!!
char *linebuf = NULL;
size_t line_size = 0;
while (1) {
if (getline(&linebuf, &line_size, fp) < 0) {
break;
}
printf("%d\n", (int) strlen(linebuf));
}
fclose(fp);
exit(0);
}
2.9 tmpfile()
创建临时文件。
#include <stdio.h>
/* 以二进制更新模式(wb+)创建临时文件。被创建的临时文件会在流关闭的时候或者在程序终止的时候自动删除 */
FILE *tmpfile(void);