🔷 第八章 结构体与共用体
📄 一句话概括

声明结构体数组的格式是 Student 数组名[长度];——跟声明 int a[5] 一模一样,只是把 int 换成了 Student。初始化和 7.2.3 的二维数组同类:每条记录一对花括号,外层再套一对。引用时使用两段式:cls[i].成员。声明完不初始化 = 一整排垃圾值(和 7.1.1 的老朋友一致)。

📝 本节和前面几节的关系

8.4 讲了"结构体数组是什么、好在哪",这一节落地写法。你会发现它几乎是三样老知识的叠加:数组的声明与遍历(7.1)、二维数组的嵌套初始化(7.2.3)、结构体的成员访问(8.1.2)。唯一的新东西是"把两段式引用 cls[i].成员 写顺"——这也就是本节全部的难点。

一、声明:把 int 换成结构体名

Student cls[3];             // 3 个学生
Student grp[30];            // 30 个学生
const int N = 50;
Student all[N];             // 长度必须是编译期常量(7.1.1 的第一条规矩)

三种说法对照:

声明 含义
int a[3]; 3 个整数
char s[3][20]; 3 个字符串(二维数组)
Student cls[3]; 3 个学生(每个 32 字节)

长度是常量这条规矩从 7.1.1 延续下来:int n = 3; Student a[n]; 在标准 C++ 里不允许(g++ 的扩展会放过它,别依赖)。

二、初始化:每条记录一对花括号

Student cls[3] = {
    {"张三", 18, 92.5},         // 第 0 格:结构体初始化(8.1.3)
    {"李四", 19, 85.0},         // 第 1 格
    {"王五", 20, 78.5}          // 第 2 格
};

这个写法和 7.2.3 的二维数组初始化是一个套路——"每行一对花括号,外层再套一对",只不过这里的每一行不再是"几个数字",而是"姓名 + 年龄 + 成绩"。

四种常见写法

// ① 全部填满(上面那种)

// ② 每条记录只填一部分:其余成员自动补 0
Student a[2] = {
    {"张三"},                    // age=0、score=0
    {"李四", 19}                 // score=0
};

// ③ 一路平铺(不写内层花括号)—— 合法,但极易看错位置
Student b[2] = {"张三", 18, 92.5, "李四", 19, 85.0};   // ❌ 不推荐!位置全靠数
// ⚠️ 这种写法在多数编译器上会报"初始化元素太多"或产生意外结果,别写

// ④ 一条都不填,全部清零
Student c[3] = {};              // 3 条记录的所有成员都是 0

建议:永远写内层花括号(写法 ①②④)。平铺写法(③)省了几个字符,却让你必须"心算第 4 个值落在哪个成员上"——这是毫无意义的负担(7.2.3 也强调过分行写的价值)。

声明之后再填

Student cls[3];                     // 先声明(此刻全是垃圾值)

strcpy(cls[0].name, "张三");        // 字符数组成员:strcpy
cls[0].age   = 18;
cls[0].score = 92.5;
// ... 依次填 1、2

注意:声明完不能整体写 cls = {{...}};——初始化列表只能在声明那一句用(和 7.1.3、8.1.3 一致)。

三、引用:两段式 cls[i].成员

cls[0].name         // 第 0 个学生的姓名
cls[1].age          // 第 1 个学生的年龄
cls[i].score        // 循环里最常用

读法:从左往右——先"从排里挑第 i 格",再"进这一格取某个成员"。

记忆口诀:"下标选行、成员选列"。顺序绝不能反:cls.name[i] 是错的(cls 是数组,没有 .name 这种成员)。

cls[i] 本身也是一个结构体变量,所以它可以再传给别人:

Student s = cls[1];             // 整条记录复制出来(8.1.2 的特权)
cout << s.name << endl;
printStudent(cls[1]);           // 整条记录传给函数(8.3.1)

四、遍历:for 是标准做法

for (int i = 0; i < 3; i++) {
    cout << cls[i].name << "\t"
         << cls[i].age  << "\t"
         << cls[i].score << endl;
}

写循环边界的两条建议:

int n = sizeof(cls) / sizeof(cls[0]);       // 3 —— 不写死数字(7.1.1)
for (int i = 0; i < n; i++) { ... }         // 用 i < n,不要 i <= n

i < n 还是 i <= n?永远是 <——因为下标从 0 开始,最后一个合法下标是 n-1(7.1.2 的越界坑,越界不报错)。

五、四种最常用的操作

这四段代码几乎覆盖了所有"表格类"作业,值得背下来:

// ① 求总分 / 平均分
double sum = 0;
for (int i = 0; i < n; i++) sum += cls[i].score;
cout << "平均 " << sum / n << endl;

// ② 找最高分(记下标,不止记分数——因为之后还要用姓名)
int best = 0;
for (int i = 1; i < n; i++)
    if (cls[i].score > cls[best].score) best = i;
cout << "第一名:" << cls[best].name << " " << cls[best].score << endl;

// ③ 统计及格人数(4.x 的条件判断 + 5.x 的计数器)
int pass = 0;
for (int i = 0; i < n; i++) if (cls[i].score >= 60) pass++;
cout << "及格 " << pass << " 人" << endl;

// ④ 按成绩排序(交换整条记录,不是交换某个成员!)
for (int i = 0; i < n - 1; i++)
    for (int j = 0; j < n - 1 - i; j++)
        if (cls[j].score < cls[j+1].score) {        // 从高到低
            Student t   = cls[j];                   // 整条记录交换
            cls[j]      = cls[j+1];
            cls[j+1]    = t;
        }

②说"记下标而不是记分数":因为最后你想输出的是"谁考了最高分"。只记分数的话,还得再遍历一次找它属于谁——记下标一次到位。

六、动手 Demo:一张完整的成绩表

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

struct Student {
    char   name[20];
    int    age;
    double score;
};

int main() {
    Student cls[5] = {
        {"张三", 18, 92.5},
        {"李四", 19, 85.0},
        {"王五", 20, 78.5},
        {"赵六", 18, 58.0},
        {"孙七", 19, 66.5}
    };
    int n = sizeof(cls) / sizeof(cls[0]);

    // ① 打印整张表
    cout << "=== 成绩表 ===" << endl;
    for (int i = 0; i < n; i++)
        cout << i << "  " << cls[i].name << "\t" << cls[i].age
             << "\t" << cls[i].score << endl;

    // ② 平均分
    double sum = 0;
    for (int i = 0; i < n; i++) sum += cls[i].score;
    cout << "平均分 = " << sum / n << endl;

    // ③ 最高分(记下标)
    int best = 0;
    for (int i = 1; i < n; i++)
        if (cls[i].score > cls[best].score) best = i;
    cout << "最高分:" << cls[best].name << " " << cls[best].score << endl;

    // ④ 及格人数
    int pass = 0;
    for (int i = 0; i < n; i++) if (cls[i].score >= 60) pass++;
    cout << "及格 " << pass << "/" << n << " 人" << endl;

    // ⑤ 按成绩从高到低排序(整体交换记录)
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (cls[j].score < cls[j+1].score) {
                Student t = cls[j]; cls[j] = cls[j+1]; cls[j+1] = t;
            }
    cout << "=== 排名 ===" << endl;
    for (int i = 0; i < n; i++)
        cout << (i + 1) << ". " << cls[i].name << " " << cls[i].score << endl;

    cout << "整排 " << sizeof(cls) << " 字节,每格 " << sizeof(Student) << " 字节" << endl;
    return 0;
}

看到什么算成功:

=== 成绩表 ===
0  张三   18  92.5
1  李四   19  85
2  王五   20  78.5
3  赵六   18  58
4  孙七   19  66.5
平均分 = 76.1
最高分:张三 92.5
及格 4/5 人
=== 排名 ===
1. 张三 92.5
2. 李四 85
3. 王五 78.5
4. 孙七 66.5
5. 赵六 58
整排 160 字节,每格 32 字节

两个看点: - 排序交换的是整条记录(Student t = cls[j];)——不是只换分数。只换分数会导致"姓名和分数对不上",这是最常见的 bug; - 160 = 5 × 32,结构体数组的内存是"元素个数 × 单格大小"。

30 秒实验:把排序里的 if (cls[j].score < cls[j+1].score) 改成 >,名次立刻倒过来(从低到高)。一个符号,就是"从高到低"和"从低到高"的分水岭。

七、常见误区

  1. "结构体数组可以像普通数组那样整体赋值"——声明之后不行。初始化列表只能出现在声明那一句;之后的"搬运"只能逐格来(cls[i] = cls[j]; 是可以的,因为格与格是同类型结构体)。
  2. "排序时只交换分数就行"——绝对不行。姓名、年龄、成绩是一条记录,必须整条一起换,否则数据全乱。
  3. "写 i <= n 保险一点"——正好踩进越界。<= 会访问 cls[n],越界不报错,会悄悄改坏别的内存(7.1.2)。
  4. "结构体数组很占内存,尽量少用"——一个 Student 32 字节,1000 个学生 32KB,其实很小。真要注意的是 7.4 那种 char content[256] 的大成员。
  5. "cls[i] 是地址"——不是。cls[i] 是一个结构体变量(左值,能读能写)。要地址得写 &cls[i](8.4.2 要用)。
自测三问(点开对答案) 1. 声明"10 个学生"的数组并访问第 3 个的姓名,怎么写?——`Student cls[10];` → `cls[2].name`。 2. 结构体数组初始化为什么强烈建议"每条记录写内层花括号"?——不写就要靠数位置对齐成员,极易装错;写了编译器会帮你检查每条记录的字段数。 3. 按成绩排序时,为什么必须交换整条记录?——因为姓名、年龄、成绩是**同一条记录**,只换分数会让"名字和分数错配"。

相关笔记