在C语言编程中,文件操作和预处理是两个重要的组成部分。文件操作允许程序与外部存储设备交互,而预处理则在编译前对源代码进行文本处理。这两个功能为C程序提供了强大的扩展性和灵活性。
文件操作
文件基本概念
在C语言中,文件是存储在外部介质(如硬盘、U盘)上的数据集合。C语言将文件视为字节序列,并提供了两种文件处理模式:
- 文本模式:以字符为单位处理文件,自动处理换行符(Windows:
\r\n
↔ Unix:\n
) - 二进制模式:以字节为单位处理文件,不进行任何转换
文件指针
文件操作通过文件指针(FILE*)实现,它指向一个包含文件信息的结构体:
#include <stdio.h>
FILE *fp; // 声明文件指针
// 打开文件
fp = fopen("example.txt", "r"); // 以只读模式打开文本文件
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
// 文件操作...
// 关闭文件
fclose(fp);
文件打开模式
模式 | 描述 |
---|---|
“r” | 只读模式,文件必须存在 |
“w” | 写入模式,创建新文件或覆盖 |
“a” | 追加模式,在文件末尾添加内容 |
“r+” | 读写模式,文件必须存在 |
“w+” | 读写模式,创建新文件或覆盖 |
“a+” | 读写模式,在文件末尾添加内容 |
文件读取操作
字符读取
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
int ch;
// 逐个字符读取,EOF表示文件结束
while ((ch = fgetc(fp)) != EOF) {
putchar(ch); // 输出到屏幕
}
fclose(fp);
return 0;
}
字符串读取
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
char buffer[100];
// 读取一行文本(最多99个字符)
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
格式化读取
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
int num;
float f;
char str[50];
// 格式化读取
while (fscanf(fp, "%d %f %s", &num, &f, str) == 3) {
printf("读取: %d, %.2f, %s\n", num, f, str);
}
fclose(fp);
return 0;
}
二进制读取
#include <stdio.h>
typedef struct {
int id;
char name[50];
float score;
} Student;
int main() {
FILE *fp = fopen("students.dat", "rb"); // 二进制读取
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
Student s;
// 读取结构体数据
while (fread(&s, sizeof(Student), 1, fp) == 1) {
printf("ID: %d, 姓名: %s, 分数: %.2f\n", s.id, s.name, s.score);
}
fclose(fp);
return 0;
}
文件写入操作
字符写入
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("无法创建文件\n");
return 1;
}
char text[] = "Hello, World!";
for (int i = 0; text[i] != '\0'; i++) {
fputc(text[i], fp); // 写入单个字符
}
fclose(fp);
return 0;
}
字符串写入
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("无法创建文件\n");
return 1;
}
char *text = "这是一行文本\n";
fputs(text, fp); // 写入字符串(不自动添加换行符)
fclose(fp);
return 0;
}
格式化写入
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
printf("无法创建文件\n");
return 1;
}
int num = 42;
float f = 3.14;
char *str = "Hello";
// 格式化写入
fprintf(fp, "%d %.2f %s\n", num, f, str);
fclose(fp);
return 0;
}
二进制写入
#include <stdio.h>
typedef struct {
int id;
char name[50];
float score;
} Student;
int main() {
FILE *fp = fopen("students.dat", "wb"); // 二进制写入
if (fp == NULL) {
printf("无法创建文件\n");
return 1;
}
Student s = {1, "张三", 85.5};
// 写入结构体数据
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);
return 0;
}
文件定位操作
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
// 获取文件位置
long pos = ftell(fp); // 初始位置为0
// 移动文件指针
fseek(fp, 10, SEEK_SET); // 从文件开头移动10个字节
fseek(fp, 5, SEEK_CUR); // 从当前位置移动5个字节
fseek(fp, -20, SEEK_END); // 从文件末尾向前移动20个字节
// 重置文件指针到开头
rewind(fp);
fclose(fp);
return 0;
}
文件错误处理
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
// 打印错误信息
printf("错误: %s\n", strerror(errno));
return 1;
}
// 检查文件操作错误
if (ferror(fp)) {
printf("文件操作错误\n");
clearerr(fp); // 清除错误标志
}
fclose(fp);
return 0;
}
预处理
预处理基本概念
预处理是C编译过程的第一步,由预处理器(Preprocessor)执行。预处理指令以#
开头,它们在编译前被处理,用于修改源代码文本。
常见预处理指令
- 文件包含:
#include
- 宏定义:
#define
、#undef
- 条件编译:
#if
、#ifdef
、#ifndef
、#elif
、#else
、#endif
- 错误处理:
#error
- 行号和文件名:
#line
- 编译控制:
#pragma
文件包含指令
// 标准库头文件
#include <stdio.h> // 从标准库目录查找
#include <string.h>
// 自定义头文件
#include "myheader.h" // 从当前目录或指定目录查找
宏定义
简单宏
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
float radius = 5.0;
float area = PI * radius * radius;
int x = 10, y = 20;
int max_val = MAX(x, y);
return 0;
}
带参数的宏
#define SQUARE(x) ((x) * (x))
#define PRINT_INT(x) printf("Value: %d\n", x)
int main() {
int a = 5;
int result = SQUARE(a + 1); // 展开为 ((a + 1) * (a + 1))
PRINT_INT(result); // 展开为 printf("Value: %d\n", result)
return 0;
}
字符串化操作符(#)
#define STR(x) #x
int main() {
printf(STR(Hello World!)); // 展开为 printf("Hello World!");
printf(STR(1 + 2)); // 展开为 printf("1 + 2");
return 0;
}
标记粘贴操作符(##)
#define CONCAT(a, b) a##b
int main() {
int xy = 100;
printf("%d\n", CONCAT(x, y)); // 展开为 printf("%d\n", xy);
return 0;
}
条件编译
#ifdef 和 #ifndef
#ifdef DEBUG
printf("调试信息: 变量x = %d\n", x);
#endif
#ifndef MAX_SIZE
#define MAX_SIZE 100
#endif
#if 和 #elif
#define VERSION 2
#if VERSION == 1
printf("使用版本1\n");
#elif VERSION == 2
printf("使用版本2\n");
#else
printf("未知版本\n");
#endif
条件编译示例:平台特定代码
#ifdef _WIN32
// Windows平台代码
#include <windows.h>
#define LINE_END "\r\n"
#elif __linux__
// Linux平台代码
#include <unistd.h>
#define LINE_END "\n"
#else
#error "不支持的平台"
#endif
其他预处理指令
#error 指令
#if !defined(__STDC__)
#error "需要标准C编译器"
#endif
#line 指令
#line 100 "custom_file.c"
// 从这里开始,行号从100开始,文件名显示为custom_file.c
#pragma 指令
#pragma once // 保证头文件只被包含一次
#pragma GCC diagnostic ignored "-Wunused-variable" // 忽略未使用变量警告
预处理的优缺点
优点
- 代码复用:通过宏和头文件实现代码重用
- 条件编译:支持跨平台开发和调试版本
- 代码生成:在编译前自动生成代码
- 性能优化:宏替换可以减少函数调用开销
缺点
- 可读性降低:过度使用宏会使代码难以理解
- 调试困难:错误可能出现在预处理后的代码中
- 潜在副作用:宏参数可能被多次求值
- 命名冲突:宏定义可能与其他标识符冲突
预处理与编译的区别
特性 | 预处理阶段 | 编译阶段 |
---|---|---|
执行时间 | 编译前 | 预处理后 |
处理内容 | 文本替换、文件包含、条件编译 | 词法分析、语法分析、代码生成 |
输出结果 | 修改后的源代码 | 目标代码(汇编或机器码) |
工具 | 预处理器(cpp) | 编译器(cc、gcc) |
指令形式 | 以#开头的预处理指令 | C语言语句 |
实际应用示例
文件操作示例:学生成绩管理系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NAME_LEN 50
#define MAX_STUDENTS 100
typedef struct {
int id;
char name[MAX_NAME_LEN];
float score;
} Student;
// 保存学生信息到文件
void saveStudents(Student students[], int count) {
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) {
printf("无法创建文件\n");
return;
}
fwrite(&count, sizeof(int), 1, fp); // 写入学生数量
fwrite(students, sizeof(Student), count, fp); // 写入学生数据
fclose(fp);
printf("已保存 %d 名学生信息\n", count);
}
// 从文件加载学生信息
int loadStudents(Student students[]) {
FILE *fp = fopen("students.dat", "rb");
if (fp == NULL) {
printf("无法打开文件或文件不存在\n");
return 0;
}
int count;
fread(&count, sizeof(int), 1, fp); // 读取学生数量
fread(students, sizeof(Student), count, fp); // 读取学生数据
fclose(fp);
printf("已加载 %d 名学生信息\n", count);
return count;
}
int main() {
Student students[MAX_STUDENTS];
int count = 0;
// 添加学生信息
students[count].id = 1;
strcpy(students[count].name, "张三");
students[count].score = 85.5;
count++;
students[count].id = 2;
strcpy(students[count].name, "李四");
students[count].score = 92.0;
count++;
// 保存到文件
saveStudents(students, count);
// 清空数组
memset(students, 0, sizeof(students));
count = 0;
// 从文件加载
count = loadStudents(students);
// 显示学生信息
for (int i = 0; i < count; i++) {
printf("ID: %d, 姓名: %s, 分数: %.1f\n",
students[i].id, students[i].name, students[i].score);
}
return 0;
}
预处理示例:调试宏
#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) printf("DEBUG: " fmt, __VA_ARGS__)
#define DEBUG_LINE() printf("DEBUG: Line %d in %s\n", __LINE__, __FILE__)
#else
#define DEBUG_PRINT(fmt, ...) do {} while(0)
#define DEBUG_LINE() do {} while(0)
#endif
// 平台检测
#ifdef _WIN32
#define PLATFORM "Windows"
#include <windows.h>
#elif __linux__
#define PLATFORM "Linux"
#include <unistd.h>
#elif __APPLE__
#define PLATFORM "macOS"
#include <unistd.h>
#else
#define PLATFORM "Unknown"
#endif
int main() {
DEBUG_LINE();
DEBUG_PRINT("程序开始运行,平台: %s\n", PLATFORM);
int x = 42;
DEBUG_PRINT("变量x的值: %d\n", x);
// 正常代码...
return 0;
}
总结
文件操作和预处理是C语言中两个重要的功能,它们分别在程序运行时和编译前发挥作用:
- 文件操作允许程序与外部存储设备交互,支持文本和二进制两种模式
- 预处理在编译前对源代码进行文本处理,提供宏定义、文件包含和条件编译等功能
- 文件操作通过文件指针和标准库函数实现,需要注意文件打开模式和错误处理
- 预处理指令以#开头,它们不是C语言语句,而是由预处理器单独处理
掌握文件操作和预处理技术,能够使C程序更加灵活、可移植,并支持复杂的数据处理和代码组织。