LinuxC语言编程03:文件I/O
标准I/O
标准I/O是指标准C中定义的一组输入和输出的API,与操作系统无关,具备可移植性.
流
标准I/O将打开文件的信息抽象成为流(stream),使用FILE
结构体代表流.标准I/O的所有操作都是围绕FILE
结构体进行的.
文本流与二进制流
Windows操作系统区分文本流与二进制流,两者间最显著的区别在文本流中\n
会被替换成\r\n
,而二进制流不会做这种转换.而Linux操作系统不区分文本流与二进制流,不会做这种转换.
流的缓冲类型
流有三种缓冲类型:
- 全缓冲: 当流的缓冲区无数据或无空间时才执行实际I/O操作.使用标准I/O打开文件时默认是全缓冲.
- 行缓冲: 当在输入输出中遇到换行符
\n
时才执行实际I/O操作.当流与终端交互时(输入或输出)是行缓冲. - 无缓冲: 直接与文件进行交互,不进行缓存.向标准错误流输出时是无缓冲.
预定义的流
标准I/O中预定义了3个流,程序运行时自动打开这三个流:
预定义的流 | 文件描述符 | 关键字 |
---|---|---|
标准输入流 | 0 | stdin |
标准输出流 | 1 | stdout |
标准错误流 | 2 | stderr |
除了上面3个预定义的的流以外,一个C程序最多打开1021个流.
流的打开与关闭
流的打开
使用fopen()
函数打开一个流,其函数原型如下:
FILE *fopen(const char *pathname, const char *mode);
成功打开流时返回流的指针,出错时返回NULL
,并设置错误号errno
.
pathname
参数表示要打开的文件路径,mode
参数表示文件的打开方式,可选值如下:
文件打开方式 | 意义 |
---|---|
"r" 或"rb" | 以只读方式打开文件.若文件不存在则报错. |
"r+" 或"rb+" | 以读写方式打开文件.若文件不存在则报错. |
"w" 或"wb" | 以只写方式打开文件.若文件存在则清空原文件;若文件不存在则创建文件. |
"w+" 或"wb+" | 以读写方式打开文件.若文件存在则清空原文件;若文件不存在则创建文件. |
"a" 或"ab" | 以只写方式打开文件.若文件存在则将数据追加到文件末尾;若文件不存在则创建文件. |
"a+" 或"ab+" | 以读写方式打开文件.若文件存在则将数据追加到文件末尾;若文件不存在则创建文件. |
上面的
"b"
参数表示以二进制方式打开文件,Linux下不区分文本流与二进制流,该参数可忽略.
下面表格对比了各种打开方式的区别:
要求 | r | w | a | r+ | w+ | a+ |
---|---|---|---|---|---|---|
文件必须存在 | ✔ | ✔ | ||||
清空原文件中的内容 | ✔ | ✔ | ||||
流可以读 | ✔ | ✔ | ✔ | ✔ | ||
流可以写 | ✔ | ✔ | ✔ | ✔ | ✔ | |
流只能在尾端写 | ✔ | ✔ |
使用fopen()
函数创建的文件的访问权限为0666
(rw-rw-rw-
),但系统的umask
设定会影响文件的访问权限,实际权限的表达式为实际权限 = 默认权限&~umask
.默认的umask
值为022
,因此使用fopen()
函数创建的文件的实际访问权限为0644
(rw-r--r--
).
流的关闭
使用fclose()
函数打开一个流,其函数原型如下:
int fclose(FILE *stream);
成功时关闭流时返回0,出错时返回EOF
,并设置错误号errno
.
流关闭时自动刷新缓冲区中的数据并释放缓冲区,流关闭后不能对其执行任何操作.
当一个程序正常终止时,所有打开的流都会被关闭.
处理错误信息
当打开或关闭流的过程中发生错误时,程序会将错误号存放在变量errno
中,该变量被定义在头文件errno.h
中.使用perror()
和strerror()
可以得到错误的详细信息,两个函数的函数原型如下:
void perror(const char *s);
char *strerror(int errno);
perror()
函数先输出字符串s
,再输出错误的详细信息.strerror()
函数根据错误号errno
返回对应的错误的详细信息.
下面例子演示了一个常见的打开文件并处理错误的方式:
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main() {
FILE *fp;
if ((fp = fopen("file.txt", "r+")) == NULL) {
// 下面两种写法是等价的
perror("打开流的过程中发生错误");
printf("打开流的过程中发生错误: %s\n", strerror(errno));
return -1;
}
if ((fclose(fp)) == EOF) {
// 下面两种写法是等价的
perror("关闭流的过程中发生错误");
printf("关闭流的过程中发生错误: %s\n", strerror(errno));
return -1;
}
return 0;
}
流的读写
流支持三种不同的读写方式:
- 按字符读写:
fgetc()
/fputc()
每调用一次读写一个字符. - 按行读写:
fgets()
/fputs()
每调用一次读写一行. - 按对象读写:
fread()
/fread()
每调用一次读写若干个具有相同长度的对象.
按对象读写最灵活,最常被使用.
按字符读写
按字符输入
下列函数用来读取一个字符:
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
其中fgetc()
与getc()
是等价的,它们从stream
中读取一个字符,getchar()
从标准输入stdin
中读取一个字符,等同于fgetc(stdin)
.
成功读取时返回读取到的字符,读到文件末尾或出错时返回EOF
.
按字符输出
下列函数用来输出一个字符:
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
其中fgetc()
与getc()
是等价的,它们将c
转化为unsigned int
类型变量并将其写入到stream
中,getchar()
向标准输入stdin
中写入一个字符,等同于fputc(c, stdout)
.
成功写入时返回写入的字符,出错时返回EOF
并设置错误号errno
.
下面的例子使用fgetc
和fputc
实现文件的复制:
#include<stdio.h>
int main(const int argc, const char *argv[]) {
// 若传入参数不够则提醒用户输入完整参数
if (argc < 3) {
printf("Usage: %s <src_file> <dst_file>\n", argv[0]);
return -1;
}
// 打开输入流与输出流
FILE *fps, *fpd;
if ((fps = fopen(argv[1], "r")) == NULL) {
perror("打开src_file的过程中发生错误");
return -1;
}
if ((fpd = fopen(argv[2], "w")) == NULL) {
perror("创建dst_file的过程中发生错误");
return -1;
}
// 进行复制
char ch;
while((ch = fgetc(fps)) != EOF) {
fputc(ch, fpd);
}
// 关闭两个流
fclose(fps);
fclose(fpd);
return 0;
}
按行读写
按行输入
下列函数用于读取一行:
char *gets(char *s);
char *fgets(char *s, int size, FILE* stream);
gets()
从标准输入stdin
中读取一行数据,不建议使用,容易造成缓冲区溢出.fget()
从流stream
中读取数据,遇到\n
或读到size-1
个字符时返回,因此独到的字符串总是包含\0
.
成功读取时返回读到的缓冲区字符串指针s
,读到文件末尾或出错时返回NULL
.
按行输出
下列函数用于输出一行:
int puts(const char *s);
int fputs(const char *s, FILE *stream);
puts()
将缓冲区s
中的字符串写入到标准输出流stdout
中,并追加\n
.fputs()
将缓冲区s
中的字符串写入到流stream
中,并追加\n
.
成功写入时返回输出的字符个数,出错时返回EOF
并设置错误号errno
.
尽管我们称其为按行输出,但puts()
和fputs()
仅仅是将字符串s
中的内容输出,不会受到s
中可能存在的换行符\n
影响.
下面程序用于统计传入的文件的行数:
#include<stdio.h>
#include<string.h>
#define N 20 // 指定缓冲区大小
int main(const int argc, const char *argv[]) {
// 若传入参数不够则提醒用户输入完整参数
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return -1;
}
// 打开文件
FILE *fp;
if ((fp = fopen(argv[0], "a+")) == NULL) {
perror("打开文件的过程中发生错误");
return -1;
}
// 读取到文件末尾,计算行数
int line = 0;
char buff[N];
while (fgets(buff, N, fp) != NULL) {
// 若缓冲区内容以\n结尾,才表示读完一整行
if (buff[strlen(buff)-1] == '\n') line++;
}
return 0;
}
按对象读写
下列函数用于从流中读写若干个对象:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fread()
和fwrite()
用于对流stream
读写最多nmemb
个大小为size
字节的对象,并存入缓冲区*ptr
中.
读写成功时返回读写的对象个数,出错时返回EOF
并设置错误号errno
.
下面的例子使用fread()
和fwrite()
实现文件的复制:
#include<stdio.h>
#define N 20 // 指定缓冲区大小
int main(const int argc, const char *argv[]) {
// 若传入参数不够则提醒用户输入完整参数
if (argc < 3) {
printf("Usage: %s <src_file> <dst_file>\n", argv[0]);
return -1;
}
// 打开输入流与输出流
FILE *fps, *fpd;
if ((fps = fopen(argv[1], "r")) == NULL) {
perror("打开src_file的过程中发生错误");
return -1;
}
if ((fpd = fopen(argv[2], "w")) == NULL) {
perror("创建dst_file的过程中发生错误");
return -1;
}
// 进行复制
char buff[N];
int nmemb;
while((nmemb = fread(buff, 1, N, fps)) > 0) {
fwrite(buff, 1, nmemb, fpd);
}
// 关闭两个流
fclose(fps);
fclose(fpd);
return 0;
}
流的刷新
流的刷新指的是立即执行实际I/O操作并清空缓冲区,在以下三种情况下,流会刷新:
- 全缓冲情况下缓冲区满或行缓冲情况下遇到换行符.
- 流关闭时.
- 调用
fflush()
函数显式刷新缓冲区.
fflush()
函数的函数原型如下:
int fflush(FILE *stream);
若刷新成功则返回0,出错则返回EOF
.
下面例子演示刷新缓冲区的作用:若不刷新缓冲区,则不会进行实际I/O,文件的大小不会改变:
#include<stdio.h>
int main() {
// 打开一个一直不关闭的输出流
FILE *fp;
if ((fp = fopen("test.txt", "w")) == NULL) {
perror("打开文件的过程中发生错误");
return -1;
}
fputc('a', fp);
// fflush(fp) // 若注释掉该句,则发现创建的文件一直为空
while(1);
return 0;
}
运行上述程序,我们发现创建的文件的大小一直为0,说明不进行缓冲的情况下不会进行实际的I/O操作.
流的定位
下列函数与流的定位有关:
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
-
ftell()
函数返回流stream
的当前读写位置,出错时返回EOF
并设置errno
. -
fseek()
函数对流进行定位,成功时返回0,出错时返回EOF
并设置errno
.该函数将流定位到
whence
加上offset
个字节的位置,whence
的取值为下面三个常量之一:whence
意义 SEEK_SET
文件开头 SEEK_CUR
文件的当前读写位置 SEEK_END
文件末尾 -
rewind()
函数将流定位到文件开始位置,相当于fseek(fp, 0, SEEK_SET)
下面例子演示了fseek()
函数的用法:
#include <stdio.h>
int main () {
FILE *fp = fopen("file.txt","w+");
fputs("This is a file", fp);
fseek(fp, 7, SEEK_SET);
fputs(" C Programming Langauge", fp);
fclose(fp);
return 0;
}
最终文件中的内容如下:
This is C Programming Langauge
判断流的出错与结束
-
下面函数用于判断流是否出错:
int ferror(FILE *stream);
若流
stream
出错则返回非零数,否则返回0. -
下面函数用于判断流是否已读到文件末尾:
int feof(FILE *stream);
若已读到流
stream
末尾则返回非零数,否则返回0.
格式化输入输出
fscanf()
和fprintf()
两个函数用于对流进行格式化输入输出,用法类似于scanf()
和printf()
.
int fscanf(FILE *stream, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
读写成功时返回读写的字符个数,出错时返回EOF
并设置errno
.
下面例子以追加形式每隔一秒向文件log.txt
中写入当前时间以及行号:
#include<stdio.h>
#include<string.h>
#include<unistd.h> // sleep()函数的头文件
#include<time.h> // time()和localtime()函数的头文件
#define N 20 // 指定缓冲区大小
int main() {
// 打开log.txt文件
FILE *fp;
if ((fp = fopen("log.txt", "a+")) == NULL) {
perror("打开log.txt的过程中发生错误");
return -1;
}
// 读取到文件末尾,计算行数
int line = 0;
char buff[N];
while (fgets(buff, N, fp) != NULL) {
if (buff[strlen(buff)-1] == '\n') line++;
}
// 不断循环向文件末尾追加内容
time_t t; // 存储以秒数表示的时间
struct tm *tp; // 存储以可读格式表示的时间
while(1) {
time(&t);
tp = localtime(&t);
fprintf(fp, "%02d, %04d-%02d-%02d %02d:%02d:%02d\n", ++line,
tp->tm_year+1900, tp->tm_mon+1, tp->tm_mday,
tp->tm_hour, tp->tm_min, tp->tm_sec);
fflush(fp);
sleep(1);
}
return 0;
}
运行上述程序一段时间后,按Ctrl+c终止程序,发现时间信息已经按照期望的格式存入log.txt
文件中.再次运行该程序一段时间,发现时间信息被追加进log.txt
文件中且行号能正确的按照顺序接上.
01, 2019-12-10 08:30:53
02, 2019-12-10 08:30:54
03, 2019-12-10 08:30:55
04, 2019-12-10 08:31:12
05, 2019-12-10 08:31:13
06, 2019-12-10 08:31:14
07, 2019-12-10 08:31:15
文件I/O
文件I/O是一套只适用于Linux系统的文件操作的API,不带有缓冲机制.下面表格对比标准I/O与文件I/O的区别:
标准I/O | 文件I/O | |
---|---|---|
标准 | ANSI C | POSIX |
是否带有缓冲机制 | 有缓冲 | 无缓冲 |
核心对象 | 流 | 文件描述符 |
在linux操作系统下,标准I/O是基于文件I/O实现的.因此,标准I/O也被称为高级I/O,文件I/O也被称为低级I/O.
文件描述符
文件描述符是一个非负整数,Linux为程序中每个打开的文件分配一个文件描述符,文件I/O的所有操作都是围绕文件描述符进行的.
其中,预留的三个文件描述符0
,1
,2
分别代表标准输入流stdin
,标准输出流stdout
和标准错误流stderr
.
文件的打开与关闭
文件的打开
使用open()
函数打开文件,被定义在头文件fcntl.h
中,函数原型如下:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
成功打开文件时返回文件描述符,出错时返回EOF
并设置错误号errno
.
pathname
参数表示文件名;flags
参数表示文件的打开方式,是一系列基本常量相与|
得到的结果;mode
参数仅在创建新文件时有效,表示被打开文件的权限,同样受到umask
影响.flags
参数的取值及其作用如下:
取值 | 作用 | |
---|---|---|
O_RDONLY | 以只读方式打开文件 | 这三个参数互斥 |
O_WRONLY | 以只写方式打开文件 | |
O_RDWR | 以读写方式打开文件 | |
O_CREAT | 若该文件不存在则创建新文件,并设置其权限为mode 参数 | |
O_EXCL | 设置O_CREAT 时该位有效,若文件存在则报错. | |
O_NOCTTY | 若欲打开的文件为终端机设备时,则不会将该终端机当成进程控制终端 | |
O_TRUNC | 若文件存在且以可写的方式打开时则清空文件中原有数据 | |
O_APPEND | 以追加方式打开文件 |
文件的关闭
使用close()
函数关闭一个打开的文件,被定义在头文件unistd.h
中,函数原型如下:
int close(int fd)
成功关闭文件时返回0,出错时返回EOF
并设置错误号errno
.
与标准I/O类似,当一个程序正常终止时,所有打开的文件都会被关闭.当文件被关闭后,对应的文件描述符就不再代表该文件.
文件I/O中发生错误时同样会设置错误号errno
,并使用perror()
和strerror()
得到错误的详细信息.
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main() {
int fd;
if ((fd = open("test.txt", O_RDWR|O_CREAT|O_EXCL, 0666) == EOF)) {
perror("创建文件过程中出错");
return -1;
}
if (close(fd) == EOF) {
perror("关闭文件过程中出错");
return -1;
}
return 0;
}
文件的读写
read()
函数和write()
函数用于进行文件读写,被定义在头文件unistd.h
中,函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
上述函数通过缓冲区buf
向文件描述符fd
表示的文件中读写最多count
个字节的数据.读写成功时返回读写的字节数,出错时返回EOF
并设置错误号errno
.
下面程序使用文件I/O实现文件复制:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
# define N 20
int main(const int argc, const char *argv[]) {
// 若传入参数不够则提醒用户输入完整参数
if (argc < 3) {
printf("Usage: %s <src_file> <dst_file>\n", argv[0]);
return -1;
}
// 打开原文件与目标文件
int fds, fdt;
if ((fds = open(argv[1], O_RDONLY)) == EOF) {
perror("打开src_file的过程中发生错误");
return -1;
}
if ((fdt = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0666)) == EOF) {
perror("创建dst_file的过程中发生错误");
return -1;
}
// 进行复制
char buff[N];
int n;
while((n = read(fds, buff, N)) > 0) {
write(fdt, buff, n);
}
// 关闭两个文件
close(fds);
close(fdt);
return 0;
}
文件的定位
lseek()
函数用于对文件进行定位,被定义在头文件unistd.h
中,函数原型如下:
off_t lseek(int fd, off_t offset, int whence);
其fd
参数表示被定位文件的文件描述符,offset
和whence
参数与fseek()
函数的用法完全相同.
目录的访问
使用opendir()
函数打开一个目录文件,使用readdir()
函数读取目录流中的内容,使用closedir()
函数关闭一个目录文件,它们都被定义在头文件dirent.h
中,函数原型如下:
DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
opendir()
函数成功打开目录文件时返回一个DIR
结构体指针,出错时返回NULL
并设置错误号errno
.readdir()
函数成功读取到目录流中的下一个目录项时返回一个struct dirent
结构体指针,读到文件流末尾时或出错时返回NULL
并设置错误号errno
.closedir()
函数成功关闭目录文件时返回0,出错时返回EOF
并设置错误号errno
.
下面程序打印给定目录的所有目录项:
#include<stdio.h>
#include<dirent.h>
int main(const int argc, const char *argv[]) {
// 若传入参数不够则提醒用户输入完整参数
if (argc < 2) {
printf("Usage: %s <directory>\n", argv[0]);
return -1;
}
// 打开目录流
DIR *dirp;
if ((dirp = opendir(argv[1])) == NULL) {
perror("打开目录流出现错误");
return -1;
}
// 遍历输出所有目录项
struct dirent *dp;
while ((dp = readdir(dirp)) != NULL) {
printf("%s\n", dp->d_name);
}
return 0;
}
文件的访问权限
chmod()
和fchmod()
函数用于修改文件的访问权限,它们都被定义在头文件sys/stat.h
中,函数原型如下:
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
成功修改访问权限时返回0,出错时返回EOF
并设置错误号errno
.
文件的属性
stat()
,lstat()
和fstat()
函数用来获取文件属性,函数原型如下:
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
上述函数读取文件属性并将其存入参数statbuf
指向的结构体中.成功读取到文件参数时并返回0,出错时返回EOF
并设置错误号errno
.
stat()
函数和lstat()
函数在大多数情况下是等价的,区别在于:若传入的文件是符号链接,则stat()
函数获取的是目标文件的属性,而lstat()
函数获取的是链接文件本身的属性.
结构体struct stat
存储文件的属性,包含的成员参数如下:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
};
其中st_mode
成员存储了文件类型和访问权限信息.
-
可以根据判断表达式
st_mode&0170000
的运算结果来确定文件类型:表达式 st_mode&0170000
的运算结果文件类型 0140000 套接字文件 0120000 符号链接文件 0100000 普通文件 0060000 块设备文件 0040000 目录文件 0020000 目录文件 0010000 管道文件 -
可以根据
st_mode
与以下掩码相与的结果判断用户的访问权限:掩码 值 意义 S_IRWXU 00700 拥有者是否具有读写执行权限 S_IRUSR 00400 拥有者是否具有读权限 S_IWUSR 00200 拥有者是否具有写权限 S_IXUSR 00100 拥有者是否具有执行权限 S_IRWXG 00070 拥有组其他成员是否具有读写执行权限 S_IRGRP 00040 拥有组其他成员是否具有读权限 S_IWGRP 00020 拥有组其他成员是否具有写权限 S_IXGRP 00010 拥有组其他成员是否具有执行权限 S_IRWXO 00007 其他用户是否具有读写执行权限 S_IROTH 00004 其他用户是否具有读权限 S_IWOTH 00002 其他用户是否具有写权限 S_IXOTH 00001 其他用户是否具有执行权限
下面程序仿照ls -l
的格式输出传入文件的属性信息:
#include<stdio.h>
#include<time.h>
#include<sys/stat.h>
int main(const int argc, const char *argv[]) {
// 若传入参数不够则提醒用户输入完整参数
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return -1;
}
// 获取文件的属性
struct stat statbuf;
if (lstat(argv[1], &statbuf) == EOF) {
perror("获取文件属性失败");
return -1;
}
// 判断文件类型并输出
switch (statbuf.st_mode & S_IFMT) {
case S_IFREG:
printf("-");
break;
case S_IFDIR:
printf("d");
break;
// 省略其他文件类型的判断过程
}
// 判断文件权限并输出
for (int n=8; n>=0; n--) {
if (statbuf.st_mode & (1<<n)) {
switch (n%3) {
case 2:
printf("r");
break;
case 1:
printf("w");
break;
case 0:
printf("x");
break;
}
} else {
printf("-");
}
}
//输出文件大小
printf("\t%lu", statbuf.st_size);
// 输出文件最后修改时间
struct tm *tp = localtime(&statbuf.st_mtime);
printf("\t%04d-%02d-%02d %02d:%02d:%02d",
tp->tm_year+1900, tp->tm_mon+1, tp->tm_mday,
tp->tm_hour, tp->tm_min, tp->tm_sec);
// 输出文件名
printf("%s\n", argv[1]);
return 0;
}