🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

结构体基础 — 创建自己的数据类型

C语言学习教程

结构体基础 — 创建自己的数据类型

从一个真实场景说起

假设你是班主任,需要管理全班 40 个学生的信息。每个学生有:姓名、学号、年龄、语文成绩、数学成绩、英语成绩。

如果没有结构体,你只能这样写:

char  name1[20], name2[20], name3[20];  // 40 个名字……
int   id1, id2, id3;                     // 40 个学号……
int   age1, age2, age3;                  // 40 个年龄……
float chinese1, chinese2, chinese3;       // 40 个语文成绩……
float math1, math2, math3;                // 40 个数学成绩……
float english1, english2, english3;        // 40 个英语成绩……
// 共 6 × 40 = 240 个变量!写到崩溃!

更糟的是,当你想把”一个学生的全部信息”传给函数时,你得传 6 个参数——顺序还不能错。这根本没法维护。

用结构体,世界就清爽了:

struct Student {
    char  name[20];
    int   id;
    int   age;
    float chinese;
    float math;
    float english;
};

struct Student class[40];  // 一个数组,搞定全班!

结构体的本质是:把描述同一个事物的一组相关数据打包成一个整体。就像现实世界中,一个学生的档案表(姓名、学号、年龄、成绩)是一张纸,不会被拆成六张纸条。

定义结构体类型

struct 结构体名 {
    类型1 成员1;
    类型2 成员2;
    // ...
};  // 注意:这里必须有分号!

完整示例:

struct Student {
    char name[20];    // 姓名:字符数组
    int  id;          // 学号:整数
    int  age;         // 年龄:整数
    float chinese;    // 语文成绩:浮点数
    float math;       // 数学成绩:浮点数
    float english;    // 英语成绩:浮点数
};

这行代码并不创建任何变量,只是告诉编译器:“以后有一种新类型叫 struct Student,它长这样。“

声明结构体变量

定义完类型后,声明变量就像使用 intfloat 一样:

struct Student s1;           // 声明一个变量
struct Student s2, s3;       // 同时声明多个
struct Student class[40];    // 声明一个数组

也可以在定义类型的同时声明变量:

struct Point {
    int x;
    int y;
} p1, p2;  // 同时声明 p1 和 p2

访问成员:. 运算符

用点号 . 来访问结构体的”内部小格子”:

#include <stdio.h>
#include <string.h>

struct Student {
    char name[20];
    int  id;
    int  age;
    float chinese;
    float math;
    float english;
};

int main() {
    struct Student s1;

    // 字符串不能直接赋值,用 strcpy
    strcpy(s1.name, "张三");
    s1.id      = 2024001;
    s1.age     = 18;
    s1.chinese = 92.5;
    s1.math    = 88.0;
    s1.english = 95.5;

    // 计算平均分
    float avg = (s1.chinese + s1.math + s1.english) / 3.0;

    printf("姓名:%s\n", s1.name);
    printf("学号:%d\n", s1.id);
    printf("年龄:%d\n", s1.age);
    printf("语文:%.1f  数学:%.1f  英语:%.1f\n",
           s1.chinese, s1.math, s1.english);
    printf("平均分:%.1f\n", avg);

    return 0;
}

输出:

姓名:张三
学号:2024001
年龄:18
语文:92.5  数学:88.0  英语:95.5
平均分:92.0

. 的本质:给每个小格子起个名字

可以这样理解:s1.name 就是 s1 这个大格子里,贴了 “name” 标签的那个小格子。赋值 s1.age = 18 就是往那个小格子里放数字 18。

结构体的三种初始化方式

方式一:按定义顺序初始化

struct Student s1 = {"张三", 2024001, 18, 92.5, 88.0, 95.5};

花括号里的值顺序必须和结构体定义时成员出现的顺序一致。如果成员很多,容易写错位置。

方式二:指定成员名初始化(C99,推荐)

struct Student s1 = {
    .name    = "张三",
    .id      = 2024001,
    .chinese = 92.5,
    .math    = 88.0,
    .english = 95.5,
    .age     = 18          // 顺序随便,想先写哪个写哪个
};

不指定的成员会被自动初始化为 0(或 0.0'\0')。所以:

struct Student s2 = {.name = "李四"};  // 其他成员全为 0

方式三:声明后逐个赋值

struct Student s3;
strcpy(s3.name, "王五");
s3.id      = 2024002;
s3.age     = 19;
s3.chinese = 78.0;
s3.math    = 85.5;
s3.english = 80.0;

typedef — 告别重复的 struct

每次声明变量都要写 struct Student,两个单词很啰嗦。typedef 给类型起个别名:

// 原来的写法
struct Student { ... };
struct Student s1;  // 每次都要写 struct

// 用 typedef
typedef struct {
    char name[20];
    int  id;
    int  age;
} Student;

Student s1, s2;  // 简洁!和 int、float 一样自然

typedef 的两种惯用形式

// 形式 1:匿名结构体 + typedef(最常用)
typedef struct {
    int x;
    int y;
} Point;

// 形式 2:先定义结构体,再 typedef(需要自引用时用)
typedef struct Node {
    int data;
    struct Node *next;  // 这里还不能用 Node,因为 typedef 还没完成
} Node;

初学者建议:先用形式 1,等学到链表时自然会遇到形式 2。

结构体数组 — 管理整个班级

有了 typedef,创建学生数组就很自然了:

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[20];
    int  id;
    int  age;
    float chinese;
    float math;
    float english;
} Student;

int main() {
    // 全班 40 人,但示例用 3 人
    Student class[3] = {
        {"张三", 2024001, 18, 92.5, 88.0, 95.5},
        {"李四", 2024002, 19, 78.0, 85.5, 80.0},
        {"王五", 2024003, 18, 65.0, 72.0, 68.5},
    };

    // 遍历:打印每个人信息
    printf("===== 全班学生信息 =====\n");
    for (int i = 0; i < 3; i++) {
        float avg = (class[i].chinese + class[i].math + class[i].english) / 3.0;
        printf("%d. %s (学号%d, %d岁)  平均分:%.1f\n",
               i + 1,
               class[i].name,
               class[i].id,
               class[i].age,
               avg);
    }

    return 0;
}

输出:

===== 全班学生信息 =====
1. 张三 (学号2024001, 18岁)  平均分:92.0
2. 李四 (学号2024002, 19岁)  平均分:81.2
3. 王五 (学号2024003, 18岁)  平均分:68.5

思考:class[i].name 怎么读?

从左往右:class 是数组 → class[i] 是第 i 个元素(一个 Student 结构体)→ class[i].name 是这个结构体的 name 成员。C 语言的点号就是一层层往里剥。

嵌套结构体 — 一个结构体里装着另一个

生日是一个”日期”——它本身有年、月、日三个小部分。可以先定义一个 Date 结构体,再把它作为 Student 的成员:

#include <stdio.h>

// 先定义日期类型
typedef struct {
    int year;
    int month;
    int day;
} Date;

// 再把 Date 作为 Student 的成员
typedef struct {
    char    name[20];
    int     id;
    Date    birthday;     // 嵌套!一个 Date 结构体
    float   chinese;
    float   math;
    float   english;
} Student;

int main() {
    Student s1 = {
        .name     = "张三",
        .id       = 2024001,
        .birthday = {2005, 6, 15},  // 花括号嵌套
        .chinese  = 92.5,
        .math     = 88.0,
        .english  = 95.5,
    };

    // 两层点号访问
    printf("姓名:%s\n", s1.name);
    printf("生日:%d%d%d\n",
           s1.birthday.year,    // s1 的 birthday 成员的 year 子成员
           s1.birthday.month,   // s1 的 birthday 成员的 month 子成员
           s1.birthday.day);    // s1 的 birthday 成员的 day 子成员

    return 0;
}

嵌套结构体就像是”表格里套表格”——学生档案表里有一个日期栏,日期栏本身又有三个小格子。

结构体与函数

按值传递:复制一份副本

#include <stdio.h>
#include <string.h>

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

// 参数是结构体本身(不是指针)
void grow_up(Student s) {
    s.age += 1;                      // 修改的是副本!
    printf("函数内部:%s %d\n", s.name, s.age);
}

int main() {
    Student s1 = {"张三", 18};
    grow_up(s1);                     // 张三 19岁
    printf("函数外部:%s %d\n", s1.name, s1.age);
    // 输出:张三 18岁   ← 根本没变!
    return 0;
}

grow_up 接收到的是 s1 的一个完整复制品。修改复制品不影响原件——就像复印了一份表格,你在复印件上涂改,原件纹丝不动。

缺点:如果结构体很大(几百字节),复制整个结构体开销大、速度慢。

按地址传递:指针指向原件

void grow_up_pointer(Student *s) {
    s->age += 1;                     // 通过指针修改原件!
    printf("函数内部:%s %d\n", s->name, s->age);
}

int main() {
    Student s1 = {"张三", 18};
    grow_up_pointer(&s1);            // 传地址
    printf("函数外部:%s %d\n", s1.name, s1.age);
    // 输出:张三 19岁   ← 真的变了!
    return 0;
}

关于 -> 运算符:(下一课会展开讲)

写法含义使用场景
s.age直接访问成员s 是结构体变量
(*s).age先解引用再访问s 是指针(麻烦)
s->age等价于 (*s).ages 是指针(推荐)

sizeof — 结构体在内存中占多大?

#include <stdio.h>

typedef struct {
    char name[20];   // 20 字节
    int  id;         // 4 字节
    int  age;        // 4 字节
    float chinese;   // 4 字节
    float math;      // 4 字节
    float english;   // 4 字节
} Student;

int main() {
    printf("sizeof(Student) = %zu 字节\n", sizeof(Student));
    return 0;
}

你可能会想:20 + 4 + 4 + 4 + 4 + 4 = 40 字节,对吗?

实际输出可能是 40,也可能是 44 甚至 48——原因在于编译器的”内存对齐”。

什么是内存对齐?

CPU 读取内存时,不是逐字节读,而是每次读一块(比如 4 字节一块)。如果一个 4 字节的 int 横跨两块,CPU 就得读两次才能拼出一个完整的 int——效率降低。

因此,编译器会在结构体成员之间插入一些”空白填充字节”(padding),让每个成员的起始地址都是它大小(或 4 字节)的整数倍。

内存布局示意(假设 int 和 float 都是 4 字节对齐):
┌──────────────────────┐
│ name[20]   20 字节   │ 偏移 0  ~ 19
├──────────────────────┤
│ (padding)  0 字节    │ 偏移 20 ~ 23  (刚好对齐到 4 的倍数)
├──────────────────────┤
│ id          4 字节   │ 偏移 24 ~ 27
├──────────────────────┤
│ age         4 字节   │ 偏移 28 ~ 31
├──────────────────────┤
│ chinese     4 字节   │ 偏移 32 ~ 35
├──────────────────────┤
│ math        4 字节   │ 偏移 36 ~ 39
├──────────────────────┤
│ english     4 字节   │ 偏移 40 ~ 43
└──────────────────────┘
总计:44 字节(含 4 字节填充)

初学者不用纠结对齐细节,知道三件事就够了:

  1. sizeof(结构体) 可能比你手工算的大一点,这是正常的。
  2. sizeof 而不是手算,就不会出错。
  3. 把大成员放在前面可以在某些平台上减少填充(后面学数据结构时会讲到)。

实战练习:简易学生成绩管理

综合以上知识,写一个完整可运行的程序:

#include <stdio.h>
#include <string.h>

#define MAX_STUDENTS 100

// 日期类型
typedef struct {
    int year;
    int month;
    int day;
} Date;

// 学生类型:嵌套了 Date
typedef struct {
    char  name[20];
    int   id;
    Date  birthday;
    float chinese;
    float math;
    float english;
} Student;

// ===== 工具函数 =====

// 计算三科平均分
float get_average(const Student *s) {
    return (s->chinese + s->math + s->english) / 3.0;
}

// 打印单个学生
void print_student(const Student *s) {
    float avg = get_average(s);
    printf("  %s | 学号:%d | 生日:%d/%d/%d | 语文:%.1f 数学:%.1f 英语:%.1f | 平均:%.1f\n",
           s->name,
           s->id,
           s->birthday.year, s->birthday.month, s->birthday.day,
           s->chinese, s->math, s->english,
           avg);
}

// 打印全班
void print_all(const Student arr[], int count) {
    printf("\n========== 全班学生信息 (%d人) ==========\n", count);
    for (int i = 0; i < count; i++) {
        printf("[%d]", i + 1);
        print_student(&arr[i]);
    }
}

// 查找最高分的学生
int find_best(const Student arr[], int count) {
    int best_index = 0;
    float best_avg = get_average(&arr[0]);
    for (int i = 1; i < count; i++) {
        float avg = get_average(&arr[i]);
        if (avg > best_avg) {
            best_avg = avg;
            best_index = i;
        }
    }
    return best_index;
}

// 计算全班总平均
float class_average(const Student arr[], int count) {
    float total = 0.0;
    for (int i = 0; i < count; i++) {
        total += get_average(&arr[i]);
    }
    return total / count;
}

int main() {
    Student students[MAX_STUDENTS];
    int count = 0;

    // 添加几个学生(模拟从键盘输入)
    students[count++] = (Student){
        .name = "张三", .id = 2024001,
        .birthday = {2005, 6, 15},
        .chinese = 92.5, .math = 88.0, .english = 95.5
    };
    students[count++] = (Student){
        .name = "李四", .id = 2024002,
        .birthday = {2005, 3, 22},
        .chinese = 78.0, .math = 85.5, .english = 80.0
    };
    students[count++] = (Student){
        .name = "王五", .id = 2024003,
        .birthday = {2006, 11, 8},
        .chinese = 65.0, .math = 72.0, .english = 68.5
    };
    students[count++] = (Student){
        .name = "赵六", .id = 2024004,
        .birthday = {2005, 9, 30},
        .chinese = 88.0, .math = 96.0, .english = 91.0
    };

    // 打印全班
    print_all(students, count);

    // 统计信息
    int best = find_best(students, count);
    printf("\n===== 统计 =====\n");
    printf("全班总平均分:%.1f\n", class_average(students, count));
    printf("最高分同学:%s (平均 %.1f)\n",
           students[best].name, get_average(&students[best]));

    return 0;
}

输出:

========== 全班学生信息 (4人) ==========
[1]  张三 | 学号:2024001 | 生日:2005/6/15 | 语文:92.0 数学:88.0 英语:95.5 | 平均:92.0
[2]  李四 | 学号:2024002 | 生日:2005/3/22 | 语文:78.0 数学:85.5 英语:80.0 | 平均:81.2
[3]  王五 | 学号:2024003 | 生日:2006/11/8 | 语文:65.0 数学:72.0 英语:68.5 | 平均:68.5
[4]  赵六 | 学号:2024004 | 生日:2005/9/30 | 语文:88.0 数学:96.0 英语:91.0 | 平均:91.7

===== 统计 =====
全班总平均分:83.3
最高分同学:张三 (平均 92.0)

常见错误

1. 忘了分号

struct Student {
    char name[20];
    int age;
}   // ← 编译错误!必须加分号!

2. 对字符数组直接赋值

struct Student s;
s.name = "张三";  // 编译错误!数组不能这样赋值
// 必须用 strcpy
strcpy(s.name, "张三");

3. 初始化顺序搞错

struct Student { char name[20]; int age; float score; };
struct Student s = {18, "张三", 92.5};  // 逻辑错误!age 收到了字符串

要点速查

概念说明
struct定义复合数据类型的关键字
.通过变量访问成员:s.name
->通过指针访问成员:p->name 等价于 (*p).name
typedef给类型起别名,避免反复写 struct
结构体数组Student class[40],每个元素是一个结构体
嵌套结构体一个结构体的成员是另一个结构体
sizeof(结构体)获取结构体实际占用的字节数(含对齐填充)
传值 vs 传指针传值安全但慢(复制整个结构体);传指针快且能修改原件

结构体是 C 语言从”简单数据”迈向”组织化数据”的关键一步。掌握了结构体,你就能用程序模拟现实世界中的各种”表格”和”档案”。下一课,我们将深入结构体与指针的配合——这是链表、树等动态数据结构的基础。