第5章:结构体与文件操作

学习目标

本周将深入学习C/C++中的结构体、联合体、枚举等复合数据类型,掌握文件I/O操作,理解预处理器指令的使用,为构建复杂应用程序打下基础。

核心概念

  • 结构体的定义和使用
  • 结构体数组和指针
  • 联合体和枚举类型
  • 文件的打开、读写和关闭
  • 文本文件和二进制文件操作
  • 文件定位与错误处理
  • 预处理器指令
  • 头文件设计与保护

4.1 结构体的定义和使用

4.1.1 结构体的基本概念

结构体(struct)是C/C++中用户自定义的复合数据类型,允许将不同类型的数据组合成一个整体。结构体提供了一种组织相关数据的方式,使得程序更加模块化和易于维护。

结构体的特点:

  • 可以包含多个不同类型的成员变量
  • 成员可以是基本数据类型、数组、指针或其他结构体
  • 在C++中,结构体可以包含成员函数
  • 结构体变量占用内存的大小是所有成员大小之和(考虑内存对齐)

4.1.2 结构体的定义语法

// C风格结构体定义
struct 结构体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    // ...
};

// C++结构体定义(可以包含成员函数)
struct Point {
    double x;  // x坐标
    double y;  // y坐标
    
    // 成员函数
    double distanceToOrigin() const {
        return sqrt(x * x + y * y);
    }
    
    void move(double dx, double dy) {
        x += dx;
        y += dy;
    }
};

4.1.3 结构体变量的声明和初始化

// 先定义结构体,再声明变量
struct Student {
    char name[50];
    int age;
    float gpa;
};

int main() {
    // 方式1:先声明后赋值
    Student stu1;
    strcpy(stu1.name, "张三");
    stu1.age = 20;
    stu1.gpa = 3.8;
    
    // 方式2:声明时初始化(C风格)
    Student stu2 = {"李四", 21, 3.9};
    
    // 方式3:C++11统一初始化
    Student stu3{"王五", 19, 3.7};
    
    return 0;
}

4.1.4 结构体的内存布局

结构体在内存中的存储遵循特定的对齐规则,这对理解结构体的大小和性能优化很重要。

struct Example {
    char c;     // 1字节
    int i;      // 4字节
    double d;   // 8字节
};

// 使用sizeof运算符查看结构体大小
cout << "结构体大小: " << sizeof(Example) << "字节" << endl;
// 输出可能不是13字节,而是16字节(考虑内存对齐)

4.2 结构体数组和指针

4.2.1 结构体数组

结构体数组允许存储多个相同类型的结构体变量,是管理一组相关数据的常用方式。

struct Employee {
    int id;
    char name[50];
    double salary;
};

// 声明结构体数组
Employee staff[100];  // 可以存储100个员工信息

// 初始化结构体数组
Employee team[3] = {
    {1001, "张三", 8000.0},
    {1002, "李四", 9000.0},
    {1003, "王五", 7500.0}
};

4.2.2 结构体指针

结构体指针用于动态管理结构体内存,以及通过引用传递大型结构体。

// 结构体指针的声明和使用
Student* ptr = &stu1;

// 访问结构体成员的两种方式
cout << ptr->name << endl;  // 箭头运算符(推荐)
cout << (*ptr).name << endl; // 解引用后使用点运算符

// 动态分配结构体内存
Student* dynamicStu = new Student;
strcpy(dynamicStu->name, "动态学生");
dynamicStu->age = 22;

// 使用完毕后释放内存
delete dynamicStu;

4.2.3 结构体作为函数参数

结构体可以通过值传递、指针传递或引用传递(C++)方式传递给函数。

// 值传递(复制整个结构体)
void printStudentByValue(Student s) {
    cout << s.name << ", " << s.age << endl;
}

// 指针传递(传递地址)
void updateStudentGPA(Student* s, float newGPA) {
    if (s != nullptr) {
        s->gpa = newGPA;
    }
}

// 引用传递(C++特性,避免复制)
void displayStudent(const Student& s) {
    cout << "姓名: " << s.name << endl;
    cout << "年龄: " << s.age << endl;
    cout << "GPA: " << s.gpa << endl;
}

4.3 联合体和枚举类型

4.3.1 联合体(union)

联合体是一种特殊的数据类型,允许在同一内存位置存储不同类型的数据。联合体的大小等于其最大成员的大小。

联合体的特点:

  • 所有成员共享同一块内存
  • 一次只能存储一个成员的值
  • 常用于节省内存或与硬件交互
union Data {
    int i;
    float f;
    char str[20];
};

// 联合体使用示例
Data data;
data.i = 10;      // 存储整数
cout << data.i << endl;

data.f = 3.14;    // 现在存储浮点数,整数数据被覆盖
cout << data.f << endl;

4.3.2 枚举类型(enum)

枚举类型用于定义一组命名的整数常量,提高代码的可读性和可维护性。

// 基本枚举定义
enum Weekday {
    MONDAY = 1,    // 可以指定起始值
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
};

// 使用枚举
today = WEDNESDAY;
if (today == WEEKEND) {
    cout << "周末" << endl;
}

// C++11强类型枚举
enum class Color : int {  // 指定底层类型
    RED = 0,
    GREEN = 1,
    BLUE = 2
};

Color c = Color::RED;  // 必须使用作用域解析运算符

4.3.3 枚举与switch语句的结合

枚举类型特别适合与switch语句结合使用,实现清晰的多分支选择。

enum MenuOption {
    ADD_ITEM = 1,
    DELETE_ITEM,
    MODIFY_ITEM,
    DISPLAY_ITEMS,
    EXIT_PROGRAM
};

void handleMenu(MenuOption choice) {
    switch (choice) {
        case ADD_ITEM:
            addNewItem();
            break;
        case DELETE_ITEM:
            deleteItem();
            break;
        case MODIFY_ITEM:
            modifyItem();
            break;
        case DISPLAY_ITEMS:
            displayAllItems();
            break;
        case EXIT_PROGRAM:
            cout << "退出程序" << endl;
            break;
        default:
            cout << "无效选择" << endl;
    }
}

4.4 文件的打开、读写和关闭

4.4.1 文件基本概念

文件是存储在外部存储设备上的数据集合。C/C++提供了标准库来处理文件操作,主要包括文本文件和二进制文件两种类型。

文件操作的基本步骤:

  1. 打开文件
  2. 读写数据
  3. 关闭文件
  4. 错误处理

4.4.2 C风格文件操作(使用stdio.h)

#include <cstdio>

// 文件指针
FILE* filePtr;

// 打开文件
filePtr = fopen("data.txt", "r");  // r: 读取, w: 写入, a: 追加
if (filePtr == nullptr) {
    perror("文件打开失败");
    return;
}

// 写入数据
fprintf(filePtr, "姓名: %s, 年龄: %d\n", name, age);

// 读取数据
fscanf(filePtr, "%s %d", name, &age);

// 关闭文件
fclose(filePtr);

4.4.3 C++风格文件操作(使用fstream)

#include <fstream>
#include <string>

// 写入文件
ofstream outFile("output.txt");
if (outFile.is_open()) {
    outFile << "这是写入的文本" << endl;
    outFile << "数字: " << 42 << endl;
    outFile.close();
}

// 读取文件
ifstream inFile("input.txt");
string line;
if (inFile.is_open()) {
    while (getline(inFile, line)) {
        cout << line << endl;
    }
    inFile.close();
}

4.4.4 文件打开模式

不同的文件打开模式决定了对文件的操作权限:

模式 描述
“r” 只读模式,文件必须存在
“w” 写入模式,文件存在则清空,不存在则创建
“a” 追加模式,在文件末尾添加数据
“r+” 读写模式,文件必须存在
“w+” 读写模式,文件存在则清空,不存在则创建
“a+” 读写追加模式,在文件末尾添加数据

4.5 文本文件和二进制文件操作

4.5.1 文本文件操作

文本文件以人类可读的字符形式存储数据,每行以换行符结束。

// 文本文件写入示例
void writeTextFile(const string& filename) {
    ofstream file(filename);
    
    if (!file.is_open()) {
        cerr << "无法创建文件: " << filename << endl;
        return;
    }
    
    // 写入格式化的文本数据
    file << "# 学生信息文件" << endl;
    file << "# 格式: ID,姓名,年龄,GPA" << endl;
    file << endl;
    
    file << "1001,张三,20,3.8" << endl;
    file << "1002,李四,21,3.9" << endl;
    file << "1003,王五,19,3.7" << endl;
    
    file.close();
}

// 文本文件读取示例
void readTextFile(const string& filename) {
    ifstream file(filename);
    string line;
    
    if (!file.is_open()) {
        cerr << "无法打开文件: " << filename << endl;
        return;
    }
    
    while (getline(file, line)) {
        // 跳过注释行和空行
        if (line.empty() || line[0] == '#') {
            continue;
        }
        
        // 解析数据行
        stringstream ss(line);
        string id, name, age, gpa;
        
        if (getline(ss, id, ',') && 
            getline(ss, name, ',') && 
            getline(ss, age, ',') && 
            getline(ss, gpa, ',')) {
            
            cout << "ID: " << id << ", 姓名: " << name 
                 << ", 年龄: " << age << ", GPA: " << gpa << endl;
        }
    }
    
    file.close();
}

4.5.2 二进制文件操作

二进制文件以字节序列的形式存储数据,不进行任何字符转换,适合存储结构化数据。

// 二进制文件写入
void writeBinaryFile(const string& filename) {
    ofstream file(filename, ios::binary);
    
    if (!file.is_open()) {
        cerr << "无法创建二进制文件: " << filename << endl;
        return;
    }
    
    // 写入整数
    int count = 3;
    file.write(reinterpret_cast<const char*>(&count), sizeof(count));
    
    // 写入结构体数组
    Student students[3] = {
        {1001, "张三", 20, 3.8},
        {1002, "李四", 21, 3.9},
        {1003, "王五", 19, 3.7}
    };
    
    for (int i = 0; i < count; i++) {
        file.write(reinterpret_cast<const char*>(&students[i]), sizeof(Student));
    }
    
    file.close();
}

// 二进制文件读取
void readBinaryFile(const string& filename) {
    ifstream file(filename, ios::binary);
    
    if (!file.is_open()) {
        cerr << "无法打开二进制文件: " << filename << endl;
        return;
    }
    
    // 读取记录数量
    int count;
    file.read(reinterpret_cast<char*>(&count), sizeof(count));
    
    cout << "找到 " << count << " 条记录" << endl;
    
    // 读取每条记录
    for (int i = 0; i < count; i++) {
        Student stu;
        file.read(reinterpret_cast<char*>(&stu), sizeof(Student));
        
        cout << "ID: " << stu.id << ", 姓名: " << stu.name 
             << ", 年龄: " << stu.age << ", GPA: " << stu.gpa << endl;
    }
    
    file.close();
}

4.5.3 文本文件vs二进制文件

特性 文本文件 二进制文件
可读性 人类可读 需要特殊工具
存储空间 通常较大 通常较小
处理速度 较慢 较快
跨平台 较好 需要考虑字节序
用途 配置文件、日志 数据库、图像等

4.6 文件定位与错误处理

4.6.1 文件定位操作

文件定位允许在文件中移动到特定位置进行读写操作。

#include <cstdio>

// 使用fseek进行文件定位
void filePositioningDemo() {
    FILE* file = fopen("data.bin", "rb+");
    if (file == nullptr) {
        perror("文件打开失败");
        return;
    }
    
    // 移动到文件开头
    fseek(file, 0, SEEK_SET);
    
    // 移动到文件末尾
    fseek(file, 0, SEEK_END);
    long fileSize = ftell(file);  // 获取文件大小
    
    // 移动到第100个字节
    fseek(file, 100, SEEK_SET);
    
    // 从当前位置向前移动50个字节
    fseek(file, -50, SEEK_CUR);
    
    fclose(file);
}

4.6.2 C++文件流定位

// C++文件流定位操作
void cppFilePositioning() {
    fstream file("data.bin", ios::in | ios::out | ios::binary);
    
    if (!file.is_open()) {
        cerr << "无法打开文件" << endl;
        return;
    }
    
    // 获取当前位置
    streampos currentPos = file.tellg();
    
    // 移动到文件开头
    file.seekg(0, ios::beg);
    
    // 移动到文件末尾
    file.seekg(0, ios::end);
    streampos endPos = file.tellg();
    cout << "文件大小: " << endPos << " 字节" << endl;
    
    // 移动到特定位置
    file.seekg(100, ios::beg);
    
    file.close();
}

4.6.3 文件错误处理

文件操作可能遇到各种错误,需要进行适当的错误处理。

// 文件错误处理示例
bool safeFileOperation(const string& filename) {
    ifstream file(filename);
    
    // 检查文件是否成功打开
    if (!file.is_open()) {
        cerr << "错误: 无法打开文件 '" << filename << "'" << endl;
        
        // 检查具体错误类型
        if (errno == ENOENT) {
            cerr << "文件不存在" << endl;
        } else if (errno == EACCES) {
            cerr << "权限不足" << endl;
        } else {
            cerr << "未知错误: " << strerror(errno) << endl;
        }
        
        return false;
    }
    
    // 检查文件是否为空
    file.seekg(0, ios::end);
    if (file.tellg() == 0) {
        cerr << "警告: 文件为空" << endl;
        file.close();
        return false;
    }
    
    // 重置到文件开头
    file.seekg(0, ios::beg);
    
    // 执行文件操作...
    
    // 检查操作过程中是否发生错误
    if (file.fail()) {
        cerr << "错误: 文件操作失败" << endl;
        file.close();
        return false;
    }
    
    file.close();
    return true;
}

4.7 预处理器指令

4.7.1 基本预处理器指令

预处理器在编译之前处理源代码,执行文本替换和条件编译等操作。

// 宏定义
#define PI 3.14159              // 定义常量
#define MAX(a, b) ((a) > (b) ? (a) : (b))  // 定义宏函数
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

// 条件编译
#define DEBUG_MODE              // 定义调试宏

#ifdef DEBUG_MODE
    #define DEBUG_LOG(x) cout << "[DEBUG] " << x << endl
#else
    #define DEBUG_LOG(x)        // 空定义,发布版本中不输出调试信息
#endif

// 文件包含
#include <iostream>            // 包含标准库头文件
#include "myheader.h"          // 包含自定义头文件

4.7.2 条件编译的应用

条件编译允许根据不同的编译环境或配置选择性地编译代码。

// 平台相关的代码
#ifdef _WIN32
    #define PLATFORM_NAME "Windows"
    #include <windows.h>
#elif defined(__linux__)
    #define PLATFORM_NAME "Linux"
    #include <unistd.h>
#elif defined(__APPLE__)
    #define PLATFORM_NAME "macOS"
    #include <TargetConditionals.h>
#endif

// 版本控制
#define VERSION_MAJOR 1
#define VERSION_MINOR 2
#define VERSION_PATCH 3

#if VERSION_MAJOR >= 2
    // 2.0及以上版本的特性
    void newFeature() {
        cout << "这是新版本特性" << endl;
    }
#elif VERSION_MAJOR == 1 && VERSION_MINOR >= 1
    // 1.1版本的特性
    void improvedFeature() {
        cout << "这是改进的特性" << endl;
    }
#else
    // 旧版本的兼容代码
    void legacyFeature() {
        cout << "这是旧版本特性" << endl;
    }
#endif

4.7.3 宏的高级用法

// 字符串化操作符
#define STRINGIFY(x) #x
#define TO_STRING(x) STRINGIFY(x)

// 连接操作符
#define CONCAT(a, b) a##b

// 变参宏(C99/C++11支持)
#define LOG(format, ...) printf(format, ##__VA_ARGS__)

// 使用示例
void macroDemo() {
    cout << "行号: " << __LINE__ << endl;           // 当前行号
    cout << "文件名: " << __FILE__ << endl;         // 当前文件名
    cout << "日期: " << __DATE__ << endl;           // 编译日期
    cout << "时间: " << __TIME__ << endl;           // 编译时间
    
    cout << "字符串化: " << TO_STRING(123) << endl; // 将数字转为字符串
    
    int CONCAT(var, 1) = 100;  // 创建变量名var1
    cout << "var1 = " << var1 << endl;
}

4.8 头文件设计与保护

4.8.1 头文件的作用

头文件(.h文件)包含函数声明、类定义、常量定义、宏定义等,用于在多个源文件之间共享代码。

4.8.2 防止重复包含

防止头文件被重复包含是头文件设计的重要原则,常用的方法有:

// 方法1:使用#ifndef保护(传统方法)
#ifndef MY_HEADER_H
#define MY_HEADER_H

// 头文件内容
class MyClass {
public:
    void doSomething();
private:
    int data;
};

#endif // MY_HEADER_H

// 方法2:使用#pragma once(现代编译器支持)
#pragma once

// 头文件内容
class MyClass {
public:
    void doSomething();
private:
    int data;
};

4.8.3 头文件设计原则

// 好的头文件设计示例 (student.h)
#ifndef STUDENT_H
#define STUDENT_H

#include <string>      // 包含必要的头文件
using std::string;     // 避免在头文件中使用using namespace

// 常量定义
const int MAX_NAME_LENGTH = 50;
const int MAX_STUDENTS = 100;

// 枚举定义
enum GradeLevel {
    FRESHMAN = 1,
    SOPHOMORE,
    JUNIOR,
    SENIOR
};

// 结构体定义
struct StudentInfo {
    int id;
    char name[MAX_NAME_LENGTH];
    GradeLevel level;
    float gpa;
};

// 类声明(实现放在.cpp文件中)
class StudentManager {
public:
    StudentManager();                    // 构造函数
    ~StudentManager();                     // 析构函数
    
    bool addStudent(const StudentInfo& info);
    bool removeStudent(int id);
    StudentInfo* findStudent(int id);
    void displayAllStudents() const;
    
private:
    StudentInfo students[MAX_STUDENTS];
    int studentCount;
    
    // 私有辅助函数
    int findStudentIndex(int id) const;
};

// 内联函数(简单函数可以定义在头文件中)
inline bool isValidGPA(float gpa) {
    return gpa >= 0.0f && gpa <= 4.0f;
}

#endif // STUDENT_H

4.8.4 头文件组织最佳实践

  1. 包含顺序:系统头文件 -> 第三方库 -> 项目内部头文件
  2. 最小化依赖:只包含必要的头文件,使用前向声明减少依赖
  3. 避免定义:不在头文件中定义变量或函数实现(内联函数除外)
  4. 命名空间:避免在头文件中使用using namespace
  5. 文档注释:为公共接口添加清晰的文档注释
// 实现文件示例 (student.cpp)
#include "student.h"           // 对应的头文件
#include <iostream>            // 实现中需要的额外头文件
#include <algorithm>

using namespace std;

// 构造函数实现
StudentManager::StudentManager() : studentCount(0) {
    // 初始化代码
}

// 析构函数实现
StudentManager::~StudentManager() {
    // 清理代码
}

// 其他成员函数实现...
bool StudentManager::addStudent(const StudentInfo& info) {
    if (studentCount >= MAX_STUDENTS) {
        return false;
    }
    
    if (!isValidGPA(info.gpa)) {
        return false;
    }
    
    students[studentCount] = info;
    studentCount++;
    return true;
}

4.9 综合应用示例

4.9.1 配置文件管理器

结合结构体、文件操作和预处理器指令,创建一个实用的配置文件管理器:

// config.h - 配置文件管理器头文件
#ifndef CONFIG_H
#define CONFIG_H

#include <string>
#include <map>

using std::string;
using std::map;

// 配置项结构体
struct ConfigItem {
    string key;
    string value;
    string description;
    bool isReadOnly;
};

// 配置管理器类
class ConfigManager {
private:
    map<string, ConfigItem> configs;
    string configFile;
    
public:
    ConfigManager(const string& filename);
    ~ConfigManager();
    
    bool loadConfig();
    bool saveConfig();
    bool setValue(const string& key, const string& value);
    string getValue(const string& key, const string& defaultValue = "") const;
    void displayAllConfigs() const;
    
    // 预定义的常用配置项
    static const char* VERSION;
    static const char* DEFAULT_LANGUAGE;
    static const int MAX_CONFIG_ITEMS = 100;
};

#endif // CONFIG_H

4.10 常见错误和调试技巧

4.10.1 结构体常见错误

// 错误1:结构体大小计算错误
struct BadExample {
    char c;      // 1字节
    // 3字节填充
    int i;       // 4字节
    char c2;     // 1字节
    // 7字节填充
    double d;    // 8字节
};  // 总大小:20字节,不是14字节

// 正确做法:了解内存对齐
#pragma pack(push, 1)  // 设置1字节对齐
struct GoodExample {
    char c;
    int i;
    char c2;
    double d;
};
#pragma pack(pop)  // 恢复默认对齐

4.10.2 文件操作常见错误

// 错误检查示例
bool safeFileOperation(const string& filename) {
    // 检查文件是否存在
    ifstream testFile(filename);
    bool exists = testFile.good();
    testFile.close();
    
    if (!exists) {
        ofstream createFile(filename);
        if (!createFile.is_open()) {
            cerr << "无法创建文件: " << filename << endl;
            return false;
        }
        createFile.close();
    }
    
    // 检查文件权限
    ifstream readTest(filename);
    if (!readTest.is_open()) {
        cerr << "无法读取文件: " << filename << endl;
        return false;
    }
    readTest.close();
    
    return true;
}

4.11 练习题目

基础练习

  1. 结构体基础:定义一个包含日期(年、月、日)的结构体,编写函数计算两个日期之间的天数差
  2. 枚举应用:使用枚举定义不同的用户权限级别(访客、普通用户、管理员、超级管理员),编写权限检查函数
  3. 文件写入:创建一个程序,将10个随机数写入文本文件,每个数字占一行
  4. 文件读取:编写程序读取上题创建的文件,计算所有数字的平均值
  5. 联合体使用:使用联合体设计一个可以存储整数、浮点数或字符串的数据结构

进阶练习

  1. 结构体数组排序:定义学生结构体数组,按成绩从高到低排序
  2. 二进制文件操作:将产品信息(名称、价格、库存)保存到二进制文件,然后读取并显示
  3. 文件搜索:在文本文件中搜索包含特定关键词的行,显示行号和内容
  4. 预处理器应用:使用条件编译实现跨平台的文件路径处理(Windows使用反斜杠,Linux使用正斜杠)
  5. 头文件设计:设计一个数学工具库的头文件,包含常用的数学函数和常量

综合项目

  1. 通讯录管理系统:使用结构体管理联系人信息,支持添加、删除、修改、查询,数据保存到文件
  2. 学生成绩管理系统:完整的学生信息管理,包括成绩录入、统计、排名,支持文本和二进制文件格式
  3. 库存管理系统:商品信息的增删改查,库存预警,销售记录,数据持久化存储
  4. 配置文件解析器:实现INI格式配置文件的读取和写入,支持节(section)和键值对
  5. 日志系统:实现分级别的日志记录(调试、信息、警告、错误),支持文件轮转和格式化输出

4.12 总结

本周我们深入学习了C/C++中结构体与文件操作的核心概念:

结构体相关:

  • 结构体的定义语法和内存布局
  • 结构体数组和指针的使用方法
  • 结构体作为函数参数的不同传递方式
  • 联合体和枚举类型的特性和应用场景

文件操作相关:

  • 文件的基本概念和操作流程
  • 文本文件与二进制文件的区别和应用
  • 文件的打开、读写、定位和关闭操作
  • 文件错误处理的重要性和实现方法

预处理器相关:

  • 常用预处理器指令的使用
  • 条件编译的应用场景
  • 宏定义的高级技巧
  • 头文件设计的最佳实践

这些知识为构建复杂的数据处理应用程序奠定了坚实基础。结构体提供了组织复杂数据的机制,文件操作实现了数据的持久化存储,预处理器增强了代码的灵活性和可维护性。在后续的学习中,我们将运用这些知识开发更加复杂和实用的应用程序。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐