目录
一、C语言文件接口回顾
C语言基础知识
C++中文件操作示例
二、系统文件概念及接口
重定向基本理解的回顾
文件的基本概念
系统调用接口
open
read
write
close
lseek
什么是当前路径
一、C语言文件接口回顾
引言:我们并不理解文件!从语言角度(绝对不可能理解)。我们要进行文件操作,前提是我们的程序跑起来了。文件的打开和关闭,是CPU在执行我们的代码
C语言基础知识
几个函数:
FILE *fopen(const char *path, const char *mode);`
fopen
是C语言标准库函数,用于打开一个文件并返回一个文件指针以便进行读写操作。如果文件打开成功,它会返回一个指向 FILE
类型的指针,该指针后续可以用于其他文件操作函数。如果打开失败,则返回 NULL
。
其中:
-
const char *path
:这是一个字符串,表示要打开的文件的路径名。可以是相对路径或绝对路径。 -
const char *mode
:这也是一个字符串,表示打开文件的模式,即如何使用文件
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
fread
函数是 C 标准库中的一个函数,用于从指定的流中读取数据到缓冲区中。该函数同样定义在 stdio.h
头文件中。fread
主要用于二进制输入操作。fread
函数返回成功读取的元素个数,这个数可能小于 nmemb
,如果遇到错误或文件末尾。如果文件末尾在读取任何字节之前到达,fread
将返回 0。
其中:
-
ptr
:指向内存块的指针,该内存块至少要有size * nmemb
字节的大小。函数从流中读取数据到这个内存块中。 -
size
:要读取的每个元素的大小,以字节为单位。 -
nmemb
:要读取的元素个数,每个元素的大小为size
字节。 -
stream
:指向FILE
对象的指针,该对象指定了输入流。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite
函数是 C 标准库中的一个函数,用于将数据写入到指定的流中。这个函数也是定义在 stdio.h
头文件中。fwrite
主要用于执行二进制输出操作。fwrite
函数返回成功写入的元素个数,这个数可能小于 nmemb
,如果遇到写入错误。如果成功写入 nmemb
个元素,则返回值等于 nmemb
。
其中:
-
ptr
:指向要写入数据的内存块的指针。 -
size
:要写入的每个元素的大小,以字节为单位。 -
nmemb
:要写入的元素个数,每个元素的大小为size
字节。 -
stream
:指向FILE
对象的指针,该对象指定了输出流
int fprintf(FILE *stream, const char *format, ...);
fprintf
是 C 标准库中的一个函数,用于将格式化的数据写入到指定的流(通常是文件流)。这个函数与 printf
类似,但是 fprintf
允许你指定写入数据的流,而 printf
默认写入到标准输出(通常是控制台)。如果成功,fprintf
返回写入的字符数。如果发生错误,fprintf
返回一个负数。
其中
-
stream
:指向FILE
对象的指针,该对象标识了要写入数据的流。 -
format
:指向一个以空字符结尾的字符串,该字符串包含格式规范,用于指定如何写入后续参数。 -
...
:可变数量的参数,它们将被格式化并写入到流中。
int fclose(FILE *stream);
fclose
是 C 标准库中的一个函数,用于关闭由 fopen
、freopen
或其他文件打开函数创建的文件流。关闭文件是非常重要的,因为它会释放与文件流关联的所有内部缓冲区,并且确保所有写入的数据都已经被正确地刷新到磁盘上。
其中:
-
如果文件成功关闭,
fclose
返回0
。 -
如果在关闭文件时发生错误,
fclose
返回EOF
(通常定义为-1
)。
文件打开方式:
几个例子:
【示例1】
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL){
perror("fopen");
return 1;
}
fprintf(fp, "helloworld, %d, %s, %lf\n", 10, "wuxu", 3.14);
fclose(fp);
return 0;
}
执行结果:
[wuxu@Nanyi lesson18]$ ls
makefile mytest test1.c
[wuxu@Nanyi lesson18]$ ./mytest
[wuxu@Nanyi lesson18]$ ls
log.txt makefile mytest test1.c
[wuxu@Nanyi lesson18]$ cat log.t
可以发现,我们以写的方式打开log.txt,我们原先并没有log.txt,是“w”的方式,创建了一个log.txt 并向内格式化写入内容
如果我们修改一下 main函数里的格式化输出,再进行执行
将代码修改为
fprintf(fp, "helloworld, %d, %s, %d\n", 10, "wuxu", 666);
可以发现:原文件内容被内容完全替代,因此我们对于“w”有个结论
-
如果文件不存在,就在当前路径下,新建指定的文件
-
默认打开文件的时候,就会先把目标文件清空
如果我们想追加对文件进行写入就需要讲模式换为“a”模式
C++中文件操作示例
#include <iostream>
#include <string>
#include <fstream>
#define FILENAME "log.txt"
int main()
{
std::ofstream out(FILENAME);
if(!out.is_open()) return 1;
std::string message = "hello C++\n";
out.write(message.c_str(), message.size());
out.close();
return 0;
}
二、系统文件概念及接口
重定向基本理解的回顾
我们第一次理解重定向是在 echo
命令上理解的,echo 命令本身是向显示器进行输出
[wuxu@Nanyi lesson18]$ echo "hello I am echo"
hello I am echo
我们也可以使用echo命令,向指定文件进行写入
我们也可以使用echo命令,向文件进行追加写入,也就是追加重定向
那么我们也就发现 >
相当于 "w"
, >>
相当于 “a“
,如果是的话,我们也可以使用 > 或 >> 进行创建文件
所以我们也就发现:输出重定向一定是文件操作
文件的基本概念
当我们使用ls-l
查看文件信息时,会显示文件的类型、权限、引用计数、所有者、所属组、大小、文件最新修改时间等信息,这些都是文件的属性
由此我们可以知道:文件 = 内容+属性
文件被打开,需要先被加载到内存。由于内存空间有限,不可能将所有的文件全部打开,所以文件可以分为已被打开文件,和未打开文件。
打开文件:本质是进程打开文件。文件没有打开的时候,文件在磁盘中;
一个进程可以打开很多文件,一个系统中可以存在很多进程;所以很多情况下,在操作系统内部,一定存在大量被打开的文件,那么操作系统是如何对这些文件进行管理呢?--->**先描述,在组织
系统调用接口
文件是存在磁盘中的,由冯诺伊曼体系我们可知,磁盘是一个外设,外设是一个硬件。我们向文件中写入,本质是向硬件中写入;但是用户没有权利直接写入,由于操作系统是硬件的管理着,那我们就可以通过操作系统给我们提供的系统调用(OS不相信任何人)来访问文件
下面是系统调用接口的函数,我们来一一介绍使用,看看与C/C++文件操作有什么共同点与不同点
open
其中:
-
pathname
:指向一个以空字符结尾的字符串,该字符串指定了要打开或创建的文件的路径名。 -
flags
:指定文件的打开模式。这个参数是以下选项中的一个或多个通过按位或操作(|
)组合起来的值:-
O_RDONLY
: 只读打开。 -
O_WRONLY
: 只写打开。 -
O_RDWR
: 读写打开。 -
O_CREAT
: 如果文件不存在,则创建它。需要与mode
参数一起使用。 -
O_EXCL
: 与O_CREAT
一起使用,确保调用open
时创建文件。如果文件已存在,则返回错误。 -
O_TRUNC
: 如果文件已存在且为写入或读写打开,则将其长度截断为 0。 -
O_APPEND
: 追加模式。在每次写入时,数据会被添加到文件的末尾。 -
其他标志请参考
open
的手册页。
-
-
mode
:当创建新文件时,这个参数指定了文件的模式(权限)。它是一个八进制数,通常由umask
的值与要求的权限按位取反后相与得到。常见的权限值包括:-
S_IRUSR
: 用户读权限。 -
S_IWUSR
: 用户写权限。 -
S_IXUSR
: 用户执行权限。 -
S_IRGRP
,S_IWGRP
,S_IXGRP
: 分别对应组成员的读、写、执行权限。 -
S_IROTH
,S_IWOTH
,S_IXOTH
: 分别对应其他用户的读、写、执行权限。 -
也可以使用数字来表示权限
-
返回值:
-
成功时,
open
返回一个最小的非负整数,称为文件描述符,用于后续的读写操作。 -
出错时,
open
返回-1
,并设置errno
来指示错误。
flag选项 | 描述 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_CREAT | 不存在就创建 |
O_TRUNC | 清空文件 |
O_APPEND | 追加写入 |
这些选项都是大写,我们在C/C++中什么情况下用上大写?没错就是宏,这里的flag选项与宏并未区别,我们先自己用宏来创建一个传参示例
测试内容:
#include <stdio.h>
#define ONE 1 // 1 0000 0001
#define TWO (1<<1) // 2 0000 0010
#define THREE (1<<2) // 4 0000 0100
#define FOUR (1<<3) // 8 0000 1000
void print(int flag)
{
if(flag&ONE)
printf("one\n"); //替换成其他功能
if(flag&TWO)
printf("two\n");
if(flag&THREE)
printf("three\n");
if(flag&FOUR)
printf("four\n");
}
int main()
{
print(ONE);
printf("\n");
print(TWO);
printf("\n");
print(ONE|TWO);
printf("\n");
print(ONE|TWO|THREE);
printf("\n");
print(ONE|FOUR);
printf("\n");
print(ONE|TWO|THREE|FOUR);
printf("\n");
return 0;
}
输出结果:
因此我们发现可以定制宏,通过传入不同的宏,实现定制的不同功能。它的原理底层是位图;一个int类型包含32个比特位,如果我们让低位的 1 表示ONE,次低位的 1 表示 TWO......
若此时要实现ONE、TWO、THREE,只需要对这三个数做或位运算ONE|TWO|THREE
则会得到一个值为00000000 00000000 00000000 00011010的flags。将它传递给处理函数中,处理函数会将flag与ONE、TWO、THREE这三个数挨个做按位与,如果按位与出来的结果不为0,则表示该选项会被选择
而我们系统调用的flags也是一样的道理
接下来让我们用系统调用接口来操作文件
【示例】
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
//system call
int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fda: %d\n", fda);
int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fdb: %d\n", fdb);
int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fdc: %d\n", fdc);
int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fdd: %d\n", fdd);
return 0;
}
输出结果:
【问题1】我们定制的权限是666 ,为什么这里是 662?是umask的原因
PS:我们使用系统接口必须要指明权限,否则创建的文件权限是随机的
在 POSIX 兼容的操作系统中,umask
是一个系统调用,用于设置进程的文件模式创建掩码(file mode creation mask)。这个掩码定义了新创建文件的默认权限,通过从权限掩码中“掩蔽”掉一些权限位来限制新文件的权限。我们来查看一下系统的umask
接下来我们修改一下umask,看文件权限能不能达到我们想要的结果
在test2.c 加入umask(0)
即可
测试结果:
【问题2】open系统调用函数返回的数字是什么?这个输出结果有什么含义?
open打开文件后会返回一个数字,这个数字被称为文件描述符,我们一会在下文详细讲解
read
要从某个文件中读取内容时,第一个参数需要传入该文件的文件描述符,第二个参数需要传入接收文件内容的缓冲区首地址,第三个参数表示要从文件中读取多少字节的内容。
下面程序模拟实现了C语言fopen的r(只读)打开模式
#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);
char buffer[1024];
read(fd, buffer, sizeof(buffer));
printf("%s", buffer);
return 0;
}
write
write
是 POSIX 系统调用之一,用于将数据写入文件描述符指向的文件或设备。这个函数在 C 语言中被广泛使用,尤其是在 UNIX、Linux 和其他 POSIX 兼容的操作系统中。
参数:
-
fd
:要写入数据的文件描述符。这个文件描述符可以是open
系统调用返回的,也可以是标准输入输出错误文件描述符(例如STDOUT_FILENO
或STDERR_FILENO
)。 -
buf
:指向要写入数据的缓冲区的指针。 -
count
:要写入的字节数。
返回值:
-
成功时,
write
返回实际写入的字节数。 -
出错时,
write
返回-1
,并设置errno
来指示错误。
下面程序模拟实现C语言fopen的w(只读)模式打开
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
char* msg = "Have a good day!\n";
write(fd, msg, strlen(msg));
return 0;
}
下面程序模拟实现C语言fopen的a(追加)模式打开
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("./log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
char* msg = "Have a good day!\n";
write(fd, msg, strlen(msg));
return 0;
}
close
传入文件描述符即可关闭对应文件,这里就不掩饰了
lseek
lseek
是 POSIX 系统调用之一,用于设置文件描述符 fd
指向的文件的偏移量。这个函数允许你查询或修改文件流的当前位置,这对于随机访问文件非常有用。
参数:
-
fd
:文件描述符,指向已经打开的文件。 -
offset
:偏移量,根据whence
的值,这个偏移量可以是绝对值,也可以是相对值。 -
whence
:用于确定如何解释offset
的值。它可以是以下三个常量之一:-
SEEK_SET
: 将偏移量设置为从文件开头算起的绝对位置。 -
SEEK_CUR
: 将偏移量设置为相对于当前文件位置的相对位置。 -
SEEK_END
: 将偏移量设置为相对于文件末尾的相对位置。
-
返回值:
-
成功时,
lseek
返回新的文件偏移量。 -
出错时,
lseek
返回-1
,并设置errno
来指示错误。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("./log.txt", O_RDONLY | O_APPEND, 0666);
char buffer[1024];
read(fd, buffer, sizeof(buffer));
printf("%s", buffer);
//回到文件头再读一遍
lseek(fd, 0, SEEK_SET);
read(fd, buffer, sizeof(buffer));
printf("%s", buffer);
close(fd);
return 0;
}
什么是当前路径
当我们没有指明文件路径,open一个文件时,则open将在当前路径下创建文件那为什么会这个样子呢?
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_CREAT);
close(fd);
sleep(200);
return 0;
}
当程序运行起来后,我们可以使用ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
查看运行该程序的进程pid,进入/proc/进程pid
目录,可以看到两个链接文件cwd和exe。其中,cwd是程序的工作路径,也就是我们常说的当前路径,而exe是可执行程序的保存位置。