文件与结构体

对应教学日历:讲次 23(6月18日)


一、文件操作

文件的基本概念

  • C 语言将文件视为字节流(文本文件 vs 二进制文件)
  • 使用 FILE * 类型指针操作文件
  • 文件使用前必须打开fopen),使用后必须关闭fclose

文件打开与关闭

FILE *fp;
fp = fopen("文件名", "模式");
if (fp == NULL) {
    printf("文件打开失败!\n");
    exit(1);
}
// ... 读写操作 ...
fclose(fp);

常用打开模式

模式含义
"r"只读,文件必须存在
"w"只写,文件存在则清空,不存在则新建
"a"追加写入,文件不存在则新建
"r+"读写,文件必须存在
"w+"读写,文件存在则清空,不存在则新建
"a+"读写,追加模式
"rb" / "wb"二进制模式读写

文件读写函数

字符读写

ch = fgetc(fp);      // 从文件读一个字符,返回 EOF 表示结束
fputc(ch, fp);       // 写入一个字符到文件

字符串读写

fgets(str, n, fp);       // 读一行(最多 n-1 字符),保留 \n
fputs(str, fp);          // 写入字符串(不自动加 \n)

格式化读写

fscanf(fp, "%d %s", &n, name);     // 从文件按格式读取
fprintf(fp, "%d %s\n", n, name);   // 按格式写入文件

fscanffprintf 的用法与 scanf/printf 相同,只是多了一个 FILE * 参数。

文件结束判断

while (!feof(fp)) { ... }         // feof 判断是否到达文件末尾
while (fscanf(fp, "%s", s) != EOF) { ... }  // scanf 系列返回 EOF
while (fgets(str, 100, fp) != NULL) { ... } // fgets 返回 NULL

完整文件读写流程

#include <stdio.h>
int main() {
    FILE *fin, *fout;
    fin = fopen("input.txt", "r");
    if (fin == NULL) { printf("无法打开输入文件\n"); return 1; }
    fout = fopen("output.txt", "w");
    if (fout == NULL) { printf("无法创建输出文件\n"); fclose(fin); return 1; }
    
    char line[256];
    while (fgets(line, sizeof(line), fin) != NULL) {
        fprintf(fout, "%s", line);   // 逐行复制
    }
    
    fclose(fin);
    fclose(fout);
    return 0;
}

二、结构体(struct)

为什么需要结构体

结构体将不同类型的数据组合成一个整体。如:学生(姓名+学号+成绩)、股票(名称+价格+收益)。

结构体定义

struct 结构体名 {
    类型1 成员1;
    类型2 成员2;
    ...
};
 
struct Student {
    char name[20];
    int id;
    float score;
};

结构体变量

// 方法1:先定义结构体,再定义变量
struct Student s1, s2;
 
// 方法2:定义结构体的同时定义变量
struct Student {
    char name[20];
    int id;
    float score;
} s1, s2;
 
// 方法3:定义时初始化
struct Student s = {"Tom", 1001, 95.5};

访问结构体成员

s1.id = 1001;                 // . 运算符
strcpy(s1.name, "Tom");
printf("%s %d %.1f\n", s1.name, s1.id, s1.score);

结构体数组

struct Student class[30];
for (i = 0; i < 30; i++) {
    scanf("%s %d %f", class[i].name, &class[i].id, &class[i].score);
}

结构体指针

struct Student s, *p = &s;
p->id = 1001;          // 等价于 (*p).id = 1001
p->score = 90.5;       // -> 运算符:指针访问成员
 
// . 和 -> 的区别
s.name         // 变量用 .
p->name        // 指针用 ->
(*p).name      // 解引用后用 .

结构体作函数参数

void printStudent(struct Student s)      // 传值(复制整个结构体)
{
    printf("%s %d\n", s.name, s.id);
}
 
void modifyStudent(struct Student *p)    // 传地址(高效)
{
    p->score += 5;    // 修改实参
}

三、typedef —— 类型别名

typedef 原类型 新别名;
 
typedef int Integer;               // Integer 就是 int
typedef struct Student Student;    // 简化:Student 代替 struct Student
typedef struct {
    char name[20];
    double price;
} Stock;                           // 定义结构体的同时 typedef
// 使用 typedef 后可以省略 struct 关键字
Stock s1, s2;                       // 而不是 struct Stock s1;
Portfolio best;                      // 而不是 struct Portfolio best;

作业示例

ex14-5:文件读写——身份证提取生日

#include <stdio.h>
#include <string.h>
 
int main() {
    FILE *fin, *fout;
    char name[50];
    char idcard[20];
    char birthday[9];
 
    fin = fopen("idcard.txt", "r");
    if (fin == NULL) {
        printf("无法打开文件 idcard.txt\n");
        return 1;
    }
 
    fout = fopen("birthday.txt", "w");
    if (fout == NULL) {
        printf("无法创建文件 birthday.txt\n");
        fclose(fin);
        return 1;
    }
 
    while (fscanf(fin, "%s", name) == 1) {
        if (fscanf(fin, "%s", idcard) != 1) break;
 
        strncpy(birthday, idcard + 6, 8);   // 身份证第7-14位是出生日期
        birthday[8] = '\0';
 
        fprintf(fout, "%s\n", name);
        fprintf(fout, "%s\n", birthday);
    }
 
    fclose(fin);
    fclose(fout);
    printf("生日信息已成功写入 birthday.txt\n");
    return 0;
}

要点

  • fopen("r") 读模式,fopen("w") 写模式
  • 必须先检查文件是否打开成功fin == NULL
  • fscanf(fin, "%s", name) 从文件读取,返回成功匹配的项数
  • strncpy(dst, src, n) 截取 n 个字符
  • idcard + 6 指针偏移,指向第 7 个字符(身份证生日起始位置)
  • 用完后必须 fclose 关闭文件

exprp/ex3.c:综合项目——投资组合优化系统(结构体 + 文件 + 函数)

这是本课程的大作业,综合运用了结构体、文件操作、函数模块化、蒙特卡洛模拟等知识。这里列出关键片段:

// 结构体定义
typedef struct {
    char name[20];
    double prices[MAX_DAYS];
    double returns[MAX_DAYS];
    int num_days;
    double mean_return;
    double annual_return;
    double volatility;
} Stock;
 
typedef struct {
    double weights[MAX_STOCKS];
    double portfolio_return;
    double portfolio_risk;
    double sharpe_ratio;
} Portfolio;
 
// 文件读取
int load_from_file() {
    FILE *fp = fopen("stock_data.txt", "r");
    if (!fp) { printf("错误:无法打开文件\n"); return 0; }
    
    fscanf(fp, "%d %d", &num_stocks, &days);
    for (int i = 0; i < num_stocks; i++) {
        fscanf(fp, "%19s", stocks[i].name);
        for (int j = 0; j < days; j++)
            fscanf(fp, "%lf", &stocks[i].prices[j]);
        calculate_returns(&stocks[i]);   // 函数调用
    }
    fclose(fp);
    return 1;
}
 
// 写入结果报告
void save_results(Portfolio *best, Portfolio *min_risk) {
    FILE *fp = fopen("portfolio_result.txt", "w");
    fprintf(fp, "=== 投资组合优化分析报告 ===\n");
    fprintf(fp, "夏普比率: %.4f\n", best->sharpe_ratio);
    // ...
    fclose(fp);
}

本项目综合要点

  • 结构体:定义 StockPortfolio 类型,组织多维数据
  • typedef:简化类型名,Stock 代替 struct { ... }
  • 文件操作fopen/fclosefscanf/fprintf、错误处理
  • 函数模块化load_from_filecalculate_returnsmonte_carlo_simulationsave_results
  • 菜单驱动switch(choice) 结构实现交互菜单
  • 结构体指针calculate_returns(&stocks[i]) 传结构体地址
  • 动态内存与算法:蒙特卡洛模拟、协方差矩阵计算

作业重点

文件操作

  • fopen("文件名", "模式") 打开文件,模式 "r"/"w"/"a"
  • 打开后必须检查 NULL,用完后必须 fclose
  • fscanf/fprintfscanf/printf 多一个 FILE * 参数
  • fgets 读一行(更安全),会保留换行符
  • feof(fp) 判断文件末尾

结构体

  • struct 类型名 { 成员; }; 定义新类型
  • 访问成员:变量用 .(如 s.name),指针用 ->(如 p->name
  • 结构体可嵌套定义(一个结构体的成员可以是另一个结构体)
  • 结构体数组:struct Student class[30];
  • typedef 简化类型名,避免重复写 struct