08_C++结构体详解

0 阅读9分钟

08 C++结构体详解

8.1 为什么要学结构体

在前面的学习中,我们已经接触了 intdoublechar、数组等数据类型。它们都很重要,但有一个共同限制:

  • 基本类型一次只能表达一种“单一信息”;
  • 数组虽然能存多个值,但要求元素类型一致;
  • 真实项目中的对象往往由多种属性共同组成。

例如,一个学生通常至少有姓名、年龄、学号、成绩;一本书会有书名、作者、价格、库存;一个游戏角色可能有名称、等级、血量、攻击力、坐标等信息。这些属性的类型并不一样。如果仅靠基本类型和普通数组来组织,代码会很快变得混乱,变量数量爆炸,维护困难。

结构体(struct)的价值就在于:

  1. 把“同一对象”的多个属性打包成一个整体;
  2. 让代码语义更清晰,变量名更少但信息更完整;
  3. 能与数组、指针、函数组合,形成工程化代码风格。

你可以把结构体理解为“程序员自定义的数据模板”。定义模板后,就能批量创建同类型对象。


8.2 结构体的基本定义与使用

8.2.1 语法结构

struct 结构体名 {
    成员类型 成员名;
    成员类型 成员名;
    ...
};

注意最后的分号不能漏。

8.2.2 示例:定义学生结构体

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    int score;
};

int main() {
    Student s1;
    s1.name = "张三";
    s1.age = 18;
    s1.score = 92;

    cout << "姓名: " << s1.name << endl;
    cout << "年龄: " << s1.age << endl;
    cout << "成绩: " << s1.score << endl;

    return 0;
}

8.2.3 初始化方式

结构体变量常见初始化方式:

  1. 先定义再逐个赋值;
  2. 定义时使用花括号一次性初始化。
Student a;
a.name = "李四";
a.age = 19;
a.score = 85;

Student b = {"王五", 20, 88};

8.3 结构体成员访问与内存理解

结构体变量使用点号 . 访问成员:

cout << s1.name << endl;
cout << s1.score << endl;

从内存角度看,结构体是“一段连续内存中的复合数据”。每个成员占据一部分空间,编译器可能会因为对齐规则在成员之间插入空隙。这意味着:

  • sizeof(结构体类型) 往往不等于所有成员 sizeof 之和;
  • 对齐是为了提高访问效率;
  • 不要主观假设结构体大小,使用 sizeof 才准确。

示例:

#include <iostream>
using namespace std;

struct A {
    char c;
    int x;
};

int main() {
    cout << "sizeof(A) = " << sizeof(A) << endl;
    return 0;
}

在不同编译器或平台上,结果可能不同。


8.4 结构体数组:管理一组同类对象

当我们有很多学生对象时,用结构体数组比散乱变量更高效。

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    int score;
};

int main() {
    Student cls[3] = {
        {"张三", 18, 90},
        {"李四", 19, 85},
        {"王五", 18, 95}
    };

    for (int i = 0; i < 3; i++) {
        cout << "第" << i + 1 << "位: "
             << cls[i].name << " "
             << cls[i].age << "岁 "
             << "成绩=" << cls[i].score << endl;
    }

    return 0;
}

8.4.1 应用:找最高分

int bestIndex = 0;
for (int i = 1; i < 3; i++) {
    if (cls[i].score > cls[bestIndex].score) {
        bestIndex = i;
    }
}
cout << "最高分: " << cls[bestIndex].name
     << " " << cls[bestIndex].score << endl;

这个场景非常常见:查找最大值、排序、统计平均分、筛选及格学生等。


8.5 结构体指针:用地址操作对象

8.5.1 基本写法

Student stu = {"小明", 18, 91};
Student* p = &stu;

访问成员使用 ->

cout << p->name << endl;
p->score = 100;

完整示例:

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    int score;
};

int main() {
    Student stu = {"小明", 18, 91};
    Student* p = &stu;

    cout << "修改前: " << p->name << " " << p->score << endl;
    p->score = 96;
    cout << "修改后: " << p->name << " " << p->score << endl;

    return 0;
}

8.5.2 什么时候优先使用指针

  • 需要在函数中修改原对象;
  • 避免值传递拷贝带来的开销;
  • 需要动态分配对象(后续面向对象和堆内存会用到)。

8.6 结构体作为函数参数

结构体传参有两大方式:

  1. 值传递:会复制一份,函数里修改不影响实参;
  2. 地址传递:传入地址,函数里可直接改实参。

8.6.1 值传递示例

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    int score;
};

void increaseByValue(Student s) {
    s.score += 10;
    cout << "函数内(值传递)成绩: " << s.score << endl;
}

int main() {
    Student stu = {"赵六", 18, 70};
    increaseByValue(stu);
    cout << "主函数成绩: " << stu.score << endl;
    return 0;
}

运行后你会看到:函数内分数变了,主函数原值没变。

8.6.2 地址传递示例

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    int score;
};

void increaseByAddress(Student* s) {
    s->score += 10;
}

int main() {
    Student stu = {"赵六", 18, 70};
    increaseByAddress(&stu);
    cout << "主函数成绩: " << stu.score << endl;
    return 0;
}

8.6.3 更推荐的只读传参

如果函数只读取数据,不需要修改对象,建议使用:

void printStudent(const Student& s)

这是“常量引用传参”,可避免拷贝,且防止误改数据,性能和安全性更平衡。


8.7 结构体嵌套结构体

现实对象常有层级关系,比如“老师带学生”,“订单里有收货地址”,“公司里有部门”。结构体嵌套能很好表达这种关系。

示例:老师结构体里包含学生结构体。

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    int score;
};

struct Teacher {
    int id;
    string name;
    Student apprentice;
};

int main() {
    Teacher t;
    t.id = 1001;
    t.name = "陈老师";
    t.apprentice.name = "小王";
    t.apprentice.age = 18;
    t.apprentice.score = 89;

    cout << "老师: " << t.name << "(" << t.id << ")" << endl;
    cout << "学生: " << t.apprentice.name
         << " 年龄=" << t.apprentice.age
         << " 分数=" << t.apprentice.score << endl;

    return 0;
}

8.8 const 与结构体:防误改的关键

const 在结构体章节非常实用,主要用于函数参数和指针约束。

8.8.1 const 指针参数

void show(const Student* s) {
    cout << s->name << " " << s->score << endl;
    // s->score = 100; // 编译错误,避免误改
}

8.8.2 const 引用参数

void show(const Student& s) {
    cout << s.name << " " << s.score << endl;
}

在工程中,只读函数优先 const 引用 是很常见的规范。


8.9 综合案例一:老师与学生分配系统

8.9.1 需求描述

  • 有 3 位老师;
  • 每位老师带 5 名学生;
  • 需要自动生成姓名并分配随机成绩;
  • 最后打印完整信息。

8.9.2 代码实现

#include <iostream>
#include <string>
#include <ctime>
using namespace std;

struct Student {
    string name;
    int score;
};

struct Teacher {
    string name;
    Student students[5];
};

void allocateData(Teacher teachers[], int len) {
    string teacherPrefix = "教师";
    string studentPrefix = "学生";
    string seed = "ABCDE";

    for (int i = 0; i < len; i++) {
        teachers[i].name = teacherPrefix + seed[i];
        for (int j = 0; j < 5; j++) {
            teachers[i].students[j].name = studentPrefix + seed[j];
            teachers[i].students[j].score = rand() % 61 + 40; // 40~100
        }
    }
}

void printData(const Teacher teachers[], int len) {
    for (int i = 0; i < len; i++) {
        cout << "====================" << endl;
        cout << teachers[i].name << endl;
        for (int j = 0; j < 5; j++) {
            cout << "  " << teachers[i].students[j].name
                 << " 成绩=" << teachers[i].students[j].score << endl;
        }
    }
}

int main() {
    srand((unsigned int)time(nullptr));

    Teacher teachers[3];
    int len = sizeof(teachers) / sizeof(teachers[0]);

    allocateData(teachers, len);
    printData(teachers, len);

    return 0;
}

8.9.3 案例讲解

这个案例很适合训练以下能力:

  • 结构体嵌套结构体;
  • 结构体数组的批量处理;
  • 函数分层:数据生成和数据展示分离;
  • const 只读参数习惯。

在真实项目中,分层思想比“把所有代码塞到 main”重要得多。


8.10 综合案例二:英雄年龄排序(结构体 + 冒泡)

8.10.1 需求描述

创建 5 个英雄信息:姓名、年龄、性别。按年龄升序排序并输出。

8.10.2 代码实现

#include <iostream>
#include <string>
using namespace std;

struct Hero {
    string name;
    int age;
    string gender;
};

void bubbleSort(Hero arr[], int len) {
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j].age > arr[j + 1].age) {
                Hero temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

void printHeros(const Hero arr[], int len) {
    for (int i = 0; i < len; i++) {
        cout << arr[i].name << " "
             << arr[i].gender << " "
             << arr[i].age << endl;
    }
}

int main() {
    Hero arr[5] = {
        {"刘备", 23, "男"},
        {"关羽", 22, "男"},
        {"张飞", 20, "男"},
        {"赵云", 21, "男"},
        {"貂蝉", 19, "女"}
    };

    int len = sizeof(arr) / sizeof(arr[0]);

    bubbleSort(arr, len);
    printHeros(arr, len);

    return 0;
}

8.10.3 思路重点

  • 比较条件是 arr[j].age > arr[j + 1].age
  • 交换的单位是整个 Hero 对象;
  • 打印函数使用 const 参数,表达“只读”。

8.11 结构体常见错误与规避

8.11.1 忘记末尾分号

struct Student {
    string name;
    int age;
} // 少了分号

8.11.2 误用 .->

  • 普通对象用 .
  • 指针对象用 ->

8.11.3 值传递导致“改不动原对象”

函数参数是结构体值时,改的是副本。若要修改原对象请传地址或引用。

8.11.4 忽略初始化

结构体成员未初始化可能产生不确定值。建议定义时直接初始化,或在创建后立即赋值。

8.11.5 在头文件中滥用 using namespace std;

小项目问题不大,但工程化开发建议避免在头文件使用,以防命名污染。


8.12 提升建议:从“会用”到“用好”

  1. 先建模再写代码:先想清楚“这个对象有哪些属性”。
  2. 函数职责单一:创建、修改、打印尽量拆分。
  3. 优先 const 引用参数:读操作更安全,也更高效。
  4. 养成命名规范:结构体名用 PascalCase,成员名风格保持一致。
  5. 合理组织文件:结构体声明写到头文件,函数实现放源文件。

8.13 练习题(含思路提示)

练习 1:图书管理(基础)

定义 Book 结构体,成员包含 titleauthorpricestock。创建 3 本书,打印价格最高的书。

提示:

  • 用结构体数组存储图书;
  • 遍历比较 price
  • 保存最大值下标。

练习 2:通讯录(进阶)

定义 Contact 结构体,成员含 namephoneemail。实现函数:

  • addContact 添加联系人;
  • findByName 按姓名查找;
  • printAll 打印全部。

提示:

  • 先用定长数组实现;
  • 再尝试扩展成 vector<Contact>(学完 STL 后再做)。

练习 3:班级成绩分析(综合)

定义 Student 结构体并建立数组,完成:

  • 计算班级平均分;
  • 找出最高分和最低分;
  • 按成绩降序排序并输出。

提示:

  • 平均分注意类型转换,避免整除误差;
  • 排序时交换整个结构体对象。

8.14 小结

这一章的核心不是“记语法”,而是建立数据建模思维。结构体让你能够把零散变量组织成有意义的对象,并且与数组、指针、函数协同工作,形成清晰可维护的程序结构。你应重点掌握以下能力:

  • 能定义并初始化结构体;
  • 能使用结构体数组管理批量对象;
  • 能通过指针与函数参数灵活读写对象;
  • 能借助 const 写出更安全的接口;
  • 能通过综合案例把“结构体 + 流程控制 + 排序算法”串起来。

如果你把本章示例全部独立敲一遍,并在此基础上做小改造(比如新增字段、改变排序条件、增加过滤功能),你会明显感受到自己对 C++ 程序组织能力的提升。这种提升不仅服务于基础语法,更会直接影响后续类、对象、STL、项目实践的学习效率。

继续前进时,请记住一句话:

好程序不是只会跑,而是数据结构清晰、接口边界明确、代码容易维护。结构体正是这条路上的第一块基石。