在嵌入式编程中,参数使用指针类型还是非指针类型是一个重要的设计决策,这主要涉及性能、内存使用和函数行为等方面的考虑。下面我详细解析两者的区别、用法和应用场景,并提供STM32F103的代码示例。
一、指针的值传递的基本区别
| 特性 | 非指针类型(值传递) | 指针类型(地址传递) |
|---|---|---|
| 内存开销 | 复制整个数据到栈 | 只复制地址(4字节) |
| 性能 | 大数据类型性能差 | 大数据类型性能好 |
| 修改原数据 | 不能修改原数据 | 可以修改原数据 |
| 数据一致性 | 有独立副本 | 共享同一数据 |
| NULL检查 | 不需要 | 需要检查NULL指针 |
二、详细解析与代码示例
1. 基本数据类型:uint8_t vs uint8_t*
objectivec
#include "stm32f1xx_hal.h"
// 场景1:不需要修改原值,小数据 - 使用值传递
uint8_t process_value(uint8_t data) {
// 对data进行处理但不影响原数据
return data * 2;
}
// 场景2:需要修改原值 - 使用指针传递
void increment_value(uint8_t *data) {
if (data != NULL) { // 指针必须检查NULL
*data += 1;
}
}
// 场景3:嵌入式硬件寄存器操作 - 必须使用指针
void configure_gpio(GPIO_TypeDef *GPIOx) {
if (GPIOx != NULL) {
GPIOx->CRL = 0x44444444; // 直接操作寄存器
GPIOx->ODR = 0x0000;
}
}
void example_basic(void) {
uint8_t sensor_value = 100;
uint8_t result;
// 值传递:原数据不变
result = process_value(sensor_value);
// sensor_value仍然是100
// 指针传递:修改原数据
increment_value(&sensor_value);
// sensor_value变为101
// 硬件操作
configure_gpio(GPIOA); // 传递GPIOA的地址
}
2. 结构体:struct vs struct*
objectivec
// 定义传感器数据结构
typedef struct {
uint16_t temperature;
uint16_t humidity;
uint32_t timestamp;
uint8_t status;
} SensorData;
// 场景1:小结构体,不需要修改 - 值传递(不常见)
void print_sensor_data(SensorData data) {
printf("Temp: %d, Hum: %d\n",
data.temperature, data.humidity);
// 这里修改data不会影响原结构体
}
// 场景2:大结构体或需要修改 - 指针传递(推荐)
void update_sensor_data(SensorData *data) {
if (data != NULL) {
data->temperature = read_temperature();
data->humidity = read_humidity();
data->timestamp = HAL_GetTick();
data->status = 0x01;
}
}
// 场景3:只读访问大结构体 - 使用const指针
void log_sensor_data(const SensorData *data) {
if (data != NULL) {
// 只能读取,不能修改
uint16_t temp = data->temperature;
uint16_t hum = data->humidity;
// data->temperature = 0; // 编译错误!
}
}
void example_struct(void) {
// 值传递:产生副本(占用栈空间)
SensorData sensor1 = {25, 60, 0, 0};
print_sensor_data(sensor1); // 复制整个结构体
// 指针传递:高效
SensorData sensor2;
update_sensor_data(&sensor2); // 只传递地址
// const指针:安全读取
log_sensor_data(&sensor2);
}
3. 数组:必须使用指针传递
objectivec
// 数组作为参数总是退化为指针
void process_array(uint8_t *array, uint32_t size) {
for (uint32_t i = 0; i < size; i++) {
array[i] = i * 2; // 修改原数组
}
}
// 二维数组必须指定第二维大小
void process_matrix(uint8_t matrix[][4], uint32_t rows) {
for (uint32_t i = 0; i < rows; i++) {
for (uint32_t j = 0; j < 4; j++) {
matrix[i][j] = i + j;
}
}
}
void example_array(void) {
uint8_t buffer[100];
uint8_t matrix[3][4];
process_array(buffer, sizeof(buffer));
process_matrix(matrix, 3);
}
4. 嵌入式特定场景示例
objectivec
// 场景1:DMA传输 - 必须使用指针
void start_adc_dma(ADC_HandleTypeDef *hadc, uint16_t *buffer, uint32_t size) {
HAL_ADC_Start_DMA(hadc, (uint32_t*)buffer, size);
}
// 场景2:中断回调 - 指针传递上下文
typedef struct {
UART_HandleTypeDef *huart;
uint8_t rx_buffer[64];
uint32_t rx_index;
} UART_Context;
void uart_receive_callback(UART_Context *ctx) {
if (ctx != NULL) {
// 处理接收数据
ctx->rx_buffer[ctx->rx_index++] =
(uint8_t)(ctx->huart->Instance->DR & 0xFF);
}
}
// 场景3:RTOS任务参数传递
void os_task_function(void *argument) {
TaskParams *params = (TaskParams*)argument;
if (params != NULL) {
// 使用参数
}
}
// 场景4:节省内存的配置结构
typedef struct {
uint32_t baud_rate;
uint8_t data_bits;
uint8_t parity;
uint8_t stop_bits;
} UART_Config;
// 传递指针,避免复制
void uart_init(UART_HandleTypeDef *huart, const UART_Config *config) {
if (huart != NULL && config != NULL) {
huart->Init.BaudRate = config->baud_rate;
huart->Init.WordLength = config->data_bits;
// ... 其他配置
HAL_UART_Init(huart);
}
}
5. 性能对比示例
objectivec
#include <stdint.h>
#include <string.h>
// 大型结构体
typedef struct {
uint8_t data[1024]; // 1KB数据
uint32_t checksum;
} LargeData;
// 错误:值传递大型结构体 - 栈溢出风险!
void process_large_data_bad(LargeData data) {
// 每次调用复制1KB数据到栈
// 在STM32F103(可能只有20KB RAM)中可能导致问题
}
// 正确:使用指针传递
void process_large_data_good(const LargeData *data) {
if (data != NULL) {
// 只传递4字节地址
uint32_t checksum = data->checksum;
// 处理数据
}
}
// 测试函数
void test_performance(void) {
LargeData my_data;
// 填充数据
memset(&my_data, 0xAA, sizeof(LargeData));
// 错误用法:复制1KB数据到栈
// process_large_data_bad(my_data); // 危险!
// 正确用法:只传递指针
process_large_data_good(&my_data);
}
三、何时使用指针类型 vs 非指针类型
使用非指针类型(值传递)的情况:
1.基本数据类型且不需要修改原值
objectivec
uint8_t calculate(uint8_t a, uint8_t b) {
return a + b; // 不需要修改a和b
}
2.枚举类型 或小数据类型
objectivec
typedef enum {RED, GREEN, BLUE} LED_Color;
void set_color(LED_Color color); // 枚举很小,值传递即可
使用指针类型的情况:
1.需要修改函数外部的变量
objectivec
void get_sensor_value(uint16_t *output) {
*output = read_adc();
}
2.传递大型结构体或数组
objectivec
void process_frame(const FrameBuffer *frame); // FrameBuffer很大
3.动态内存分配的数据
objectivec
void free_buffer(uint8_t *buffer) {
free(buffer); // 必须用指针
}
4.硬件寄存器操作
objectivec
void enable_clock(RCC_TypeDef *rcc) {
rcc->APB2ENR |= RCC_APB2ENR_IOPAEN;
}
5.实现"多个返回值"
objectivec
uint8_t parse_packet(const uint8_t *data,
uint16_t *length,
uint8_t *type) {
*length = data[0] << 8 | data[1];
*type = data[2];
return data[3];
}
最后链表、树等数据结构
objectivec
void insert_node(Node **head, Node *new_node) {
new_node->next = *head;
*head = new_node;
}
四、最佳实践总结
嵌入式黄金规则:
objectivec
// 对于结构体,总是使用指针
void process_data(const DataStruct *input, DataStruct *output);
// 对于基本类型,根据是否需要修改决定
uint16_t read_value(void); // 返回值
void write_value(uint16_t *dest); // 通过指针写入
使用const正确性:
objectivec
// 输入参数:const指针
void display_data(const SensorData *data);
// 输出参数:非const指针
void read_sensor(SensorData *result);
// 输入输出参数:无const
void transform_data(DataStruct *data);
STM32特定建议:
objectivec
// HAL库风格:总是传递句柄指针
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, ...);
// 寄存器操作:直接使用预定义指针
GPIOA->ODR = 0xFFFF; // GPIOA就是指针
// DMA:必须传递缓冲区指针
HAL_DMA_Start(&hdma, src_addr, dst_addr, length);
最后是内存相关的处理
objectivec
// 在栈空间有限的嵌入式系统中
void process_on_stack(void) { // 小心栈溢出
LargeStruct data; // 可能太大
}
void process_with_pointer(void) { // 更安全
LargeStruct *data = malloc(sizeof(LargeStruct));
// 处理数据
free(data);
}
五、完整示例:STM32F103传感器数据处理
objectivec
#include "stm32f1xx_hal.h"
#include <stdbool.h>
// 传感器配置结构
typedef struct {
uint8_t address;
uint32_t sample_rate;
bool calibrated;
float calibration_factor;
} SensorConfig;
// 传感器数据结构
typedef struct {
int16_t raw_value;
float processed_value;
uint32_t timestamp;
uint8_t status;
} SensorData;
// 初始化传感器(需要修改配置,用指针)
bool sensor_init(SensorConfig *config) {
if (config == NULL) return false;
config->calibrated = false;
config->calibration_factor = 1.0f;
// 硬件初始化代码
// ...
return true;
}
// 读取传感器数据(输出参数用指针)
bool sensor_read(SensorData *output, const SensorConfig *config) {
if (output == NULL || config == NULL) return false;
// 读取ADC值
output->raw_value = read_adc_channel(config->address);
output->timestamp = HAL_GetTick();
// 应用校准
if (config->calibrated) {
output->processed_value =
output->raw_value * config->calibration_factor;
} else {
output->processed_value = output->raw_value;
}
return true;
}
// 批量处理数据(数组用指针)
void process_sensor_batch(SensorData *batch, uint32_t count) {
if (batch == NULL || count == 0) return;
for (uint32_t i = 0; i < count; i++) {
// 滤波处理
batch[i].processed_value =
apply_low_pass_filter(batch[i].processed_value);
// 检查状态
if (batch[i].raw_value > 4095) { // 假设12位ADC
batch[i].status = 0x80; // 溢出标志
}
}
}
// 主函数示例
int main(void) {
// HAL初始化
HAL_Init();
SystemClock_Config();
// 初始化配置
SensorConfig my_config = {
.address = ADC_CHANNEL_0,
.sample_rate = 1000,
.calibrated = false,
.calibration_factor = 1.0f
};
sensor_init(&my_config); // 传递指针以修改配置
// 读取传感器数据
SensorData current_data;
if (sensor_read(¤t_data, &my_config)) {
// 数据处理...
}
// 批量处理
SensorData data_batch[10];
for (int i = 0; i < 10; i++) {
sensor_read(&data_batch[i], &my_config);
}
process_sensor_batch(data_batch, 10);
while (1) {
// 主循环
}
}
六、关键要点总结
-
指针传递用于:
-
修改函数外部变量
-
传递大型数据避免复制
-
硬件寄存器操作
-
动态内存管理
-
数组操作
-
-
值传递用于:
-
基本数据类型且不需要修改
-
小型数据(< 4-8字节)
-
确保数据不被修改的场景
-
-
在STM32F103中特别注意:
-
栈空间有限(通常4-20KB),避免大数据值传递
-
硬件外设操作必须用指针
-
DMA操作需要缓冲区指针
-
中断回调函数通常需要上下文指针
-
-
安全方面:
-
指针参数总是检查NULL
-
输入参数使用const修饰
-
文档化函数的参数所有权
-
在性能关键路径避免不必要的数据复制
-