为什么需要结构体:把相关数据绑在一起

写稍微大一点的程序时,你很快会遇到这种场景:一个“学生”有姓名、学号、三门课成绩。如果各自用一个独立数组来存,name[i]、id[i]、score[i] 全靠“下标 i 是同一个人”这种口头约定维持,一旦漏对齐就全错。结构体(struct)的本质,就是把逻辑上属于同一个实体的若干字段打包成一个新的自定义类型。

有了结构体,“一个学生”就是一个完整的值,可以整体传参、整体放进数组、整体写进文件。它不增加任何运行时魔法——编译器只是替你算出每个字段在这块连续内存里的偏移量。

/* 先约定一个新类型:一名学生长什么样 */
struct Student {
    char  name[24];   /* 姓名 */
    int   id;         /* 学号 */
    float score[3];   /* 三门课成绩 */
};

/* 于是数组里每一项都是“完整的一个学生” */
struct Student class[40];
struct 不改变内存布局的朴素性:字段按声明顺序相邻摆放,中间可能插入少量填充字节以满足对齐。这也是 sizeof 常常比你把字段肉眼相加要大的原因。
本文核心知识点
  • struct把相关字段打包成一个自定义复合类型,字段相邻存储并可能对齐填充。
  • ->结构体指针访问成员用箭头,普通结构体用点号。
  • union多个成员共用同一块内存,同一时刻只应理解其中一个。
  • 模块化头文件放声明、.c 放定义,用 include 守卫防止重复包含。
C 结构体与模块化配图:蓝调渐变背景上多个圆角立方体方块像记录条目一样层叠嵌套连接,发光箭头串联,底部排列整齐的位宽小方格阵列与刻度导轨
结构体就像一条记录:把姓名、学号、成绩等原本散着的字段,收拢进同一个可整体搬运的盒子里。

定义、声明与初始化

定义结构体类型时,可以在花括号后直接列出变量名;也可以用 typedef 给它起个短名字,省得每次都写 struct。初始化用花括号按顺序给值,C99 之后还能按字段名指定初始化,不必记顺序——这在字段多、又只填其中几个时特别好用。

#include <stdio.h>

typedef struct {
    char  *name;
    int   id;
    float score[3];
} Student;                 /* 以后直接写 Student,不用再写 struct */

int main(void) {
    /* 按顺序初始化 */
    Student a = {"张伟", 2024001, {88.5, 91.0, 79.0}};

    /* 按字段名初始化,未列出的字段自动清零 */
    Student b = {.id = 2024002, .name = "李娜"};

    printf("%s %d\n", a.name, a.id);   /* 张伟 2024001 */
    printf("%s %d\n", b.name, b.id);   /* 李娜 2024002 */
    return 0;
}

两条经验:一是未初始化的局部结构体字段是垃圾值,用 = {0} 或指定初始化把它整体清零最省心;二是结构体可以直接相互赋值,a = b; 会按字节复制所有字段,对纯数据结构既方便又安全。

结构体指针与 -> 运算符

结构体作为函数参数时,如果整个传值,会发生一次完整拷贝——字段一多,开销不小。更常见的做法是传指针,函数只拿到地址,既能改原对象,又很快。访问指针指向的成员,用的是箭头 -> 而不是点号。

#include <stdio.h>

typedef struct {
    char *name;
    int  id;
    float total;
} Student;

/* 传指针:函数内部能直接修改原结构体 */
void add_bonus(Student *s, float bonus) {
    s->total += bonus;          /* (*s).total 的简写 */
}

int main(void) {
    Student z = {"张伟", 2024001, 258.5f};
    add_bonus(&z, 5.0f);
    printf("%.1f\n", z.total);  /* 263.5 */
    return 0;
}

记住这个对照:s.name 是“结构体本身”取成员,s->name 是“指向结构体的指针”取成员。s->x 完全等价于 (*s).x,箭头只是让代码少写一层括号。当你发现自己在写 (*p).field,就该换成 p->field 了。

位域:把标志位压进一个字节

有些字段只占一两个二进制位,比如开关状态、权限标志。如果每个都用一个 int,内存浪费明显。位域(bit-field)允许你声明“这个成员占几位”,把多个布尔标志打包进同一个字节或字里。

typedef struct {
    unsigned int read    : 1;   /* 1 位:可读 */
    unsigned int write   : 1;   /* 1 位:可写 */
    unsigned int execute : 1;   /* 1 位:可执行 */
    unsigned int level   : 4;   /* 4 位:权限等级 0~15 */
} Perm;

int main(void) {
    Perm p = {.read = 1, .write = 0, .execute = 1, .level = 7};
    /* p 实际只占几个位,而不是四个 int */
    return 0;
}

要清醒地认识到:位域虽然写法清爽,但位在内存里的具体排布顺序由编译器决定,跨平台、跨编译器未必一致。因此位域适合“省内存的内部标志”,不适合用来对齐硬件寄存器或网络协议字节序——后者应当用位运算(& | ~ <<)手动、显式地控制每一位。

联合体 union:一块内存多种解读

联合体(union)和结构体长得几乎一样,但它的所有成员共用同一块起始地址的内存,联合体的大小等于其最大成员的大小。它适合“同一数据,在不同时刻按不同类型解释”的场景。

对比项struct 结构体union 联合体
内存关系各成员各占一块,顺序排列所有成员共用同一块内存
大小各成员大小之和(含填充)等于最大成员的大小
同时有效性所有字段同时有效同一时刻只应读最近写入的那个成员
典型用途把相关数据打包节省内存、做“带类型标签”的变体
typedef struct {
    int  type;        /* 标签:1=整数, 2=浮点 */
    union {
        int   i;
        float f;
    } data;
} Value;

Value v = {.type = 1, .data.i = 42};
/* 读哪个成员,必须先看 type 标签:v.type==1 才读 v.data.i */

这种“标签 + 联合体”的组合,是 C 语言里实现类似“可变类型”的经典手法,也是现代 C++ std::variant 的思想源头。关键纪律是:永远先看标签,再决定读哪个成员,否则就是把同一段内存按错误类型解读,得到莫名其妙的值。

枚举 enum 与 typedef:给类型起名字

当一个变量只应该取有限几个离散值时(比如状态机的状态、菜单的选项),用裸数字 0、1、2 会让代码满是“魔数”。枚举(enum)给这些常量起可读的名字,可读性立刻上来。

typedef enum {
    STATE_IDLE,      /* 0 */
    STATE_RUNNING,   /* 1 */
    STATE_PAUSED,    /* 2 */
    STATE_STOPPED    /* 3 */
} State;

void handle(State s) {
    if (s == STATE_RUNNING) {
        /* ... */
    }
}

int main(void) {
    State cur = STATE_RUNNING;
    handle(cur);
    return 0;
}

枚举本质上就是一组整型常量,默认从 0 依次递增,也可以显式赋值。配合 typedef,自定义类型用起来才顺手:typedef struct {...} Student; 之后,Student 就能当普通类型名用。把 typedef 想成“给已有类型起别名”,它不创造新类型,只是让名字更好写、语义更清楚。