17 共用体、枚举、位操作与堆内存管理
本文介绍了C语言中四种重要的编程技术:共用体、枚举类型、位操作和堆内存管理。共用体通过内存共享特性实现高效数据转换和大小端判断;枚举类型规范变量取值范围,提升代码可读性;位操作直接操作二进制位,是寄存器配置的核心方法;堆内存管理则遵循"申请-检查-使用-释放-置空"流程实现动态内存分配。这些技术在嵌入式开发中具有广泛应用,如硬件寄存器配置、状态机设计等场景。文中通过代码示例详细
·
共用体、枚举、位操作与堆内存管理
一、目录
共用体:内存共享,适用于大小端判断、数据格式转换,节省嵌入式设备有限内存
枚举类型:规范变量取值,提升代码可读性,是状态机设计的核心工具
位操作:直接操作二进制位,是寄存器配置、位掩码控制的底层基础,高效且节省资源
堆内存管理:动态申请释放内存,需严格遵循 "申请 - 检查 - 使用 - 释放 - 置空" 流程,避免内存泄漏和野指针
二、共用体(联合体):内存共享的高效数据类型
2.1 核心定义与特征
共用体是一种自定义类型,其所有成员变量共享同一块内存空间,核心特征如下:
- 成员共享内存:同一时间只能存储一个成员的值,修改一个成员会覆盖其他成员
- 大小规则:共用体的大小 = 最大成员变量的大小(内存对齐优化)
- 适用场景:节省内存空间、数据格式转换、硬件相关操作(如大小端判断)
2.2 基础语法示例
#include <stdio.h>
#include <stdlib.h>
// 定义共用体
union DATA {
int a; // 4字节
char c; // 1字节
float f; // 4字节
};
int main() {
union DATA data;
union DATA *pdata = &data;
// 共用体大小 = 最大成员大小(4字节)
printf("共用体大小:%lu 字节\n", sizeof(data));
// 成员共享内存,修改一个会覆盖其他
data.a = 100;
printf("data.a = %d\n", data.a); // 输出:100
data.c = 'a'; // 'a'的ASCII码为97,覆盖int的低1字节
printf("data.c = %d('a'的ASCII码)\n", data.c); // 输出:97
printf("data.a = %d(被覆盖后的值)\n", data.a); // 输出:97(高3字节未变,低1字节被覆盖)
data.f = 3.14f;
printf("data.f = %.2f\n", data.f); // 输出:3.14
printf("data.c = %c(float的低1字节解析为字符)\n", data.c); // 输出随机字符(内存二进制解析差异)
printf("data.a = %d(float的二进制解析为int)\n", data.a); // 输出随机整数(数据类型二进制格式不同)
// 指针访问共用体成员
printf("pdata->f = %.2f\n", pdata->f); // 输出:3.14
return 0;
}
2.3 嵌入式核心应用:大小端判断
大小端是数据在内存中的存储顺序,嵌入式开发中需适配不同CPU架构,共用体是判断大小端的高效方式:
#include <stdio.h>
#include <stdlib.h>
union DATA {
int a; // 4字节:0x12345678
char c; // 取低1字节
};
int main() {
union DATA data;
data.a = 0x12345678; // 假设存储的十六进制数
// 小端模式:低字节存低地址(0x78在低地址,c=0x78)
// 大端模式:高字节存低地址(0x12在低地址,c=0x12)
if (data.c == 0x78) {
printf("当前系统为小端模式(嵌入式CPU主流模式)\n");
} else {
printf("当前系统为大端模式\n");
}
return 0;
}
三、枚举类型:规范取值范围,提升代码可读性
3.1 核心定义与赋值规则
枚举类型用于约定变量的取值范围,将离散值赋予语义化名称,核心规则如下:
- 枚举值为常量,默认类型为int
- 未显式赋值时,第一个枚举值为0,后续依次+1
- 支持显式赋值,未赋值的枚举值在上一个值基础上+1
3.2 基础语法与应用示例
枚举常与switch语句搭配,提升代码可读性(嵌入式开发中常用于状态机设计):
#include <stdio.h>
// 定义枚举类型(星期)
enum WEEK { MON, TUE, WED, THU, FRI, SAT, SUN }; // 默认值:0~6
// 定义枚举类型(月份),支持显式赋值
typedef enum {
JAN, // 0
FEB, // 1
MARCH, // 2
APRI, // 3
MAY, // 4
JUN, // 5(修正原笔记笔误JU)
JULY // 6
} MONTH;
int main() {
enum WEEK week;
MONTH mon = MAY; // 直接使用枚举值,语义清晰
int num = 0;
printf("输入星期编号(0~6):");
scanf("%d", &num);
week = num;
// 枚举与switch搭配,逻辑清晰
switch (week) {
case MON: printf("周一:去上学\n"); break;
case TUE: printf("周二:去游泳\n"); break;
case WED: printf("周三:去跑步\n"); break;
case THU: printf("周四:去玩耍\n"); break;
case FRI: printf("周五:玩电脑\n"); break;
case SAT: printf("周六:去学习\n"); break;
case SUN: printf("周日:睡觉\n"); break;
default: printf("无效编号\n");
}
return 0;
}
3.3 嵌入式价值
- 替代魔法数字:用
MON替代0,GPIO_OUTPUT替代1,代码可读性大幅提升 - 类型安全:限制变量取值范围,避免非法值(编译时检查)
- 状态机设计:嵌入式设备的状态管理(如设备初始化、运行、休眠状态)
四、位操作:嵌入式底层编程的核心工具
位操作直接对变量的二进制位进行运算,是嵌入式开发中寄存器配置、位掩码操作的基础,适用于高效控制硬件。
4.1 六大核心运算符
| 运算符 | 名称 | 规则 | 核心应用场景 |
|---|---|---|---|
| & | 按位与 | 两 bit 均为1则为1,否则为0 | 清0指定位、位掩码匹配 |
| | | 按位或 | 两 bit 有一个为1则为1,否则为0 | 置1指定位 |
| ^ | 按位异或 | 两 bit 不同则为1,相同则为0 | 翻转指定位、交换变量 |
| ~ | 按位取反 | 0变1,1变0(注意数据位数) | 构造掩码 |
| << | 左移 | 高位丢弃,低位补0(等价于×2^n) | 快速乘法、位运算优化 |
| >> | 右移 | 无符号补0(逻辑右移),有符号补符号位(算术右移) | 快速除法、位提取 |
4.2 实战案例:寄存器位操作
嵌入式开发中常用位操作配置GPIO、外设寄存器,以下是两个典型场景:
场景1:指定位清0(如P0口bit0、bit3清0)
#include <stdio.h>
int main() {
unsigned short P0 = 0x55; // 初始值:0000 0000 0101 0101
// 构造掩码:bit0和bit3为0,其余为1(~(1<<0 | 1<<3) = 0xFFF6)
unsigned short mask = ~((1 << 0) | (1 << 3));
P0 = P0 & mask; // 按位与清0指定位
printf("bit0、bit3清0后 P0 = 0x%04X\n", P0); // 输出:0x0054(0000 0000 0101 0100)
return 0;
}
场景2:指定位置1(如P2口能被3整除的bit位置1)
#include <stdio.h>
int main() {
unsigned int P2 = 0xF0F0F0F0; // 初始值:11110000 11110000 11110000 11110000
unsigned int mask = 0;
// 构造掩码:能被3整除的bit位(0、3、6...30)置1
for (int i = 0; i < 32; i++) {
if (i % 3 == 0) {
mask |= (1 << i); // 按位或置1指定位
}
}
P2 = P2 | mask; // 按位或操作,指定位置1
printf("能被3整除的bit位置1后 P2 = 0x%08X\n", P2); // 输出:0xF9F4F2F9
return 0;
}
4.3 嵌入式注意事项
- 数据类型:位操作仅适用于整形变量(char、int、unsigned int等)
- 无符号类型:嵌入式开发中优先使用unsigned类型,避免右移时的符号位问题
- 寄存器操作:配置硬件寄存器时,需先读-改-写(避免覆盖其他位)
五、堆内存管理:动态内存的申请与释放
堆内存是程序运行时动态分配的内存空间,核心函数为malloc(申请)和free(释放),适用于需要灵活调整大小的内存场景(如结构体数组、字符串)。
5.1 核心函数与语法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 堆内存申请字符串
char *fun() {
// 申请10字节堆内存,返回void*需强制转换
char *s = (char *)malloc(10);
if (s == NULL) { // 必须检查申请结果,避免空指针
printf("内存申请失败\n");
exit(1);
}
strcpy(s, "hello");
return s;
}
// 堆内存申请结构体
typedef struct {
char name[50];
int age;
} PER;
int main() {
// 1. 字符串堆内存操作
char *main_s = fun();
printf("堆内存字符串:%s\n", main_s); // 输出:hello
free(main_s); // 释放堆内存,避免内存泄漏
main_s = NULL; // 置空指针,避免野指针
// 2. 结构体堆内存操作
PER *pper = (PER *)malloc(sizeof(PER)); // 申请结构体大小的内存
if (pper == NULL) {
printf("结构体内存申请失败\n");
return 1;
}
strcpy(pper->name, "zhang san");
pper->age = 20;
printf("堆内存结构体:%s %d\n", pper->name, pper->age); // 输出:zhang san 20
// 错误示例:pper++后指针指向非法地址,free会崩溃
// pper++;
// free(pper); // 崩溃:释放的不是申请时的首地址
free(pper); // 正确:释放申请时的首地址
pper = NULL;
return 0;
}
5.2 嵌入式避坑指南
- 内存泄漏:申请的堆内存必须用
free释放,且只能释放一次,否则会导致内存泄漏(嵌入式设备内存有限,长期运行会崩溃) - 野指针:释放内存后,指针需置为
NULL,避免后续误操作 - 申请检查:
malloc可能返回NULL(内存不足),必须检查返回值 - 释放规则:只能释放堆内存,不能释放栈内存;释放的地址必须是
malloc返回的首地址 - 内存对齐:堆内存申请时会自动内存对齐,无需手动处理
更多推荐
所有评论(0)