Home

cdeveloper

读书,写作

Linux 高级编程 - 标准 IO 库


版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!

标准 IO 库

上一篇文章我们学习了 5 个底层的 IO 函数,这次我们来学习标准的 IO 函数,既然是标准那肯定在多个平台都有使用到,例如 Linux,Windows…,上层应用用标准库开发还是很常见的,因为它们可以跨平台。

标准 IO 库是由 Dennis Ritchie 在 1975 年左右写的,后来被美国国际标准化组织(ISO)制定成了 C 语言的标准库,称为 ANSI C。因为现在所有的 UNIX 系统都提供 C 标准中定义的库函数,所以学习标准 C 库非常重要。

这篇文章主要介绍一些常用的 C 库函数,例如 fopen, fclose, fread, fwrite…,不可能将全部的库函数介绍完毕,你需要根据那本经典的Unix 环境高级编程来深入的学习,一定掌握学习的方法:

  1. man 手册
  2. glibc 官网
  3. Google

流和 FILE 对象

在学习标准库之前,有两个概念需要理解,我们的开发都是以它们为基础

1. 文件流

当用标准库打开或操作文件时,我们可以看作用一根水管与这个文件连接,里面流淌的是文件数据。文件流又分为下面 2 种:

  1. 文本流:传输文本
  2. 字节流:传输字节

他们主要有 3 点不同:

  1. 从文本流中读取的数据被划分成以换行 \n 字符终止的行,而二进制流只是一系列长的字符
  2. 在某些系统上,文本文件只能包含打印字符,水平制表符和换行符,因此文本流可能不支持其他字符,而二进制流可以处理任何字符值。
  3. 当文件再次读入时,在文本流中的换行符之前立即写入的空格字符可能会消失。更一般地说,不需要在从文本流中读取或写入文本流的字符与实际文件中的字符之间进行一对一映射。

注意:在 GNU C 库和所有 POSIX 系统中,文本流和二进制流之间没有区别,当您打开流时,无论是否要求二进制,都可以获得相同的流,此流可以处理任何文件内容,并且没有文本流有时具有的限制。

文本流和字符流更加详细的区别请参考这里

2. FILE 对象

我们用 FILE 对象来在用户空间表示一个打开的文件,该对象是一个结构体,包含了标准 IO 库管理当前文件流的信息。

例如,fopen 函数成功打开文件的操作:

FILE *fp = fopen("1.txt", "r");

fopen & fclose

使用 fopenfclose 来打开和关闭一个文件,来分别看看它们的声明:

fopen

使用 fopen 来打开一个文件:

#include <stdio.h>

/*
 * path: 要打开的文件名称,必须时包含路径
 * mode: 文件的打开模式
 * return: 成功返回 FILE 指针,失败返回 NULL,并设置 errno
 */
FILE *fopen (const char *path, const char *mode);

其中文件的打开模式分为 6 种:

  1. r:只读,文件必须存在
  2. r+: 可读可写
  3. w: 可读可写,会清空文件
  4. w+: 可读可写,文件不存在就创建
  5. a: 在文件尾部追加内容,如果文件不存在就创建
  6. a+:可读可写

其中 r = readw = writea = append,意思分别是:读,写,追加

fclose

使用 fclose 来关闭一个文件:

#include <stdio.h>

/*
 * stream: 要关闭的文件指针
 * return: 成功返回 0,失败返回 EOF,并设置 errno
 */
int fclose(FILE *stream);

来看一个打开和关闭文件的例子。

实例 1: fopen_close.c

这个例子使用 fopenfclose 来打开和关闭一个文件 1.txt

/*
 * fopen_close.c
 * fun: test fopen and fclose function.
 */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
	if (argc < 2) {
		printf("Usage: ./fopen_close 1.txt\n");
		exit(1);
	}

	FILE *fp = fopen(argv[1], "r");

	if (NULL == fp) {
		printf("file open fail.\n");
		exit(1);
	}

	printf("file open success.\n");

	fclose(fp);

	printf("file close success.\n");
	return 0;
}


gcc 编译:

gcc fopen_close.c -o fopen_close

运行,因为我们r 打开文件,所以必须先建立 1.txt 文件

./fopen_close 1.txt

file open success.
file close success.

文本流:单字符读写

在标准 IO 库中有下面 3 组对应的输入输出字符函数:

#include <stdio.h>

// 从文件中读取或写入一个字符
int getc(FILE *fp);
int putc(int c, FILE *fp);

// 从文件中读取或写入一个字符
int fgetc(FILE *fp);
int fputc(int c, FILE *fp);

// 从标准输入读取,或者输出到标准输出
int getchar(void);
int putchar(int c);

这些函数的参数都很好理解,我们以 fgetcfputc 为例:

实例 2:fget_putc.c

我们用 fgetcfputc拷贝一个文件:

/*
 * fgetc_putc.c
 * fun: using fgetc and fputc to copy file.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
	if (argc < 3)
	{
		printf("Usage: ./fgetc_putc 1.txt 1.txt.bak.\n");
		exit(1);
	}

	FILE *fp_in = fopen(argv[1], "r");
	FILE *fp_out= fopen(argv[2], "w+");

	if (NULL == fp_in || NULL == fp_out) {
		printf("file open failed\n");
		exit(1);
	}

	char ch = 0;
	while ((ch = fgetc(fp_in)) != EOF)
		fputc(ch, fp_out);

	fclose(fp_in);
	fclose(fp_out);

	return 0;
}

编译:

gcc fgetc_putc.c -o copy_file

运行:

./copy_file 1.txt 1.txt.bak

想必你也发现了这样一个一个字符的读取和写入效率肯定不高,那有没有一次读取或者写入一行的函数呢?我们来看看行 IO。

文本流:行读写 IO

行 IO 每次读取或者写入文件中的一行,比单个字符的输入输出更加有效率,常用的函数有 2 组:

#include <stdio.h>

char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);

// gets 有缓存区溢出漏洞,建议弃用。
char *gets(char *buf);
int puts(const char *s);

实例 3:fgets_puts.c

这里例子中程序输出我们输入的每一行

/*
 * fgets_puts.c
 * fun: print stdin to stdout using fgets and fputs.
 */

#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 100

int main(void) {
	char buf[MAXSIZE] = { 0 };

	while (fgets(buf, MAXSIZE, stdin) != NULL)
		fputs(buf, stdout);

	return 0;
}

编译:

gcc fgets_puts.c -o fgets_puts

运行:

./fgets_puts
hello world
hello world

记住,不要使用 gets 它很危险

字节流:二进制 IO

前面我们每次传输的都是一个个的字符,如果我们要读写指定大小的结构,则我们需要知道一个结构用字符表示的大小等信息,非常麻烦。因此标准库提供了二进制 IO 来读写指定数量的字节,因为我们可以使用 sizeof(type) 来得到一个类型的字节大小,所以可以使用字节流方便的读取和写入数据:

#include <stdio.h>

/*
 * ptr: 存储读取内容的指针
 * size: 每次读取的数据大小
 * nmemb: 要读取的数据个数或者次数
 * stream: 读取的文件指针
 */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

/*
 * ptr: 要写入的数据指针
 * size: 每次写入的数据大小
 * nmmeb: 写入的数据个数或者次数
 * stream: 要写入的文件指针
 */
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

实例 4:fread_write.c

/*
 * fread_write.c
 * fun: write bin data to file and read, print.
 */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
	if (argc < 2) {
		printf("Usage: ./fread_write 1.txt.\n");
		exit(1);
	}

	// write
	FILE *fp = fopen(argv[1], "w+");
	int data[5] = { 1, 2, 3, 4, 5 };
	fwrite(data, sizeof(int), 5, fp);
	fclose(fp);

	// read
	fp = fopen(argv[1], "r");
	int data_bak[5] = { 0 };
	fread(data_bak, sizeof(int), 5, fp);
	fclose(fp);

	// show
	for (int i = 0; i < 5; ++i)
		printf("%d ", data_bak[i]);

	printf("\n");
	return 0;
}

使用二进制 IO 可以也可以将文件读入内存或者写入内存文件到磁盘上。

文件流的定位

在底层 IO 我们可以使用 lseek 来移动文件指针,在标准 C 中我们也有 3 个非常常用的移动文件指针的函数

#include <stdio.h>

// 与 lseek 参数相同
int fseek(FILE *stream, long offset, int whence);

// 返回当前文件指针的位置
long ftell(FILE *stream);

// 移动文件指针到开头
void rewind(FILE *stream);

实例 5:file_length.c

我们来使用标准库的文件定位函数来求一个文件的长度:

#include <stdio.h>

int main(int argc, char *argv[]) {

	FILE *fp = fopen(argv[1], "r");

	// 移动文件指针到文件末尾
	fseek(fp, 0, SEEK_END);

	// 求出文件长度
	long file_length = ftell(fp);

	printf("file length = %ld\n", file_length);

	// 定位到文件开头
	rewind(fp);

	// 查看当前文件指针位置
	printf("cur pos = %ld\n", ftell(fp));

	return 0;
}

编译:

gcc file_length.c -o file_length

运行,1.txt 文件的内容是 hello,加上末尾的 ‘\0’ 一共 6 个字节:

./file_length 1.txt

file length = 6
cur pos = 0

成功求出了文件的长度 = 6 。

格式化 IO

在标准库中,我们可以使用 printf 的许多衍生函数来格式化输出到文件,buf 中等:

#include <stdio.h>

// 输出到 stdout
int printf(const char *format, ...);

// 输出到文件
int fprintf(FILE *stream, const char *format, ...);

// 输出到缓存 str 中
int sprintf(char *str, const char *format, ...);

实例 6:test_printf.c

我们来格式化打印一段文本,来练习上面的 3 个函数:

#include <stdio.h>

int main(int argc, char *argv[]) {
	// stdout
	printf("Hello World\n");

	// file
	FILE *fp = fopen(argv[1], "w+");
	fprintf(fp, "%s", "Hello World");
	fclose(fp);

	// buffer
	char buf[20] = { 0 };
	sprintf(buf, "%s", "Hello World");

	puts(buf);

	return 0;
}

编译:

gcc test_printf.c -o test_printf

运行:

./test_printf 1.txt
Hello World
Hello World

# 查看 1.txt
cat 1.txt
Hello World

可以看到我们成功在 stdoutbuf1.txt 中写入了 Hello World 文本,实现了格式化输出的功能。

结语

这次主要介绍了 C 标准库的一些常用的函数,掌握这些常用的函数即可应付大多数的工作,如果遇到不能实现的功能,一定要搜索 Google,因为 API 那么多不可能全部介绍完啊。就算我全部介绍完,你也不一定有耐心看下去是不,所以啊,最重要的是形成自己的学习方法,则比什么都重要。

最后,感谢你的阅读,我们下次再见 :)

欢迎关注我的公众号:「cdeveloper」

cdeveloper at 07/28/17