C程序设计基础_文件简介
文件简介
Part 1 文件概念
引例:操作系统中的文件
- 一切皆文件,文件是操作系统用于数据管理的基本单位
- 为什么需要文件?
| 特性 | 寄存器 | 内存 (RAM) | 外存(如硬盘、SSD) | 缓存 (Cache) |
|---|---|---|---|---|
| 主要用途 | 存储 CPU 中临时数据和指令 | 存储正在运行的程序和数据 | 存储长期数据、程序、文档 | 提高 CPU 访问速度,存储临时数据 |
| 存取速度 | 极快(皮秒级) | 快速(纳秒级) | 较慢(毫秒级) | 非常快(纳秒级) |
| 存储介质 | CPU 内部的硬件电路 | 半导体材料(如 DRAM 芯片) | 磁盘、固态硬盘(Flash 存储) | 高速静态随机存取存储器(SRAM) |
| 易失性 | 易失性 | 易失性 | 非易失性 | 非易失性 |
| 容量 | 极小 | 较小(通常 GB 级) | 较大(通常 GB/TB 级) | 较小(KB/MB 级) |
| 成本 | 极高 | 较高 | 较低 | 非常高 |
| 数据访问方式 | 随机访问 | 随机访问 | 顺序访问 / 随机访问 | 随机访问 |
写 建立
内存 ⇌ 文件 ⇌ 外存
读 找到
文件的基本概念
- 文件:存储于外存上已命名的相关数据的有序集合,命名称为文件名,结构一般为
主文件名.扩展名,如stdio.h - 文件的基本操作:创建文件、写文件、读文件、重定向文件、删除文件、截断文件
- 文件的操作流程:创建/打开文件、I/O 文件、关闭文件
- 文件分类
- 按格式分:文本文件(ASCII 码)、二进制文件(机器码/数值)
- 按属性分:普通文件、设备文件
文本文件与二进制文件(以 12345 为例)
内存中的 12345(2 个字节)
00110000
00111001
文本文件中的 12345(逐字符存储,共 5 个字节)
| 字符 | '1' | '2' | '3' | '4' | '5' |
|---|---|---|---|---|---|
| ASCII 编码 | 00110001 | 00110010 | 00110011 | 00110100 | 00110101 |
二进制文件中的 12345(2 个字节连续存放,与内存中相同)
00110000 00111001
箭头方向:内存中的数据 分别以文本方式 、二进制方式 写入文件。
【注】
00110000 00111001(0x3039)= 十进制 12345。
标准文件(设备文件)
| 标准文件 | 文件指针 | 常用设备 | 文件描述符 |
|---|---|---|---|
| 标准输入 | stdin |
键盘 | 0 |
| 标准输出 | stdout |
显示器 | 1 |
| 标准错误 | stderr |
显示器 | 2 |
键盘、显示器等设备通常称为终端设备。
文件系统的层次结构
- 路径:描述文件或目录名称,指向文件系统中唯一位置
- 根目录(最顶层目录):Linux:
/Windows:驱动器号:\ - 子目录:与当前目录直接邻接的下层目录
- 父目录:与当前目录直接邻接的上层目录
- 路径分隔符:Linux:
/Windows:\ - 路径分类:
- 绝对路径:路径下的绝对位置,从根目录起的完整路径
- 相对路径:相对于当前目录的简化路径
- 当前目录:
.父目录:.. - 通常情况下,
.可省略
注意
在程序中,
\必须转义后使用(即写为\\)
缓冲机制的引入
- 为什么需要缓冲机制?
| 特性 | 寄存器 | 内存 (RAM) | 外存(如硬盘、SSD) | 缓存 (Cache) |
|---|---|---|---|---|
| 主要用途 | 存储 CPU 中临时数据和指令 | 存储正在运行的程序和数据 | 存储长期数据、程序、文档 | 提高 CPU 访问速度,存储临时数据 |
| 存取速度 | 极快(皮秒级) | 快速(纳秒级) | 较慢(毫秒级) | 非常快(纳秒级) |
| 存储介质 | CPU 内部的硬件电路 | 半导体材料(如 DRAM 芯片) | 磁盘、固态硬盘(Flash 存储) | 高速静态随机存取存储器(SRAM) |
| 易失性 | 易失性 | 易失性 | 非易失性 | 非易失性 |
| 容量 | 极小 | 较小(通常 GB 级) | 较大(通常 GB/TB 级) | 较小(KB/MB 级) |
| 成本 | 极高 | 较高 | 较低 | 非常高 |
| 数据访问方式 | 随机访问 | 随机访问 | 顺序访问 / 随机访问 | 随机访问 |
- 缓冲机制核心思想:在传输速度存在明显差异的存储器间引入一段速度介于二者之间的中间区域,某一存储器发出数据在中间区域积累,待达到一定量或满足一定条件时,中间区域批量传输给另一存储器,并自我刷新
缓冲区
- 缓冲区:缓冲机制的具体实现,一般用内存作为缓冲区
- 缓冲策略:全缓冲(达到容量)、行缓冲(遇换行符)、无缓冲
- 缓冲区刷新时机:缓冲区满、行缓冲模式下遇换行符、关闭文件、程序正常结束、手动刷新
fflush:手动刷新缓冲区(一般只用于输出流)
int fflush(FILE *stream);
缓冲区溢出
固定大小的缓冲区写入的数据超出其容量,剩余数据覆盖相邻内存区域,且无有效机制阻止,导致内存的非法越界访问。
常见诱发函数:
gets、strcpy
文件缓冲区
- 文件缓冲区:缓冲机制在文件系统中的具体应用,用于在程序与磁盘文件之间优化数据传输
┌──────┐ 程序控制 ┌────────────┐ 操作系统 ┌──────────┐
│ C 程序│ ──读写操作─────▶ │ 文件缓冲区 │ ──自动完成───▶ │ 磁盘文件 │
└──────┘ └────────────┘ └──────────┘
- 分类:输入缓冲区、输出缓冲区(普通文件流通常为全缓冲;stdin/stdout 连接终端时为行缓冲,stderr 为无缓冲)
问题
为什么在终端输入数据时,在本行正在输入的数据可被修改?按下回车后就无法修改了?
以文件缓冲区看文件操作流程
- 创建文件:操作系统返回一个文件描述符,用于标识被创建的文件
- 打开文件:操作系统自动在内存中创建文件缓冲区,缓冲区大小由编译器/标准库决定(如 glibc 中
BUFSIZ为 8192 字节,早期 UNIX 系统常见为 512B)。执行打开操作后需对文件进行测试。 - I/O 文件:读数据时,操作系统自动将数据从磁盘扇区导入文件缓冲区,程序处理完后会继续导入下一个扇区的数据;写数据时,程序将数据写入文件缓冲区,缓冲区满后操作系统自动写入磁盘扇区并清空缓冲区。
- 关闭文件:不及时关闭文件就会造成资源泄露;关闭文件同时会强制将文件缓冲区的数据写入文件,以免数据丢失。
Part 2 C 语言对文件的处理
C 语言中的文件
- 文件的逻辑结构:流式文件(字节流),每个字节都能被单独处理,以文件结尾标识符(EOF)结束
- 文件访问模式:文本模式(自动处理换行符和字符编码)、二进制模式(不自动进行任何转换和处理)
- FILE:
stdio.h中定义的结构体,将文件流抽象表示,用于描述文件的基本信息和内部结构
文件指针
FILE *stream;
文件的打开、重定向与关闭
以文件指针角度看文件的打开与关闭:
- 打开文件:将文件指针指向该文件
- 关闭文件:断开文件指针与文件间的联系
fopen:打开文件
FILE *fopen(char *filename, char *mode);
filename:文件名字符串(包含文件路径)mode:文件打开方式字符串- 组成:操作类型 + 模式类型
- 若打开成功,返回指向该文件的指针,否则返回空指针
模式类型
| 模式类型 | 含义 |
|---|---|
t (text) |
文本模式 |
b (binary) |
二进制模式 |
缺省则以文本模式打开
操作类型
| 操作类型 | 含义 | 读 | 写 | 若文件不存在 | 若文件已存在 | 文件当前位置指针 |
|---|---|---|---|---|---|---|
r (read) |
以只读方式打开已存在的文件 | ✓ | × | 出错 | 正常打开 | 文件首 |
r+ |
以读写方式打开已存在的文件 | ✓ | ✓ | 出错 | 正常打开 | 文件首 |
w (write) |
以只写方式打开文件 | × | ✓ | 创建新文件 | 清空原文件 | 文件首 |
w+ |
以读写方式打开文件 | ✓ | ✓ | 创建新文件 | 清空原文件 | 文件首 |
a (append) |
以追加方式打开文件 | × | ✓ | 创建新文件 | 从文件尾继续写,无法覆盖原内容 | 文件尾 |
a+ |
以追加/读写方式打开文件 | ✓ | ✓ | 创建新文件 | 从文件尾继续写,无法覆盖原内容 | 文件尾 |
+位于操作类型/文件类型之后均可注意:
a/a+的写入始终发生在文件尾;a+打开后读取的起始位置由实现定义,读取前通常需要fseek/rewind明确定位。
fopen 返回 NULL 的可能原因
- 以只读方式打开时文件不存在
- 路径不正确
- 文件已被打开
测试文件是否正常打开
FILE *fp = fopen(...);
if (!fp) {...}
freopen:文件的重定向
FILE *freopen(char *filename, char *mode, FILE *stream);
- 流的重定向:替换原流指向,将原流指向其他流。后续对原流的操作全部被作用到新流上
freopen重定向stream到filename,常用于重定向设备文件(stdin、stdout 等)到普通文件- 若重定向成功,则返回该流指针,否则返回 NULL
fclose:关闭已打开的文件
int fclose(FILE *stream);
若关闭成功则返回 0,否则返回 EOF
文件当前位置指针
- 文件的访问形式:顺序访问、随机访问
- 文件的前方为指向文件头的方向,文件的后方为指向文件尾的方向
- 文件当前位置指针:记录在 FILE 结构体内部(具体成员名由实现定义,不对外暴露),它是相对于文件头的偏移字节数,用于指向文件的当前 I/O 位置
- 打开文件时,编译器根据文件的打开方式,自动将文件当前指针设置为文件首或文件尾,并随 I/O 操作自动向后移动
- 文件当前位置指针范围:0 - 文件长度
关于文件当前位置指针中的"指针"
文件当前位置指针中的"指针",只是一个为了表示当前文件处理进度的形象化模型,它并非真正的内存地址指针,而是一个逻辑位置,帮助我们理解如何在文件中移动和访问数据。
文件结尾标识符
- 文件结尾标识符 (EOF):
stdio.h中定义的值为 -1 的符号常量,用于表示文件结束,或发生文件 I/O 错误。空文件的文件头与 EOF 重合 - 手动插入 EOF:Windows:
Ctrl+ZLinux:Ctrl+D
注意
EOF 是一种抽象概念,并非实际字符或数据,而是指代文件中最后一个字节的之后位置的逻辑状态,用于告知进程何时停止读取
以下两种写法是等价的:
while (scanf(...) != EOF) {...}
while (~scanf(...)) {...}
ferror:检测文件 I/O 是否发生错误
int ferror(FILE *stream);
若发生文件 I/O 错误则返回非零值(真),否则返回 0(假)
feof:检测文件当前指针是否指向文件尾
int feof(FILE *stream);
在 I/O 操作后,若因文件当前位置指针指向文件尾而返回 EOF 导致 I/O 失败,则返回真,否则返回假
典型滥用:feof 并不主动检测文件
#include <stdio.h>
int main(){
FILE *fp=fopen("1.txt","r"); // 文件是否为空无关紧要
fseek(fp,0,SEEK_END); // 移动当前位置指针至文件尾
if(feof(fp)) printf("Yes."); // 典型滥用 feof
else printf("No."); // 由于 feof 原理,本例永远只输出 No
fclose(fp);
return 0;
}
feof表示 I/O 之后的状态,仅当因文件当前位置指针指向文件结尾而导致的 I/O 失败后,调用feof才返回真- 绝对禁止在无任何 I/O 就调用 feof,否则会出错!
文件的随机访问
ftell:获取文件当前位置指针
long ftell(FILE *stream);
若文件当前位置有效,则返回文件当前位置指针,否则返回 -1L
rewind:移动文件当前位置指针到文件头
void rewind(FILE *stream);
fseek:移动文件当前位置指针
int fseek(FILE *stream, long offset, int whence);
若操作成功则返回 0,否则返回 -1
- offset:相对于 whence 的偏移
| offset 取值范围 | 含义 |
|---|---|
| > 0 | 相对于 whence 向文件尾方向移动 |
| = 0 | 相对于 whence 不动 |
| < 0 | 相对于 whence 向文件头方向移动 |
文件的前方为指向文件头的方向,文件的后方为指向文件尾的方向
- whence:移动后的文件当前位置指针的起始点位置模式,由
stdio.h中定义了模式的符号常量
| 模式(符号常量) | 起始位置 | 整数值(字面常量) |
|---|---|---|
SEEK_SET |
文件首 | 0 |
SEEK_CUR |
文件当前位置 | 1 |
SEEK_END |
文件尾 | 2 |
实例:文件处理简易进度条
思路分析:
- 打开文件:
fopen以r模式打开文件 - 获取文件长度:
fseek移动文件当前位置指针到文件尾,ftell获取文件当前位置指针(此处即为文件长度),rewind/fseek将文件当前位置指针归位到文件首 - 循环处理:开始处理文件中的每个字符,直到 EOF 结束,在每次循环处理中用
ftell获取文件当前位置指针,并与文件大小相除,即为文件当前处理进度条 - 关闭文件:
fclose关闭文件
文件 I/O
格式化 I/O
scanf fprintf
终端 ⇌ 内存 ⇌ 文件
printf fscanf
fscanf:格式化读取文件
int fscanf(FILE *stream, char *format[, …]);
若读取成功,返回读取的数据个数;若读到文件尾或出错,返回 EOF
fprintf:格式化写入文件
int fprintf(FILE *stream, char *format[, …]);
若写入成功,返回写入的数据个数;若写入出错,返回 EOF
扩展内容:字符串的格式化 I/O
scanf sprintf
终端 ⇌ 内存 ⇌ 内存中的字符串
printf sscanf
sscanf:从字符串中格式化读取
int sscanf(char *source, char *format[,...]);
若读取成功,返回成功读取个数;若到达文件尾或出错,返回 EOF
sprintf:格式化输出到字符串
int sprintf(char *buffer, char *format[,...]);
若写入成功返回写入字符总数 count−1(除去末尾
\0),否则返回 EOF
字符 I/O
getchar/fgetchar putc/fputc
终端 ⇌ 内存 ⇌ 文件
putchar/fputchar getc/fgetc
getc/fgetc:字符方式读文件
int fgetc(FILE *stream);
若读取成功,返回读取字符的 ASCII 码值,否则返回 EOF
putc/fputc:字符方式写入文件
int fputc(int c, FILE *stream);
若写入成功,返回写入字符的 ASCII 码值,否则返回 EOF
字符串 I/O
gets fputs
终端 ⇌ 内存 ⇌ 文件
puts fgets
fgets:字符串方式读文件
char *fgets(char *buffer, int n, FILE *stream);
中止条件:换行符 / EOF / 读入 n-1 个字符(结尾
\0占用),不丢弃\n若读取成功,返回字符串指针;若读到文件尾或出错,返回 NULL
fputs:字符串方式写文件
int fputs(char *buffer, FILE *stream);
不写入
\n。若成功,返回最后写入字符 ASCII 码;否则返回 EOF
文件的二进制 I/O
fread:以数据块方式读文件
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
从文件当前位置指针处起,连续读取 n 个大小为 size 字节的数据块,存储到 ptr 中。返回值为成功读取块的数量(0-n)
fwrite:以数据块方式写文件
size_t fwrite(void *ptr, size_t size, size_t n, FILE *stream);
将 ptr 中连续 n 个大小为 size 字节的数据块写入到文件当前位置指针处。返回值为成功写入块的数量(0-n)
总结:I/O 函数
| 函数名 | 功能 |
|---|---|
scanf、printf |
stdin、stdout 格式化 I/O |
fscanf、fprintf |
文件格式化 I/O |
getchar、putchar |
stdin、stdout 以单个字符 I/O |
getc/fgetc、putc/fputc |
文件中以单个字符 I/O |
gets、puts |
stdin、stdout 以字符串 I/O;gets 丢弃 \n,puts 加 \n |
fgets、fputs |
文件中以字符串 I/O;fgets 读 \n,fputs 不加 \n |
sscanf、sprintf |
字符串中 I/O |
fread、fwrite |
文件的二进制 I/O |
Part 3 文件对程序结构的影响
多文件程序
- 按源代码文件数量分:单文件程序、多文件程序
- 多文件程序的设计思想:高度模块化,一个文件只实现一个/一类功能,多个文件组合完成复杂功能
- 模块划分:
- .h 头文件:包含程序文件模块的共享信息,如头文件、符号常量定义、全局变量定义、函数原型、数据结构的定义等
- .c/.cpp 程序源代码文件:包含
main函数和其他函数定义。有且仅能有一个程序源代码文件包含main函数
作用域、链接、存储期
- 标识符:用于区分和标识不同对象,包括变量名、函数名等
- 作用域:程序中标识符的可访问区域,包括块作用域(块/循环头)、函数作用域、函数原型作用域(函数形参)、文件作用域(全局)
- 链接:程序中对标识符的可引用情况
- 外部链接:文件作用域,标识符可在所有文件中使用,通过
extern关键字声明 - 内部链接:文件作用域,仅在当前文件中有效,通过
static关键字声明 - 无链接:块作用域、函数作用域和函数原型作用域,标识符仅在其对应的块或函数内有效,外部无法访问
- 外部链接:文件作用域,标识符可在所有文件中使用,通过
- 存储期:程序中标识符的生存周期,包括静态存储期(文件作用域)、线程存储期、自动存储期(块作用域/auto)、动态分配存储期(手动申请到手动释放/进程结束)
存储类别修饰符
变量:
- auto:自动变量,默认省略
auto关键字。若未进行初始化,将会被填充随机的垃圾数据 - register:寄存器变量,一种直接放入寄存器中的无地址变量,使用
register关键字请求将该变量存放在寄存器中,能否放入寄存器取决于编译器和操作系统 - extern:外部变量
- static:静态变量
函数:
- extern:外部函数
- static:静态函数
- inline:内联函数,旨在尽可能快速调用函数,常见实现是将函数调用替换函数体内容
存储类别对照表
原图为左右两个并排对照表,下表按关键字合并,字段内容与原图一致。
| 关键字 | 存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
|---|---|---|---|---|---|
auto |
自动 | 自动 | 块 | 无 | 块内 |
register |
寄存器 | 自动 | 块 | 无 | 块内 |
extern |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
static |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外 |
static |
静态无链接 | 静态 | 块 | 无 | 块内 |
extern、static
- 声明:可进行多次,仅告知编译器某个标识符的存在,但并不分配内存
- 定义:只能进行一次,通常需要先声明再定义,定义包含声明,自动在相应内存区域中申请内存,并与标识符绑定
- extern:声明一个对象,表明其来自外部,通常用于扩展对象作用域
- 变量:外部链接静态变量,简称外部变量,即默认定义的全局变量
- 函数:外部函数,即默认定义的函数
extern定义的对象,可被所有文件所访问,定义时默认省略extern关键字,在所需文件中使用extern声明,从而扩展对象作用域
- static:表示一个对象为静态,在进程运行前其就被存储于内存静态存储区中
- 变量:
- 局部静态变量:块内定义的、用
static修饰的局部变量,仅被初始化一次,进程运行期间持续存在,退出作用域后其值保持不变 - 内部链接静态变量:简称静态全局变量,将作用域限制于当前文件,其他文件无法调用
- 局部静态变量:块内定义的、用
- 函数:内部函数,用
static修饰的函数,将作用域限制于当前文件,其他文件无法调用
- 变量:
预处理命令
define、undef
- 预处理器:执行预处理命令、处理注释(所有注释替换为空格)等
- 预处理命令:以
#开头的指令 - define:宏(符号常量)定义命令
- 宏是一种文本替换,替换过程为宏展开,替换列表中可使用先前定义的宏
- 类对象宏:将宏调用替换为替换列表中的内容
#define MACRO_NAME REPLACEMENT_LIST
替换列表为数值时,应当添加括号
#include <stdio.h>
#define MOD 1000000000+7
int main(){
printf("%lld",(long long)MOD*2); // 宏展开为 (long long)1000000000+7*2 = 1000000014
return 0;
}
为避免出现此情况,应对数值宏加括号:
#define MOD (1000000000+7) // 展开为 (long long)(1000000000+7)*2 = 2000000014
- 类函数宏:具有形参表的宏,接受传入实参,对应地传递到替换列表,之后宏调用会被替换列表替代
#define MACRO_NAME(PARAMETER_LIST) REPLACEMENT_LIST
类函数宏举例
#define SUM(X,Y) (X+Y) // 宏定义
SUM(2,3) // 宏调用
替换列表为数值时,应当添加括号
#include <stdio.h>
#define CALC(X) X*X
int main(){
printf("%lld",100/CALC(2+3)); // 宏展开后为 100/2+3*2+3
return 0;
}
为避免出现此情况,应对数值宏加括号:
#define SQUARE(X) ((X)*(X))
注意:即使加了括号,类函数宏也只是文本替换,传入带副作用的参数(如
SQUARE(i++))会使i++被求值两次,应避免这样使用。
undef:取消定义宏
#undef MACRO_NAME
适用于类对象宏、类函数宏
include
include:文件包含命令
#include <HEADER> // 系统目录
#include "HEADER" // 指定路径 + 系统目录
条件编译
- 嵌套包含:同一个头文件被重复包含
- 条件编译:常与宏配合使用,根据条件选择性地编译代码
- #if … #endif 命令:检查宏值或宏是否被定义
#if Condition 1 // 布尔表达式
// Compiled Code 1
#elif Condition 2
// Compiled Code 2
...
#else
// Compiled Code n
#endif
#elif、#else可省略
defined:检查宏是否定义
预处理运算符,常作为 #if … #endif 的条件:
#if defined MACRO_NAME 1
// Compiled Code 1
#elif defined MACRO_NAME 2
// Compiled Code 2
...
#else
// Compiled Code n
#endif
#ifdef … #endif 命令:检查宏是否已定义
#ifdef MACRO_NAME 1
// Compiled Code 1
#elif MACRO_NAME 2
// Compiled Code 2
...
#else
// Compiled Code n
#endif
与
#if defined MACRO_NAME等价;#elif、#else可省略
#ifndef … #endif 命令:检查宏是否未定义
#ifndef MACRO_NAME 1
// Compiled Code 1
#elif MACRO_NAME 2
// Compiled Code 2
...
#else
// Compiled Code n
#endif
与
#if !defined MACRO_NAME等价;#elif、#else可省略
实例:#ifndef 检查是否在 OJ 评测环境
多数 OJ 在评测环境中会通过编译命令行定义宏 ONLINE_JUDGE,用于表明在评测环境。因此可将调试代码放在条件编译中,避免提交代码时反复删除调试信息。
举例:#ifndef + freopen
I/O:本地 → 普通文件;OJ → stdin/stdout
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
编译指示
#pragma:向编译器发送指令
#pragma PARA(...)
常用命令
#pragma once // 用于头文件中,确保其只能被包含一次
#pragma warning(disable:4996) // 忽略 MSVC 编译器对不安全函数检查
#pragma GCC optimize(2) // 手动开启 O2 优化

浙公网安备 33010602011771号