C语言结构体:定义、访问、指针、嵌套

C语言结构体是 C 语言中最常用的复合数据类型之一。它不仅能把多个不同类型的数据组织成一个整体,还经常出现在函数传参、链表设计、设备寄存器映射、通信协议解析等场景中。理解结构体,基本就掌握了 C 语言中“描述复杂对象”的核心手段。

一、C语言结构体是什么?

C语言结构体是一种用户自定义的数据类型,可以将多个不同类型的数据组合在一起,形成一个逻辑整体。它特别适合描述一个对象的多种属性,比如学生的姓名、年龄和成绩,或者设备的寄存器状态、通信数据帧等。

ℹ️ 核心理解:数组存储的是相同类型的数据集合,C语言结构体存储的是不同类型的数据集合。按类型分类,结构体属于聚合类型

1.1 语法格式

struct 结构体名 {
    数据类型 成员1;
    数据类型 成员2;
    ...
};

1.2 typedef 简化写法

typedef struct {
    数据类型 成员1;
    数据类型 成员2;
    ...
} 别名;
💡 使用建议
使用 typedef 之后,声明变量时可以省略 struct 关键字,代码会更简洁,也更适合日常开发。

二、C语言结构体的基本用法

先看最基础的结构体定义、赋值和输出,再看结构体数组这种更常见的组合形式。

2.1 基本示例

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

// 定义结构体
struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    // 方式一:先声明再赋值
    struct Student stu1;
    strcpy(stu1.name, "张三");
    stu1.age = 20;
    stu1.score = 92.5;

    // 方式二:声明时初始化
    struct Student stu2 = {"李四", 21, 88.0};

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

    return 0;
}

2.2 结构体数组

结构体数组就是“把多个相同结构体类型的数据,像数组一样连续存放并用下标访问的一组变量”

#include <stdio.h>

// 使用 typedef 简化结构体定义
typedef struct {
    char name[20];
    int age;
    float score;
} Student;

int main() {
    // 直接用 Student 声明,无需 struct 关键字
    Student class[] = {
        {"张三", 20, 92.5},
        {"李四", 21, 88.0},
        {"王五", 19, 95.0}
    };

    int n = sizeof(class) / sizeof(class[0]);

    for (int i = 0; i < n; i++) {
        printf("%-6s %d岁 %.1f分\n", class[i].name, class[i].age, class[i].score);
    }

    return 0;
}

三、结构体成员访问方式

结构体成员的访问方式,取决于你操作的是结构体变量,还是结构体指针

💬 核心区别:结构体变量用 .(点运算符),结构体指针用 ->(箭头运算符)。

3.1 点运算符 .

通过结构体变量直接访问成员:

struct Student stu = {"张三", 20, 92.5};

// 访问成员
printf("%s\n", stu.name);   // 张三
printf("%d\n", stu.age);    // 20

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

3.2 箭头运算符 ->

通过结构体指针访问成员,本质上是 (*p).成员 的简化写法:

struct Student stu = {"张三", 20, 92.5};
struct Student *p = &stu;

// 等价写法
p->age = 21;        // 箭头运算符(推荐)
(*p).age = 21;      // 解引用 + 点运算符

// 三种方式对比
printf("%s\n", stu.name);   // 通过变量访问
printf("%s\n", p->name);    // 通过指针访问(推荐)
printf("%s\n", (*p).name);  // 通过解引用访问
💡 为什么优先使用 ->

  • (*p).name 需要额外括号,因为 . 的优先级高于 *
  • p->name 更简洁,也更容易一眼看出“这是一个指针在访问成员”

3.3 访问方式对比

场景 语法 示例
结构体变量访问成员 变量.成员 stu.age
结构体指针访问成员 指针->成员 p->age
指针解引用后访问 (*指针).成员 (*p).age
📋 速记

  • 变量访问用 .
  • 指针访问用 ->
  • p->name 等价于 (*p).name

四、结构体指针

结构体指针是一个指向结构体变量的指针。它最大的价值在于:可以避免传递整个结构体带来的拷贝开销,并且方便修改原对象内容。

ℹ️ 一句话理解:结构体指针 = 指针 + 结构体,用 -> 访问成员,等价于 (*p).成员

4.1 语法格式

struct 结构体名 *指针变量名;

4.2 基本用法

#include <stdio.h>

struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student stu = {"张三", 20, 92.5};
    struct Student *p = &stu; // 指针指向结构体变量

    // 三种访问方式(结果相同)
    printf("姓名: %s\n", stu.name);  // 点运算符
    printf("姓名: %s\n", p->name);   // 箭头运算符(推荐)
    printf("姓名: %s\n", (*p).name); // 解引用

    // 通过指针修改成员
    p->age = 21;
    p->score = 95.0;
    printf("修改后: %s, %d岁, %.1f分\n", p->name, p->age, p->score);

    return 0;
}

4.3 结构体指针作为函数参数

#include <stdio.h>

typedef struct {
    char name[20];
    int age;
    float score;
} Student;

// 传指针:避免拷贝整个结构体,高效!
void printStudent(const Student *p) {
    printf("姓名: %s, 年龄: %d, 成绩: %.1f\n", p->name, p->age, p->score);
}

// 通过指针修改结构体
void addBonus(Student *p, float bonus) {
    p->score += bonus;
}

int main() {
    Student stu = {"张三", 20, 88.0};

    printStudent(&stu); // 姓名: 张三, 年龄: 20, 成绩: 88.0
    addBonus(&stu, 5.0); // 加 5 分
    printStudent(&stu); // 姓名: 张三, 年龄: 20, 成绩: 93.0

    return 0;
}

4.4 动态内存分配结构体

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

typedef struct {
    char name[20];
    int age;
} Student;

int main() {
    // 在堆上动态创建一个结构体
    Student *p = (Student *)malloc(sizeof(Student));

    if (p == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    strcpy(p->name, "张三");
    p->age = 20;

    printf("姓名: %s, 年龄: %d\n", p->name, p->age);

    free(p); // 释放内存
    p = NULL; // 避免悬空指针

    return 0;
}
⚠️ 注意:只要结构体是通过 malloc 动态申请的,就要记得配对 free。释放后最好把指针置为 NULL,避免悬空指针问题。

五、嵌套结构体

当一个结构体的成员本身也是结构体时,就形成了嵌套结构体。这种写法非常适合描述层级化数据,比如“人包含地址”“订单包含收货信息”“设备包含配置参数”等。

💡 核心理解
嵌套结构体就是“结构体套结构体”。访问成员时需要层层展开,因此会出现多个 .-> 连续使用的情况。

5.1 嵌套结构体示例

#include <stdio.h>

// 定义地址结构体
typedef struct {
    char city[20];
    char street[30];
    int zipCode;
} Address;

// 定义学生结构体(嵌套地址)
typedef struct {
    char name[20];
    int age;
    Address addr;  // 嵌套结构体,无需 struct 关键字
} Person;

int main() {
    // 初始化嵌套结构体
    Person p = {
        "张三",
        25,
        {"北京", "中关村大街1号", 100080}
    };

    // 访问嵌套成员:用多个点运算符
    printf("姓名: %s\n", p.name);
    printf("城市: %s\n", p.addr.city);
    printf("街道: %s\n", p.addr.street);
    printf("邮编: %d\n", p.addr.zipCode);

    return 0;
}

5.2 通过指针访问嵌套成员

Person p = {"张三", 25, {"北京", "中关村大街1号", 100080}};
Person *ptr = &p;

// 方式一:箭头 + 点
printf("城市: %s\n", ptr->addr.city);

// 方式二:解引用 + 点
printf("城市: %s\n", (*ptr).addr.city);
⚠️ 容易写错的地方ptr->addr.city 不能写成 ptr->addr->city,因为 addr 本身是结构体变量,不是指针。

5.3 嵌套结构体的内存布局

嵌套结构体的总大小依然遵循对齐规则,内层结构体会作为一个整体参与对齐。

struct Inner {
    char c;    // 1 字节
    int i;     // 4 字节
};             // sizeof = 8(补到 4 的倍数)

struct Outer {
    char a;         // 1 字节
    struct Inner s; // 8 字节(从 4 对齐地址开始)
    char b;         // 1 字节
};                  // sizeof = 16

内存布局:

地址:    0     1     2     3     4     5     6     7     8     9    10    11    12    13    14    15
       [a ] [pad] [pad] [pad] [    Inner s (8字节)    ] [b ] [pad] [pad] [pad]
         ↑ char              ↑ Inner 要 4 对齐        ↑ char            ↑ 总大小要 4 的倍数

六、结构体内存对齐

结构体内存对齐(Structure Alignment / Padding)是指编译器在分配结构体成员内存时,会按照一定规则在成员之间或末尾插入填充字节(padding),让每个成员的地址满足对齐要求。

ℹ️ 为什么要对齐?:CPU 访问内存时,按 2 / 4 / 8 字节 整块读取通常效率更高。如果一个 int 跨越两个 4 字节块,CPU 往往需要多次读取再拼接,因此对齐本质上是用少量空间换访问效率

6.1 三条核心规则

💬 核心规则

  1. 成员对齐:每个成员的起始地址必须是 min(该成员大小, 默认对齐数) 的整数倍
  2. 结构体总大小:必须是 最大成员对齐数 的整数倍
  3. 默认对齐数:不同编译器不同。MSVC 默认 8,GCC 通常按成员自身大小对齐

常见类型的大小和对齐要求如下:

类型 大小(字节) 对齐要求
char 1 1 字节对齐
short 2 2 字节对齐
int 4 4 字节对齐
float 4 4 字节对齐
double 8 8 字节对齐
指针 4 或 8 4 或 8 字节对齐

6.2 不同成员顺序的对比

6.2.1 不优化的顺序

struct Bad {
    char a; // 1 字节
    int b;  // 4 字节
    char c; // 1 字节
};
// sizeof = ?

内存布局:

地址:    0     1     2     3     4     5     6     7     8     9    10    11
       [a ] [pad] [pad] [pad] [    b (4字节)    ] [c ] [pad] [pad] [pad]
         ↑ char              ↑ int 要 4 对齐     ↑ char            ↑ 总大小要 4 的倍数
  • a 在地址 0,满足 1 字节对齐
  • b 需要放到地址 4,中间补了 3 字节 padding
  • c 放在地址 8
  • 总大小还要补齐到 4 的倍数,因此最终是 12 字节

6.2.2 优化的顺序

struct Good {
    int b;  // 4 字节
    char a; // 1 字节
    char c; // 1 字节
};
// sizeof = ?

内存布局:

地址:    0     1     2     3     4     5     6     7
       [    b (4字节)    ] [a ] [c ] [pad] [pad]
         ↑ int              ↑ char ↑ char ↑ 补到 4 的倍数
  • b 在地址 0
  • a 在地址 4
  • c 在地址 5
  • 总大小补到 4 的倍数,因此最终是 8 字节

6.3 用代码验证偏移量

#include <stdio.h>
#include <stddef.h> // offsetof

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

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

int main() {
    printf("===== struct Bad =====\n");
    printf("sizeof = %zu\n", sizeof(struct Bad)); // 12
    printf("a 的偏移量 = %zu\n", offsetof(struct Bad, a)); // 0
    printf("b 的偏移量 = %zu\n", offsetof(struct Bad, b)); // 4
    printf("c 的偏移量 = %zu\n", offsetof(struct Bad, c)); // 8

    printf("\n===== struct Good =====\n");
    printf("sizeof = %zu\n", sizeof(struct Good)); // 8
    printf("b 的偏移量 = %zu\n", offsetof(struct Good, b)); // 0
    printf("a 的偏移量 = %zu\n", offsetof(struct Good, a)); // 4
    printf("c 的偏移量 = %zu\n", offsetof(struct Good, c)); // 5

    return 0;
}
💡 优化技巧
如果你希望结构体更紧凑,通常可以按照成员大小从大到小排列,这样往往能减少 padding 的浪费。
// 推荐写法:大的放前面
struct Optimized {
    double d; // 8 字节
    int i;    // 4 字节
    short s;  // 2 字节
    char c;   // 1 字节
};
// sizeof = 16(紧凑)

6.4 手动控制对齐

// 方法一:#pragma pack(MSVC / GCC 都支持)
#pragma pack(push, 1) // 设置 1 字节对齐(取消 padding)
struct Packed {
    char a;
    int b;
    char c;
};
#pragma pack(pop) // 恢复默认
// sizeof = 6(无 padding,但访问可能变慢)

// 方法二:__attribute__(GCC / Clang)
struct __attribute__((packed)) Packed2 {
    char a;
    int b;
    char c;
};
// sizeof = 6
⚠️ 注意

  • 取消对齐虽然节省内存,但可能导致某些平台出现未对齐访问异常
  • 同时也可能让 CPU 访问效率下降
  • 这种写法通常只在通信协议文件格式硬件寄存器映射等必须精确控制布局的场景下使用

七、总结对比

概念 作用 关键符号 典型场景
结构体 组合不同类型数据 . 描述复杂对象、寄存器组映射
结构体指针 指向结构体的指针 -> 函数传参、动态分配、链表
嵌套结构体 结构体套结构体 外层.内层.成员 描述层级数据(地址、日期等)
内存对齐 优化访问效率与布局规则 sizeof / offsetof 底层优化、嵌入式开发、协议设计
📋 速记

  • 结构体负责“把不同类型数据打包成一个整体”
  • 变量访问成员用 .,指针访问成员用 ->
  • p->name 等价于 (*p).name
  • 嵌套结构体访问时要层层展开,例如 p.addr.city
  • 成员顺序不同,sizeof 的结果可能不同

相关阅读

  • [[98-博客/C语言指针和结构体]]:深入理解指针与结构体的配合使用
  • [[98-博客/C语言内存管理]]:动态内存分配与结构体对象的结合
  • [[98-博客/嵌入式开发中 void 的用法总结]]:通用指针与结构体参数传递
  • [[98-博客/FreeRTOS任务管理-基础架构与生命周期]]:结构体在任务控制块中的实际应用

延伸阅读