文件IO
库函数 IO
打开文件 fopen
fopen 返回一个文件的操作句柄,有了句柄才能对指定文件进行操作
FILE fopen(char pathname,char* mode);
mode—文件操作方式:
(1)r----只读打开(文件不存在报错)
(2)w----只写打开(文件不存在会创建,存在则截断长度为 0 ,丢弃原内容)
(3)a----追加写打开(文件不存在创建,存在则写入数据总是写入文件末尾)
(4)r+ -----读写打开(文件不存在报错)
(5)w+ ----读写打开(文件不存在创建,丢弃原内容)
(6)a+ ---- 读写打开(不存在创建,数据写入末尾)
(7)b ---- 以二进制形式打开(默认不使用 b ,则使用文本方式打开-----对特殊字符处理会有不同)
以文本形式打开,有可能会造成读到的数据与文件实际的数据有差别
向文件写入数据 fwrite
fwrite
size_t fwrite(char* data,int bsize,int nmem,FILE*fp);
(1)bsize:文件块个数
(2)nmem:文件块大小
从文件读取数据 fread
fread
size_t fread(char* buf,int bsize,int nmem,FILE* fp);
关闭文件 fclose
fclose
int fclose(FILE* fp);
跳转当前读写位置 fseek
fseek
int fseek(FILE* fp,int offset,int whence);
(1)offset:偏移地址
(2)whence:文件起始地址
从 whence 位置进行偏移,whence 可能取值:SEEK_SET(文件起始地址)、SEEK_END(文件末尾)、SEEK_CUR(指定起始位置)
练习
1 #include<stdio.h>
2 #include<string.h>
3
4 int main()
5 {
6 FILE* fp=fopen("./test.txt","w+"); //w+ 读写方式打开
7 if(NULL==fp){
8 perror("fopen error");
9 return -1;
10 }
11
W> 12 char *data="天黑了!\n";
13 //fwrite(数据地址,块大小,块个数,句柄)
14 size_t ret=fwrite(data,1,strlen(data),fp); //返回实际写入的完整块个数
15
16 //其实就是写入的字节长度,因为块大小为 1
17
18 if(ret!=strlen(data)){
19 perror("fwrite error!");
20 fclose(fp);
21 return -1;
22 }
23
24
25 fseek(fp,0,SEEK_SET); //从文件起始位置开始跳转
26
27
28 char buf[1024]={0};
29 ret=fread(buf,1,1023,fp); //块大小 1,块个数=想要读取的长度,这样返回值 就是实际读取到的字节长度
30 if(ret==0){
31 if(feof(fp)) //判断是否读到文件末尾
32 printf("读取到了文件末尾,end of file");
33 if(ferror(fp)) //判断上一次对 fp 的操作是否出错
34 {
35 perror("fread error");
36 fclose(fp);
37 return -1;
38 }
39 }
40
41 printf("%s\n",buf);
42 fclose(fp);
43
44 return 0;
45 }
运行结果:
以上都是库函数,库函数是对系统调用接口的封装
系统调用接口:
open、write、read、close、lseek
系统调用接口
open、write、read、lseek、close
open
int open(char* pathname,int flag);
int open(char* pathname,int flag,int mode);
(1) pathname:要打开的文件路径
(2) flag:文件的打开方式
必选其一:
O_RDONLY ----00 可读
O_WRONLY ----01 可写
O_RDWR ----- 02 可读可写
可选项:
O_CREAT -----文件不存在会创建
O_TRUNC ----- 截断文件原有内容,丢弃原内容
O_APPEND ---- 追加写
w+:可读可写,文件不存在则会创建,文件已存在则截断内容为0 ======= O_RDWR | O_CREAT | O_TRUNC
a+:可读追加写 ,文件不存在则会创建 ======= O_RDWR | 0=CREAT | O_APPED
(3) mode:当 O_CREAT 被使用时,就一定要设置 mode ,用于设定文件访问权限 0664
0664 --------- 前边的 0 不能省略,否则会涉及特殊权限位的设置
给定权限会受到 umask 影响:
实际权限=给定权限 & (~umask),默认 umask=0002
mode_t umask(mode_t mask); 将当前调用的进程掩码设置位 mask ,并不会影响外部的 umask
返回值:打开成功则返回一个文件描述符(非负整数)作为文件操作句柄;失败返回 -1
write
ssize_t write(int fd,char* buf,size_t len);
(1) fd: open 打开文件时返回的操作句柄-文件描述符
(2) buf: 要写入的数据所在空间首地址
(3) len: 要写入数据的长度(字节为单位)
返回值:成功返回实际写入数据长度,失败返回 -1
read
ssize_t read(int fd,char* buf,size_t len);
(1) fd : open打开文件时的操作句柄
(2) buf:一块空间首地址,用于存放读取到的数据
(3) len:读取长度,不能大于buf空间的大小,避免越界
返回值:成功返回实际读取到的数据长度(字节为单位),失败返回 -1,0表示读到文件末尾
lseek
off_t lseek(lint fd,off_t offset,int whence);
(1)fd:文件描述符
(2)offset:偏移量
(3)whence:偏移的起始位置
SEEK_SET; SEEK_CUR; SEEK_END
返回值:当前跳转后,读写位置相当于文件起始位置位置的偏移量(接口有一种另类用法,跳转到文件末尾,通过返回值确定文件大小)
close
int close(int fd);
fd:文件描述符
说明:
size_t :无符号整型
ssize_t :有符号整型
off_t:整型
使用练习:
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/stat.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6
7 int main()
8 {
9 umask(0); //采用默认设置的权限值,只在当前文件内有效
10 int fp=open("./tmp.txt",O_RDWR|O_CREAT|O_TRUNC,0777);
11 if(fp<0){
12 perror("open error");
13 return -1;
14 }
15
16 //打开成功
17
18 const char *str="hello world!\n";
19 ssize_t ret=write(fp,str,strlen(str));
20 if(ret<0){
21 perror("write error");
22 close(fp);
23 return -1;
24 }
25
26 //写入数据之后,文件标识到达文件末尾
27 lseek(fp,0,SEEK_SET); //起始位置开始
28
29
30 //写入成功,读取数据
31 char buf[1024];
32 ret=read(fp,buf,strlen(str));
33
34 if(ret<0){
35 perror("read error");
36 close(fp);
37 return -1;
38 }
39 printf("%s",buf);
40 close(fp);
41 return 0;
42 }
运行结果:
库函数是对系统调用接口的一个封装,一般来说对于系统调用接口来说是没有缓冲区的,而库函数封装了缓冲区以及系统调用接口(还有文件描述符)--------- 类似于前边所说的 exit 与 _exit
重定向
将原本要写入 A 位置的数据不写入 A ,而是写入 B
ls > a.txt 标准输出重定向,将原本要写入标准输出打印的数据写入 a.txt 文件
>:清空重定向,清空原来内容
>>:追加重定向,追加到原内容末尾
./main > /dev/null 2>&1:
将标准输出与标准错误都重定向到 /dev/null 文件中
(1) > /dev/null :将标准输出重定向到 /dev/null 文件
(2) 2>&1 : &1表示标准输出,相当于将标准错误重定向到标准输出 ,2 是标准错误
./main 2>&1 /dev/null:
将标准错误打印到标准输出,再将标准输出写入到 /dev/null 文件当中
文件描述符
0-标准输入
1-标准输出
2-标准错误
文件描述符是按照最小未使用原则进行分配的,因此应该从 3 开始进行分配:
#include<stdio.h>
#include<fcntl.h> //open 头文件
#include<unistd.h>
int main()
{
int fd=open("tmp.txt",O_RDWR);
if(fd<0){
perror("open error");
return -1;
}
//打开文件成功
printf("fd = %d\n",fd);
//当再打开文件之前并未关闭标准输入、标准输出、标准错误描述符时,会按照最小未使用原则进行,则此时打开的文件描述符为 3
close(fd);
return 0;
}
(1)close(0); 关闭标准输入
当关闭了 0 号描述符之后,打开一个新文件,会默认按最小未使用原则,新文件的描述符就会成为 0:
当在打开文件前关闭了标准输出,则printf 不会打印出来内容:
(2)close(1) ; 关闭标准输出
printf 是向 stdout 写入数据,stdout 本质是封装了 1 号文件描述符信息,printf 打印信息,实际是将数据信息写入了 stdout 缓冲区,但是 close 关闭文件时会直接将缓冲区的信息直接释放。
假如在最后对标准输出进行刷新:
fflush(stdout) ;
则会将要打印的数据写入到 1 号文件描述符位置,但是当前 1 号文件描述符对应的是 tmp.txt 文件,即将要写入的数据写入 tmp.txt 文件当中。
重定向本质原理:
将一个描述符所对应位置的文件描述信息地址,给替换成另一个文件的描述--------实现了在不改变其他逻辑的情况下,改变了所操纵的文件。
int dup2(int oldfd, int newfd);
让 newfd 对应位置,保存 oldfd 对应位置的信息
将 newfd 重定向到 oldfd
若 newfd 本身代表了一个已经打开的文件,则重定向前会把文件关闭释放
dup2(fd,1); 将标准输出的内容输入到 fd 文件中
在 minishell 中实现重定向 > 与 >>
(1)捕捉用户输入,并进行分割;
(2)判断输入的命令是否存在重定向符号,一般重定向符号之后内容不是指令内容,而是要重定向到的文件信息;
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include<sys/wait.h>
6 #include<fcntl.h> //open
7 #include<sys/stat.h> //open
8
9
10 int main()
11 {
12 while(1){
13 printf("【user@host~ 】$ ");
14 fflush(stdout); //刷新缓冲区
15
16 char cmd[32]={0}; //用于存储键盘输入的字符串
17 fgets(cmd,1023,stdin) ; //获取键盘输入的字符
18
19 cmd[strlen(cmd)-1]='\0'; //将键盘输入的字符后加上 \0 表示结束
20
21 printf("[%s]\n",cmd); //打印输入的字符内容
22
23 int argc=0;
24 char* argv[32]={NULL}; //用于存储分割后的字符内容
W> 25 char* ptr=cmd; //保存获取到的字符信息
26 argv[argc++]=strtok(cmd," "); //以空格对字符串进行分割处理
27
28
29 while((argv[argc]=strtok(NULL," "))!=NULL){ //strtok 第一个参数 NULL ,表示默认> 以上一次终止位置作为新分割的起始位置进行分割
30 printf("[%s]\n",argv[argc]);
31 argc++;
32 }
33
34 //打印分割后的字符内容
35 int i=0;
36 for(;argv[i]!=NULL;++i){
37 printf("[%s]\n",argv[i]);
38 }
39 //如果当前输入的第一个字符为 cd,则更改工作路径
40 if(strcmp(argv[0],"cd")==0){
41 chdir(argv[1]); //更改当前工作路径
42 continue;
43 }
44
45
46
47 //判断是否存在重定向符号
48 // ls -l -a > a.txt //默认重定向符号之后有个空格
49 //
50 int redir_flag=0; //0-没有重定向,1-清空重定向,2-追加重定向
51 char *redir_file=NULL; //标记文件
52 for(i=0;i<argc;++i){
53 if(strcmp(argv[i],">")==0){
54 //清空重定向
55 redir_flag=1;
56 argv[i]=NULL;
57 break; //重定向之后的内容为文件名,而不是指令内容
58 redir_file=argv[i+1];
59 }
60 else if(strcmp(argv[i],">>")==0){
61 //追加重定向
62 redir_flag=2;
63 argv[i]=NULL;
64 break;
65
66 redir_file=argv[i+1];
67 }
68 }
69
70
71
72 //创建子进程
73 pid_t cpid=fork();
74 if(cpid<0){
75 perror("fork error!");
76 continue;
77 }
78 else if(cpid==0){
79 //子进程实现程序替换
80 //
81 if(redir_flag==1){
82 //清空重定向
83 int fp = open("tmp.txt",O_RDWR|O_CREAT|O_TRUNC,0664); //清空打开文件
84 dup2(fp,1);
85 }
86 else if(redir_flag==2){
87 //追加重定向
88 int fp=open("tmp.txt",O_RDWR|O_CREAT|O_APPEND,0664);
89 dup2(fp,1); //将标准输出重定向到 fp 文件
90
91 }
92
93 execvp(argv[0],argv);
94 perror("execvp perror");
95 exit(-1); //只有程序替换失败才会执行这行代码
96 }
97 wait(NULL); //每一个子进程运行完毕后,才能开始捕捉下一个输入进行操作
98 }
99
100 return 0;
101 }
静态库 & 动态库
静态库
库文件:
将已经实现的代码进行打包,并不是为了生成可执行程序,而是为了给其他人提供接口使用。
静态链接:
生成可执行程序时,链接静态库,直接将库中所用到的函数实现拿到可执行程序当中,不存在依赖,运行效率高,但是生成的可执行程序很大----------------可能库中代码在内存中会存在冗余
动态库
以位置无关代码打包
动态链接:
生成可执行程序时,链接动态库,记录库中符号表,生成程序小,并且多个程序运行时在内存中可以共享动态库内容,运行时依赖动态库的存在
生成库
生成库:把大量代码打包起来
(1)先把所以源码进行编译汇编生成各自的二进制指令 gcc *.c -o *.o
(2)把所有生成的二进制文件打包在一起
:vnew 文件名
新开一个 vim 窗口 ,命名为 当前给定的文件名
ctrl + ww :进行两个 vim 文件的跳转
静态库: ar -cr
Linux 下静态库命名:以 lib 为前缀,以 .a 为后缀
gcc -c child.c -o child.o
ar -cr libmychild.a child.o
生成一个名为 mychild 的静态库
动态库:
linux 下动态库命名:以 lib 作为前缀,以 .so 作为后缀,中间是库名。
gcc -c child.c -o child.o
gcc -fPIC -c child.c -o child.o
gcc --shared child.o -o libmychild.so
生成一个名为 mychild 的动态库
-fPIC:告诉编译器,在编译生成指令的时候产生与位置无关代码(变量指令的地址都是相对偏移量)
一个动态库被映射到不同进程的虚拟地址空间时,映射的位置也不一定相同,所以在 main 中调用的库函数都只是记录一个相对偏移量,最后根据实际映射位置的起始地址进行计算。--------更加灵活
-c:只进行预处理,编译,汇编完毕后退出,不进行链接
–shared:告诉编译器要生成的是一个库文件,而不是可执行程序
使用库
直接运行 main 程序时会报错,因为未找到 printf_child 的定义:
使用 -l 来告诉编译器要使用哪个库
gcc main.c -o main -lmychid
mychild 是要链接的库文件
但是会报错,因为编译器会到系统指定目录下(/usr/lib64)来寻找库文件
因此我们可以
(1)将库放到指定位置下 :
x64------/usr/lib64 ; x32-----------/usr/lib
sudo cp libmychild.so /usr/lib64
//但是一般不推荐,因为这样会污染 /usr/lib64 库文件
(2)设置环境变量:
export LIBRARY_PATH=${LIBRARY_PATH}:./
//将库文件所在目录添加到环境变量
//当前要使用的库文件位于 ./ 当前目录下
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./
//添加运行加载库文件的路径
(3)使用 gcc -L 选项,指定库文件所在路径
gcc main.c -o main -L./ -lmychild
这种方式只适合在指定路径下,链接静态库,因为无法设置运行程序时动态库的加载路径
ps:
有任何疑问欢迎评论
留言~
~