我们知道 C语言
有文件操作接口,那么当然 C++
、JAVA
这些语言都有文件操作接口。但是最让人烦的问题是这些语言之间的 IO
接口都是不一样的,这样子导致我们学习成本很高!
这些语言拥有文件操作接口的目的找到文件,然后对文件进行操作。那么文件是在磁盘上,磁盘是属于硬件。对于硬件的访问只有操作系统才能进行。所有人想访问磁盘都不能绕开操作系统,C语言也好,其他语言也罢都是人表达出意思让操作系统理解我们想要干嘛,所以任何上层语言想要进行对磁盘进行操作,都会使用操作系统提供的接口。
所以我们只要搞清楚了操作系统提供的 IO
接口,那么其它语言只是对其的一个包装而已!下面我们一起来认识一下系统的IO接口!
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
// 功能:打开或者创建一个新文件或设备
// 返回值:打开成功返回打开文件的文件描述符(后面会讲到),打开失败则返回-1
// 参数:
// 1、pathname代表要打开或创建的目标文件
// 2、flags代表打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
// 3、mode代表打开文件时所带的权限,对于普通文件一般是0666(要考虑掩码umask的值)
其中对于参数 flags
有下面几种:
read-only
):只读打开write-only
):只写打开打开上面这三个常量,必须指定一个且只能指定一个!
mode
选项,来**指明新文件的访问权限**truncate
):清空文件的内容
上面这些 flags
选项的 本质其实就是宏,用不同的比特位来表示不同的信号,比特位之间通过按位或传递选项,下面举个例子大家就懂原理了:
#include <stdio.h>
// 用不同的比特位来表示不同的信号
#define ONE (1 << 0) // 0
#define TWO (1 << 1) // 1
#define THREE (1 << 2) // 2
#define FOUR (1 << 3) // 4
void showSig(int flags)
{
if(flags & ONE) printf("one\n");
else if(flags & TWO) printf("two\n");
else if(flags & THREE) printf("three\n");
else if(flags & FOUR) printf("four\n");
}
int main()
{
showSig(ONE);
printf("------------------------\n");
showSig(TWO);
printf("------------------------\n");
showSig(ONE|TWO);
printf("------------------------\n");
showSig(ONE|TWO|THREE);
printf("------------------------\n");
showSig(ONE|TWO|THREE|FOUR);
printf("------------------------\n");
return 0;
}
// 运行结果
one
------------------------
two
------------------------
one
------------------------
one
------------------------
one
------------------------
下面我们先介绍 close
接口再进行代码测试!
#include <unistd.h>
int close(int fd);
// 功能:关闭对应文件描述符指向的文件
// 返回值:成功关闭返回0,失败的话返回-1,并且设置errno为错误信息
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0); // 将子进程中的umask设为想要的值,这里设为0
int fd = open("log.txt", O_WRONLY | O_CREAT, 0664);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
// 关闭文件
close(fd);
return 0;
}
运行结果:
fd: 3
这里的 fd
为 3
是巧合吗?当然不是的,这个是 文件描述符(file descripter
) ,并且我们也可以猜到,这也可能和文件指针是有关系的,这个我们后面会讲!!!
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd1 = open("log1.txt", O_WRONLY | O_CREAT, 0664); // 权限设为0664
if(fd1 < 0)
{
perror("open");
return 1;
}
int fd2 = open("log2.txt", O_WRONLY | O_CREAT, 0644); // 权限设为0644
if(fd2 < 0)
{
perror("open");
return 1;
}
// 关闭文件
close(fd1);
close(fd2);
return 0;
}
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
// 功能:向文件描述符中写入内容
// 返回值:成功写入的话返回写入的字节个数,失败的话则返回-1,并且设置errno为错误信息
// 参数:
// 1、fd表示要被写入的文件描述符
// 2、buf表示写入的内容(可能是文本类也可能是二进制类,所以这里为void*)
// 3、count表示写入内容的大小,不算入'\0'(以字节为单位)
先来解析一下上面的参数设定:
buf
是要写入的内容,有可能是文本类也有可能是二进制类,但是操作系统不管,因为在操作系统看来,它要处理的都是二进制类的内容,而具体处理文本的内容其实是语言帮我们完成,这个要注意!所以buf的参数类型是 void*
,不会被传入的内容影响!count
的问题主要是在我们传入大小时候,有可能会因为受语言级别的函数影响容易传错大小导致乱码,要 注意的是操作系统接收的大小里面是不需要包括‘\0’
的,至于语言级别的函数想要传入包括 ‘\0’
,这是语言的规定,和我们系统接口是没有关系的,比如说传入一个 buffer
字符串,那么大小只需要传入 strlen(buffer)
即可,不需要加一!❓ 还要注意的一个细节:
O_TRUNC
这个选项!#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// 注意要清空内容,带上O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
// 将字符串写到文件中
char buffer[1024];
int cnt = 5;
while(cnt--)
{
// 将字符串和数字通过sprintf转化为字符串
sprintf(buffer, "%s:%d\n", "lirendada", cnt + 1);
// 将新内容写入文件,注意系统接口是不包括'\0'的大小的
write(fd, buffer, strlen(buffer));
}
// 关闭文件
close(fd);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// 注意不能清空内容,并带上O_APPEND
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
char buffer[1024];
int cnt = 5;
while(cnt--)
{
// 将字符串和数字通过sprintf转化为字符串
sprintf(buffer, "%s:%d\n", "after append!!!", cnt + 10);
// 追加新内容,注意系统接口是不包括'\0'的大小的
write(fd, buffer, strlen(buffer));
}
// 关闭文件
close(fd);
return 0;
}
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
// 功能:从文件中读取内容到指定位置
// 返回值:成功读取的话返回读取内容的字节数,实际上该返回值就在'\0'的位置;失败的话返回-1,并且设置errno为错误信息
// 参数:
// 1、fd表示读取内容的文件描述符
// 2、buf表示读取内容的目标处
// 3、count表示读取内容的大小(以字节为单位)
有几个值得注意的点:
read
在读取文件中的内容后,假如我们放到字符数组中,它是不会帮我们添加 \0
的,所以我们必须要手动添加,而添加 \0
的位置刚好就是返回值的位置,因为返回值代表内容的大小!详细的看下面代码是如何读取的!\0
结尾的,所以我们在读取的时候,如果是用字符数组接收的话,那么要多留一个位置出来,方便我们读取后手动添加 \0
。#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
// 读取文件中的信息
char buffer[1024];
// 这里sizeof - 1是为了留位置给我们自己添加的'\0'腾出位置,因为文件中是不含'\0'的,要多预留一个位置出来
ssize_t num = read(fd, buffer, sizeof(buffer) - 1);
// 大于0表示读取成功,并将buffer的末尾设为'\0',因为系统接口是不为我们添加'\0'的
if(num != 0)
{
buffer[num] = '\0';
printf("%s", buffer);
}
// 关闭文件
close(fd);
return 0;
}
// 运行结果:
[liren@VM-8-2-centos cfile]$ ./file
lirendada:5
lirendada:4
lirendada:3
lirendada:2
lirendada:1
after append!!!:14
after append!!!:13
after append!!!:12
after append!!!:11
after append!!!:10
[liren@VM-8-2-centos cfile]$
比如说 lseek
等其它接口,我们学以上接口就基本足够我们了解系统接口和语言级别接口的区别了,我们现实情况下也不会说去专门调用系统级别的接口,所以学习它们是为了更好的区分底层所做的逻辑工作!