1. 结构体基础概念解析
结构体(struct)是C语言中最强大的构造数据类型之一,它允许我们将不同类型的数据组合成一个整体。想象一下,如果我们要描述一个学生,需要记录姓名(字符串)、学号(整数)、成绩(浮点数)等信息,使用基本数据类型需要多个独立变量,而结构体可以将这些信息打包成一个逻辑单元。
在嵌入式开发领域(如STM32),结构体更是被广泛用于寄存器映射。通过将硬件寄存器定义为结构体成员,我们可以用更直观的方式访问硬件资源。比如一个GPIO端口可能有多个寄存器(CRL、CRH、IDR等),用结构体组织后,代码可读性大幅提升。
结构体的定义语法如下:
c复制struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
例如定义一个学生结构体:
c复制struct Student {
char name[20];
int id;
float score;
};
注意:在C语言中,struct关键字不能省略(C++中可以),这是新手常犯的错误。比如
struct Student stu1;是正确的,而Student stu1;在C中会报错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 结构体的高级特性与应用
2.1 结构体初始化技巧
结构体初始化有多种方式,每种都有其适用场景:
- 定义时初始化(顺序初始化):
c复制struct Student stu1 = {"张三", 1001, 89.5};
- 指定初始化器(C99标准引入):
c复制struct Student stu2 = {
.id = 1002,
.score = 92.0,
.name = "李四"
};
这种方式不依赖成员顺序,且可跳过某些成员(未指定的成员默认初始化为0)。
- 逐成员赋值:
c复制struct Student stu3;
strcpy(stu3.name, "王五");
stu3.id = 1003;
stu3.score = 85.5;
对于嵌套结构体(结构体包含另一个结构体),初始化可以这样操作:
c复制struct Date {
int year;
int month;
int day;
};
struct StudentEx {
char name[20];
struct Date birthday;
};
struct StudentEx stu4 = {
"赵六",
{2000, 5, 20} // 嵌套结构体初始化
};
2.2 结构体对齐与内存布局
结构体在内存中的排列不是简单的成员拼接,而是遵循对齐原则。这是因为现代CPU对对齐的数据访问效率更高。对齐规则主要有:
- 基本对齐:成员按其类型大小对齐(如int32_t按4字节对齐)
- 结构体整体对齐:结构体大小必须是最大成员大小的整数倍
例如:
c复制struct Example {
char a; // 1字节
int b; // 4字节(需要3字节填充)
short c; // 2字节
}; // 总大小:1 + 3(padding) + 4 + 2 = 10 → 12(补齐到4的倍数)
在嵌入式开发中(如IAR环境),我们常用#pragma pack指令控制对齐方式:
c复制#pragma pack(1) // 1字节对齐
struct PackedStruct {
char a;
int b;
}; // 大小=5字节
#pragma pack() // 恢复默认对齐
重要提示:不对齐的结构体虽然节省内存,但可能导致性能下降甚至硬件异常(如ARM Cortex-M的硬件错误)。在STM32寄存器访问时,必须确保结构体对齐与硬件寄存器布局完全匹配。
3. 结构体指针与链表实现
3.1 结构体指针操作
结构体指针是访问结构体的高效方式,特别是在传递大型结构体时(避免拷贝开销)。基本用法:
c复制struct Student stu = {"张三", 1001, 89.5};
struct Student *pStu = &stu;
// 通过指针访问成员
printf("姓名:%s\n", (*pStu).name); // 传统方式
printf("学号:%d\n", pStu->id); // 更常用的箭头语法
结构体指针在动态内存分配中尤为重要:
c复制struct Student *pStu2 = (struct Student*)malloc(sizeof(struct Student));
if(pStu2 != NULL) {
strcpy(pStu2->name, "李四");
pStu2->id = 1002;
// ...
free(pStu2); // 记得释放内存
}
3.2 结构体实现链表
链表是结构体和指针的经典应用。下面是一个完整的单链表实现示例:
c复制// 链表节点结构体
struct Node {
int data;
struct Node *next;
};
// 创建新节点
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
if(newNode != NULL) {
newNode->data = data;
newNode->next = NULL;
}
return newNode;
}
// 在链表末尾插入节点
void appendNode(struct Node** head, int data) {
struct Node* newNode = createNode(data);
if(*head == NULL) {
*head = newNode;
return;
}
struct Node* last = *head;
while(last->next != NULL) {
last = last->next;
}
last->next = newNode;
}
// 遍历链表
void printList(struct Node* node) {
while(node != NULL) {
printf("%d -> ", node->data);
node = node->next;
}
printf("NULL\n");
}
使用示例:
c复制struct Node* myList = NULL;
appendNode(&myList, 10);
appendNode(&myList, 20);
printList(myList); // 输出:10 -> 20 -> NULL
4. 结构体在嵌入式开发中的高级应用
4.1 STM32寄存器映射
在STM32开发中,结构体常用于寄存器映射。例如配置GPIO:
c复制typedef struct {
volatile uint32_t MODER; // 模式寄存器
volatile uint32_t OTYPER; // 输出类型寄存器
volatile uint32_t OSPEEDR; // 输出速度寄存器
volatile uint32_t PUPDR; // 上拉/下拉寄存器
volatile uint32_t IDR; // 输入数据寄存器
volatile uint32_t ODR; // 输出数据寄存器
volatile uint32_t BSRR; // 置位/复位寄存器
volatile uint32_t LCKR; // 锁定寄存器
volatile uint32_t AFR[2]; // 复用功能寄存器
} GPIO_TypeDef;
#define GPIOA_BASE 0x40020000UL
#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)
// 配置PA5为输出模式
GPIOA->MODER &= ~(3 << (5 * 2)); // 清除原有设置
GPIOA->MODER |= (1 << (5 * 2)); // 设置为输出模式
GPIOA->OTYPER &= ~(1 << 5); // 推挽输出
4.2 结构体位域
位域允许我们按位访问结构体成员,在嵌入式开发中非常有用:
c复制struct {
uint32_t enable : 1; // 1位
uint32_t mode : 3; // 3位
uint32_t reserved : 4; // 保留位
uint32_t clock_div : 8; // 分频系数
} control_reg;
使用位域时要注意:
- 位域成员不能取地址(&操作)
- 不同编译器对位域的内存布局可能不同
- 跨字节位域可能导致可移植性问题
5. 结构体使用中的常见问题与解决方案
5.1 结构体赋值与拷贝
结构体支持直接赋值(浅拷贝):
c复制struct Student stu1 = {"张三", 1001, 89.5};
struct Student stu2 = stu1; // 合法,成员值被复制
但对于包含指针成员的结构体,直接赋值会导致指针指向同一内存:
c复制struct Problematic {
char *name;
int id;
};
struct Problematic p1;
p1.name = malloc(10);
strcpy(p1.name, "Test");
struct Problematic p2 = p1; // 危险!p2.name与p1.name指向同一内存
free(p1.name); // p2.name现在成为悬垂指针
解决方案是实现深拷贝:
c复制void deepCopy(struct Problematic *dest, const struct Problematic *src) {
dest->id = src->id;
dest->name = malloc(strlen(src->name) + 1);
strcpy(dest->name, src->name);
}
5.2 结构体大小计算陷阱
由于对齐的存在,结构体大小不能简单相加。可靠的做法是:
- 使用
sizeof运算符获取准确大小 - 用
offsetof宏获取成员偏移量(在<stddef.h>中定义)
c复制#include <stddef.h>
struct Test {
char a;
int b;
short c;
};
printf("Size: %zu\n", sizeof(struct Test)); // 可能是12
printf("b offset: %zu\n", offsetof(struct Test, b)); // 可能是4
在通信协议等需要精确控制内存布局的场景,务必验证结构体大小和成员偏移。
