一、标准IO
1.1、概述
标准IO
- 是ANSI C提供的函数接口,在文件IO的基础上封装出来的。任何兼容标准C库的操作系统都可以使用接口,具有更好的移植性。
- 标准IO在执行时也会用到系统调用,为了增加内核的工作效率,减少资源浪费,在文件IO的基础上封装了缓冲区。
缓冲区
- 全缓冲:当缓冲区满或执行flush操作时才会进行磁盘操作;缓冲区满才进行实际的IO操作 4k
- 行缓冲:当在输入输出中遇到换行符时执行IO操作 1k
- 无缓冲:不对IO操作进行缓冲,在对流的读写时会立刻操作实际文件
流指针
- 当用标准IO打开一个文件时,就会创建一个FILE结构体描述该文件;把这个FILE结构体称为流。
- 标准IO操作文件返回的是文件流指针(FILE *)描述;文件IO打开后用文件描述符(fd)描述。
三种标准IO
标准输入流(键盘) | 0 | STDIN_FILENO | stdin |
---|---|---|---|
标准输出流(显示器) | 1 | STDOUT_FILENO | stdout |
标准错误流 | 2 | STDERR_FILENO | stderr |
1.2、标准IO编程
1.文件的打开
FILE *fopen (const char *path, const char *mode);
- Path: 普通文件当前路径不需要加目录,其他要使用完整的路径
- Mode:文件的打开模式
- 返回值:成功时返回流指针,出错返回NULL,所以使用fopen函数必须判断是否为空
文件的打开模式
mode | 描述 |
---|---|
“r” 或 “rb” | 以只读方式打开文件,文件必须存在。 |
“r+” 或 ”r+b” | 以读写方式打开文件,文件必须存在。 |
“w” 或 “wb” | 以只写方式打开文件,若文件存在则文件长度清为0。若文件不存在则创建。只写一次 |
“w+” 或 “w+b” | 以读写方式打开文件,其他同”w”。 |
“a” 或 “ab” | 以只写方式打开文件,若文件不存在则创建;向文件写入的数据被追加到文件末尾。 |
“a+” 或 “a+b” | 以读写方式打开文件。其他同”a” |
2.文件的关闭
int flose(FILE *stream);
- stream:以打开的流指针
- 返回值:成功:0 失败:EOF
- 流关闭时自动刷新缓冲中的数据并释放缓冲区,比如:常规文件把缓冲区内容写入磁盘
- 当一个程序正常终止时,所有打开的流都会被关闭
- 流一旦关闭后就不能执行任何操作
- fclose()函数的入参stream必须保证为非空,否则出现断错误。不能关闭一个不存在的文件
3.错误处理
- perror()
void perror(const char *s);- s:在标准错误流上输出的信息
- perror(“”);
- strerror()
- char *strerror(int errnum);
- 参数:错误码
- 返回值:错误码对应的错误信息
- printf(“%s\n”, strerror(errno)) ;
示例
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp;
if((fp = fopen("./1.txt", "r")) == NULL){
perror("fopen");
printf("%s\n",strerror(errno));
exit(1);
}
fclose(fp);
return 0;
}
//运行结果
fopen: No such file or directory
No such file or directory
4.流的读写
4.1.按字符输入/输出
- 按字符输入
int fgetc(FILE *stream);
int getc(FILE *stream); //宏
int getchar(void); 只能读取键盘输入- stream:要输入的文件流
- 返回值:成功:返回读取的字符 若到文件末尾或出错时返回EOF
- getchar() 等同与fgetc(stdin)
- getc和fgetc区别 一个是宏 一个是函数
- 函数返回值时int型不是char型,主要是为了扩展返回值的范围
- stdin也是FILE*的指针,是系统定义好的,指向的是标准输入(键盘输入)
- 打开文件后读取,是从文件开头开始读。读完一个后读写指针会后移。读写注意文件位置
- 调用getchar会造成阻塞,等待键盘输入
- 按字符输出
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);- 成功时返回写入的字符;出错时返回EOF
- putchar©等同于fputc(c, stdout)
- 注意:返回和输入参数都是int类型
练习
#include <stdio.h>
int main(){
int c;
while(1){
c = fgetc(stdin); //从键盘读取一个字符
if(c >= '0' && c <= '9') //输入的是数字,输出
fputc(c, stdout);
if(c == '\n') //遇到换行符,跳出循环
break;
}
puts("");
return 0;
}
//运行结果
q2312fadsf
2312
4.2.按行输入/输出
- 按行输入
char *gets(char *s);(不推荐使用)
char *fgets(char *s, int size, FILE *stream);(读的内容,读多少,从哪里读)- s:存放输入字符串的缓冲区首地址
- size:输入的字符串长度
- stream:对应的流
- 返回值:成功返回s;失败或到文件末尾返回NULL
- gets函数已经被淘汰,因为会导致缓冲区溢出
- fgets函数第二个参数,输入的数据超出size,size-1的字符会保存到缓冲区,最后添加‘\0’,如果输入数据小于size-1,后边会添加换行符
- 按行输出
int puts(const char *s);
int fputs(const char *s, FILE *stream);- s:存放输入字符串的缓冲区首地址
- stream:对应的流
- 成功时返回输出的字符个数;出错时返回EOF
- puts将缓冲区s中的字符串输出到stdout,并追加’\n’
- fputs将缓冲区s中的字符串输出到stream,不追加 ‘\n’
练习
//计算一个文本文件的行数
#include <stdio.h>
#include <string.h>
int main(int argc, const char *argv[])
{
int line = 0;
char buf[128];
FILE *fp;
if(argc < 2) {
printf("usage: %s<file>\n",argv[0]);
return -1;
}
if((fp = fopen(argv[1], "r")) == NULL) {
perror("fail to open");
return -1;
}
while(fgets(buf, 128, fp) != NULL) {
if(buf[strlen(buf)-1] == '\n')
line++;
}
printf("the line of %s is %d\n", argv[1], line);
return 0;
}
4.3.按指定对象大小输入/输出
size_t fread(void *ptr, size_t size, size_t n, 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);(将const void *ptr写入FILE *fp)
- const void *ptr 写的文件内容的位置指针,
- size_t size 写的块大小
- size_t n 写的的个数
- FILE *fp 要写文件指针
- 成功返回读写的对象个数;出错时返回EOF
- 既可以读写文本文件,也可以读写数据文件
- 效率高
文件写完之后,文件指针指向文件末尾,如果这时候读,读不出内容;
解决方法:移动指针;关闭文件,重新打开
5.流的刷新和定位
5.1.刷新流
int fflush(FILE *fp);
- 成功时返回0;出错时返回EOF
- 将流缓冲区中的数据写入实际的文件
- Linux下只能刷新输出缓冲区,输入缓冲区丢弃
- 如果输出到屏幕使用fflush(stdout)
5.2.定位流
long ftell(FILE stream);
- ftell() 成功时返回流的当前读写位置,出错时返回EOF
long fseek(FILE stream, long offset, int whence);
- fseek()定位一个流,成功时返回0,出错时返回EOF
- whence参数:SEEK_SET/SEEK_CUR/SEEK_END
- SEEK_SET:从距文件开头offset位移量为新的读写位置;
- SEEK_CUR:以目前读写位置往后增加offset个位移量;
- SEEK_END:将读写位置指向文件尾后再增加offset个位移量
- offset参数:偏移量,可正可负
- 打开 a 模式,fseek无效
void rewind(FILE stream);
- rewind(fp)相当于fseek(fp,0,SEEK_SET);
- rewind()将流定位到文件开始位置
- 读写流时,当前读写位置自动后移
这三个函数只适用于2G以下文件
获取文件大小
#include <stdio.h>
int main(int argc, const char *argv[])
{
FILE *fp;
if(argc < 2) {
printf("usage: %s<file>\n",argv[0]);
return -1;
}
if((fp = fopen(argv[1], "r")) == NULL) {
perror("fail to open");
return -1;
}
fseek(fp, 0, SEEK_END);
printf("the size of %s is %ld\n", argv[1], ftell(fp));
return 0;
}
6.格式化输入输出
printf系列
int printf(const char *format, …);
int fprintf(FILE *stream, const char *format, …);
int sprintf(char *str, const char *format, …);
-
功能:printf()系列中的函数根据如下所述的格式产生输出:
- printf()将输出默认写入标准输出流stdout(终端文件);
- fprintf()将输出写入给定的输出流对应的文件中;
- sprintf()将输出写入字符串str
-
参数:
-
printf():
- 参数1—const char * format一个字符串首地址。表示格式化字符串的地址
- 参数…—可以有多个参数,根据格式化字符串来决定,表示要输出的数据(可以是变量名、常量、表达式)
-
fprintf():
- 参数1—FILE * stream一个文件流指针。表示需要输出到的文件的信息结构体地址(输出到哪个文件中)]
- 参数2—const char * format一个字符串首地址。表示格式化字符串的地址
- 参数…—可以有多个参数,根据格式化字符串来决定,表示要输出的数据(可以是变量名、常量、表达式)
-
-
返回值:int类型
输出成功—返回打输出的字符数(不包括用于结束字符串输出的空字节)。
输出失败—返回EOF,并设置errno来指示错误. #define EOF -1 -
几个函数的特点:
fprintf()可以向指定文件写入数据,包括终端标准输出(stdout)
printf()默认只能给终端文件(stdout)写数据.stdout是行缓存
scanf系列
int scanf(const char *format, …);
int fscanf(FILE *stream, const char *format, …);
int sscanf(const char *str, const char *format, …);
-
功能:格式化扫描输入
- scanf()默认从终端标准输入(stdin)中格式化读取数据输入到程序中
- fscanf()从文件流指针所指向的文件中格式化读取数据输入到程序中
- sscanf()从字符串中格式化读取数据输入到程序中
-
参数:
scanf():- 参数1—const char * format一个字符串首地址。表示格式化字符串的地址
- 参数…—可以有多个参数,根据格式化字符串来决定,表示将格式化读取的数据输入到程序中的地址。(程序中变量的地址)
fscan():
- 参数1—FILE * stream一个文件流指针。表示需要读取的文件的信息结构体地址(读取哪个文件)
- 参数2—const char * format一个字符串首地址。表示格式化字符串的地址
- 参数…—可以有多个参数,根据格式化字符串来决定,表示要格式化读取的数据要输入到程序中的地址(程序中变量的地址)
-
返回值:int类型
如果成功,这些函数返回成功匹配和分配的输入数据的数量
如果失败:返回EOF。以下是错误的类型:
1.成功匹配完之前只要有一处与格式化字符串不匹配的地方,则返回值EOF。
2.如果发生读取错误,也会返回EOF -
特点:
scanf函数系列遇到换行符’\n’和空格符’ '都会停止匹配。
7.练习
1.在程序中分别打开源文件和目标文件,循环从源文件中读取内容并写入目标文件。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define N 128
int main(int argc, const char *argv[])
{
int n;
int buf[N];
FILE *fps, *fpd;
if(argc < 3){
printf("Usage:%s <src_file> <dst_file>\n",argv[0]);
return -1;
}
//打开源文件
fps = fopen(argv[1], "r");
if(fps == NULL){
fprintf(stderr, "fail to fopen %s: %s\n", argv[1], strerror(errno));
return -1;
}
//打开目标文件
fpd = fopen(argv[2], "w");
if(fpd == NULL){
fprintf(stderr, "fail to fopen %s: %s\n", argv[2], strerror(errno));
return -1;
}
//循环读写文件
n = fread(buf, 1, N, fps);
if(n != EOF)
fwrite(buf, 1, n, fpd);
//关闭文件
fclose(fps);
fclose(fpd);
return 0;
}
2.获取系统时间并保存到文件
- 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
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
int main(int argc, const char *argv[])
{
FILE *fp;
time_t ctime;
struct tm *ctimestr;
int linecount = 0;
char buf[32] = {0};
fp = fopen("time.txt", "a+");
if(fp == NULL){
perror("fopen");
return -1;
}
while(fgets(buf, 32, fp) != NULL){
if(buf[strlen(buf) - 1] == '\n')
linecount++;
}
while(1){
ctime = time(NULL);
ctimestr = localtime(&ctime);
fprintf(fp, "%d: %04d-%02d-%-2d %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);
return 0;
}
./time
time.txt:
0: 2022-08-20 09:24:25
1: 2022-08-20 09:24:26
2: 2022-08-20 09:24:27
3: 2022-08-20 09:24:28
4: 2022-08-20 09:24:29
5: 2022-08-20 09:24:30
6: 2022-08-20 09:24:31
7: 2022-08-20 09:24:32
二、文件IO
2.1.概述
文件IO,又称系统IO,系统调用。是操作系统提供的API接口函数
POSIX(可移植操作系统接口)定义的一组函数
文件IO不提供缓冲机制
标准IO和文件IO不能混用
功能 | 标准IO | 文件IO(低级) |
---|---|---|
打开 | fopen、freopen、fdopen | open |
关闭 | fclose | close |
读 | getc,fgetc,getchar,fgets,gets,fread | read |
写 | putc,fputc,putchar,fputs,puts,fwrite | write |
文件描述符
- 每个打开的文件都对应着一个文件描述符
- 文件描述符是一个非负整数,linux为每一个打开的文件分配一个文件描述符
- 文件描述符从0开始分配,依次递增
- 文件IO操作通过文件描述符来完成
- fd(fail descriptor)
- 0-1023,表示文件
- 0,1,2的含义 标准输入、标准数出、标准错误
2.2.文件IO编程
1.文件IO的打开
#include <fcntl.h>
int open(const char *pathname, int flags); 不创建文件
int open(const char *pathname, int flags, mode_t mode); 创建文件,不能创建设备文件
- 成功时返回文件描述符;
- 出错时返回EOF
- 打开文件时使用两个参数
- 创建文件时使用三个参数,指定新文件的权限
- 只能打开设备文件
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 | 其他用户有执行权限 |
文件IO和标准IO的模式对应关系
0664—普通用户权限
2.文件IO的关闭
#include <unistd.h>
int close(int fd);
成功时返回0;出错时返回EOF
程序结束时自动关闭所有打开的文件
文件关闭后,文件描述符不再代表文件
3.文件IO的读写
read函数用来从文件中读取数据
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
成功时返回实际读取的字节数;出错时返回EOF
读到文件末尾时返回0
buf是接收数据的缓冲区
count不应超过buf大小
write函数用来向文件写入数据:
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t count);
成功时返回实际写入的字节数;出错时返回EOF
buf是发送数据的缓冲区
count不应超过buf大小
4.文件IO的定位
lseek函数用来定位文件:
#include <unistd.h>
off_t lseek(int fd, off_t offset, intt whence);
- 成功时返回当前的文件读写位置;出错时返回EOF
- whence参数:SEEK_SET/SEEK_CUR/SEEK_END
- SEEK_SET:从距文件开头offset位移量为新的读写位置;
- SEEK_CUR:以目前读写位置往后增加offset个位移量;
- SEEK_END:将读写位置指向文件尾后再增加offset个位移量
- offset参数:偏移量,可正可负
示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, const char *argv[])
{
int fd, ret;
char buf[32] = "hello world";
char buf2[32] = {0};
fd = open("test.txt", O_RDWR | O_CREAT|O_APPEND, 0666);
if(fd < 0) {
perror("file to open");
return 0;
}
printf("success,fd = %d\n", fd);
ret = write(fd, buf, strlen(buf));
if(ret < 0) {
perror("fail to write");
return 0;
}
printf("write count = %d\n", ret);
lseek(fd, 0, SEEK_SET);
ret = read(fd, buf2, 32);
if(ret < 0) {
perror("read");
return 0;
}
buf2[31] = 0;
printf("read buf2 = %s\n", buf2);
return 0;
}
//运行结果
linux@linux:~/Desktop/fileIO$ ./a.out
success,fd = 3
write count = 11
read buf2 = hello world
linux@linux:~/Desktop/fileIO$ cat test.txt
hello world
5.访问目录
opendir函数用来打开一个目录文件:
#include <dirent.h>
DIR *opendir(const char *name);
DIR *fdopendir(int fd); 使用文件描述符,要配合open函数使用、
DIR是用来描述一个打开的目录文件的结构体类型
成功时返回目录流指针;出错时返回NULL
readdir函数用来读取目录流中的内容:
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent是用来描述目录流中一个目录项的结构体类型
成功时返回目录流dirp中下一个目录项;
出错或到末尾时时返回NULL
示例
#include <dirent.h>
#include <stdio.h>
int main(int argc, const char *argv[])
{
DIR *dp;
struct dirent *dt;
dp = opendir("/home/linux/Desktop");
if(dp < 0) {
perror("fail to opendir");
return -1;
}
while((dt = readdir(dp)) != NULL) {
printf("%s\n", dt->d_name);
}
closedir(dp);
return 0;
}
6.修改文件访问权限
chmod/fchmod函数用来修改文件的访问权限:
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
成功时返回0;出错时返回EOF
root和文件所有者能修改文件的访问权限
示例: chmod(“test.txt”, 0666);
7.获取文件属性
#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
函数都是获取文件(普通文件,目录,管道,socket,字符,块()的属性
struct stat是存放文件属性的结构体类型:
mode_t st_mode;类型和访问权限
uid_t st_uid;所有者id
uid_t st_gid;用户组id
off_t st_size;文件大小
time_t st_mtime; 最后修改时间
- stat结构体
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
通过系统提供的宏来判断文件类型:
S_IFMT 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 是否FIFO文件
S_ISLNK(st_mode) 0120000 是否链接文件
S_ISSOCK(st_mode) 0140000 是否SOCKET文件
- 文件访问权限ST_mode
通过系统提供的宏来获取文件访问权限:
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 其他用户拥有执行权限
示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
int main (int argc,char **argv){
struct stat buf;
int ret;
ret = stat("chmod_t.c",&buf);
if(ret<0){
perror("stat");
return 0;
}
if(S_ISREG(buf.st_mode)){
printf("-");
}
if(S_ISDIR(buf.st_mode)){
printf("d");
}
if(S_ISCHR(buf.st_mode)){
printf("c");
}
if(S_ISBLK(buf.st_mode)){
printf("b");
}
if(S_ISFIFO(buf.st_mode)){
printf("p");
}
if(S_ISSOCK(buf.st_mode)){
printf("s");
}
// printf(" ");
int i;
for(i=8;i>=0;i--){
if(buf.st_mode & (1<<i)){
switch(i%3){
case 2:
printf("r");
break;
case 1:
printf("w");
break;
case 0:
printf("x");
break;
}
}else{
printf("-");
}
}
printf(" %d",(int)buf.st_size);
struct tm *t;
t = localtime(&buf.st_ctime);
printf(" %d-%d-%d %d:%d",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min);
printf(" chmod_t.c\n");
}
//运行结果
linux@linux:~/Desktop/fileIO$ gcc stat.c
linux@linux:~/Desktop/fileIO$ ./a.out
_ rw-rw-r-- 185 2022 8 16 13:15 chmod.c
linux@linux:~/Desktop/fileIO$ ls -l chmod.c
-rw-rw-r-- 1 linux linux 185 Aug 16 13:15 chmod.c
linux@linux:~/Desktop/fileIO$