八:结构体——把不同类型的数据打包在一起

一、回顾与本篇目标

上一篇你学会了 C 语言中的函数——如何定义函数、传值调用的本质、数组作为函数参数、递归函数。

到目前为止,我们学的数据类型都是单一的:一个 int 存一个整数,一个 char 存一个字符,一个数组存相同类型的多个数据。但现实世界中的数据往往是复合的:一个学生有姓名(字符串)、年龄(整数)、成绩(浮点数);一本书有书名(字符串)、作者(字符串)、价格(浮点数)。这些数据由不同类型的字段组成,但它们属于同一个实体

C 语言提供了结构体来解决这个问题。结构体让你把多个不同类型的数据打包成一个整体。如果你学过 JavaScript 或 Python,可以把结构体理解为 C 语言中“对象”的原始形态——它是 C++ 类、Java 类的前身。

本篇的目标:

  1. 理解结构体的概念——把不同类型的数据组合成一个新类型
  2. 学会定义结构体、声明结构体变量、初始化结构体
  3. 掌握访问结构体成员的两种方式:.->
  4. 学会结构体作为函数参数的用法
  5. 理解 typedef 简化类型名

二、为什么需要结构体

假设你要在程序中管理学生信息。没有结构体的话,你只能把每个字段存成独立的变量或数组:

// 没有结构体:用多个数组存储学生信息(字段之间没有关联)
char names[3][20] = {"张三", "李四", "王五"};
int ages[3] = {20, 21, 19};
float scores[3] = {85.5, 92.0, 78.5};

// 打印第一个学生的信息——需要从三个数组分别取数据
printf("姓名:%s,年龄:%d,成绩:%.1f\n", names[0], ages[0], scores[0]);

这种方式有几个问题:

  • 字段之间没有关联names[0]ages[0] 的关联完全靠你手动保证索引一致。如果不小心搞错了索引,张三的名字可能对上了李四的年龄。
  • 代码难以维护:如果要增加一个字段(比如“班级”),需要新建一个数组,修改所有相关代码。
  • 传递不方便:要把一个学生的所有信息传给函数,需要传三个参数。

用结构体改写:

// 用结构体:把学生的所有信息打包在一起
struct Student {
    char name[20];
    int age;
    float score;
};

struct Student s1 = {"张三", 20, 85.5};
struct Student s2 = {"李四", 21, 92.0};

// 打印学生信息——通过一个变量访问所有字段
printf("姓名:%s,年龄:%d,成绩:%.1f\n", s1.name, s1.age, s1.score);

一个 s1 变量就包含了学生的所有信息,字段之间有了明确的关联。这就是结构体的核心价值:把相关数据组织在一起,形成一个逻辑整体。

三、定义结构体

结构体定义的语法:

struct 结构体名 {
    类型 成员名1;
    类型 成员名2;
    // ... 可以有任意多个成员
};

注意花括号末尾的分号不能省略。这是 C 语言初学者常见的错误——忘记写分号。

定义一个学生结构体

struct Student {
    char name[50];    // 姓名
    int age;          // 年龄
    float score;      // 成绩
    char gender;      // 性别('M' 或 'F')
};

这行代码只是定义了一个新的数据类型,叫 struct Student。它还没有占用任何内存——就像你画了一张设计图纸,但还没盖房子。

声明结构体变量(盖房子)

// 方式一:先定义结构体,再声明变量
struct Student s1;

// 方式二:定义结构体的同时声明变量
struct Student {
    char name[50];
    int age;
    float score;
} s1, s2;  // 同时声明两个变量

// 方式三:匿名结构体(只能在这一个地方使用)
struct {
    char name[50];
    int age;
} temp;  // 只能用一次,因为没有结构体名

四、初始化结构体

和数组类似,结构体可以在声明时用 {} 初始化:

// 方式一:按顺序初始化
struct Student s1 = {"张三", 20, 92.5, 'M'};

// 方式二:指定成员名初始化(C99 支持,推荐这种写法)
struct Student s2 = {
    .name = "李四",
    .age = 21,
    .score = 85.0,
    .gender = 'F'
};

// 方式三:部分初始化(未指定的成员自动设为 0 或空)
struct Student s3 = {"王五"};  // age=0, score=0.0, gender='\0'

// 方式四:先声明,后赋值
struct Student s4;
s4.age = 19;
s4.score = 78.5;
s4.gender = 'M';
// s4.name = "赵六";  // 错误!不能用 = 给字符数组赋值
strcpy(s4.name, "赵六");  // 正确:用 strcpy 复制字符串

重要:字符数组(char name[50]不能用 = 直接赋值(除了在初始化时)。已经声明的结构体变量,修改字符串成员需要用 strcpy

五、访问结构体成员

访问结构体成员用点运算符 .

struct Student s = {"张三", 20, 92.5, 'M'};

printf("姓名:%s\n", s.name);
printf("年龄:%d\n", s.age);
printf("成绩:%.1f\n", s.score);

// 修改成员
s.age = 21;
s.score = 95.0;

如果你之前学过 JavaScript 或 Python,这个 . 语法应该非常熟悉:

// JavaScript
let student = { name: "张三", age: 20 };
console.log(student.name);  // 用点号访问

# Python
class Student:
    pass
s = Student()
s.name = "张三"   # 用点号访问

C 语言结构体的点运算符和它们语法相同,但底层机制完全不同——C 语言中结构体的成员在内存中是连续存放的,点运算符在编译时就被翻译成了“基地址 + 偏移量”的内存访问。

六、结构体在内存中的布局

结构体的成员在内存中按照声明的顺序依次存放。每个成员相对于结构体起始地址的偏移量由编译时计算:

struct Student {
    char name[50];   // 偏移 0,占 50 字节
    int age;         // 偏移 54(需要对齐到 4 的倍数),占 4 字节
    float score;     // 偏移 60,占 4 字节
    char gender;     // 偏移 64,占 1 字节
};
// 结构体总大小:68 字节(实际可能因为对齐而更大)

你可以用 sizeof 查看结构体的实际大小:

printf("struct Student 大小:%zu 字节\n", sizeof(struct Student));

实际大小可能比你手动算的大,因为编译器会对成员进行内存对齐——让每个成员的地址是其大小的整数倍,以提高 CPU 读取效率。这和 JavaScript/Python 中对象的存储方式完全不同(JS 对象用哈希表,Python 对象用字典,都不是连续内存)。

七、结构体数组

结构体本身是一个类型,自然可以创建结构体数组:

// 创建包含 3 个学生的数组
struct Student students[3] = {
    {"张三", 20, 85.5, 'M'},
    {"李四", 21, 92.0, 'F'},
    {"王五", 19, 78.5, 'M'}
};

// 遍历学生数组
for (int i = 0; i < 3; i++) {
    printf("学生 %d:%s,年龄 %d,成绩 %.1f\n",
           i + 1, students[i].name, students[i].age, students[i].score);
}

结构体数组在内存中是连续的——每个结构体紧挨着下一个。这和数组的内存布局原理一致。

八、结构体指针

你可以创建指向结构体的指针,通过指针访问成员:

struct Student s = {"张三", 20, 85.5, 'M'};
struct Student *p = &s;  // p 是指向 s 的指针

// 通过指针访问成员:用 -> 运算符(箭头运算符)
printf("姓名:%s\n", p->name);    // 等价于 (*p).name
printf("年龄:%d\n", p->age);
printf("成绩:%.1f\n", p->score);

-> 运算符是 C 语言中专为结构体指针设计的。p->name 等价于 (*p).name——先解引用拿到结构体本身,再用点号访问成员。因为这种操作实在太常见了,C 语言提供了 -> 作为简洁写法。

记忆规则

  • 结构体变量用 . 访问成员。
  • 结构体指针用 -> 访问成员。

九、结构体作为函数参数

结构体作为函数参数时,同样遵循传值规则:

传结构体本身(传值——复制整个结构体)

// 打印学生信息(只读,用传值没问题)
void print_student(struct Student s) {
    printf("姓名:%s,年龄:%d,成绩:%.1f\n", s.name, s.age, s.score);
    // 修改 s 不会影响外部的原始结构体
}

int main() {
    struct Student s1 = {"张三", 20, 85.5, 'M'};
    print_student(s1);
    return 0;
}

传结构体本身有一个性能问题:如果结构体很大(比如有几十个字段),复制整个结构体会消耗大量时间和栈空间。

传结构体指针(推荐——只传地址,高效)

// 传递指针:只传 8 字节(地址),不复制整个结构体
void print_student(const struct Student *p) {
    printf("姓名:%s,年龄:%d,成绩:%.1f\n", p->name, p->age, p->score);
}

// 修改结构体的函数:必须传指针
void update_score(struct Student *p, float new_score) {
    p->score = new_score;  // 通过指针修改原始结构体
}

int main() {
    struct Student s1 = {"张三", 20, 85.5, 'M'};
    print_student(&s1);
    update_score(&s1, 95.0);
    print_student(&s1);
    return 0;
}

最佳实践

  • 如果函数只读取结构体,传指向 const 结构体的指针——const struct Student *p
  • 如果函数需要修改结构体,传结构体指针——struct Student *p
  • 几乎不要传结构体本身,除非结构体非常小(比如只有一两个 int 成员)。

十、typedef:给类型起别名

每次写 struct Student 有点啰嗦。可以用 typedef 给它起个简短的名字:

// 方式一:先定义结构体,再用 typedef 起别名
struct Student {
    char name[50];
    int age;
    float score;
};
typedef struct Student Student;  // 现在 Student 等价于 struct Student

// 方式二:定义结构体的同时起别名(更常用)
typedef struct {
    char name[50];
    int age;
    float score;
} Student;  // 现在直接用 Student,不需要写 struct

// 使用
Student s1 = {"张三", 20, 85.5};
Student s2;

有了 typedefStudent 就像一个普通的类型名(如 intfloat),不需要再加 struct 关键字。这是 C 语言中非常常见的写法。

十一、综合演示:学生管理系统

下面这段代码综合运用了结构体定义、结构体数组、结构体指针、typedef、以及函数封装:

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

#define MAX_STUDENTS 100

// 定义学生结构体
typedef struct {
    char name[50];
    int age;
    float score;
} Student;

// 函数原型
int input_students(Student students[], int max_count);
void print_student(const Student *s);
void print_all_students(const Student students[], int count);
float calculate_average(const Student students[], int count);
int find_highest(const Student students[], int count);

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

    count = input_students(students, MAX_STUDENTS);

    if (count == 0) {
        printf("没有输入任何学生信息。\n");
        return 0;
    }

    printf("\n===== 学生信息列表 =====\n");
    print_all_students(students, count);

    float avg = calculate_average(students, count);
    printf("\n平均成绩:%.1f\n", avg);

    int highest_idx = find_highest(students, count);
    printf("成绩最高的学生:");
    print_student(&students[highest_idx]);

    return 0;
}

// 输入学生信息
int input_students(Student students[], int max_count) {
    int count;
    printf("请输入学生人数(最多 %d):", max_count);
    scanf("%d", &count);

    if (count <= 0 || count > max_count) {
        return 0;
    }

    // 清理输入缓冲区中的换行符
    getchar();

    for (int i = 0; i < count; i++) {
        printf("\n--- 学生 %d ---\n", i + 1);

        printf("姓名:");
        fgets(students[i].name, sizeof(students[i].name), stdin);
        // 去掉 fgets 读取的换行符
        students[i].name[strcspn(students[i].name, "\n")] = '\0';

        printf("年龄:");
        scanf("%d", &students[i].age);

        printf("成绩:");
        scanf("%f", &students[i].score);

        // 再次清理输入缓冲区
        getchar();
    }

    return count;
}

// 打印单个学生信息
void print_student(const Student *s) {
    printf("%s,年龄 %d,成绩 %.1f\n", s->name, s->age, s->score);
}

// 打印所有学生信息
void print_all_students(const Student students[], int count) {
    for (int i = 0; i < count; i++) {
        printf("%d. ", i + 1);
        print_student(&students[i]);
    }
}

// 计算平均成绩
float calculate_average(const Student students[], int count) {
    float total = 0.0;
    for (int i = 0; i < count; i++) {
        total += students[i].score;
    }
    return total / count;
}

// 查找成绩最高的学生(返回索引)
int find_highest(const Student students[], int count) {
    int highest = 0;
    for (int i = 1; i < count; i++) {
        if (students[i].score > students[highest].score) {
            highest = i;
        }
    }
    return highest;
}

代码设计要点:

  • typedef struct { ... } Student;:定义结构体的同时起别名,后面直接用 Student 作为类型名。
  • const Student *s:函数只读取学生信息,用 const 指针明确表示“不会修改”。
  • getchar():清理输入缓冲区。在 scanf 读取数字后,缓冲区里会残留一个换行符。如果接下来用 fgets 读字符串,这个残留的换行符会被当作输入吃掉。用 getchar() 手动把这个换行符读掉。
  • sizeof(students[i].name):获取 name 数组的大小,传给 fgets 防止越界。

十二、本篇动手练习

练习 1:图书管理结构体

新建 practice8-1.c,定义一个 Book 结构体,包含书名、作者、价格、页数。创建 3 本书的数组,输出所有书的信息,找出最贵的书。

练习 2:点坐标计算

新建 practice8-2.c,定义一个 Point 结构体(包含 x 和 y 坐标)。写一个函数计算两点之间的距离(公式:√((x1-x2)² + (y1-y2)²)),写另一个函数判断三点是否共线。

练习 3:结构体嵌套

新建 practice8-3.c,定义一个 Date 结构体(年、月、日),然后定义一个 Employee 结构体,包含姓名、入职日期(Date 类型)、工资。创建数组并输出工龄最长的员工。

练习 4:链表节点

新建 practice8-4.c,定义一个 Node 结构体,包含一个整数数据和一个指向自身类型的指针struct Node *next)。创建三个节点并手动串联成链表,遍历输出。这是数据结构中链表的前置练习。

十三、本篇小结

这一篇你学会了 C 语言中最重要的复合数据类型——结构体:

  • 结构体的定义struct 结构体名 { 成员列表 };。定义只是创建了一个新类型,不占内存。
  • 结构体变量的声明和初始化struct Student s = {"张三", 20, 85.5};。可以用指定成员名的方式初始化(.name = "张三")。
  • 访问成员:结构体变量用 .,结构体指针用 ->p->name 等价于 (*p).name
  • 结构体作为函数参数:推荐传结构体指针,避免复制整个结构体。只读时加 const 修饰。
  • typedef:给类型起别名,简化结构体类型名的书写。
  • 内存布局:结构体成员在内存中连续存放,但编译器会进行内存对齐以提升访问效率。

结构体是 C 语言面向对象编程的基石。下一篇,我们学习 C 语言中另一个重要概念——动态内存分配。你会学到 mallocfree,理解堆和栈的区别,学会在程序运行时动态地创建和销毁数据。

下一篇预告

下一篇——《动态内存分配——在运行时申请和释放内存》:栈和堆的区别、malloc 申请内存、free 释放内存、callocrealloc、内存泄漏的危险和防范。这是 C 语言中最容易出错的领域,也是区分 C 和高级语言(JS/Python 自动管理内存)的关键差异。

C/C++ 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容