C语言IO操作
文件的概念和类型
概念:一组相关数据的有序集合
文件类型:
- 常规文件 r
- 目录文件 d
- 字符设备文件 c
- 块设备文件 b
- 管道文件 p, 进程间通信的机制
- 套接字文件 s, 进程间通信的机制
- 符号链接文件 l
如何理解标准IO
标准IO由ANSIC标准定义,就是用标准C语言定义好的一组用来输入和输出的API
主流操作系统(Linux,Windows)上都实现了C库
标准IO通过缓冲机制减少系统调用,实现更高的效率
流(FILE)的含义
标准IO用一个结构体数据类型来存放打开的文件的相关信息
标准IO的所有操作都围绕FILE来进行
FILE又被称为流(stream)
流分为两种流分别是
文本流:Windows系统中文本流的换行符占用两个字节用“\r\n”表示,LInux中用‘\n’表示
二进制流:Windows系统二进制流的换行符占用一个字节用“\n”表示,LInux中用‘\n’表示
流的缓冲
全缓冲:当流的缓冲区无数据或无空间时才执行实际IO操作
行缓冲:当在输入和输出中遇到换行符“\n”时,进行IO操作;当流和一个终端关联时,是典型的行缓冲
无缓冲:数据直接写入文件,流不进行缓冲,一般在打印错误信息时使用
标准IO预定义3个流,程序运行时自动打开
标准输入流 | 0 | STDIN_FILENO | stdin |
标准输出流 | 1 | STDOUT_FILENO | stdout |
标准错误流 | 2 | STDERR_FILENO | stderr |
流的打开
下列函数可用于打开一个标准IO流
FILE *fopen(const char *path, const char *modle);
成功时返回流指针;出错时返回NULL
model参数
模式 | 描述 |
---|---|
r或rb | 打开一个已有的文本文件,允许读取文件。 |
w或wb | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
a或ab | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
r+或r+b | 打开一个文本文件,允许读写文件。 |
w+或w+b | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+或a+b | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
当给定b参数时,表示以二进制方式打来文件,但linux下忽略该参数
fopen新建文件权限
fopen()创建的文件访问权限是0666(rw-rw-rw),0表是8进制数
Linux系统中umask设定会影响文件的访问权限,其规则为(0666 & (~umask)),可以通过umask命令查看,默认为0022
0022 ----> 000 010 010
取反 ----> 111 101 101
0666 ----> 110 110 110
结果:---> 110 100 100 ---> 0644(rw-r--r--)
用户可以通过umask函数修改相关设定,将umask设置为0时,umask不影响文件访问权限
错误信息处理
extern int errno;//存放错误号
void perror(const char *s);//向输出字符串s,再输出错误号对应的错误信息
char *strerror(int errno);//根据错误号返回对应的错误信息
流的关闭
int fclose(FILE *stream)
fclose()调用成功返回0,失败返回EOF,并设置errno
流关闭时自动刷新缓冲中的数据并释放缓冲区
当一个程序正常终止时,所有打开的流都会被关闭,但是为了安全期间,程序员要主动关闭
流一旦关闭后就不能执行任何操作
程序中能够打开的文件或流的个数有限制,写程序测试:
程序结果是1021,因为启动一个程序,默认打开stdin、stdout、stderr三个流,所以一个是1024;
#include <stdio.h> int main() { int count; while (1) { if (fopen("mycp.c", "r") == NULL) { break; } count++; } printf("%d\n", count);//1021 return 0; }
读写流
流支持不同的读写方式
读写一个字符:fgetc()/fputc()一次读/写一个字符
读写一行:fgets()/fputs()一次读/写一行数据,一般用于文本文件,一般不适用于二进制文件
读写若干个对象:fread()/fwrite()每次读/写若干个对象,而每个对象具有相同的长度,效率高,推荐使用
按字符输入
下列函数用来输入一个字符
#include <stdio.h>
int fgetc(FILE *stream);//与getc()函数功能完全相同,成功时返回读取的字符,若到文件末尾或出错时返回EOF
int getc(FILE *stream);
int getchar(void);//从标准输入中获取字符等同于fgetc(stdin)
按字符输出
下列函数用来输出一个字符
#include <stdio.h>
int fputc(int c, FILE *stream);//与putc()函数功能完全相同,第一个参数是输出的字符,成功时返回写入的字符;出错时返回EOF
int putc(int c, FILE *stream);
int putchar(int c);//向标准输出流写入一个字符,等同于fputc(c, stdout);
利用fgetc()/fputc()实现文件复制
#include <stdio.h> #include <string.h> //提供strerror()函数 #include <errno.h> //提供errno变量 int main(int argc, char *argv[]) { FILE *fps, *fpd; //定义两个流指针分别指向源文件和目标文件 int ch; //保存读出的字符 if (argc < 3) //检验命令行参数 { printf("Usage : %s <src_file> <dst_file>\n", argv[0]); return -1; } /* 打开源文件 */ if ((fps = fopen(argv[1], "r")) == NULL) { //perror("fopen src file"); printf("fopen src file: %s\n",strerror(errno)); return -1; } /* 打开目标文件 */ if ((fpd = fopen(argv[2], "w")) == NULL) { perror("fopen dst file"); return -1; } while ((ch = fgetc(fps)) != EOF) { fputc(ch, fpd); } fclose(fps); fclose(fpd); return 0; }
按行输入
下列函数用来输入一行:
#include <stdio.h>
char *gets(char *s);//从标准输入读入一行数据,不推荐使用,因为没有执行缓冲区的大小,容易造成缓冲区溢出
char *fgets(char *s, int size, FILE *stream);//成功时返回s,到文件末尾或出错时返回NULL,遇到"\n"或已输出size-1个字符时返回,总是包含"\0"
按行输出
下列函数用来输出一行字符串:
#include <stdio.h>
int puts(const char *s);//将缓冲区s中的字符串输出到stdout,并追加"\n"
int fputs(const chars *s, FILE *stream);//将缓冲区s中的字符串输出到stream中,不追加"\n"
成功时返回输出的字符个数;出错时返回EOF
写程序统计一个文本中包含多少行?
#include <stdio.h> #include <string.h> #define SIZE 37 int main(int argc, const char *argv[]) { int count = 0;//统计文本行数 FILE *fp; char buf[SIZE] = {};//如果buf不指定大小会报栈溢出错误 if ((fp = fopen("b.txt", "r")) == NULL) { perror("fopen"); return -1; } while (fgets(buf, SIZE+1, fp) != NULL) { if (buf[strlen(buf) -1] == '\n') { count ++; } } fclose(fp); printf("lines:%d\n", count); return 0; }
按指定对象输入/输出
下列函数用来从流中读写若干个对象:
#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);
参数说明:
ptr是缓冲区的首地址
size是对象数据的大小,对象为字符时值为1,对象为数字时值为4.
n是要读写的对象个数
fp为流指针
成功返回实际读写的对象个数;出错时返回EOF
既可以读写文件,也可以读写数据文件
效率高
使用fread()/fwrite()函数实现文件拷贝
#include <stdio.h> #include <string.h> //提供strerror()函数 #include <errno.h> //提供errno变量 #define N 64 int main(int argc, char *argv[]) { FILE *fps, *fpd; //定义两个流指针分别指向源文件和目标文件 char buf[N]; int n; if (argc < 3) //检验命令行参数 { printf("Usage : %s <src_file> <dst_file>\n", argv[0]); return -1; } /* 打开源文件 */ if ((fps = fopen(argv[1], "r")) == NULL) { //perror("fopen src file"); printf("fopen src file: %s\n",strerror(errno)); return -1; } /* 打开目标文件 */ if ((fpd = fopen(argv[2], "w")) == NULL) { perror("fopen dst file"); return -1; } while ((n = fread(buf, 1, N, fps)) > 0) { fwrite(buf, 1, n, fpd); } fclose(fps); fclose(fpd); return 0; }
流的刷新
自动刷新
全缓冲:当流的缓冲区满的时候将自动刷新,打开文件时默认是全缓冲
行缓冲:流的缓冲区满的时候或遇到换行符"\n"将自动刷新
关闭流的时候将会自动刷新
手动刷新
#include <stdio.h>
int fflush(FILE *fp);
成功返回0;出错时返回EOF
将流缓冲区中的数据写入到实际的文件
Linux下只能刷新输出缓冲区
流的定位
#include <stdio.h>
long ftell(FILE (stream);//成功的时候返回当前读写位置,出错时返回EOF
long fseek(FILE *stream, long offset, int whence);//定位一个流,成功返回0,出错时返回EOF,whence参数:SEEK_SET(文件的开始)/SEEK_CUR(当前位置)/SEEK_END(文件的结尾)
void rewind(FILE *stream);//将流定位到文件的起始位置
检测流结束和出错
#include <stdio.h>
int ferror(FILE *stream);//返回1表示流出错,否则返回0
int feof(FILE *stream);//返回1表示文件已到末尾;否则返回0
格式化输出
#include <stdio.h>
int printf(const char *fmt,...);
int fprintf(FILE *stream, const char *fmt,...);//向指定流中输出格式化后的数据
int sprintf(char *s, const char *fmt,...);//向指定缓冲区中输出格式化后的数据
写程序实现一下功能:
每个一秒向文件test.txt文件中写入系统时间,格式如下:
1, 2020-02-19 15:30:20
2, 2020-02-19 15:30:21
该程序无限循环,直到ctrl+c结束程序,下次再执行该代码时接着之前的格式接续写入:3, 2020-02-19 15:31:21
#include <stdio.h> #include <string.h> //strlen() #include <time.h> //time()/localtime() #include <unistd.h> //sleep() #define SIZE 3 int main() { int line = 0;//用于记录行号 FILE *fp; char buf[SIZE]; time_t t;//用于存放当前的时间 struct tm *tp;//存放格式化之后的时间 { }; if ((fp = fopen("test.txt", "a+")) == NULL) { perror("fopen"); return -1; } while (fgets(buf, SIZE, fp) !=NULL) { if (buf[strlen(buf) - 1] == '\n') line++; } while (1) { time(&t); tp = localtime(&t); fprintf(fp, "%02d, %d-%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; }
文件IO
什么是文件IO
posix(可移植操作系统接口)定义的一组函数
不提供缓冲机制,每次读写操作都引起系统调用
核心概念是文件描述符
访问各种类型文件(标准io一般只能访问普通文件和终端文件)
Linux下,标准IO基于文件IO实现
文件描述符
每打开的文件都对应一个文件描述符
文件描述符是一个非负整数,Linux为程序中每个打开的文件分配一个文件描述符
文件描述符从0开始分配,依次递增
每个程序打开的文件描述符都是相互独立的
文件IO操作通过文件描述符来完成
0,1,2分别表示标准输入,标准输出,标准错误
open函数用来创建或打开一个文件
#include <fcntl.h>
int open(const char *path, int oflag, ...);
成功时返回文件描述符;出错时返回EOF
打开文件时使用两个参数
创建文件时第三个参数指定新文件的权限
只能打开设备文件
原型 | int open(const char *pathname, int flags, mode_t mode); | ||
参数 | pathname | 被打开的文件名(可包括路径名) | |
flags | O_RDONLY:只读方式打开文件 | 这三个参数互斥 | |
O_WRONLY:可写方式打开文件 | |||
O_RDWR:读写方式打开文件 | |||
O_CREAT:如果文件不存在,就创建一个新的文件,并用第三个参数为其设置权限 | |||
O_EXCL:如果使用O_CREAT是文件存在,则可返回错误信息。这一参数可测试文件是否存在 | |||
O_NOCTTY:使用本参数时,如文件为终端,那么终端不可以作为调用open()系统调用的哪个进程的控制终端 | |||
O_TRUNC:如文件已经存在,那么打开文件时先删除文件中原有数据 | |||
O_APPEND:以添加方式打开文件,所以对文件的写操作都在文件的末尾进行 | |||
mode | 被打开文件的存储权限,为8进制表示 |
以只写方式打开文件1.txt。如果文件不存在则创建,如果文件存在则清空:
int fd;
if ((fd = open("1.txt", O_WRONLY| O_CREAT|O_TRINC, 0666)) < 0) {
perror("open");
return -1;
}
以读写方式打开文件1.txt,如果文件不存在则创建,如果文件存在则报错
int fd;
if((fd = open("1.txt", O_RDWR|O_CREAT|O_EXCL, 0666)) < 0) {
if (errno == EEXIST){
perror("exist error");
}else{
perror("other error");
}
}
close 函数用来关闭一个打开的文件
#include <unistd.h>
int close(int fd);
成功时返回0;出错时返回EOF
程序结束时自动关闭所有打开的文件
文件关闭后,文件描述符不再代表文件
读取文件
read函数用来从文件中读取数据
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t, count);
成功时返回实际读取的字节数;出错时返回EOF
读到文件末尾时返回0
buf是接收数据的缓冲区
count不应超过buf大小
从指定的文件(文本文件)中读取内容并统计大小
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(int argc, const char *argv[]) { int fd, n, total = 0; char buf[64]; if (argc < 2) { printf("Usage: %s <file>\n", argv[0]); return -1; } if ((fd = open(argv[1], O_RDONLY)) < 0 ) { perror("open"); return -1; } while ((n = read(fd, buf, 64)) > 0) { total += n; } close(fd); printf("total: %d\n", total); return 0; }
写入文件
write函数用来向文件写入数据
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t, count);
成功时返回实际写入的字节数;出错时返回EOF
buf是发送数据的缓冲区
count不应超过buf的大小
将键盘输入的内容写入文件,直到输入quit
#include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define N 20 int main(int argc, char *argv[]) { int fd; char buf[N]; if (argc < 2) { printf("Usage: %s <file>\n", argv[0]); return -1; } if ((fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0 ) { perror("open"); return -1; } while (fgets(buf, N + 1, stdin) != NULL) { if(strcmp(buf, "quit\n") == 0) break; write(fd, buf, strlen(buf)); } close(fd); return 0; }
定位文件
lseek函数用来定位文件
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
成功时返回当前文件读写位置;出错时返回EOF
参数offset和参数whence同fseek完全一样
读取目录
opendir函数用来打来一个目录文件
#include <dirent.h>
DIR *opendir(const char *name);
DIR是用来描述一个打开的目录文件的结构体类型
成功时返回目录流指针;出错时返回NULL
readdir函数用来读取目录流中的内容
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent是用来描述目录流中一个目录项的结构体类型
包含成员char d_name[256] 参数帮助文档
成功时返回目录流dirp中下一个目录项;出错或到末尾时返回NULL
close函数用来关闭一个目录文件
#include <dirent.h>
int closedir(DIR *drip);
成功返回0;失败返回EOF
打印指定目录下所有文件名称(只会打印指定目录下的文件和目录,不会打印子目录下的文件)
#include <dirent.h> #include <stdio.h> int main(int argc, char *argv[]) { DIR *dirp; struct dirent *dp; if (argc < 2) { printf("Usage: %s <directory>\n", argv[0]); return -1; } if ((dirp = opendir(argv[1])) == NULL) { perror("opendir"); return -1; } while ((dp = readdir(dirp)) != NULL) { printf("%s\n", dp->d_name); } closedir(dirp); return 0; }
修改文件访问权限
chmod/fchmod函数用来修改文件的访问权限
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
成功返回0;出错返回EOF
root和文件所有者能修改文件的访问权限
获取文件属性
stat/lstat/fstat函数用来获取文件的属性
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);//推荐使用
int fstat(int fd, struct stat *buf);
成功返回0;出错返回EOF
如果path是符号连接stat获取的是目标文件色的属性;而lstat获取的是连接文件的属性
struct stat是存放文件属性的结构体类型
mode_t st_mode: 类型和访问权限
uid_t st_uid: 所有者id
uid_t st_gid: 用户组id
off_t st_size: 文件大小
time_t st_mtime: 最后修改时间
st_mode
通过系统提供的宏来判断文件类型:
st_mode & 0170000 (001 111 000 000 000 000,文件类型掩码)
S_ISREG(st_mode) 0100000
S_ISDIR(st_mode) 0040000
S_ISCHR(st_mode) 0020000
S_ISBLK(st_mode) 0060000
S_ISFIFO(st_mode) 0010000
S_ISLNK(st_mode) 0120000
S_ISSOCK(st_mode) 0140000
通过系统提供的宏来获取文件访问权限
S_IRUSR 00400 8
S_IWUSR 00200 7
S_IXUSR 00100 6
S_IRGRP 00040 5
S_IWGRP 00020 4
S_IXGRP 00010 3
S_IROTH 00004 2
S_IWOTH 00002 1
S_IXOTH 00001 0
获取并显示文件属性
#include <stdio.h> #include <sys/stat.h> #include <unistd.h> #include <time.h> #include <sys/types.h> int main(int argc, char *argv[]) { struct stat buf; int n; struct tm *tp; if (argc < 2) { printf("Usage %s <file>\n", argv[0]); return -1; } if (lstat(argv[1], &buf) < 0) { perror("lstat"); return -1; } switch (buf.st_mode & S_IFMT) { case S_IFREG: printf("-"); break; case S_IFDIR: printf("d"); break; case S_IFCHR: printf("c"); break; case S_IFBLK: printf("b"); break; case S_IFIFO: printf("p"); break; case S_IFLNK: printf("l"); break; case S_IFSOCK: printf("s"); break; } for (n=8; n>=0; n--) { if (buf.st_mode & (1 << n)) //1左移n位与一个数求&获得的是该数第n位的值 { switch (n % 3) { case 2: printf("r"); break; case 1: printf("w"); break; case 0: printf("x"); break; } } else { printf("-"); } } printf(" %lu" , buf.st_size); tp = localtime(&buf.st_mtime); printf(" %d-%02d-%02d", tp->tm_year+1900, tp->tm_mon+1, tp->tm_mday); printf(" %s\n", argv[1]); return 0; }
库
库的概念
库是一个二进制文件,包含的代码可被程序调用
标准C库、数学库、线程库......
库有源码,可下载后编译;也可以直接安装二进制包
库默认的安装路径在/lib 或/usr/lib
库是事先编译好的,可以复用的代码
在OS上运行的程序基本上都要使用库。使用库可以提高开发效率
Windows和Linux下库文件的格式不兼容
Linux下包含静态库和共享库
静态库
编译(链接)时把静态库中相关代码复制到可执行文件中
程序中已经包含代码,运行时不再需要静态库
程序运行时无需加载库,运行速度更快
占用更多磁盘和内存空间
静态库升级后,程序需要重新编译链接
静态库的创建
确定库中函数的功能、接口
编写库源码hello.c
#include <stdio.h> void hello(void) { printf("hello world!\n"); }
编译生成目标文件hello.o:gcc -c hello.c -Wall
创建静态库hello:ar crs libhello.a hello.o(库文件名为libhell.a,库名为hello)
查看库中符号信息:
xdl@xdl-gj:~/C语言/lib$ nm libhello.a
hello.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T hello
U puts
链接静态库
编写应用程序test.c
void hello(void); int main() { hello(); return 0; }
编译test.c并链接静态库libhello.a
gcc -o test test.c -L. -lhello
-L:用来指定库的搜索路径,. 表示当前目录
-l:指定要链接的库名
共享库
编译(链接)时仅记录用到哪个共享库中的哪个符号(hs),不复制共享库中相关代码
程序不包含库中代码,尺寸小
多个程序可共享同一个库
程序运行时需要加载库
库升级方便,无需重新编译程序
使用更加广泛
共享库的创建
确定库中函数功能、接口
编写库源码hello.c bye.c
#include <stdio.h> void hello(void) { printf("hello world!\n"); } /**********************************/ #include <stdio.h> void bye(void) { printf("bye!\n"); }
编译生成目标文件:gcc -c -fPIC hello.c by2.c -Wall;
-fPIC:告诉编译器要生成.o文件可以被任何位置的程序调用
创建共享库common:gcc -shared -o libcommon.so.1 hello.o bye.o ;.1表示版本
为共享库文件创建链接文件:ln -s libcommon.so.1 libcommon.so
链接共享库
编写应用程序test.c
#include "common.h" int main() { hello(); bye(); return 0; } /*common.h*/ void hello(void); void bye(void);
编译test.c并链接共享库libcommon.so:
gcc -o test test.c -L. -lcommon (默认先找共享库,其次才会寻找静态库,可以通过-static 参数直接连接静态库)
加载共享库
执行程序:
xdl@xdl-gj:~/C语言/lib$ ./test
./test: error while loading shared libraries: libcommon.so: cannot open shared object file: No such file or directory
添加共享库的加载路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
xdl@xdl-gj:~/C语言/lib$ ./test
hello world!
bye!
如何找到共享库
为了让系统能找到要加载的共享库,有三种方法:
1、把库拷贝到/usr/lib 或 /lib目录下
2、在LD_LIBRARY_PATH环境变量中添加库所在路径
3、添加/etc/ld.so.conf.d/*.conf文件,执行ldconfig刷新
sudo vim my.conf
/home/xdl/C语言/lib
sudo ldconfig