21. 【C语言】打包不同类型:结构体

前面二十篇文章,我们和数据类型打了很多交道——intdoublechar、数组、指针……但它们都有一个共同点:一个变量只能存一种类型的数据。

可真实世界里,一个“东西”往往由很多属性拼成。比如一个学生,有姓名(字符串)、学号(整数)、成绩(浮点数);一个坐标点,有 x 和 y 两个坐标值。如果各存各的,数据散落一地,很难管理。

C 语言给了我们一个“打包”工具:结构体(struct)。它能把多个不同类型的数据捆在一起,变成一个自定义的复合类型。今天我们就来学会怎么定义它、初始化它、访问它的成员,以及如何用指针优雅地操作它。


一、结构体的定义

定义结构体的基本语法:

struct 结构体名 {
    类型 成员1;
    类型 成员2;
    // ... 更多成员
};  // 分号!分号!分号!

例如,定义一个“学生”结构体:

struct Student {
    char name[50];
    int id;
    float score;
};

这就创造了一个新的类型叫 struct Student。注意:struct 关键字和 Student 是一体的,不能只用 Student(除非配合 typedef,后面会讲)。


二、声明结构体变量

有了 struct Student 这个类型,就可以像用 intdouble 一样声明变量:

struct Student s1;                 // 未初始化
struct Student s2 = {"Alice", 1001, 92.5};  // 声明并初始化

也可以在定义结构体时顺便声明变量:

struct Point {
    int x;
    int y;
} p1, p2;   // 同时声明两个全局变量

甚至可以不写结构体名(匿名结构体),但这种只能在那一次声明变量,后续无法再使用该类型,不常用:

struct {
    int day;
    int month;
    int year;
} birthday;

推荐做法:老老实实写好结构体定义,然后在需要的地方声明变量。


三、初始化结构体

1. 完全初始化

按成员顺序列出所有初始值:

struct Student s = {"Bob", 1002, 88.0};

2. 部分初始化

只写前几个值,剩余的成员会被自动初始化为 0(或空字符):

struct Student s = {"Charlie"};  // id=0, score=0.0

这和数组的部分初始化规则一致。

3. C99 指定初始化器(Designated Initializer)

可以点名初始化某些成员,不按顺序也可以:

struct Student s = {.score = 95.5, .name = "Diana", .id = 1003};

这种写法清晰、不怕记错顺序,推荐使用。


四、访问结构体成员:点运算符 .

变量名.成员名 读写:

#include <stdio.h>

struct Student {
    char name[50];
    int id;
    float score;
};

int main(void) {
    struct Student s = {"Eve", 1004, 89.5};

    printf("姓名: %s\n", s.name);
    printf("学号: %d\n", s.id);
    printf("成绩: %.1f\n", s.score);

    s.score = 92.0;   // 修改成员
    printf("新成绩: %.1f\n", s.score);

    return 0;
}

输出:

姓名: Eve
学号: 1004
成绩: 89.5
新成绩: 92.0

五、结构体数组

结构体本身是一种类型,自然可以拿来组成数组。比如一个班的学生:

#include <stdio.h>

struct Student {
    char name[50];
    int id;
    float score;
};

int main(void) {
    struct Student class[3] = {
        {"Alice", 1001, 92.5},
        {"Bob",   1002, 85.0},
        {"Carol", 1003, 78.5}
    };

    for (int i = 0; i < 3; i++) {
        printf("%s (%d): %.1f\n", class[i].name, class[i].id, class[i].score);
    }

    return 0;
}

class[i] 是数组的第 i 个元素,它是一个 struct Student,再用 . 访问其成员。

结构体数组结合循环和排序算法,就能做出学生成绩管理系统的基础功能。后面的实战篇会不断用到它。


六、结构体指针与箭头运算符 ->

每个结构体变量在内存中都有地址,可以声明指向它的指针:

struct Student s = {"Frank", 1005, 76.0};
struct Student *p = &s;   // p 指向 s

要通过指针访问成员,有两种等价写法:

方式一:(*p).成员 —— 先解引用,再用点

printf("%s\n", (*p).name);

括号必须加,因为 . 优先级高于 **p.name 会被解释成 *(p.name),那是错的。

方式二:p->成员 —— 箭头运算符(推荐)

printf("%s\n", p->name);   // 简洁明了

p->name 就是 (*p).name 的语法糖。几乎所有人都用箭头,所以请记住它。

示例:

#include <stdio.h>

struct Student {
    char name[50];
    int id;
    float score;
};

int main(void) {
    struct Student s = {"Grace", 1006, 88.0};
    struct Student *p = &s;

    printf("姓名: %s\n", p->name);
    printf("学号: %d\n", p->id);
    printf("成绩: %.1f\n", p->score);

    p->score = 91.5;   // 用指针修改成员
    printf("新成绩: %.1f\n", s.score);   // s.score 也变了

    return 0;
}

七、结构体作为函数参数

按值传递:整个结构体被复制

void print_student(struct Student s) {
    printf("%s %d %.1f\n", s.name, s.id, s.score);
}

调用时,print_student(s1) 会把整个 s1 复制一份给形参 s。如果结构体很小(比如几个 int),这没什么;但若结构体很大(比如包含一个大数组),复制开销就大了。而且,函数内修改 s 不会影响原始结构体。

按指针传递:只复制一个地址(推荐)

void print_student(const struct Student *s) {
    printf("%s %d %.1f\n", s->name, s->id, s->score);
}

调用时传 &s1,只复制 4 或 8 字节的指针。const 修饰符表示函数不会修改结构体内容,让调用者放心。

同理,如果要让函数修改结构体的成员,传指针也很自然:

void raise_score(struct Student *s, float delta) {
    s->score += delta;
}

结论:传递结构体时,优先传指针,并用 const 标明只读意图。


八、常见错误与陷阱

1. 结构体定义忘记末尾分号

struct Point {
    int x;
    int y;
}   // 错误!缺少分号

这个错误会导致后面连续的代码报一堆莫名其妙的错。看到奇怪错误时,先检查前一个结构体定义是否少了 ;

2. 用 == 比较两个结构体

struct Student a = {"Tom", 1, 80};
struct Student b = {"Tom", 1, 80};
if (a == b) { ... }  // 错误!不能直接用 == 比较结构体

C 语言不允许对结构体直接使用 ==。需要自己写函数逐个成员比较,或使用 memcmp(但要小心内存对齐产生的填充字节,后面会讲)。

3. 结构体指针未初始化就使用

struct Student *p;
p->id = 100;   // 危险!p 没有指向有效内存

指针必须指向已存在的结构体变量,或通过 malloc 分配空间。

4. 返回局部结构体的指针

struct Point *get_point(void) {
    struct Point p = {0, 0};
    return &p;   // 危险!p 在函数返回后消失
}

和普通变量一样,局部结构体在栈上,函数返回后失效。如果要返回结构体,可以直接返回值(结构体类型,会复制),或返回动态分配的结构体指针。

5. 混淆 .->

  • 用变量本身访问成员:.s.name
  • 用指针访问成员:->p->name

初学时常会写出 p.name(p 是指针,应该用 ->)或 s->name(s 不是指针,应该用 .)。编译器通常会给出清晰的错误提示,仔细读。


九、小结

今天你学会了把各种数据打包成一个“复合类型”——结构体:

  • struct 关键字定义,最后别忘了分号。
  • 声明变量、初始化(包括指定初始化器)。
  • . 访问成员,用 -> 通过指针访问成员。
  • 结构体可以组成数组,解决了批量管理复杂数据的问题。
  • 传参时优先传指针,既高效又安全。

结构体是 C 语言面向对象思想的雏形。后面你写的链表节点、树节点、哈希表条目,全都靠它来定义。下一篇,我们会深入结构体在内存中的真实布局——内存对齐到底是什么?为什么结构体的大小往往比成员加起来更大?以及那个诡异又实用的 柔性数组 是怎么工作的。这些底层细节,会让你对内存的理解再上一个台阶。


课后小练习

  1. 定义一个 struct Point(包含 int xint y),写一个函数 double distance(const struct Point *a, const struct Point *b),计算两点之间的欧几里得距离并返回。在 main 中测试。
  2. 用前面定义的 struct Student 创建一个包含 5 个学生的数组,从键盘输入他们的信息,然后找出成绩最高和最低的学生并打印其信息。
  3. 用结构体指针和动态内存分配,写一个程序:先让用户输入学生数量,然后用 malloc 分配一个 struct Student 的动态数组,输入数据、打印、最后 free
  4. (陷阱修复)下面的代码有错误,请找出并修正:
    struct Book {
        char title[100];
        int pages;
    }
    int main(void) {
        struct Book b;
        b.title = "C Programming";
        b.pages = 500;
        printf("%s\n", b.title);
        return 0;
    }
    

我们下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值