1.7. 结构体与联合体

C 语言允许将不同类型的数据组合成复合类型。

1.7.1. 结构体(struct)

结构体将多个不同类型的变量组合为一个整体。

1.7.1.1. 定义与声明

// 结构体定义
struct Student {
    int    id;           // 学号
    char   name[50];     // 姓名
    int    age;          // 年龄
    float  score;        // 成绩
};                      // 注意分号!

// 声明结构体变量
struct Student stu1;
struct Student stu2 = {1001, "Alice", 20, 95.5f};

// 定义+声明合并
struct Point {
    int x;
    int y;
} p1, p2;

// 匿名结构体(仅用于声明变量,不可复用)
struct {
    int x;
    int y;
} point;

1.7.1.2. 访问成员

struct Student stu = {1001, "Bob", 20, 88.5f};

// 使用 . 运算符访问
printf("ID: %d\n", stu.id);
printf("Name: %s\n", stu.name);
printf("Age: %d\n", stu.age);
printf("Score: %.1f\n", stu.score);

// 修改成员
stu.score = 92.0f;

// 整体赋值
stu = (struct Student){1002, "Charlie", 21, 76.0f};

1.7.1.3. 结构体指针

struct Student stu = {1001, "David", 22, 90.0f};
struct Student *p = &stu;

// 使用 -> 运算符访问(等价于 (*p).xxx)
printf("Name: %s\n", p->name);        // 推荐写法
printf("Age: %d\n", (*p).age);        // 等价写法

// 结构体指针作为函数参数(传指针效率高)
void print_student(const struct Student *s) {
    printf("ID=%d, Name=%s, Score=%.1f\n",
           s->id, s->name, s->score);
}

小技巧

结构体较大时,传指针比传值更高效 (避免复制整个结构体)。 用 const 保护不修改的指针参数。

1.7.1.4. 结构体嵌套

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

struct Employee {
    int id;
    char name[50];
    struct Date hire_date;      // 嵌套结构体
    struct Date birth_date;
};

struct Employee emp = {
    2001, "Eve",
    {2020, 3, 15},              // 嵌套初始化
    {1990, 7, 22}
};

// 访问嵌套成员
printf("入职年份: %d\n", emp.hire_date.year);

1.7.2. 结构体大小与对齐

#include <stddef.h>   // offsetof

struct Sample1 {
    char   a;    // 1 byte
    int    b;    // 4 bytes
    char   c;    // 1 byte
};

/* 实际内存布局 (默认 4 字节对齐):
 * a (1) + padding(3) + b (4) + c (1) + padding(3) = 12 字节
 */
printf("sizeof(Sample1) = %zu\n", sizeof(struct Sample1));

/* 重新排列成员以减少填充 */
struct Sample2 {
    int    b;    // 4 bytes
    char   a;    // 1 byte
    char   c;    // 1 byte
    // padding(2)
};
/* 大小: b(4) + a(1) + c(1) + padding(2) = 8 字节 */

VC10 中编译指示:

#pragma pack(push, 1)   // 1 字节对齐 (取消对齐优化)
struct Packed {
    char a;
    int  b;              // 可能未对齐, 访问性能降低
};
#pragma pack(pop)

重要

  • 默认对齐到成员最大类型的倍数

  • 重新排列成员(从大到小)可以减少填充

  • #pragma pack 可控制对齐,但可能导致性能下降或运行错误

1.7.3. typedef 类型别名

// 为结构体定义别名
typedef struct {
    int x;
    int y;
} Point;                // 现在可以直接用 Point 声明

Point p1 = {10, 20};
Point p2;               // 不用写 struct Point

// 为函数指针定义别名
typedef int (*CompareFn)(const void *, const void *);

// 使用
CompareFn cmp = &my_compare;

小技巧

使用 typedef 可以简化复杂类型声明,提高代码可读性。 但不要过度使用,避免隐藏了重要类型信息。

1.7.4. 联合体(union)

联合体的所有成员**共享同一块内存**,大小等于最大成员的大小。

union Data {
    int    i;
    float  f;
    char   str[20];
};

union Data data;

data.i = 42;
printf("i = %d\n", data.i);     // 42

data.f = 3.14f;
printf("f = %f\n", data.f);     // 3.14(覆盖了 i 的值)
printf("i = %d\n", data.i);     // 不确定(已被覆盖)

printf("sizeof(union Data) = %zu\n", sizeof(union Data));
// 20(最大成员 char[20] 的大小)

1.7.4.1. 联合体的典型用途

类型标记联合体(Tagged Union)
#include <stdio.h>

typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STR } DataType;

typedef struct {
    DataType type;          // 标记当前存储的类型
    union {
        int    i;
        float  f;
        char   *s;
    } value;
} Variant;

void print_variant(Variant v) {
    switch (v.type) {
        case TYPE_INT:
            printf("Integer: %d\n", v.value.i);
            break;
        case TYPE_FLOAT:
            printf("Float: %f\n", v.value.f);
            break;
        case TYPE_STR:
            printf("String: %s\n", v.value.s);
            break;
    }
}

int main(void) {
    Variant v1 = {TYPE_INT,   .value.i = 42};
    Variant v2 = {TYPE_FLOAT, .value.f = 3.14f};

    print_variant(v1);
    print_variant(v2);

    return 0;
}

备注

联合体常用于: - 节省内存(不同时使用的字段共享空间) - 解析网络协议或二进制文件格式 - 类型擦除(类似 C++ 的 std::variant)

1.7.5. 位域(Bit Field)

允许以比特位为单位定义结构体成员,常用于嵌入式硬件编程。

// 设备寄存器定义
struct DeviceReg {
    unsigned int enabled  : 1;    // 1 bit
    unsigned int mode     : 2;    // 2 bits
    unsigned int status   : 3;    // 3 bits
    unsigned int reserved : 2;    // 2 bits
};

struct DeviceReg reg = {0};
reg.enabled = 1;
reg.mode    = 2;

printf("sizeof = %zu\n", sizeof(reg));   // 4(占用 1 个 int)

小心

位域的行为因编译器而异(位序、对齐规则、跨越边界等), 可移植性较差,VC10 与其他编译器可能存在差异。