12. 结构体.md 7.1 KB

12 结构体

12.1 完整概念讲解

结构体是 C 语言中将多个不同类型变量组合在一起的复合数据类型。结构体允许你创建自定义的数据结构,是面向对象编程中"类"概念的前身。

核心特性:

  • 结构体成员在内存中按声明顺序排列
  • 每个成员占用独立的内存空间
  • 结构体变量可以整体赋值(浅拷贝)
  • 结构体的大小受内存对齐规则影响

12.2 核心API/语法

语法 说明
struct Tag { ... }; 结构体类型声明
struct Tag var; 声明结构体变量
struct Tag var = { .x = 1 }; C99 指定初始化器
var.member . 访问成员
ptr->member -> 通过指针访问成员
sizeof(struct Tag) 结构体大小(含对齐填充)
offsetof(type, member) 成员偏移量
container_of(ptr, type, member) 由成员指针反推结构体指针

12.3 代码示例(完整可编译,附gcc命令)

示例1:结构体定义、初始化与访问

// 12_struct_basics.c
#include <stdio.h>
#include <stddef.h>

struct Person {
    char name[32];
    int age;
    double salary;
};

int main(void)
{
    // C89 风格初始化
    struct Person p1 = {"Alice", 30, 7500.50};

    // C99 指定初始化器(可乱序、可省略)
    struct Person p2 = {
        .salary = 8200.00,
        .name   = "Bob",
        .age    = 25
    };

    // . 和 -> 操作符
    printf("p1: %s, %d, %.2f\n", p1.name, p1.age, p1.salary);

    struct Person *pp = &p2;
    printf("p2: %s, %d, %.2f\n", pp->name, pp->age, pp->salary);

    // 整体赋值(浅拷贝)
    struct Person p3 = p1;
    printf("p3: %s, %d, %.2f\n", p3.name, p3.age, p3.salary);

    return 0;
}

编译运行:

gcc -Wall -Wextra -std=c99 -o 12_struct_basics 12_struct_basics.c
./12_struct_basics

示例2:内存对齐与 sizeof 计算

// 12_struct_align.c
#include <stdio.h>
#include <stddef.h>

struct Aligned {
    char a;     // 1 byte + 3 padding
    int b;      // 4 bytes
    char c;     // 1 byte + 3 padding
};

struct Packed {
    char a;
    char c;
    int b;
};

#pragma pack(push, 1)
struct PackedAttribute {
    char a;     // 1 byte
    int b;      // 4 bytes, no padding
    char c;     // 1 byte
};
#pragma pack(pop)

struct Nested {
    int id;
    struct {
        double x;
        double y;
    } point;
    char tag;
};

int main(void)
{
    printf("sizeof(struct Aligned)        = %zu\n", sizeof(struct Aligned));
    printf("  offset of a = %zu\n", offsetof(struct Aligned, a));
    printf("  offset of b = %zu\n", offsetof(struct Aligned, b));
    printf("  offset of c = %zu\n", offsetof(struct Aligned, c));
    printf("\n");

    printf("sizeof(struct Packed)          = %zu\n", sizeof(struct Packed));
    printf("  offset of a = %zu\n", offsetof(struct Packed, a));
    printf("  offset of c = %zu\n", offsetof(struct Packed, c));
    printf("  offset of b = %zu\n", offsetof(struct Packed, b));
    printf("\n");

    printf("sizeof(struct PackedAttribute) = %zu\n", sizeof(struct PackedAttribute));
    printf("\n");

    printf("sizeof(struct Nested)          = %zu\n", sizeof(struct Nested));
    printf("  offset of id    = %zu\n", offsetof(struct Nested, id));
    printf("  offset of point = %zu\n", offsetof(struct Nested, point));
    printf("  offset of tag   = %zu\n", offsetof(struct Nested, tag));

    return 0;
}

编译运行:

gcc -Wall -Wextra -std=c99 -o 12_struct_align 12_struct_align.c
./12_struct_align

示例3:结构体传参与 container_of

// 12_struct_pass.c
#include <stdio.h>
#include <stddef.h>

// container_of 宏原理演示
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

struct Node {
    int data;
    int priority;
    struct Node *next;
};

// 值传递:拷贝整个结构体,开销大
void print_by_value(struct Node n)
{
    printf("[by value]  data=%d, pri=%d\n", n.data, n.priority);
}

// 指针传递:只传地址,高效(推荐)
void print_by_ptr(const struct Node *n)
{
    printf("[by ptr]    data=%d, pri=%d\n", n->data, n->priority);
}

// 柔性数组成员
struct Buffer {
    int len;
    char data[];  // C99 柔性数组,必须是最后一个成员
};

int main(void)
{
    // 结构体传参
    struct Node n = {42, 10, NULL};
    print_by_value(n);
    print_by_ptr(&n);

    // container_of 演示:已知成员指针,反推结构体指针
    struct Node node = {100, 5, NULL};
    struct Node *prio_ptr = &node.priority;  // 假设外部只拿到了 priority 的指针
    struct Node *base = container_of(prio_ptr, struct Node, priority);
    printf("container_of: recovered data = %d\n", base->data);

    // 柔性数组:动态分配
    size_t msg_len = 12;
    struct Buffer *buf = malloc(sizeof(struct Buffer) + msg_len);
    buf->len = msg_len;
    memcpy(buf->data, "Hello World!", msg_len);
    printf("Buffer len=%d, data=%.*s\n", buf->len, buf->len, buf->data);
    free(buf);

    return 0;
}

编译运行:

gcc -Wall -Wextra -std=c99 -o 12_struct_pass 12_struct_pass.c
./12_struct_pass

12.4 注意事项与易错点

  1. 整体赋值是浅拷贝:指针成员共享同一块内存,修改一个会影响另一个。深拷贝需要手动复制指针指向的内容。

  2. #pragma pack 只影响对齐,不影响成员排列顺序:成员在内存中的相对顺序不变。

  3. 柔性数组要求:必须是结构体的最后一个成员;包含柔性数组的结构体不能直接赋值(无固定大小);分配时 malloc(sizeof(struct) + extra)

  4. sizeof 与实际数据大小不同sizeof 包含填充字节,strlen 只看字符串内容。

  5. 结构体嵌套时对齐以最大成员为基准:内嵌结构体的对齐要求取其最大成员的对齐值。

12.5 面试要点(5个Q&A)

Q1:结构体内存对齐的规则是什么?

  1. 每个成员的偏移量必须是自身对齐值的整数倍(不足则填充)。
  2. 结构体整体大小必须是最大对齐值的整数倍。

Q2:#pragma pack(1) 的作用和副作用?

强制 1 字节对齐,消除填充。副作用:CPU 访问非对齐地址可能性能下降甚至崩溃(取决于硬件)。

Q3:结构体值传递和指针传递的区别?

值传递复制整个结构体(大结构体开销大),指针传递只传 4/8 字节地址。注意值传递时修改不影响原数据。

Q4:什么是柔性数组成员?

C99 允许结构体最后一个成员是未知长度的数组 char data[]。必须 malloc(sizeof(struct) + length) 分配,用于变长数据包。

Q5:container_of 宏的原理?

已知成员指针 ptr,通过 ptr - offsetof(type, member) 计算出结构体起始地址。Linux 内核广泛使用此宏实现链表等数据结构。