Loading

05 文件之IO

标准IO介绍

IO的概念

I——input,指输入设备,比如键盘鼠标都是Input设备。

O——output,指输出设备,比如显示器。

优盘,网口,既是输入也是输出。

文件类型

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

文件类型:

  • 常规文件 r

  • 目录文件 d

  • 字符设备文件 c

  • 块设备文件 b

  • 管道文件 p

  • 套接字文件 s

  • 符号链接文件 l(类似快捷方式)

系统调用和库函数

​​image​​

liunx内核提供给用户一个接口,来在显卡上显示字符。这个接口的使用就叫做。而printf这个输出函数背后实际上是调用了这个接口,做了大量的工作。

从标准IO的角度来解释,可以理解为库函数提供了一组用于进行标准输入输出操作的函数。这些函数封装了底层的IO操作细节,使得程序员可以方便地进行输入和输出操作,而无需直接与底层设备进行交互。

标准IO库函数主要包括以下几个方面的功能:

  1. 输入函数:标准IO库函数提供了一系列用于读取输入数据的函数,如scanf、fgets、getchar等。这些函数可以从标准输入设备(通常是键盘)读取用户输入的数据,并将其转化为程序可以处理的格式。
  2. 输出函数:标准IO库函数还提供了一系列用于输出数据的函数,如printf、puts、fputs等。这些函数可以将程序处理的数据格式化并输出到标准输出设备(通常是终端显示器),供用户查看。
  3. 缓冲处理:标准IO库函数通常会使用缓冲区来提高IO效率。输入函数会将输入数据缓存在内存中,输出函数则会将输出数据暂存到缓冲区中,待缓冲区满或遇到特定条件时再进行实际的IO操作。这样可以减少对IO设备的频繁访问,提高程序的性能。
  4. 设备驱动程序调用:标准IO库函数通过操作系统提供的设备驱动程序进行实际的IO操作。这些驱动程序负责将数据从内核空间传输到底层硬件设备,如磁盘、网络接口等。标准IO库函数隐藏了底层设备细节,程序员无需关心具体的硬件接口和协议。

通过使用标准IO库函数,程序员可以方便地进行输入输出操作,实现与用户和外部设备的交互。标准IO库函数提供了一种统一的接口,使得,而无需修改底层IO代码。

标准I/O由标准定义,现在主流操作系统上都实现了C库,标准I/O通过缓冲机制减少系统调用,实现更高的效率。

基本概念

FILE:标准IO用一个结构体类型来存放打开的文件的相关信息。标准I/O的所有操作都是围绕FILE来进行。

流(stream):FILE又被称为流(stream)。分类:文本流/二进制流。

image

流的缓冲类型:

  • 全缓冲

    当流的缓冲区无数据或无空间时才执行实际I/O操作

  • 行缓冲

    当在输入和输出中遇到换行符(‘\n’)时,进行I/O操作。

    当流和一个终端关联时,典型的行缓冲。

  • 无缓冲

    数据直接写入文件,流不进行缓冲。

标准I/O预定义3个流,程序运行时自动打开。保证基本驱动的运行和使用。

标准输入流 0 STDIN_FILENO stdin 行缓冲
标准输出流 1 STDOUT_FILENO stdout 行缓冲
标准错误流 2 STDERR_FILENO stderr

小示例

​​image​​

image

printf是输出设备,按道理说,没有遇到换行是不应该输出的,而在这里hello world依然被打印出来了,是因为结束程序,会把缓冲区里面的内容都输出出来。

如果我们让程序一直不结束,这个程序就不会打印hello world了:

image

image

如果在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”

image

小示例——读取文件:

image

image

处理错误信息

  • ​errno 存放错误号,由系统生成。

  • ​perror先输出字符串s,再输出错误号对应的错误信息。头文件stdio.h。

  • ​strerror根据错误号返回对应的错误信息。头文件 ​ 、​。

perror和strerror 功能:打印系统的错误描述(注意:是系统错误,不是你自己代码错误)。

有1.txt的情况下:

​​image​​

image

image.png

strerror的演示:

image

image

常见编译错误:

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。参数必须为非空,否则出现断错误。

  • 流关闭时自动刷新缓冲中的数据并释放缓冲区。

  • 当一个程序正常终止时,所有打开的流都会被关闭。

  • 流一旦关闭后就不能执行任何操作。

断错误:文件没有打开成功,还关闭文件就会出现。

image

读写字符

image.png

注意事项:

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)

image.png

应用场景,在休眠前先输出到文件。

流的定位

long ftell(FILE *stream);

long fseek(FILE *stream, long offset, int whence);

void rewind(FILE *stream);

image.png

  • fseek 参数whence参数:SEEK_SET/SEEK_CUR/SEEK_END

  • SEEK_SET 从距文件开头 offset 位移量为新的读写位置

  • SEEK_CUR:以目前的读写位置往后增加 offset 个位移量

  • SEEK_END:将读写位置指向文件尾后再增加 offset 个位移量

  • offset参数:偏移量,可正可负

注意事项:

  1. 文件的打开使用a模式 fseek无效

  2. rewind(fp) 相当于 fseek(fp,0,SEEK_SET);

  3. 这三个函数只适用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操作通过文件描述符来完成。

image.png

这里的fd就是文件描述符。

image.png

image.png

open

LQQ9V605OQPY6G1873Q4.png

open函数用来创建或打开一个文件:

​ #include <fcntl.h>

​ intopen(const char *path, intoflag, …);

  • 成功时返回文件描述符;出错时返回EOF

  • 打开文件时使用两个参数

  • 创建文件时第三个参数指定新文件的权限

  • 只能打开设备文件

image.png

文件的初始权限:文件目录的最大默认权限-umask权限。

image.png

close

close函数用来关闭一个打开的文件:

​ #include <unistd.h>

​ int close(intfd);

  • 成功时返回0;出错时返回EOF

  • 程序结束时自动关闭所有打开的文件

  • 文件关闭后,文件描述符不再代表文件

image.png

打开的任何新文件fd都应以 3 开头。

关闭了一次close(fd),3已经不能代表文件描述符了。

image.png

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完全一样

image.png

目录的读取

访问目录 – 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

image.png

访问目录 – 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 其他用户拥有执行权限

image.png

打印文件类型:

image.png

image.png

获取打印的权限。

打印文件大小、时间、文件名。

image.png

最后运行结果:

image.png

静态库的使用

库的概念

库是一个二进制文件,包含的代码可被程序调用。比如:标准C库、数学库、线程库……

库有源码,可下载后编译;也可以直接安装二进制包。 /lib /usr/lib

库是事先编译好的,可以复用的代码。

在OS上运行的程序基本上都要使用库。使用库可以提高开发效率。

Windows和Linux下库文件的格式不兼容。

Linux下包含静态库和共享库。

静态库特点

编译(链接)时把静态库中相关代码复制到可执行文件中。

程序中已包含代码,运行时不再需要静态库。

程序运行时无需加载库,运行速度更快。

占用更多磁盘和内存空间。

静态库升级后,程序需要重新编译链接。

静态库和动态库的区别

1.静态库在程序编译时会被连接到目标代码中。

优点:程序运行时将不再需要该静态库;运行时无需加载库,运行速度更快

缺点:静态库中的代码复制到了程序中,因此体积较大;静态库升级后,程序需要重新编译链接。

2.动态库是在程序运行时才被载入代码中。

优点:程序在执行时加载动态库,代码体积小;将一些程序升级变得简单;不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。

缺点:运行时还需要动态库的存在,移植性较差。

静态库创建

  1. 确定库中函数的功能、接口

  2. 编写库源码hello.c

     #include  <stdio.h>
     void  hello(void) {
        printf(“hello  world\n”);
        return;
     }
  1. 编译生成目标文件$ gcc -c hello.c -Wall

  2. 创建静态库 hello$ ar crs libhello.a hello.o。静态库是.a,hello是静态库名称。查看库文件:image.png

  3. 查看库中符号信息$nm libhello.a

image.png

image.png

没有main函数的生成不了可执行文件。

链接静态库

  1. 编写应用程序test.c

    #include  <stdio.h>
    void  hello(void);  
    	int  main() {
    		hello();
    	        return  0;
    }
    
  2. 编译test.c并链接静态库libhello.a

    $ gcc -o test test.c -L. -lhello:-L参数表示库所在的路径,-l后面跟库的名称。

    $ ./test

按照下面的命令直接链接出错:

image.png

image.png

动态库的使用

动态库特点

编译(链接)时仅记录用到哪个共享库中的哪个符号,不复制共享库中相关代码。

程序不包含库中代码,尺寸小。

多个程序可共享同一个库。

程序运行时需要加载库。

库升级方便,无需重新编译程序。

使用更加广泛。

动态库创建

  1. 确定库中函数的功能、接口

  2. 编写库源码hello.c bye.c:

         #include  <stdio.h>
         void  hello(void) {
            printf(“hello  world\n”);
            return;
         }
    
  3. 编译生成目标文件$ gcc -c -fPIC hello.c bye.c -Wall

    必须加-fPIC,否则结果不一样:

    image.png

    image.png

  4. 创建动态库 common$ gcc -shared -o libcommon.so.1 hello.o bye.o

链接动态库

  1. 编写应用程序test.c:

        #include  <stdio.h>
        #include “common.h”  
         int  main() {
            hello();
            bye();
            return  0;
         }
    
  2. 编译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刷新。

image.png

posted @ 2023-08-28 16:34  阿四与你  阅读(24)  评论(0编辑  收藏  举报