Linux 标准IO

标准IO介绍

所谓的IO就是指:input和output,即输入和输出。
如通过键盘鼠标输入数据,通过显示器、控制台、终端打印输出信息。

在操作系统中IO是基于文件来实现的。

文件是一组相关数据的有序集合

标准IO与缓冲机制

操作系统基于不同类型的文件进行IO操作。同时操作系统也提供了一系列丰富的函数,可供C\C++程序调用,这些丰富的函数集合,称之为:标准IO(由ANSI C标准定义)。

标准IO

标准IO库,基于缓冲机制,减少系统调用提高IO的性能。
无缓冲调用示例
alt text
有缓冲调用示例
alt text

即缓冲机制会将标准IO的输入输出先写进缓冲区中,达到一定条件时再进行系统调用。

文件流(FILE)的含义

FILE和流
标准IO的操作,都是基于一个结构体类型进行的,即FILE结构体对象。
FILE定义在stdio.h中,是一个结构体类型。
下面是他的定义:
alt text
我们打开一个文件时就会返回一个流

1
2
3
#include <stdio.h>
// 以只读方式打开文件
FILE* fp = fopen("hello", "r");

在操作系统中,我们一般将FILE称之为流(stream), 无论什么文件类型,IO操作所需的数据就像流水一样输入或输出。
流中的换行符和缓冲模式
在Windows系统中:

  • 二进制流(FILE结构体指针),数据流中换行符基于\n
  • 文本流( FILE结构体指针),数据流中换行符基于\r\n
    在Linux系统中,不区分二进制流和文本流,换行符统一是\n

标准IO通过缓冲提高性能,应用在流(FILE结构体指针)中主要有3种缓冲模式:

  • 全缓冲,缓冲区无书或无空间才执行实际的IO操作(即系统调用)
  • 行缓冲,在输入和输出中遇到换行符,进行IO操作
  • 无缓冲,直接系统调用进行IO操作,流不进行缓冲

预定义流
标准IO预定义了3个流,任何程序在运行的时候都会自动打开:

名称 文件描述符 文件描述符 流对象名称(FILE*) 描述
标准输入流 0 STDIN_FILENO stdin 向程序内输入数据,默认从键盘读取,也可以切换为其它文件,默认行缓冲模式(等待用户的回车),如果输入为文件等非交互设备,可能为全缓冲。
标准输出流 1 STDOUT_FILENO stdout 程序对外输出数据,默认输出到屏幕上(控制台),也可以切换为其它文件,Linux下默认行缓冲模式,Windows经VS IDE测试默认无缓冲。
标准错误流 2 STDERR_FILENO stderr 用于输出程序错误信息,默认输出到屏幕,可以切换为其它文件,默认无缓冲模式。
stdout演示行缓冲
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
fprintf(stdout, "1\n");
//sleep(3);
fprintf(stdout, "2, ");
slepp(3);
fprintf(stdout, "3\n");
}

程序会先输出 1 ,3秒后 2,3 一起输出。

标准IO流的打开与关闭

流的打开和关闭

标准IO中提供了fopen函数,用于打开标准IO流,原型如下:

1
2
3
4
5
6
FILE* fopen(
// 被打开的文件名称
char const* _FileName;
// 打开方式
char const* _Mode;
);

打开成功返回流指针(FILE结构体指针);出错返回NULL。

打开模式

模式 含义 规则
“r”或”rb” 只读打开 文件必须存在
“r+”或”r+b” 可读可写打开 文件必须存在,从文件起始位置写入
“w”或”wb” 仅写打开 不存在则创建;存在会清空原有内容
“w+”或”w+b” 可读可写打开 不存在则创建;存在会清空原有内容
“a”或”ab” 仅写打开 不存在则创建;如存在且有内容,在文件末尾追加新内容
“a+”或”a+b” 可读可写打开 不存在则创建;如存在且有内容,在文件末尾追加新内容
当给定参数”b”时,表示以二进制方式打开(仅Windows有效),Linux忽略b参数。

流的关闭
标准IO中提供了fclose函数,用于关闭标准IO流,原型如下:

1
2
3
4
int fclose(
// 需要关闭的流指针(FILE结构体);
FILE* _Stream;
)
  • 关闭成功返回整数0;关闭失败返回EOF(整数-1),并设置errno
  • 关闭后自动刷新缓冲区数据并释放其空间
  • 程序正常停止后,所有打开的流都会被关闭,流关闭后不可再操作

处理错误信息

在打开流的过程中,有可能出现错误,标准IO提供了3个可操作对象供处理错误信息。

  • extern int errno; 错误号 -> #include <error.h>

  • void perror(const char *s); 打印错误信息,输出用户提供字符串s和当前错误 #include <stdio.h>

  • char * strerror(int errno); 根据错误号,返回错误信息字符串 #include <string.h>

  • 使用error错误号,需要包含 errno.h 头文件

  • 使用perror函数,需要包含 stdio.h 头文件

  • 使用strerror函数,需要包含 string.h 头文件

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

int main() {

FILE* fp = fopen("hello.txt", "r");
if (fp == NULL) {
// err
printf("错误号:%d\n", errno);
printf("错误信息:%s\n", strerror(errno));
exit(1);
}
// if (fp == NULL) {
// // err
// perror("错误信息(perror):\n");
// exit(1);
// }

fclose(fp);

exit(0);
}

标准IO流的输入输出

按字符输入

标准IO支持按字符、按行、按对象的方式进行输入和输出,我们首先学习按字符输入的相关标准IO函数:

  • int getchar(void);
  • int fgetc(FILE * stream);
  • int getc(FILE * stream);
    此三个函数均来自头文件: stdio.h

getchar
getchar函数,支持从stdin(标准输入流,默认就是键盘)读取一个字符。
原型:int getchar(void); 读取成功返回字符,失败返回EOF(-1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int main() {
char ch;
ch = getchar();
if (ch == EOF) {
printf("Error:getchar()\n");
exit(1);
}
printf("%c\n", ch);

exit(0);
}

fgetc
fgetc函数,支持从流(各类文件)中读取一个字符。
原型:int fgetc(FILE * stream); 读取成功返回字符,失败返回EOF(-1)

读取标准输入流

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
char ch;
ch = fgetc(stdin);
printf("%c\n", ch);

return 0;
}

读取文件流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h> // exit()

int main() {
FILE* fp = fopen("hello.txt", "r");
if (fp == NULL) {
perror("error message");
exit(1);
}
char buff;
while ((buff = fgetc(fp)) != EOF) {
printf("%c", buff);
}

fclose(fp);
exit(0);
}

getc
getc函数,支持从流(各类文件)中读取一个字符。
原型:int getc(FILE* stream) 读取成功返回字符,失败返回EOF(-1)

getc函数和fgetc函数的用法一致,但不同的点在于:

  • fgetc是库(stdio.h)函数
  • getc是一个宏函数
    getc不可以传递带有副作用的表达式作为参数

读取操作和fgetc一样。

带有副作用的表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

// 宏函数
#define FUNC_(x) (x)*(x)
// 普通函数
int func_sqrt(int x) {
return x * x;
}
int main() {
int x = 10, y = 10;
int xx = func_sqrt(++x);
int yy = FUNC_(++y);

printf("x:%d, xx:%d\n", x, xx);
printf("y:%d, yy:%d\n", y, yy);

return 0;
}

结果如下:
alt text

fgetc和getc区别
所以fgetc和getc的区别如下
功能:

  • 两者完全一致

性能:
getc是宏函数,避免了调用函数的堆栈操作,所以getc在极限性能上高于fgetc

场景:

  • 如无特殊需要,建议使用fgetc
  • getc的性能提升很多时候用不到,但是传入带有副作用的参数,会导致预期结果不一致,而fgetc则一切正常

按字符输出

按字符输出在标准IO库中也有3个常用函数:

  • int putchar(int c);
  • int fputc(int c, FILE* stream);
  • int putc(int c, FILE* stream);

需要包含 stdio.h,成功返回输出的字符;失败返回EOF(-1)

fputc (标准函数)和putc (宏函数)功能基本一致,区别和字符输入的fgetc和getc一样
putchar函数
puchar函数可以将一个字符输出到stdout(标准输出、默认就是控制台):
原型:int putchar(int c) 传入参数为被输出的字符

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
char ch = 'a';
putchar(ch);
putchar('\n');

return 0;
}

fputc和putc都是把字符输出到指定流中,略~

实现文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "用法:%s <源文件> <目标文件>\n", argv[0]);
exit(1);
}

const char* sourceName = argv[1];
const char* targetName = argv[2];

FILE* fpSrc = fopen(sourceName, "r");
if (fpSrc == NULL) {
perror("ERROR(fpSrc)");
exit(1);
}
FILE* fpTarget = fopen(targetName, "w");
if (fpTarget == NULL) {
perror("ERROR(fpTarget)");
exit(1);
}

char ch;
while ((ch = fgetc(fpSrc)) != EOF) {
fputc(ch, fpTarget);
}

fclose(fpSrc);
fclose(fpTarget);

exit(0);
}

按行输入

按行输入在标准IO中有2个函数:
#include <stdio.h>

  • char* gets(char* s);
  • char* fgets(char* s, int buf_size, FILE *stream);

成功输入则返回字符串s的指针,到文件末尾或出错返回NULL

gets不推荐使用,容易造成缓冲区溢出。

fgets函数
fgets函数用于从指定文件流读取一行内容(换行符为界定),将读入的内容记录到指定的字符串中:
原型:char* fgets(char* s, int buf_size, FILE* stream);

  • 参数1:记录读入数据的字符数组
  • 参数2:缓冲区大小(byte), 1个字符占用1个byte
  • 参数3:文件流
    同理,存入数据的字符数据最后1位用以记录\0, 有效记录字符数为buf_size-1

从文件test.txt中读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {

char buff[1024];
FILE* fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("ERROR(fp)");
return -1;
}
int len = sizeof(buff) / sizeof(buff[0]);
while (fgets(buff, len - 1, fp) != NULL) {
printf("%s", buff);
}

fclose(fp);

return 0;
}

按行输出

有两个函数:
#include <stdio.h>

  • int puts(const char* s);
  • int fputs(const char* s, FILE* _Stream);

成功返回0,失败返回EOF(-1);

puts函数
puts函数用以将指定字符串输出到stdout:
原型:int puts(const char* s) 传入参数为被输出的字符串

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char buf[] = "hello world";
puts(buf);

return 0;
}

fputs函数
原型: int fputs(const char* s, FILE* stream);
参数1:被输出字符串 , 参数2 : 输出文件流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {
char* buf = "你好";
FILE* fp = fopen("hello.txt", "w");
int res = fputs(buf, fp);
if (res == EOF) {
perror("ERROR(fputs)");
return -1;
}

fclose(fp);

return 0;
}

按对象输入输出

标准IO也提供了按对象进行输入和输出的相关函数。
#include <stdio.h>

  • size_t fread(void* ptr, size_t size, size_t n, FILE* fp);

  • size_t fwrite(const void* ptr, size_t size, size_t n, FILE* fp);

  • size_t 是类型重定义,本体是 unsigned long long

  • 两个函数的返回值是实际输入/输出的对象个数;出错则返回EOF(-1)

  • 可以输入/输出任意类型的文件(文本或二进制)

按对象输入

fread函数
fread函数可以从任意类型的文件中读取数据
原型: size_t fread(void* ptr, size_t size, size_t n, FILE* fp);

  • 参数1: 缓冲区指针,在调用函数前定义好,用来存入读取的数据
  • 参数2: 1个对象的大小
  • 参数3: 本次读取多少个对象
  • 参数4: 文件流指针

工作流程:

  1. 读取指定的文件流指针
  2. 本次调用读取n个对象,每个对象
  3. 大小为size
  4. 将读取的内容,存入缓冲区指针

按对象输出

fwrite函数
fwrite函数可以从任意类型的文件中读取数据
原型:size_t fwrite(void* ptr, size_t size, size_t n , FILE* fp);

  • 参数1: 缓冲区指针,被输出的数据
  • 参数2: 1个对象的大小
  • 参数3: 本次输出多少个对象
  • 参数4: 文件流指针
    工作流程:
  1. 输出内容到指定的文件流指针
  2. 本次调用输出n个对象,每个对象大小为size

对比标准IO的三种输入输出方式

函数 操作内容 适应文件类型
getchar/fgetc/putchar/fputc等 单个字符 文本文件
gets/fgets/puts/fputs等 一行字符(字符串) 文本文件
fread/fwrite 任意类型 文本/二进制

文件IO介绍

文件IO介绍

POSIX标准

文件IO和标准IO不同,它们遵循不同的标准,标准IO遵循的是ANSI C标准,文件IO遵循的是POSIX标准。
1983年,美国国家标准协会(ANSI)组成了一个委员会,X3J11,为了创立 C 的一套标准。经过漫长而艰苦的过程,该标准于1989年完成,这个版本的语言经常被称作ANSI C,或有时称为C89。
POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX )

什么是文件I/O?

  • posix(可移植操作系统接口)定义的一组函数
  • 不提供缓冲机制,每次读写操作都引起系统调用
  • 核心概念是文件描述符
  • 访问各类型文件
  • Linux下,标准IO基于文件IO实现
标准IO 文件IO
ANSI C标准 POSIX标准
带缓冲 无缓冲
流FILE管理打开文件 通过文件描述符管理打开文件

文件描述符

  • 每打开的文件都对应一个文件描述符
  • 文件描述符是一个非负整数。Linux为程序中每个打开的文件分配一个文件描述符。
  • 文件描述符从0开始分配,一次递增,且每个进程独立分配,比如进程1描述符从0开始分配,进程2描述符也是从0开始分配。
  • 文件IO操作通过文件描述符来完成
  • 描述符0代表标准输入,描述符1代表标准输出,描述符2代表错误输出。

打开文件

open函数用于创建或打开文件

1
2
#include <fcntl.h>
int open(const char* path, int oflag, mode_t mode );
  • 成功时返回文件描述符,出错时返回EOF
  • 打开文件时使用两个参数
  • 创建文件时使用第三个参数指定新文件的权限
  • 设备文件只能通过open打开,而不能通过open创建

mode的含义

mode 参数是一个 mode_t 类型的值,表示新创建文件的权限设置。它指定了文件的访问权限,通常是一个八进制数,可以由以下几种权限组合而成:

  • 用户权限(Owner permissions):
    • S_IRUSR:用户可读(4)
    • S_IWUSR:用户可写(2)
    • S_IXUSR:用户可执行(1)
  • 组权限(Group permissions):
    • S_IRGRP:组可读(4)
    • S_IWGRP:组可写(2)
    • S_IXGRP:组可执行(1)
  • 其他用户权限(Other permissions):
    • S_IROTH:其他用户可读(4)
    • S_IWOTH:其他用户可写(2)
    • S_IXOTH:其他用户可执行(1)
      比如mode 为0666, 表示的是用户权限为可读可写,组权限为可读可写,其他用户权限为可读可写。

关闭文件

1
2
#include <unistd.h>
int close(int fd);
  • 成功时返回0; 出错时返回EOF;
  • 程序结束时自动关闭所有打开的文件
  • 文件关闭后,文件描述符不再代表文件,不要使用。

文件IO操作

读文件

read函数用来从文件中读取数据:

1
2
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t count);
  • 成功时返回实际读取的字节数,出错时返回EOF
  • 读到文件末尾时返回0
  • buf是接受数据的缓冲区
  • count不应超过buf大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
int fd = open("hello.txt", O_RDONLY, 0666);
if (fd < 0) {
perror("open");
return -1;
}

char buf[64];


while (read(fd, buf, 64) != 0) {
printf("%s", buf);
}

close(fd);
}

写文件

write函数用来向文件写入数据:

1
2
#include <unistd.h>
ssize_t write(int fd, void* buf, size_t count);
  • 成功时返回实际写入的字节数,出错时返回EOF
  • buf是发送数据的缓冲区
  • count不应超过buf大小

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
将键盘输入的字符串写入文件
直到输入quit
*/

#include <fcntl.h> // open();
#include <unistd.h> // read() & write();
#include <stdio.h> // printf();
#include <stdlib.h> // exit();
#include <string.h> // strncmp();

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("格式为:%s <目标文件>\n", argv[0]);
exit(1);
}

const char* dest = argv[1];
int fd = open(dest, O_WRONLY | O_CREAT |O_TRUNC, 0666);
if (fd == EOF) {
perror("ERROR(open)");
exit(1);
}

char buf[1024];

while (fgets(buf, 1024, stdin) != NULL) {
if (strncmp(buf, "quit", 4) == 0) {
exit(0);
}
int res = write(fd, buf, strlen(buf));
if (res == EOF) {
perror("ERROR(write)");
exit(1);
}
memset(buf, 0, strlen(buf));
}

close(fd);

exit(0);
}

定位文件

lseek函数用来定位文件:

1
2
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • 成功时返回当前文件读写位置,出错时返回EOF
  • 参数offset和参数whence同fseek完全一样
  • whence包括 SEEK_SET, SEEK_CUR,SEEK_END

总结

  • read从描述符中读取内容到缓存中,接受三个参数,要读取的文件描述符,接受数据的缓存,以及读取多少字节
  • write向描述符中写入数据,接受三个参数,第一个参数为要写入的文件描述符,第二个参数为写入数据来自的缓存,第三个参数为要写入多少字节。
  • lseek定位文件,返回当前文件的读写位置,第一个参数为要定位的文件描述符,第二个参数为偏移量,第三个参数为位置宏

目录操作和文件属性

打开目录

opendir函数用来打开一个目录文件:

1
2
#include <fcntl.h>
DIR* opendir(const char* name);
  • DIR是用来描述一个打开的目录文件的结构体类型
  • 成功时返回目录流指针;出错时返回NULL

读取目录

readdir用来读取目录流中的内容:

1
2
#include <dirent.h>
struct dirent* readdir(DIR* dirp);
  • struct dirent是用来描述目录流中一个目录项的结构体类型
  • 包含成员char d_name[256] 参考帮助文档
  • 成功时返回目录流dirp中下一个目录项
  • 出错或者到末尾时返回NULL

关闭目录

closedir用来关闭一个目录文件:

1
2
#include <dirent.h>
int closedir(DIR* dirp);
  • 成功返回0,失败返回EOF

练习

打印指定目录下所有文件名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>

int main(int argc, char* argv[]){
if(argc < 2){
printf("Usage : %s <directory>\n", argv[0]);
return -1;
}

DIR * dir_stream;
struct dirent* dip;
if((dir_stream = opendir(argv[1])) == NULL){
perror("open dir failed: ");
return -1;
}

while((dip = readdir(dir_stream)) != NULL){
printf("%s ", dip->d_name);
}

closedir(dir_stream);

}