【Linux C/C++开发】第5章:结构体与文件操作
第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++提供了标准库来处理文件操作,主要包括文本文件和二进制文件两种类型。
文件操作的基本步骤:
- 打开文件
- 读写数据
- 关闭文件
- 错误处理
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 头文件组织最佳实践
- 包含顺序:系统头文件 -> 第三方库 -> 项目内部头文件
- 最小化依赖:只包含必要的头文件,使用前向声明减少依赖
- 避免定义:不在头文件中定义变量或函数实现(内联函数除外)
- 命名空间:避免在头文件中使用
using namespace - 文档注释:为公共接口添加清晰的文档注释
// 实现文件示例 (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 练习题目
基础练习
- 结构体基础:定义一个包含日期(年、月、日)的结构体,编写函数计算两个日期之间的天数差
- 枚举应用:使用枚举定义不同的用户权限级别(访客、普通用户、管理员、超级管理员),编写权限检查函数
- 文件写入:创建一个程序,将10个随机数写入文本文件,每个数字占一行
- 文件读取:编写程序读取上题创建的文件,计算所有数字的平均值
- 联合体使用:使用联合体设计一个可以存储整数、浮点数或字符串的数据结构
进阶练习
- 结构体数组排序:定义学生结构体数组,按成绩从高到低排序
- 二进制文件操作:将产品信息(名称、价格、库存)保存到二进制文件,然后读取并显示
- 文件搜索:在文本文件中搜索包含特定关键词的行,显示行号和内容
- 预处理器应用:使用条件编译实现跨平台的文件路径处理(Windows使用反斜杠,Linux使用正斜杠)
- 头文件设计:设计一个数学工具库的头文件,包含常用的数学函数和常量
综合项目
- 通讯录管理系统:使用结构体管理联系人信息,支持添加、删除、修改、查询,数据保存到文件
- 学生成绩管理系统:完整的学生信息管理,包括成绩录入、统计、排名,支持文本和二进制文件格式
- 库存管理系统:商品信息的增删改查,库存预警,销售记录,数据持久化存储
- 配置文件解析器:实现INI格式配置文件的读取和写入,支持节(section)和键值对
- 日志系统:实现分级别的日志记录(调试、信息、警告、错误),支持文件轮转和格式化输出
4.12 总结
本周我们深入学习了C/C++中结构体与文件操作的核心概念:
结构体相关:
- 结构体的定义语法和内存布局
- 结构体数组和指针的使用方法
- 结构体作为函数参数的不同传递方式
- 联合体和枚举类型的特性和应用场景
文件操作相关:
- 文件的基本概念和操作流程
- 文本文件与二进制文件的区别和应用
- 文件的打开、读写、定位和关闭操作
- 文件错误处理的重要性和实现方法
预处理器相关:
- 常用预处理器指令的使用
- 条件编译的应用场景
- 宏定义的高级技巧
- 头文件设计的最佳实践
这些知识为构建复杂的数据处理应用程序奠定了坚实基础。结构体提供了组织复杂数据的机制,文件操作实现了数据的持久化存储,预处理器增强了代码的灵活性和可维护性。在后续的学习中,我们将运用这些知识开发更加复杂和实用的应用程序。
更多推荐


所有评论(0)