【C语言】第八课 输入输出与文件操作
C语言中的输入输出(I/O)和文件操作是与计算机交互和管理数据的基础。
📝 1. 标准输入输出(标准I/O)
标准I/O主要用于与控制台(终端)进行交互。
1.1 输出函数 printf
printf
函数用于向标准输出(通常是屏幕)发送格式化后的数据。
-
函数原型:
int printf(const char *format, ...);
format
: 这是一个字符串,包含了要输出的文本以及格式控制说明符(占位符)。...
: 可变参数列表,提供需要插入到格式字符串中的数据,数量与类型必须与格式控制说明符匹配。
-
常用格式说明符:
说明符 含义 %d
有符号十进制整数 %u
无符号十进制整数 %f
/%lf
float
/double
类型浮点数 (对于printf
,%f
也可用于double
)%c
单个字符 %s
字符串 %p
指针地址 %x
十六进制整数(小写字母) %%
输出一个百分号 -
示例:
#include <stdio.h> int main() {int age = 25;double height = 1.75;printf("Age: %d, Height: %.2f meters\n", age, height); // 保留两位小数return 0; }
1.2 输入函数 scanf
scanf
函数用于从标准输入(通常是键盘)读取格式化输入。
-
函数原型:
int scanf(const char *format, ...);
format
: 格式控制字符串,指定如何解析输入数据。...
: 必须是变量的地址(使用&
取地址操作符,字符串名除外)。
-
重要注意事项:
- 安全风险:
scanf
在读取字符串时不会检查目标缓冲区的大小,极易导致缓冲区溢出。绝对避免使用%s
而不指定长度。 - 安全替代:使用
fgets
读取一行到缓冲区,再用sscanf
解析,或使用带宽度的scanf
(如%9s
用于大小为10的char数组)。 - 返回值:返回成功读取并赋值的输入项数,可用于检查输入是否成功或遇到文件结尾(EOF)。
- 安全风险:
-
示例:
#include <stdio.h> int main() {int num;char str[10];printf("Enter a number and a string (max 9 chars): ");if (scanf("%d %9s", &num, str) == 2) { // 检查返回值,并限制字符串读取长度printf("You entered: %d and %s\n", num, str);} else {printf("Input error!\n");}return 0; }
1.3 其他标准I/O函数
getchar()
/putchar(char c)
: 用于读取和写入单个字符。gets()
/puts()
: 避免使用gets()
,因为它无法限制输入长度,极其危险。用fgets(char *s, int size, stdin)
代替。
💾 2. 文件输入输出(文件I/O)
文件I/O用于将数据持久化保存到存储设备,或从存储设备读取数据。
2.1 文件指针与 fopen
在C语言中,操作文件通常使用 FILE
结构体指针(文件流指针)。
-
打开文件 -
fopen
:FILE *fopen(const char *filename, const char *mode);
filename
: 要打开的文件名(包含路径)。mode
: 文件打开模式。- 返回值:成功时返回指向FILE对象的指针,失败时返回
NULL
。务必检查返回值!
-
常用文件打开模式:
模式 含义 文件不存在时 "r"
只读打开文本文件 返回NULL "w"
只写打开文本文件。截断文件长度至0(清空原内容) 创建新文件 "a"
追加模式打开文本文件,写入数据被加到文件末尾 创建新文件 "r+"
读写打开文本文件 返回NULL "w+"
读写打开文本文件。截断文件长度至0(清空原内容) 创建新文件 "a+"
读写打开文本文件,写入数据被加到文件末尾 创建新文件 "rb"
,"wb"
,"ab"
,"r+b"
, etc.与上述类似,但以二进制模式打开文件,用于非文本数据 示例:
#include <stdio.h> int main() {FILE *fp;fp = fopen("data.txt", "r"); // 尝试以只读模式打开文件if (fp == NULL) { // 必须检查文件是否成功打开perror("Error opening file");return 1;}// ... 文件操作fclose(fp); // 最后别忘了关闭文件!return 0; }
2.2 文件读写函数
文件打开后,可以使用多种函数进行读写。
-
格式化文件读写:
int fprintf(FILE *stream, const char *format, ...);
- 类似于printf
,但输出到文件。int fscanf(FILE *stream, const char *format, ...);
- 类似于scanf
,但从文件读取。
fprintf(fp, "Number: %d\n", 100); // 将格式化字符串写入文件 fscanf(fp, "%d", &num); // 从文件中读取格式化数据
-
字符读写:
int fgetc(FILE *stream);
- 从文件读取一个字符。int fputc(int char, FILE *stream);
- 向文件写入一个字符。
-
字符串(行)读写:
char *fgets(char *str, int n, FILE *stream);
- 从文件读取一行字符串,最多读取n-1
个字符,自动添加'\0'
。相对安全。int fputs(const char *str, FILE *stream);
- 向文件写入一个字符串。
-
二进制数据块读写 (非常重要!):
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr
: 指向要读取或写入的数据内存块的指针。size
: 每个数据项的字节大小(常用sizeof()
获取)。nmemb
: 要读取或写入的数据项个数。stream
: 文件指针。- 返回值:成功读取或写入的数据项个数(若非
nmemb
,可能遇到错误或文件尾)。
示例:读写结构体数组
#include <stdio.h> struct Student {char name[20];int age; }; int main() {struct Student stu_list[2] = {{"Alice", 20}, {"Bob", 21}};struct Student read_list[2];FILE *fp = fopen("data.bin", "wb"); // 二进制写入if (fp == NULL) return 1;// 将整个结构体数组写入文件size_t written = fwrite(stu_list, sizeof(struct Student), 2, fp);fclose(fp);fp = fopen("data.bin", "rb"); // 二进制读取if (fp == NULL) return 1;// 从文件中读取整个结构体数组size_t read = fread(read_list, sizeof(struct Student), 2, fp);fclose(fp);printf("Read %zu students.\n", read);return 0; }
2.3 文件随机存取
文件位置指针指示当前读写位置。你可以移动它来实现随机访问。
int fseek(FILE *stream, long offset, int whence);
- 移动文件位置指针。offset
: 偏移量。whence
: 基准位置,常用:SEEK_SET
- 文件开头。SEEK_CUR
- 当前位置。SEEK_END
- 文件末尾。
long ftell(FILE *stream);
- 返回当前文件位置指针的位置(相对于文件开头的字节偏移量)。void rewind(FILE *stream);
- 将文件位置指针重置回文件开头。
2.4 关闭文件与错误处理
int fclose(FILE *stream);
- 非常重要! 关闭已打开的文件流,释放系统资源并确保缓冲区数据写入磁盘。文件操作完毕后必须关闭。- 错误处理函数:
void perror(const char *s);
- 打印最近一次系统错误的信息,前面可加上自定义字符串s
。int ferror(FILE *stream);
- 测试给定流上的错误标识符,如果设置了则返回非零值。
🔧 3. 文件描述符与系统调用(底层I/O)
在Unix/Linux系统中,C标准库的文件操作函数(如fopen, fread)底层是基于系统调用实现的。
-
文件描述符(File Descriptor, fd):
- 是一个小的非负整数,用于在进程内唯一标识一个打开的文件。
- 每个进程启动时默认打开三个标准流,其文件描述符为:
0
: 标准输入(stdin
)1
: 标准输出(stdout
)2
: 标准错误(stderr
)
- 用户打开文件时,系统会分配一个新的、当前未用的最小文件描述符(如3, 4, 5…)。
-
常用系统调用函数 (需包含
<unistd.h>
和<fcntl.h>
):系统调用 功能说明 类比标准I/O函数 open()
打开或创建文件,返回文件描述符 fopen()
read()
从文件描述符读取数据 fread()
,fgetc()
write()
向文件描述符写入数据 fwrite()
,fputc()
close()
关闭文件描述符 fclose()
lseek()
移动文件偏移指针(类似 fseek
)fseek()
-
示例:使用系统调用复制文件
#include <unistd.h> #include <fcntl.h> #define BUF_SIZE 8192 int main() {int src_fd = open("source.txt", O_RDONLY);int dest_fd = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);char buf[BUF_SIZE];ssize_t num_read;while ((num_read = read(src_fd, buf, BUF_SIZE)) > 0) {write(dest_fd, buf, num_read);}close(src_fd);close(dest_fd);return 0; }
💎 核心概念总结与对比
概念/操作 | 标准I/O (缓冲I/O) | 底层I/O (系统调用,无缓冲I/O) |
---|---|---|
核心对象 | FILE 结构体指针 (文件流) | 文件描述符 (fd) - 整数 |
打开 | fopen() | open() |
关闭 | fclose() | close() |
读取 | fread() , fscanf() , fgetc() , fgets() | read() |
写入 | fwrite() , fprintf() , fputc() , fputs() | write() |
定位 | fseek() , ftell() , rewind() | lseek() |
优点 | 带缓冲,通常效率更高;移植性好;格式化读写方便 | 更接近操作系统,控制更精细;某些特定操作必须使用 |
缺点 | 缓冲可能带来延迟;某些底层操作不支持 | 需要自己管理所有细节;通常更复杂 |
🛡️ 4. 最佳实践与安全注意事项
- 始终检查返回值:无论是
fopen
,fread
,fwrite
,scanf
还是系统调用,必须检查其返回值以确保操作成功,这是编写健壮程序的基础。 - 始终关闭文件:使用
fclose
或close
释放资源,防止资源泄漏。 - 警惕缓冲区溢出:避免使用不安全的函数(如
gets
, 不加限制的%s
inscanf
),优先使用fgets
或指定宽度。 - 理解文本模式与二进制模式:在Windows平台上,文本模式(无"b")会对换行符(
\n
和\r\n
)进行转换,而二进制模式(有"b")则不会。处理非文本文件(如图片、视频)或需要跨平台时,务必使用二进制模式。 - 注意字节序(Endianness):在不同系统间通过二进制格式传输数据时,要考虑多字节数据(如int)的字节序问题。
!