在 C 语言中,共用体(Union)和结构体(Structure)是两种用于组合不同数据类型的自定义数据类型,它们在内存使用、数据存储和访问方式等方面存在明显差异,下面为你详细介绍:
结构体
定义和语法
结构体是一种将不同类型的数据组合在一起的自定义数据类型,它允许用户将多个相关的变量封装在一个单独的实体中。结构体的定义使用 struct
关键字,语法如下:
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// 可以有更多成员
};
示例代码
#include <stdio.h>
// 定义一个结构体表示学生信息
struct Student {
char name[50];
int age;
float score;
};
int main() {
// 声明一个结构体变量
struct Student stu;
// 给结构体成员赋值
strcpy(stu.name, "John");
stu.age = 20;
stu.score = 85.5;
// 输出结构体成员的值
printf("Name: %s\n", stu.name);
printf("Age: %d\n", stu.age);
printf("Score: %.2f\n", stu.score);
return 0;
}
特点
- 内存分配:结构体的每个成员都有自己独立的内存空间,结构体的总大小是其所有成员大小之和,可能还会有一些内存对齐带来的额外开销。
- 成员访问:可以通过点运算符(
.
)来访问结构体的各个成员,例如stu.name
、stu.age
等。 - 数据存储:结构体中的成员可以同时存储不同的值,各个成员之间相互独立,修改一个成员的值不会影响其他成员。
共用体
定义和语法
共用体也是一种自定义数据类型,它允许不同的数据类型共享同一块内存空间。共用体的定义使用 union
关键字,语法如下:
union 共用体名 {
数据类型 成员1;
数据类型 成员2;
// 可以有更多成员
};
示例代码
#include <stdio.h>
// 定义一个共用体
union Data {
int i;
float f;
char str[20];
};
int main() {
// 声明一个共用体变量
union Data data;
// 给共用体成员赋值
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 220.5;
printf("data.f: %.2f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
return 0;
}
特点
- 内存分配:共用体的所有成员共享同一块内存空间,共用体的大小是其最大成员的大小。
- 成员访问:同样通过点运算符(
.
)来访问共用体的各个成员,例如data.i
、data.f
等。 - 数据存储:在同一时间,共用体只能存储一个成员的值。当给一个成员赋值时,会覆盖之前存储在该内存空间中的其他成员的值。
共用体和结构体的区别
- 内存使用:结构体的成员各自占用独立的内存空间,内存大小是所有成员大小之和;共用体的成员共享同一块内存空间,内存大小取决于最大成员的大小。
- 数据存储:结构体可以同时存储多个成员的值,各个成员相互独立;共用体同一时间只能存储一个成员的值,赋值操作会覆盖其他成员的值。
- 应用场景:结构体适用于需要将多个相关的数据组合在一起的场景,如表示一个人的信息、一个设备的配置参数等;共用体适用于需要在不同数据类型之间共享内存,以节省内存空间的场景,如在解析不同类型的数据时,或者在某些硬件寄存器操作中。
在嵌入式系统开发中,结构体和共用体是非常实用的数据类型,它们各自具有独特的特性,在不同的场景下发挥着重要作用,以下为你详细介绍它们的应用:
结构体的应用
1. 数据封装与管理
- 设备配置信息:嵌入式系统中会涉及各种设备,如传感器、通信模块等。可以使用结构体将设备的配置参数封装在一起,方便管理和传递。
// 定义一个结构体表示传感器的配置信息
struct SensorConfig {
uint8_t samplingRate; // 采样率
uint16_t resolution; // 分辨率
uint8_t gain; // 增益
};
// 使用示例
void configureSensor(struct SensorConfig *config) {
// 根据配置信息对传感器进行配置
// ...
}
int main() {
struct SensorConfig sensorConfig = {10, 12, 2};
configureSensor(&sensorConfig);
return 0;
}
- 任务控制块:在实时操作系统(RTOS)中,每个任务都有自己的控制块,用于存储任务的状态、优先级、堆栈指针等信息。结构体可以很好地实现任务控制块的封装。
// 定义一个结构体表示任务控制块
struct TaskControlBlock {
uint8_t taskID; // 任务 ID
uint8_t priority; // 任务优先级
uint32_t *stackPointer; // 堆栈指针
uint8_t taskState; // 任务状态
};
2. 数据传输与通信
- 协议数据单元(PDU):在通信协议中,数据通常以特定的格式进行传输,结构体可以用来定义这些协议数据单元。
// 定义一个结构体表示简单的通信协议帧
struct CommunicationFrame {
uint8_t header; // 帧头
uint16_t dataLength; // 数据长度
uint8_t data[100]; // 数据内容
uint8_t checksum; // 校验和
};
// 发送帧的函数
void sendFrame(struct CommunicationFrame *frame) {
// 将帧数据通过通信接口发送出去
// ...
}
3. 硬件寄存器映射
- 访问外设寄存器:嵌入式系统需要与各种外设进行交互,通过结构体可以将外设的寄存器映射到内存中,方便对寄存器进行读写操作。
// 定义一个结构体表示 GPIO 寄存器
struct GPIO_Registers {
volatile uint32_t MODER; // 模式寄存器
volatile uint32_t OTYPER; // 输出类型寄存器
volatile uint32_t OSPEEDR; // 输出速度寄存器
volatile uint32_t PUPDR; // 上拉/下拉寄存器
// 其他寄存器...
};
// 假设 GPIOA 的基地址为 0x40020000
#define GPIOA_BASE_ADDRESS 0x40020000
struct GPIO_Registers *GPIOA = (struct GPIO_Registers *)GPIOA_BASE_ADDRESS;
// 使用示例:设置 GPIOA 的模式
GPIOA->MODER = 0x00000001;
共用体的应用
1. 节省内存空间
- 不同类型数据的复用:在嵌入式系统中,内存资源通常比较有限。当需要存储不同类型的数据,但同一时间只使用其中一种类型时,可以使用共用体来节省内存。
// 定义一个共用体表示不同类型的传感器数据
union SensorData {
int temperature; // 温度数据
float pressure; // 压力数据
uint16_t humidity; // 湿度数据
};
// 使用示例
void processSensorData(union SensorData data, uint8_t sensorType) {
switch (sensorType) {
case 0:
// 处理温度数据
printf("Temperature: %d\n", data.temperature);
break;
case 1:
// 处理压力数据
printf("Pressure: %.2f\n", data.pressure);
break;
case 2:
// 处理湿度数据
printf("Humidity: %u\n", data.humidity);
break;
}
}
2. 数据解析与转换
- 位操作与数据拆分:共用体可以用于将一个数据按照不同的方式进行解析,例如将一个整数拆分为多个字节,或者将多个字节组合成一个整数。
// 定义一个共用体用于数据解析
union DataParser {
uint32_t value;
uint8_t bytes[4];
};//正是因为公用一块内存,下面才能通过bytes[]分别访问每个字节
// 使用示例:将一个 32 位整数拆分为 4 个字节
union DataParser parser;
parser.value = 0x12345678;
printf("Byte 0: 0x%02X\n", parser.bytes[0]);
printf("Byte 1: 0x%02X\n", parser.bytes[1]);
printf("Byte 2: 0x%02X\n", parser.bytes[2]);
printf("Byte 3: 0x%02X\n", parser.bytes[3]);
3. 硬件寄存器操作
- 不同访问方式的统一:有些硬件寄存器可以通过不同的方式进行访问,例如按位访问或按字节访问。共用体可以将这些不同的访问方式统一起来。
// 定义一个共用体表示一个 16 位的寄存器
union Register16 {
uint16_t value;
struct {
uint8_t lowByte;
uint8_t highByte;
} bytes;
struct {
uint16_t bit0: 1;
uint16_t bit1: 1;
// 其他位...
} bits;
};
// 使用示例:按字节访问寄存器
union Register16 reg;
reg.bytes.lowByte = 0x12;
reg.bytes.highByte = 0x34;
printf("Register value: 0x%04X\n", reg.value);
在给定的共用体 Register16
中,第二个结构体 bits
的作用是提供对 16 位寄存器中每一位的单独访问。通过位域(bit-field)的方式,我们可以将一个 16 位的无符号整数 value
拆分成 16 个独立的位,每个位都可以单独进行读写操作。这种方式在需要对寄存器的每一位进行精确控制的场景中非常有用,比如在嵌入式系统中对硬件寄存器的位操作。
下面是一个详细的示例,展示了如何使用 bits
结构体来访问和操作 16 位寄存器的每一位:
#include <stdio.h>
#include <stdint.h>
// 定义一个共用体表示一个 16 位的寄存器
union Register16 {
uint16_t value;
struct {
uint8_t lowByte;
uint8_t highByte;
} bytes;
struct {
uint16_t bit0: 1;
uint16_t bit1: 1;
uint16_t bit2: 1;
uint16_t bit3: 1;
uint16_t bit4: 1;
uint16_t bit5: 1;
uint16_t bit6: 1;
uint16_t bit7: 1;
uint16_t bit8: 1;
uint16_t bit9: 1;
uint16_t bit10: 1;
uint16_t bit11: 1;
uint16_t bit12: 1;
uint16_t bit13: 1;
uint16_t bit14: 1;
uint16_t bit15: 1;
} bits;
};
int main() {
// 创建一个 Register16 类型的共用体变量
union Register16 reg;
// 初始化寄存器的值
reg.value = 0xABCD; // 二进制表示为 1010 1011 1100 1101
// 访问寄存器的某一位
printf("Bit 0: %d\n", reg.bits.bit0); // 输出第 0 位的值
printf("Bit 15: %d\n", reg.bits.bit15); // 输出第 15 位的值
// 修改寄存器的某一位
reg.bits.bit3 = 1; // 将第 3 位设置为 1
reg.bits.bit12 = 0; // 将第 12 位设置为 0
// 输出修改后寄存器的值
printf("Modified register value: 0x%04X\n", reg.value);
return 0;
}
代码解释
- 共用体定义:定义了一个名为
Register16
的共用体,其中包含三个成员:value
用于存储 16 位的整数值,bytes
结构体用于将 16 位值拆分为高字节和低字节,bits
结构体用于将 16 位值拆分为 16 个独立的位。 - 创建共用体变量:在
main
函数中,创建了一个Register16
类型的共用体变量reg
。 - 初始化寄存器值:将
reg.value
初始化为0xABCD
,即二进制的1010 1011 1100 1101
。 - 访问寄存器的某一位:使用
reg.bits.bitX
的方式访问寄存器的第X
位,并将其值输出。 - 修改寄存器的某一位:通过赋值操作修改寄存器的某一位,例如
reg.bits.bit3 = 1
将第 3 位设置为 1。 - 输出修改后寄存器的值:使用
printf
函数输出修改后寄存器的值。
通过这种方式,我们可以方便地对 16 位寄存器的每一位进行单独的读写操作。