05 文件之IO
标准IO介绍
IO的概念
I——input,指输入设备,比如键盘鼠标都是Input设备。
O——output,指输出设备,比如显示器。
优盘,网口,既是输入也是输出。
文件类型
文件指一组相关数据的有序集合。
文件类型:
-
常规文件 r
-
目录文件 d
-
字符设备文件 c
-
块设备文件 b
-
管道文件 p
-
套接字文件 s
-
符号链接文件 l(类似快捷方式)
系统调用和库函数
liunx内核提供给用户一个接口,来在显卡上显示字符。这个接口的使用就叫做。而printf这个输出函数背后实际上是调用了这个接口,做了大量的工作。
从标准IO的角度来解释,可以理解为库函数提供了一组用于进行标准输入输出操作的函数。这些函数封装了底层的IO操作细节,使得程序员可以方便地进行输入和输出操作,而无需直接与底层设备进行交互。
标准IO库函数主要包括以下几个方面的功能:
- 输入函数:标准IO库函数提供了一系列用于读取输入数据的函数,如scanf、fgets、getchar等。这些函数可以从标准输入设备(通常是键盘)读取用户输入的数据,并将其转化为程序可以处理的格式。
- 输出函数:标准IO库函数还提供了一系列用于输出数据的函数,如printf、puts、fputs等。这些函数可以将程序处理的数据格式化并输出到标准输出设备(通常是终端显示器),供用户查看。
- 缓冲处理:标准IO库函数通常会使用缓冲区来提高IO效率。输入函数会将输入数据缓存在内存中,输出函数则会将输出数据暂存到缓冲区中,待缓冲区满或遇到特定条件时再进行实际的IO操作。这样可以减少对IO设备的频繁访问,提高程序的性能。
- 设备驱动程序调用:标准IO库函数通过操作系统提供的设备驱动程序进行实际的IO操作。这些驱动程序负责将数据从内核空间传输到底层硬件设备,如磁盘、网络接口等。标准IO库函数隐藏了底层设备细节,程序员无需关心具体的硬件接口和协议。
通过使用标准IO库函数,程序员可以方便地进行输入输出操作,实现与用户和外部设备的交互。标准IO库函数提供了一种统一的接口,使得,而无需修改底层IO代码。
标准I/O由标准定义,现在主流操作系统上都实现了C库,标准I/O通过缓冲机制减少系统调用,实现更高的效率。
基本概念
FILE:标准IO用一个结构体类型来存放打开的文件的相关信息。标准I/O的所有操作都是围绕FILE来进行。
流(stream):FILE又被称为流(stream)。分类:文本流/二进制流。
流的缓冲类型:
-
全缓冲
当流的缓冲区无数据或无空间时才执行实际I/O操作
-
行缓冲
当在输入和输出中遇到换行符(‘\n’)时,进行I/O操作。
当流和一个终端关联时,典型的行缓冲。
-
无缓冲
数据直接写入文件,流不进行缓冲。
标准I/O预定义3个流,程序运行时自动打开。保证基本驱动的运行和使用。
标准输入流 | 0 | STDIN_FILENO | stdin | 行缓冲 |
---|---|---|---|---|
标准输出流 | 1 | STDOUT_FILENO | stdout | 行缓冲 |
标准错误流 | 2 | STDERR_FILENO | stderr |
小示例
printf是输出设备,按道理说,没有遇到换行是不应该输出的,而在这里hello world依然被打印出来了,是因为结束程序,会把缓冲区里面的内容都输出出来。
如果我们让程序一直不结束,这个程序就不会打印hello world了:
如果在printf加个换行符,因为行缓冲就会输出,或者写满缓冲区程序也会自动打印hello world。
文件的打开和关闭
打开文件
文件的打开就是占用资源,关闭就是释放资源。
下列函数可用于一个标准I/O流:
path路径,普通文件当前不需要加目录,其他要使用完整的路径。成功时返回流指针;出错时返回NULL。
模式 | 解释 |
---|---|
“r”或“rb” | 以只读方式打开文件,文件必须存在。 |
“r+”或”r+b” | 以读写方式打开文件,文件必须存在。 |
“w”或“wb” | 以只写方式打开文件,若文件存在则文件长度清为0。若文件不存在则创建。 |
“w+”或“w+b” | 以读写方式打开文件,其他同”w”。 |
“a”或“ab” | 以只写方式打开文件,若文件不存在则创建;向文件写入的数 据被追加到文件末尾。 |
“a+”或“a+b” | 以读写方式打开文件。其他同”a” |
小示例——读取文件:
处理错误信息
-
errno 存放错误号,由系统生成。
-
perror先输出字符串s,再输出错误号对应的错误信息。头文件stdio.h。
-
strerror根据错误号返回对应的错误信息。头文件 、。
perror和strerror 功能:打印系统的错误描述(注意:是系统错误,不是你自己代码错误)。
有1.txt的情况下:
strerror的演示:
常见编译错误:
f_open.c:9:38: error: ‘errno’ undeclared (first use in this function)
printf("fopen:%s\n",strerror(errno));
:表示errno变量没有定义。
解决方法:如果是系统变量用include 头文件,如果是你自己的,自己手动定义。
f_open.c:10:29: warning: implicit declaration of function ‘strerror’ [-Wimplicit-function-declaration]
printf("fopen:%s\n",strerror(errno));
:表示strerror函数隐示的声明。
解决方法:include 添加对应的头文件。
关闭文件
-
fclose()调用成功返回0,失败返回EOF(-1),并设置errno。参数必须为非空,否则出现断错误。
-
流关闭时自动刷新缓冲中的数据并释放缓冲区。
-
当一个程序正常终止时,所有打开的流都会被关闭。
-
流一旦关闭后就不能执行任何操作。
断错误:文件没有打开成功,还关闭文件就会出现。
读写字符
注意事项:
1函数返回值是int类型不是char类型,主要是为了扩展返回值的范围。
2 tdin 也是FILE *的指针,是系统定义好的,指向的是标准输入(键盘输入)
3 打开文件后读取,是从文件开头开始读。读完一个后读写指针会后移。读写注意文件位置!
4 调用getchar会阻塞,等待你的键盘输入。
int
fputc(int c, FILE *stream);
int
putc(int c, FILE *stream);
int
putchar(int c);
-
成功时返回写入的字符;出错时返回EOF。
-
putchar(c)等同于fputc(c, stdout)
注意事项:
1返回和输入参数都是int类型
2遇到这种错误:Bad file descriptor
, 很可能是文件打开的模式错误(只读模式去写,只写模式去读)
行读写
行输入(读取整个行)
char *gets(char *s);
读取标准输入到缓冲区s
char *fgets(char *s, int size, FILE *stream);
-
成功时返回s,到文件末尾或出错时返回NULL
-
遇到’\n’或已输入size-1个字符时返回,总是包含’\0’
注意事项:
-
gets函数已经被淘汰,因为会导致缓冲区溢出
-
fgets 函数第二个参数,输入的数据超出size,size-1个字符会保存到缓冲区,最后添加’\0’,如果输入数据少于size-1 后面会添加换行符。
行输出(写整行)
int puts(const char *s);
int fputs(const char *s, FILE *stream);
-
成功时返回非负整数;出错时返回EOF。
-
puts将缓冲区s中的字符串输出到stdout,并追加’\n’。
-
fputs将缓冲区s中的字符串输出到stream,不追加 ‘\n’。
二进制读写
文本文件和二进制的区别:
存储的格式不同:文本文件只能存储文本。
计算机内码概念:文本符号在计算机内部的编码(计算机内部只能存储数字0101001....,所以所有符号都要编码)
二进制读写函数格式:
size_t fread(void *ptr, size_t size, size_tn, FILE *fp);
-
void *ptr 读取内容放的位置指针
-
size_t size 读取的块大小
-
size_t n 读取的个数
-
FILE *fp 读取的文件指针
size_t fwrite(const void *ptr, size_t size,size_t n, FILE *fp);
-
void *ptr 写文件的内容的位置指针
-
size_t size 写的块大小
-
size_t n 写的个数
-
FILE *fp 要写的文件指针
注意事项:
- 文件写完后,文件指针指向文件末尾,如果这时候读,读不出来内容。解决办法:移动指针(后面讲解)到文件头;关闭文件,重新打开
刷新流
流的刷新
int fflush(FILE *fp);
-
成功时返回0;出错时返回EOF
-
将流缓冲区中的数据写入实际的文件
-
Linux下只能刷新输出缓冲区,输入缓冲区丢弃
-
如果输出到屏幕使用fflush(stdout)
应用场景,在休眠前先输出到文件。
流的定位
long ftell(FILE *stream);
long fseek(FILE *stream, long offset, int whence);
void rewind(FILE *stream);
-
fseek 参数whence参数:SEEK_SET/SEEK_CUR/SEEK_END
-
SEEK_SET 从距文件开头 offset 位移量为新的读写位置
-
SEEK_CUR:以目前的读写位置往后增加 offset 个位移量
-
SEEK_END:将读写位置指向文件尾后再增加 offset 个位移量
-
offset参数:偏移量,可正可负
注意事项:
-
文件的打开使用a模式 fseek无效
-
rewind(fp) 相当于 fseek(fp,0,SEEK_SET);
-
这三个函数只适用2G以下的文件
编译告警错误:
ffseek_t.c:13:11: warning: format ‘%d’
expects argument of type ‘int’, but argument 2 has type ‘long int’ [-Wformat=]
printf("current fp=%d\n",ftell(fp));
表示参数类型不匹配
格式化IO
格式化输出
int fprintf(FILE *stream, const char *fmt, …);
int sprintf(char *s, const char *fmt, …);
成功时返回输出的字符个数;出错时返回EOF
格式化输入
int fscanf(FILE stream, const char format, ...);
标准io练习
#include<time.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main(){
FILE *fp;
time_t ctime;
struct tm *ctimestr;//时间结构体
int linecount=0;//行号
char buf[32];//缓存区
fp=fopen("test.txt","a+");//打开文件
if(fp==NULL){
perror("fopen");
return 0;
}
while(fgets(buf,32,fp)!=NULL){//获取之前文件的所有行号
if(buf[strlen(buf)-1]=='\n')
{ linecount++; }
}
while(1){
ctime=time(NULL);//获取当前时间秒数
ctimestr=localtime(&ctime);//秒数转换到结构体
printf("%04d-%02d-%02d %02d:%02d:%02d\n",ctimestr->tm_year+1900,ctimestr->tm_mon+1,ctimestr->tm_mday,ctimestr->tm_hour,ctimestr->tm_min,ctimestr->tm_sec);
fprintf(fp,"%d, %04d-%02d-%02d %02d:%02d:%02d\n",linecount,ctimestr->tm_year+1900,ctimestr->tm_mon+1,ctimestr->tm_mday,ctimestr->tm_hour,ctimestr->tm_min,ctimestr->tm_sec);
fflush(fp);
linecount++;
sleep(1);
}
fclose(fp);
}
time()用来获取系统时间(秒数)
time_t time(time_t *seconds) 1970.1.1 0:0:0
localtime()将系统时间转换成本地时间
struct tm *localtime(const time_t *timer)
struct tm {
int tm_sec; /* 秒,范围从 0 到 59 */
int tm_min; /* 分,范围从 0 到 59 */
int tm_hour; /* 小时,范围从 0 到 23 */
int tm_mday; /* 一月中的第几天,范围从 1 到 31 */
int tm_mon; /* 月份,范围从 0 到 11 */
int tm_year; /* 自 1900 起的年数 */
int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
int tm_isdst; /* 夏令时 */
};
注意:
-
int tm_mon; 获取的值要加1是正确的月份
-
int tm_year; 获取的值加1900是正确的年份
获取文件内的所有行数量:
while(fgets(buf,32,fp)!=NULL){
if(buf[strlen(buf)-1] =='\n'){ //注意判断是否是一行结束
linecount++;
}
}
写完文件记得fflush ,写到磁盘里面去。
标准IO磁盘文件的缓冲区一般为4096。
注意和标准输出的全缓冲区别,标准输出是1024
文件IO
文件IO
posix(可移植操作系统接口)定义的一组函数。
不提供缓冲机制,每次读写操作都引起系统调用。
核心概念是文件描述符。
访问各种类型文件。
Linux下, 标准IO基于文件IO实现。
文件描述符
-
每个打开的文件都对应一个文件描述符。
-
文件描述符是一个非负整数。Linux为程序中每个打开的文件分配一个文件描述符。
-
文件描述符从0开始分配,依次递增。
-
文件IO操作通过文件描述符来完成。
这里的fd就是文件描述符。
open
open函数用来创建或打开一个文件:
#include <fcntl.h>
intopen(const char *path, intoflag, …);
-
成功时返回文件描述符;出错时返回EOF
-
打开文件时使用两个参数
-
创建文件时第三个参数指定新文件的权限
-
只能打开设备文件
文件的初始权限:文件目录的最大默认权限-umask权限。
close
close函数用来关闭一个打开的文件:
#include <unistd.h>
int close(intfd);
-
成功时返回0;出错时返回EOF
-
程序结束时自动关闭所有打开的文件
-
文件关闭后,文件描述符不再代表文件
打开的任何新文件fd都应以 3 开头。
关闭了一次close(fd),3已经不能代表文件描述符了。
read
read函数用来从文件中读取数据:
#include<unistd.h>
ssize_t read(intfd, void *buf, size_t count);
-
成功时返回实际读取的字节数;出错时返回EOF。
-
读到文件末尾时返回0。
-
buf是接收数据的缓冲区。
-
count不应超过buf大小。
lseek
lseek函数用来定位文件:
#include<unistd.h>
off_t lseek(intfd, off_t offset, intt whence);
-
成功时返回当前的文件读写位置;出错时返回EOF
-
参数offset和参数whence同fseek完全一样
目录的读取
访问目录 – opendir
opendir函数用来打开一个目录文件:
#include<dirent.h>
DIR *opendir(const char *name);
-
DIR是用来描述一个打开的目录文件的结构体类型
-
成功时返回目录流指针;出错时返回NULL
访问目录 – readdir
readdir函数用来读取目录流中的内容:
#include<dirent.h>
struct dirent *readdir(DIR *dirp);
-
structdirent是用来描述目录流中一个目录项的结构体类型
-
包含成员char d_name[256] 参考帮助文档
-
成功时返回目录流dirp中下一个目录项;
-
出错或到末尾时时返回NULL
访问目录 – closedir
closedir函数用来关闭一个目录文件:
#include<dirent.h>
int closedir(DIR*dirp);
- 成功时返回0;出错时返回EOF
文件属性获取
修改文件访问权限 – chmod/fchmod
chmod/fchmod函数用来修改文件的访问权限:
#include<sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(intfd, mode_t mode);
-
成功时返回0;出错时返回EOF
-
root和文件所有者能修改文件的访问权限
示例: chmod("test.txt", 0666);
获取文件属性 –stat/lstat/fstat
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(intfd, 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;//最后修改时间
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
文件类型 – st_mode
通过系统提供的宏来判断文件类型:
st_mode & 0170000
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 bit: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 其他用户拥有执行权限
打印文件类型:
获取打印的权限。
打印文件大小、时间、文件名。
最后运行结果:
静态库的使用
库的概念
库是一个二进制文件,包含的代码可被程序调用。比如:标准C库、数学库、线程库……
库有源码,可下载后编译;也可以直接安装二进制包。 /lib /usr/lib
库是事先编译好的,可以复用的代码。
在OS上运行的程序基本上都要使用库。使用库可以提高开发效率。
Windows和Linux下库文件的格式不兼容。
Linux下包含静态库和共享库。
静态库特点
编译(链接)时把静态库中相关代码复制到可执行文件中。
程序中已包含代码,运行时不再需要静态库。
程序运行时无需加载库,运行速度更快。
占用更多磁盘和内存空间。
静态库升级后,程序需要重新编译链接。
静态库和动态库的区别
1.静态库在程序编译时会被连接到目标代码中。
优点:程序运行时将不再需要该静态库;运行时无需加载库,运行速度更快
缺点:静态库中的代码复制到了程序中,因此体积较大;静态库升级后,程序需要重新编译链接。
2.动态库是在程序运行时才被载入代码中。
优点:程序在执行时加载动态库,代码体积小;将一些程序升级变得简单;不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
缺点:运行时还需要动态库的存在,移植性较差。
静态库创建
-
确定库中函数的功能、接口
-
编写库源码hello.c
#include <stdio.h>
void hello(void) {
printf(“hello world\n”);
return;
}
-
编译生成目标文件
$ gcc -c hello.c -Wall
-
创建静态库 hello
$ ar crs libhello.a hello.o
。静态库是.a,hello是静态库名称。查看库文件: -
查看库中符号信息
$nm libhello.a
没有main函数的生成不了可执行文件。
链接静态库
-
编写应用程序test.c
#include <stdio.h> void hello(void); int main() { hello(); return 0; }
-
编译test.c并链接静态库libhello.a
$ gcc -o test test.c -L. -lhello
:-L参数表示库所在的路径,-l后面跟库的名称。$ ./test
按照下面的命令直接链接出错:
动态库的使用
动态库特点
编译(链接)时仅记录用到哪个共享库中的哪个符号,不复制共享库中相关代码。
程序不包含库中代码,尺寸小。
多个程序可共享同一个库。
程序运行时需要加载库。
库升级方便,无需重新编译程序。
使用更加广泛。
动态库创建
-
确定库中函数的功能、接口
-
编写库源码hello.c bye.c:
#include <stdio.h> void hello(void) { printf(“hello world\n”); return; }
-
编译生成目标文件
$ gcc -c -fPIC hello.c bye.c -Wall
必须加-fPIC,否则结果不一样:
-
创建动态库 common
$ gcc -shared -o libcommon.so.1 hello.o bye.o
链接动态库
-
编写应用程序test.c:
#include <stdio.h> #include “common.h” int main() { hello(); bye(); return 0; }
-
编译test.c并链接共享库libcommon.so:
$ gcc -o test test.c -L. -lcommon
如何找到共享库
为了让系统能找到要加载的共享库,有三种方法:
-
把库拷贝到/usr/lib和/lib目录下。容易把lib库函数弄得很乱。
-
在LD_LIBRARY_PATH环境变量中添加库所在(当前)路径。
$ export LD_LIBRARY_PATH=./
(环境变量只在当前文件下有效,所以我们要把变量加入到配置文件里:$ vim ~/.bashrc
,插入刚刚的那行代码,就在任何目录下有效了,记得还要source刷新一下文件source ~/.bashrc
) -
添加/etc/ld.so.conf.d/*.conf文件,执行ldconfig刷新。