你们好,我是一名嵌入式工程师。我正在持续更新《嵌入式工程师的C语言实战》系列教程,目前已更新10篇。如果你也在学习嵌入式,欢迎点个关注,我们一起从零到一。


前言

结构体(struct)是C语言中最核心的自定义数据类型,它允许你把不同类型的数据打包成一个整体。无论是操作系统内核、网络协议还是数据库,底层都离不开结构体。


一、什么是结构体?

1. 核心概念

结构体把多个不同类型的变量组合成一个整体,每个成员独立占用内存

类比:像一张学生信息表,包含姓名(字符串)、年龄(整数)、成绩(浮点数)。

2. 为什么需要结构体?

没有结构体时,管理多个相关数据很麻烦:

// ❌ 没有结构体:数据分散,难以管理
char name1[50] = "张三";
int age1 = 20;
float score1 = 85.5;

char name2[50] = "李四";
int age2 = 21;
float score2 = 92.0;

// 如果学生有100个,需要维护300个变量!

有了结构体后:

// ✅ 使用结构体:数据打包,清晰易用
struct Student {
    char name[50];
    int age;
    float score;
};

struct Student stu1 = {"张三", 20, 85.5};
struct Student stu2 = {"李四", 21, 92.0};

二、结构体的声明与定义

1. 基本语法

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

2. 三种定义方式

方式1:先声明类型,后定义变量(最常用)

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

struct Student stu1;                    // 未初始化
struct Student stu2 = {"张三", 20, 85.5}; // 初始化

方式2:声明类型的同时定义变量

struct Student {
    char name[50];
    int age;
    float score;
} stu1, stu2;  // 直接定义变量

// 后续可以继续使用该类型
struct Student stu3;

方式3:匿名结构体(一次性使用)

struct {
    int x;
    int y;
} point = {10, 20};  // 直接定义变量,类型无法再次使用

// 错误:后续不能再定义 struct point p2;

3. 使用 typedef 简化(工业界最常用)

typedef struct {
    char name[50];
    int age;
    float score;
} Student;  // 现在 Student 是一个类型别名

Student stu1 = {"张三", 20, 85.5};  // 不需要写 struct 关键字
Student *ptr = &stu1;

为什么推荐 typedef

  • 少写 struct 关键字,代码更简洁。
  • 与C++兼容(C++中 struct Student 可以直接写 Student)。

三、结构体的初始化

1. 顺序初始化

struct Student stu = {"张三", 20, 85.5};
// 按成员声明顺序依次赋值

2. 指定初始化(C99)

struct Student stu = {
    .score = 85.5,
    .name = "张三",
    .age = 20
};
// 顺序可以打乱,更清晰

3. 部分初始化

struct Student stu = {"张三"};  // 只初始化 name,其他成员为 0
printf("%d\n", stu.age);        // 0
printf("%.1f\n", stu.score);    // 0.0

4. 嵌套结构体初始化

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

struct Employee {
    char name[50];
    struct Date birthday;
    float salary;
};

struct Employee emp = {
    "张三",
    {1990, 5, 15},  // 嵌套初始化
    8000.0
};

四、访问结构体成员

1. 普通变量用 .

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

printf("%s\n", stu.name);   // 张三
printf("%d\n", stu.age);    // 20
printf("%.1f\n", stu.score); // 85.5

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

2. 指针用 ->

struct Student stu = {"张三", 20, 85.5};
struct Student *ptr = &stu;

printf("%s\n", ptr->name);   // 张三
printf("%d\n", ptr->age);    // 20

ptr->age = 22;               // 通过指针修改
printf("%d\n", stu.age);     // 22

等价关系

ptr->age  等价于  (*ptr).age

3. 嵌套结构体访问

struct Employee emp = {"张三", {1990, 5, 15}, 8000.0};

printf("%d\n", emp.birthday.year);   // 1990
printf("%d\n", emp.birthday.month);  // 5

// 指针访问
struct Employee *p = &emp;
printf("%d\n", p->birthday.year);    // 1990

五、内存布局(重点难点)

1. 内存对齐规则

结构体的大小不等于成员大小之和,因为存在内存对齐

规则

  1. 每个成员相对于结构体起始地址的偏移量,必须是该成员大小的整数倍。
  2. 结构体总大小必须是最大成员大小的整数倍。

2. 示例分析

struct A {
    char c;    // 1 字节
    int i;     // 4 字节
    char d;    // 1 字节
};

printf("%zu\n", sizeof(struct A));  // 通常是 12,不是 6

内存图解

偏移: 0    1    2    3    4    5    6    7    8    9    10   11
     [c]  [填充][填充][填充][i  i  i  i][d]  [填充][填充][填充]
  • c 在偏移 0,占 1 字节。
  • iint,需要 4 字节对齐,所以偏移到 4。
  • d 在偏移 8,占 1 字节。
  • 总大小需要是 4 的倍数,所以补齐到 12。

3. 调整成员顺序可以节省内存

// 优化前:12 字节
struct A {
    char c;    // 1
    int i;     // 4
    char d;    // 1
};

// 优化后:8 字节
struct B {
    int i;     // 4
    char c;    // 1
    char d;    // 1
    // 补齐 2 字节
};

printf("%zu\n", sizeof(struct A));  // 12
printf("%zu\n", sizeof(struct B));  // 8

4. 为什么要内存对齐?

  • 硬件要求:某些CPU只能从对齐的地址读取数据,否则会崩溃或效率降低。
  • 性能优化:对齐的数据访问速度更快。

5. 如何控制对齐?

// 方式1:使用 #pragma pack(编译器相关)
#pragma pack(push, 1)  // 设置为 1 字节对齐
struct Packed {
    char c;
    int i;
};
#pragma pack(pop)      // 恢复默认对齐

printf("%zu\n", sizeof(struct Packed));  // 5

// 方式2:使用 _Alignas(C11)
struct Aligned {
    _Alignas(16) int x;  // 16 字节对齐
};

六、结构体作为函数参数

1. 值传递(不推荐,会拷贝整个结构体)

void print_student(struct Student s) {  // 拷贝整个结构体
    printf("%s %d %.1f\n", s.name, s.age, s.score);
}

struct Student stu = {"张三", 20, 85.5};
print_student(stu);  // 传值

缺点:如果结构体很大,拷贝开销大。

2. 指针传递(推荐)

void print_student(const struct Student *s) {  // 只传指针
    printf("%s %d %.1f\n", s->name, s->age, s->score);
}

struct Student stu = {"张三", 20, 85.5};
print_student(&stu);  // 传地址

优点

  • 只拷贝指针(8 字节),效率高。
  • const 防止函数内部修改。

3. 返回结构体

struct Point {
    int x, y;
};

struct Point create_point(int x, int y) {
    struct Point p = {x, y};
    return p;  // 返回结构体(编译器会优化,通常不产生额外拷贝)
}

int main() {
    struct Point p = create_point(10, 20);
    printf("(%d, %d)\n", p.x, p.y);  // (10, 20)
    return 0;
}

七、结构体的高级用法

1. 自引用结构体(链表节点)

struct Node {
    int data;
    struct Node *next;  // 指向同类型的指针
};

// 创建链表
struct Node *head = NULL;
struct Node *n1 = malloc(sizeof(struct Node));
n1->data = 10;
n1->next = NULL;
head = n1;

2. 结构体数组

struct Student class[3] = {
    {"张三", 20, 85.5},
    {"李四", 21, 92.0},
    {"王五", 19, 78.5}
};

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

3. 结构体指针数组

struct Student stu1 = {"张三", 20, 85.5};
struct Student stu2 = {"李四", 21, 92.0};

struct Student *arr[] = {&stu1, &stu2};

for (int i = 0; i < 2; i++) {
    printf("%s\n", arr[i]->name);
}

4. 位域(Bit-field)

struct Flags {
    unsigned int is_active : 1;   // 1 位
    unsigned int is_admin : 1;    // 1 位
    unsigned int level : 4;       // 4 位
};

printf("%zu\n", sizeof(struct Flags));  // 4 字节

struct Flags f = {1, 0, 5};
printf("%u\n", f.is_active);  // 1
printf("%u\n", f.level);      // 5

用途:节省内存,常用于硬件寄存器、协议头。


八、完整实战示例:学生管理系统

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

#define MAX_STUDENTS 100

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

// 打印学生信息
void print_student(const Student *s) {
    printf("姓名: %-10s 年龄: %2d 成绩: %.1f\n", 
           s->name, s->age, s->score);
}

// 查找最高分学生
Student* find_top(Student arr[], int n) {
    if (n <= 0) return NULL;
    
    Student *top = &arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i].score > top->score) {
            top = &arr[i];
        }
    }
    return top;
}

// 按成绩排序(冒泡)
void sort_by_score(Student arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j].score < arr[j + 1].score) {
                Student temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    Student class[MAX_STUDENTS] = {
        {"张三", 20, 85.5},
        {"李四", 21, 92.0},
        {"王五", 19, 78.5},
        {"赵六", 22, 88.0}
    };
    int n = 4;
    
    printf("=== 所有学生 ===\n");
    for (int i = 0; i < n; i++) {
        print_student(&class[i]);
    }
    
    printf("\n=== 最高分学生 ===\n");
    Student *top = find_top(class, n);
    if (top != NULL) {
        print_student(top);
    }
    
    printf("\n=== 按成绩排序后 ===\n");
    sort_by_score(class, n);
    for (int i = 0; i < n; i++) {
        print_student(&class[i]);
    }
    
    return 0;
}

输出

=== 所有学生 ===
姓名: 张三       年龄: 20 成绩: 85.5
姓名: 李四       年龄: 21 成绩: 92.0
姓名: 王五       年龄: 19 成绩: 78.5
姓名: 赵六       年龄: 22 成绩: 88.0

=== 最高分学生 ===
姓名: 李四       年龄: 21 成绩: 92.0

=== 按成绩排序后 ===
姓名: 李四       年龄: 21 成绩: 92.0
姓名: 赵六       年龄: 22 成绩: 88.0
姓名: 张三       年龄: 20 成绩: 85.5
姓名: 王五       年龄: 19 成绩: 78.5

九、常见陷阱与最佳实践

1. 结构体不能直接比较

struct Point p1 = {1, 2}, p2 = {1, 2};
// if (p1 == p2) { }  // ❌ 编译错误

// ✅ 逐个成员比较
if (p1.x == p2.x && p1.y == p2.y) { }

2. 结构体不能直接赋值字符串

struct Student s;
// s.name = "张三";  // ❌ 编译错误

// ✅ 使用 strcpy
strcpy(s.name, "张三");

3. 结构体赋值是浅拷贝

struct Student s1 = {"张三", 20, 85.5};
struct Student s2 = s1;  // 逐字节拷贝,s2 是独立副本

s2.age = 30;
printf("%d\n", s1.age);  // 20(不受影响)

但如果成员是指针,浅拷贝会共享内存

struct Data {
    char *name;
};

struct Data d1;
d1.name = malloc(20);
strcpy(d1.name, "Hello");

struct Data d2 = d1;  // 浅拷贝:d2.name 和 d1.name 指向同一块内存
free(d1.name);
// d2.name 现在是悬空指针!

4. 最佳实践

  1. typedef 简化类型名
  2. 用指针传递大结构体,加 const 防止修改。
  3. 注意内存对齐,合理安排成员顺序。
  4. 深拷贝时手动复制指针成员

📝 总结

特性说明
本质把不同类型的数据打包成一个整体
内存各成员独立占用,存在对齐填充
访问普通变量用 .,指针用 ->
传参推荐指针传递,避免拷贝
典型用途学生信息、链表节点、网络协议包

记忆口诀
结构体,打包器,不同类型放一起。
点号访问普通量,箭头专指指针体。
内存对齐要注意,顺序调整省空间。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,主播是一个马上粉丝量超10个的嵌入式内容主播。谢谢!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐