前言:欢迎各位光临本博客,这里小编带你直接手撕**,文章并不复杂,愿诸君**耐其心性,忘却杂尘,道有所长!!!!

IF’Maxue:个人主页
🔥 个人专栏: 《C语言》 《C++深度学习》 《Linux》 《数据结构》 《数学建模》
⛺️生活是默默的坚持,毅力是永久的享受。不破不立!
文章目录
-
- 1. 空文件(内容为0)也占磁盘空间吗?
- 2. 文件存在哪?
- 3. 谁在操作文件?
- 4. 读/写文件的本质
- 5. 举个例子
- 6. 怎么用纯C把内容输出到显示器?
- 7. `fwrite`函数怎么用?
- 8. 键盘、显示器也是“文件
- 9. `main`不是C程序的“第一站”?
- 10. 打开文件的方式很关键
- 11. 输出重定向(`>`)
- 12. 写文件别带`\\0`!
- 13. 读文件比写难?用结构体就能“对得上”格式
- 14. 直接调用操作系统的`open`:文件描述符和位图传参
-
- 为什么用“位图传参”?`flags`的小技巧
1. 空文件(内容为0)也占磁盘空间吗?
你可能会想:如果我在电脑上建一个文件,里面什么都不写(内容是0),它是不是就像“空气”一样,不占磁盘空间?其实不是——因为文件不只是“里面写的内容”,还包含它的“身份信息”(属性) ,这俩加起来才是一个完整的文件。
就像你给朋友寄快递,快递盒里的东西是“内容”,但快递单上的收件人、地址、寄件时间、快递单号这些“信息”,也是快递的一部分,不能少。文件的属性也一样,比如文件名、创建时间、修改时间、权限、存储位置——这些属性不管文件内容是不是空的,都得存在磁盘上,操作系统要靠它们找到文件、判断能不能操作它。所以哪怕文件内容是0,属性也会占一点磁盘空间(比如Linux里空文件默认占4KB,因为磁盘按“块”存数据,最小块就是4KB)。
而且我们对文件的所有操作(读、写、复制),本质都是围绕“内容+属性”展开的。这一点从文件在磁盘的存储逻辑上也能直观看到,比如下面这张图就展示了文件内容与属性在磁盘中的关联关系: 
2. 文件存在哪?
我们平时说的“文件”,默认都是存在磁盘里的——不管是机械硬盘、固态硬盘还是U盘,都算“永久性存储介质”(内存是临时存储,断电就没了,所以文件不能只存内存)。
但磁盘是“外部设备”,系统不能直接操作它,必须通过“输入输出操作”(简称“IO”)交换数据:比如读文件是“输入”(把磁盘内容读到内存),写文件是“输出”(把内存数据写到磁盘)。换句话说,所有对文件的操作(打开、读、写、关闭),本质都是系统和磁盘之间的“IO操作”——没有IO,就没法跟磁盘里的文件打交道。
3. 谁在操作文件?
你双击打开文件、用cat看文件内容,背后其实是“进程”在干活(比如记事本进程、cat进程)——系统里只有进程能发起操作,你点击鼠标、输命令,本质是让某个进程执行“操作文件”的任务。
那进程怎么操作文件?比如用C语言写fopen("test.txt", "r"),这是调用C标准库的“库函数”(fopen、fclose这些),但库函数不是“最终执行者”——它是“中间人”,底层会调用操作系统的“系统调用” (比如Linux的open、close)。因为只有操作系统能直接指挥磁盘,不管你用C、Python还是Java,想操作文件,最后都得走系统调用的路子。
操作系统管理文件的核心逻辑是“先描述,再组织”:当进程打开文件时,操作系统不会把整个磁盘文件搬内存(太浪费),而是在内存建一个“文件描述结构”(比如Linux的struct file)——这就是“内存级文件”,记录文件当前读写位置、权限、磁盘位置等;而磁盘里的是“磁盘级文件”(原始文件)。操作系统会把所有“内存级文件”组织起来(比如用链表),方便管理。这个逻辑可以通过下面的示意图理解: 
4. 读/写文件的本质
不管你用printf写显示器,还是用fwrite写文件,本质都不是靠库函数——库函数只是“打包”请求,最终要调用操作系统的“读/写系统调用”(比如Linux的read、write)。因为读/写要跟外设打交道,只有操作系统能指挥外设。
而且有个铁律:访问文件必须先打开。打开文件的过程,是操作系统帮你做“准备工作”(查权限、建“内存级文件”、分配文件描述符),没准备好,后续读/写都没法做。
5. 举个例子
我们在Linux终端用cat 文件名看内容(比如cat test.txt),背后是完整的“打开→读→写→关闭”流程,正好能理解文件操作逻辑。
比如下面这张图,执行cat test.txt后,终端清晰显示了文件里的内容(比如“Hello, I’m a test file!”): 
这个过程的底层逻辑,在下面这张流程图里更直观: 
具体步骤是:
6. 怎么用纯C把内容输出到显示器?
显示器是外设,但操作系统把它当成“文件”处理(统一接口,方便操作)。用纯C输出到显示器有好几种方式,下面这张图展示了最常用的两种: 
比如用printf:
#include <stdio.h>
int main() {
printf("Hello, this goes to screen!\\n"); // 默认写往stdout(显示器)
return 0;
}
或者用fwrite直接指定stdout(stdout是系统预定义的“标准输出”文件,对应显示器):
#include <stdio.h>
#include <string.h>
int main() {
char buf[] = "Hi, fwrite to screen!\\n";
fwrite(buf, 1, strlen(buf), stdout); // 把buf写到stdout
return 0;
}
运行后,内容都会显示在显示器上——本质都是调用系统调用,把数据写到显示器对应的外设接口。
7. fwrite函数怎么用?
fwrite是C标准库的“写文件函数”,不仅能写磁盘文件,还能写显示器,下面这张图详细展示了它的用法和代码示例: 
先看fwrite的函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
四个参数的意思:
- ptr:要写的内容地址(比如字符串、数组的地址);
- size:每个元素的大小(比如字符是1字节,int是4字节);
- nmemb:要写的元素个数;
- stream:写的目标(可以是fopen返回的文件指针,也可以是stdout)。
比如写内容到test.txt的代码:
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("test.txt", "w"); // 只写方式打开,不存在则新建
if (fp == NULL) { // 必须判断打开是否成功(比如权限不够)
perror("fopen error");
return 1;
}
char content[] = "I'm written by fwrite!";
// 写content到fp:1字节/元素,共strlen(content)个元素
size_t write_num = fwrite(content, 1, strlen(content), fp);
if (write_num != strlen(content)) {
printf("Write failed! Only wrote %zu bytes\\n", write_num);
} else {
printf("Write success!\\n");
}
fclose(fp); // 一定要关闭文件,避免缓存数据没写到磁盘
return 0;
}
运行后打开test.txt,就能看到写进去的内容;如果把fp换成stdout,内容就会显示在显示器上。
8. 键盘、显示器也是“文件
操作系统的重要设计:把所有能输入输出的东西都统一成“文件” ——磁盘文件、键盘、显示器、打印机,都用“打开→读→写→关闭”操作。这样进程不用记“读键盘用A函数,读磁盘用B函数”,只要记“读文件用read”就行。
Linux里每个进程启动时,系统会自动打开3个“标准文件”,不用手动fopen:
- stdin(标准输入):对应键盘,fd=0(scanf、fgets默认读这里);
- stdout(标准输出):对应显示器,fd=1(printf默认写这里);
- stderr(标准错误):对应显示器,fd=2(专门输出错误信息,比如perror)。
比如你用scanf("%d", &a),就是从stdin(键盘)读输入;用printf,就是往stdout(显示器)写内容——这三个文件是进程和用户交互的默认通道。
9. main不是C程序的“第一站”?
我们以为main是程序入口,但其实main是“我们写的代码的入口”——C程序启动前,需要初始化栈、全局变量,还要打开stdin/stdout/stderr,这些工作靠编译器和操作系统加的“启动代码”(比如Linux的crt0.o)完成。
整个流程是:
所以main不是“第一站”——系统已经悄悄帮我们做好了所有准备,我们才能直接用printf、scanf。
10. 打开文件的方式很关键
用fopen打开文件时,第二个参数是“打开方式”,比如"r"(只读)、"w"(只写)、"a"(追加),下面这张图展示了不同方式的效果差异: 
- "w"(只写):文件存在就清空内容,不存在就新建;
- "a"(追加):文件存在就把指针移到末尾(写内容加在最后),不存在就新建;
- "r"(只读):文件必须存在,否则打开失败,不会清空内容。
比如用"w"打开test.txt,原本的内容会被删掉;用"a"打开,新内容会跟在原有内容后面——这两种方式的选择直接影响文件操作结果,一定要注意。
11. 输出重定向(>)
我们在终端用echo "hello" > test.txt,内容会写到test.txt而不是显示器,还会清空原有内容——为什么?因为重定向时,系统用了"w"方式打开文件。下面这张图解释了重定向的底层逻辑: 
具体步骤:
如果不想清空,就用>>(追加重定向),这时系统用"a"方式打开文件,新内容会追加到末尾,下面这张图对比了>和>>的效果: 
12. 写文件别带\\0!
C语言字符串以\\0结尾,但\\0是C的“专属标记”,文件不认识它——写文件时带\\0,会导致文件里多一个“空字符”,用cat看可能显示乱码(比如hello^@)。
下面这张图强调了“写文件别带\\0”的注意事项和正确代码: 
正确写法是用strlen控制写的长度(只写实际内容):
char str[] = "hello";
fwrite(str, 1, strlen(str), fp); // 写5个字节(不含\\0)
// 错误写法:fwrite(str, 1, sizeof(str), fp); // 写6个字节(含\\0)
记住:文件只存“实际内容”,不用带C语言的\\0标记。
13. 读文件比写难?用结构体就能“对得上”格式
很多人觉得“读文件难”,是因为不知道“文件内容按什么格式存的”——比如写了“张三 20”,读的时候按“读整数→读字符串”就会错。解决办法是用“结构体”定义存储格式,写和读都按结构体来,就能“对得上”。
比如存“用户信息”(名字、年龄、身高),定义结构体struct User,写的时候按结构体写,读的时候也按结构体读——这样读出来的数据不会乱。
14. 直接调用操作系统的open:文件描述符和位图传参
fopen是库函数,底层调用操作系统的open系统调用——如果想“跳过中间人”,可以直接用open,它返回“文件描述符(fd)”(非负整数,0/1/2是stdin/stdout/stderr,新文件从3开始)。
下面这张图展示了open的函数原型、参数含义和代码示例: 
open的原型(Linux下):
// 新建文件时需要传权限,已有文件不用
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- pathname:文件名(比如"test.txt");
- flags:打开标记(用位图传参,下面详细说);
- mode:文件权限(比如0644,只有flags带O_CREAT时需要)。
比如新建并打开test.txt的代码:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// 只写+新建+清空(O_WRONLY|O_CREAT|O_TRUNC),权限0644
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == –1) { perror("open error"); return 1; }
char buf[] = "Write via open!";
write(fd, buf, strlen(buf)); // 用write系统调用写内容
close(fd);
return 0;
}
为什么用“位图传参”?flags的小技巧
flags是open的关键参数,用“位图传参”——一个整数的每一位代表一个功能标记,比如第0位是“只读”,第2位是“新建”。这样做高效灵活:组合标记用“或(|)”,判断标记用“与(&)”。
下面这张图展示了常见flags的位图含义: 
比如O_WRONLY | O_CREAT | O_TRUNC,就是“只写+新建+清空”的组合——对应的二进制位分别置1,操作系统通过判断这些位,就知道要执行哪些操作。位图传参不仅用在open,很多系统调用(比如fcntl)都用这种方式,是操作系统设计的常用技巧。
理解这些逻辑,再看各种语言的文件操作代码,就能从“记函数”变成“懂原理”——不管是C的fwrite、Python的open,还是Java的File,背后都是这套操作系统级的设计思路。




