嵌入式软件知识点汇总(day2)
6、野指针的概念
野指针是指指向无效内存地址的指针。这些指针指向的内存可能已经被释放、未初始化或者根本不存在。
野指针的常见成因
1. 指针未初始化
#include <stdio.h>
#include <stdlib.h>void uninitialized_pointer() {int *p; // 野指针:未初始化,指向随机地址// 危险操作!可能导致程序崩溃// *p = 100; // 未定义行为// 正确做法:初始化为NULL或有效地址int *safe_ptr = NULL;// 或者 int *safe_ptr = &some_variable;
}
2. 指针指向已释放的内存
void freed_memory_pointer() {int *p = (int*)malloc(sizeof(int));*p = 100;free(p); // 释放内存// 此时p成为野指针,指向的内存已不可用// *p = 200; // 危险!访问已释放内存// 正确做法:释放后立即置为NULLp = NULL;
}
3. 返回局部变量的地址
int* return_local_variable() {int local_var = 100; // 局部变量,函数结束时销毁return &local_var; // 错误!返回局部变量的地址// 调用者得到的将是野指针
}// 正确做法:返回动态分配的内存或静态变量
int* safe_return() {int *p = (int*)malloc(sizeof(int));*p = 100;return p; // 正确:返回堆内存地址// 或者使用静态变量// static int static_var = 100;// return &static_var;
}
4. 数组越界访问
void array_out_of_bounds() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr;// 越界访问,p指向数组外的未知内存p = p + 10; // 野指针!// *p = 100; // 危险操作
}
5. 类型转换错误
void wrong_type_cast() {int num = 100;// 错误的类型转换char *p = (char*)num; // 将整数值当作地址使用// *p = 'A'; // 危险!可能访问受保护的内存区域
}
完整的野指针示例与解决方案
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 示例1:未初始化指针
void demo_uninitialized() {printf("=== 未初始化指针示例 ===\n");int *wild_ptr; // 野指针:未初始化// printf("%d\n", *wild_ptr); // 危险!可能崩溃// 解决方案:始终初始化指针int *safe_ptr = NULL;int value = 42;safe_ptr = &value; // 指向有效变量printf("安全指针值: %d\n", *safe_ptr);
}// 示例2:释放后使用
void demo_use_after_free() {printf("\n=== 释放后使用示例 ===\n");char *str = (char*)malloc(100);strcpy(str, "Hello World");printf("分配的内存: %s\n", str);free(str); // 释放内存// str现在成为野指针// printf("释放后: %s\n", str); // 危险!未定义行为// 解决方案:释放后立即置NULLstr = NULL;// 使用前检查if(str != NULL) {printf("%s\n", str);} else {printf("指针已失效\n");}
}// 示例3:返回局部变量地址
int* create_dangerous_array() {int local_arr[3] = {1, 2, 3}; // 局部变量return local_arr; // 错误!返回局部变量地址
}int* create_safe_array() {// 解决方案1:使用动态内存int *arr = (int*)malloc(3 * sizeof(int));arr[0] = 1; arr[1] = 2; arr[2] = 3;return arr;// 解决方案2:使用静态变量// static int static_arr[3] = {1, 2, 3};// return static_arr;
}void demo_return_local() {printf("\n=== 返回局部变量示例 ===\n");// int *dangerous = create_dangerous_array(); // 得到野指针// printf("%d\n", dangerous[0]); // 危险!int *safe = create_safe_array(); // 安全的方式printf("安全数组: %d, %d, %d\n", safe[0], safe[1], safe[2]);free(safe); // 记得释放
}// 嵌入式开发中的实际场景
typedef struct {int sensor_id;float value;uint32_t timestamp;
} sensor_data_t;// 危险的传感器数据处理
sensor_data_t* process_sensor_data_unsafe() {sensor_data_t data; // 局部变量data.sensor_id = 1;data.value = 25.5;data.timestamp = 123456789;return &data; // 错误!返回局部变量地址
}// 安全的传感器数据处理
sensor_data_t* process_sensor_data_safe() {sensor_data_t *data = (sensor_data_t*)malloc(sizeof(sensor_data_t));if(data != NULL) {data->sensor_id = 1;data->value = 25.5;data->timestamp = 123456789;}return data; // 正确:返回堆内存
}int main() {demo_uninitialized();demo_use_after_free();demo_return_local();// 嵌入式应用示例sensor_data_t *sensor_data = process_sensor_data_safe();if(sensor_data != NULL) {printf("\n传感器数据: ID=%d, 值=%.1f, 时间=%u\n", sensor_data->sensor_id, sensor_data->value, sensor_data->timestamp);free(sensor_data);sensor_data = NULL; // 避免野指针}return 0;
}
野指针的危害
程序崩溃:访问受保护的内存区域导致段错误
数据损坏:错误地修改了其他变量或系统数据
安全漏洞:可能被利用进行恶意代码执行
难以调试:问题可能随机出现,难以复现和定位
预防野指针的最佳实践
1. 初始化规则
// 错误
int *p;// 正确
int *p = NULL;
int value = 10;
int *p = &value;
2. 释放后置空
char *str = malloc(100);
// ... 使用str ...
free(str);
str = NULL; // 重要!避免悬空指针
3. 使用前检查
if(ptr != NULL) {// 安全使用指针*ptr = value;
} else {// 错误处理printf("错误:指针为空\n");
}
4. 作用域管理
// 避免返回局部变量地址
int* safe_function() {static int data; // 或者使用mallocreturn &data;
}
嵌入式开发中的特别注意事项
在嵌入式系统中,野指针可能导致更严重的后果:
硬件寄存器被意外修改
看门狗触发系统复位
关键数据丢失
防御性编程在嵌入式开发中尤为重要!
7.数组和链表的区别
核心区别总结
特性 | 数组 | 链表 |
---|---|---|
内存分配 | 连续内存块 | 离散内存,通过指针连接 |
大小固定性 | 大小固定(静态数组) | 大小动态可变 |
访问方式 | 随机访问,O(1)时间复杂度 | 顺序访问,O(n)时间复杂度 |
插入/删除 | 效率低,需要移动元素,O(n) | 效率高,只需修改指针,O(1) |
内存效率 | 无额外开销 | 每个节点需要额外指针空间 |
1. 数组
基本概念
数组在内存中分配连续的存储空间,所有元素依次排列。
#include <stdio.h>#define MAX_SIZE 100 // 固定大小// 静态数组
int static_array[5] = {1, 2, 3, 4, 5};// 动态数组
int* create_dynamic_array(int size) {return (int*)malloc(size * sizeof(int));
}void array_operations() {int arr[5] = {10, 20, 30, 40, 50};// 随机访问 - O(1)时间复杂度printf("arr[2] = %d\n", arr[2]); // 直接计算地址访问// 插入操作 - 需要移动后面所有元素// 在索引2处插入99:需要将30,40,50向后移动for(int i = 4; i > 2; i--) {arr[i] = arr[i-1];}arr[2] = 99;// 删除操作 - 同样需要移动元素// 删除索引2的元素:需要将40,50向前移动for(int i = 2; i < 4; i++) {arr[i] = arr[i+1];}
}
内存布局
数组内存布局(连续): 索引: 0 1 2 3 4 值: [10] [20] [30] [40] [50] 地址: 1000 1004 1008 1012 1016
2. 链表
基本概念
链表由节点组成,每个节点包含数据和指向下一个节点的指针,内存空间不要求连续。
#include <stdio.h>
#include <stdlib.h>// 链表节点定义
typedef struct ListNode {int data;struct ListNode* next;
} ListNode;// 创建新节点
ListNode* create_node(int value) {ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));new_node->data = value;new_node->next = NULL;return new_node;
}// 在链表头部插入节点 - O(1)
void insert_at_head(ListNode** head, int value) {ListNode* new_node = create_node(value);new_node->next = *head;*head = new_node;
}// 在指定位置插入节点
void insert_after(ListNode* prev_node, int value) {if(prev_node == NULL) return;ListNode* new_node = create_node(value);new_node->next = prev_node->next;prev_node->next = new_node;
}// 删除节点 - O(1)(如果已知前驱节点)
void delete_node(ListNode** head, int key) {ListNode* temp = *head;ListNode* prev = NULL;// 如果要删除的是头节点if(temp != NULL && temp->data == key) {*head = temp->next;free(temp);return;}// 查找要删除的节点及其前驱while(temp != NULL && temp->data != key) {prev = temp;temp = temp->next;}if(temp == NULL) return; // 没找到// 删除节点prev->next = temp->next;free(temp);
}// 遍历链表 - O(n)
void print_list(ListNode* head) {ListNode* current = head;while(current != NULL) {printf("%d -> ", current->data);current = current->next;}printf("NULL\n");
}// 查找元素 - O(n)
ListNode* search(ListNode* head, int key) {ListNode* current = head;while(current != NULL) {if(current->data == key) {return current;}current = current->next;}return NULL;
}
内存布局
链表内存布局(离散): 节点1: data=10, next→节点2地址(0x2000) 节点2: data=20, next→节点3地址(0x3000) 节点3: data=30, next→NULL 内存地址可能:节点1@0x1000, 节点2@0x2000, 节点3@0x3000
3.对比示例
#include <stdio.h>
#include <stdlib.h>
#include <time.h>// 数组操作
void array_performance() {printf("=== 数组性能测试 ===\n");int arr[10000];clock_t start, end;// 随机访问start = clock();for(int i = 0; i < 10000; i++) {arr[i] = i; // O(1)}end = clock();printf("数组随机访问时间: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 插入操作(在开头插入)start = clock();// 需要在索引0插入,移动所有元素 - O(n)for(int i = 9999; i > 0; i--) {arr[i] = arr[i-1];}arr[0] = -1;end = clock();printf("数组插入操作时间: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}// 链表操作
typedef struct Node {int data;struct Node* next;
} Node;void linked_list_performance() {printf("\n=== 链表性能测试 ===\n");Node* head = NULL;clock_t start, end;// 创建链表for(int i = 0; i < 10000; i++) {Node* new_node = (Node*)malloc(sizeof(Node));new_node->data = i;new_node->next = head;head = new_node;}// 顺序访问 - O(n)start = clock();Node* current = head;while(current != NULL) {int value = current->data; // 必须遍历current = current->next;}end = clock();printf("链表顺序访问时间: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 插入操作(在开头插入)- O(1)start = clock();Node* new_node = (Node*)malloc(sizeof(Node));new_node->data = -1;new_node->next = head;head = new_node;end = clock();printf("链表插入操作时间: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 释放链表内存while(head != NULL) {Node* temp = head;head = head->next;free(temp);}
}int main() {array_performance();linked_list_performance();return 0;
}
嵌入式开发中的实际应用场景
适合使用数组的场景:
// 1. 固定大小的配置参数
#define CONFIG_SIZE 50
uint32_t device_config[CONFIG_SIZE];// 2. 传感器数据缓冲区(大小固定)
float sensor_readings[100]; // 存储最近100次读数// 3. 查找表(LUT)
const uint16_t sin_lut[360] = {0, 1, 2, ...}; // 正弦查找表// 4. 图像像素数据
uint8_t frame_buffer[320][240]; // 固定分辨率的图像
适合使用链表的场景:
// 1. 动态任务队列(任务数量变化)
typedef struct {void (*task_func)(void*);void* parameter;struct Task* next;
} Task;Task* task_queue_head = NULL;// 2. 动态事件管理
typedef struct {uint32_t event_id;uint32_t timestamp;struct Event* next;
} Event;// 3. 动态内存管理(内存块链表)
typedef struct MemoryBlock {void* address;size_t size;struct MemoryBlock* next;
} MemoryBlock;// 4. 协议数据包队列(数量不确定)
typedef struct Packet {uint8_t* data;uint16_t length;struct Packet* next;
} Packet;
选择指南
选择数组的情况:
数据量固定或可预测上限
需要频繁随机访问
内存空间紧张(链表有指针开销)
缓存性能要求高
嵌入式资源受限环境
选择链表的情况:
数据量动态变化,频繁插入删除
主要进行顺序访问
内存碎片化严重
需要动态增长的数据结构
实现队列、栈等动态结构
嵌入式开发特别考虑
内存限制:链表每个节点有指针开销(通常4-8字节)
实时性:数组随机访问快,链表插入删除快
内存碎片:数组连续分配,链表可能产生碎片
缓存效率:数组对CPU缓存更友好
8.宏定义的特点
宏定义会对内容直接替换,而不进行语法检查
如#difine MIN(a,b) ( (a) <= (b) ? (a) : (b) )
宏定义是C/C++预处理器的核心功能之一,在嵌入式开发中应用极其广泛。以下是宏定义的主要特点:
1. 预处理阶段处理
宏在编译之前由预处理器处理,进行简单的文本替换。
#include <stdio.h>#define PI 3.14159
#define SQUARE(x) ((x) * (x))int main() {// 编译前:SQUARE(5) 被替换为 ((5) * (5))double area = PI * SQUARE(5);// 编译前被替换为:double area = 3.14159 * ((5) * (5));printf("面积: %.2f\n", area);return 0;
}
2. 简单的文本替换
宏不做语法检查,只是机械的文本替换。
#include <stdio.h>#define MAX(a, b) a > b ? a : bint main() {int x = 5, y = 10;// 正确使用int result1 = MAX(x, y); // 替换为:x > y ? x : y// 有问题的使用 - 由于运算符优先级int result2 = MAX(x & 0xFF, y & 0xFF);// 替换为:x & 0xFF > y & 0xFF ? x & 0xFF : y & 0xFF// 实际相当于:x & (0xFF > y) & 0xFF ? ...// 应该使用括号:MAX((x & 0xFF), (y & 0xFF))return 0;
}
3. 无类型检查
宏参数没有类型限制,任何类型都可以使用。
#include <stdio.h>#define MAX(a, b) ((a) > (b) ? (a) : (b))int main() {// 可以用于各种类型int int_max = MAX(10, 20);double double_max = MAX(3.14, 2.71);char char_max = MAX('A', 'B');printf("整型最大: %d\n", int_max);printf("浮点最大: %.2f\n", double_max);printf("字符最大: %c\n", char_max);return 0;
}
4. 提高代码可读性和可维护性
// 嵌入式开发中的典型应用
#define LED_ON() GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET)
#define LED_OFF() GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET)
#define LED_TOGGLE() GPIO_TogglePin(LED_PORT, LED_PIN)#define ENABLE_INTERRUPTS() __enable_irq()
#define DISABLE_INTERRUPTS() __disable_irq()#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))// 使用示例
void blink_led() {LED_ON();delay_ms(100);LED_OFF();delay_ms(100);
}
5. 避免函数调用开销
对于简单的操作,宏可以避免函数调用的开销。
#include <stdio.h>
#include <time.h>// 函数版本
int max_function(int a, int b) {return a > b ? a : b;
}// 宏版本
#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))// 测试性能
void performance_test() {int x = 10, y = 20, result;clock_t start, end;long iterations = 100000000L;// 测试函数调用start = clock();for(long i = 0; i < iterations; i++) {result = max_function(x, y);}end = clock();printf("函数版本时间: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);// 测试宏start = clock();for(long i = 0; i < iterations; i++) {result = MAX_MACRO(x, y);}end = clock();printf("宏版本时间: %f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}
6. 条件编译
宏在条件编译中起关键作用。
#include <stdio.h>// 调试模式开关
#define DEBUG 1// 平台选择
#define PLATFORM_STM32
// #define PLATFORM_ESP32#ifdef DEBUG#define DBG_PRINT(fmt, ...) printf("DEBUG: " fmt, ##__VA_ARGS__)
#else#define DBG_PRINT(fmt, ...) // 空定义,调试代码被移除
#endif#ifdef PLATFORM_STM32#define HARDWARE_INIT() stm32_hardware_init()
#elif defined(PLATFORM_ESP32)#define HARDWARE_INIT() esp32_hardware_init()
#else#define HARDWARE_INIT() default_hardware_init()
#endifint main() {HARDWARE_INIT();DBG_PRINT("系统初始化完成\n");DBG_PRINT("传感器值: %d\n", 123);return 0;
}
7. 宏的特殊操作符
#
- 字符串化操作符
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)int main() {int temperature = 25;printf("变量名: %s\n", STRINGIFY(temperature)); // 输出: 变量名: temperaturePRINT_VAR(temperature); // 输出: temperature = 25return 0;
}
8. 嵌入式开发中的实际应用
硬件寄存器操作
// STM32 GPIO寄存器操作宏
#define GPIO_BASE 0x40020000
#define GPIOA_ODR (GPIO_BASE + 0x14)#define SET_BIT(reg, bit) ((reg) |= (1U << (bit)))
#define CLEAR_BIT(reg, bit) ((reg) &= ~(1U << (bit)))
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1U << (bit)))
#define READ_BIT(reg, bit) (((reg) >> (bit)) & 1U)// 使用示例
#define LED_PIN 5
void control_led() {volatile uint32_t *gpio_odr = (uint32_t*)GPIOA_ODR;SET_BIT(*gpio_odr, LED_PIN); // 开灯CLEAR_BIT(*gpio_odr, LED_PIN); // 关灯TOGGLE_BIT(*gpio_odr, LED_PIN); // 切换
}
错误处理和安全检查
// 安全检查宏
#define CHECK_NULL(ptr) \do { \if ((ptr) == NULL) { \printf("错误: 空指针 at %s:%d\n", __FILE__, __LINE__); \return -1; \} \} while(0)#define CHECK_RANGE(val, min, max) \do { \if ((val) < (min) || (val) > (max)) { \printf("错误: 值%d超出范围[%d,%d] at %s:%d\n", \(val), (min), (max), __FILE__, __LINE__); \return -1; \} \} while(0)// 使用示例
int process_data(int *data, int size) {CHECK_NULL(data);CHECK_RANGE(size, 1, 100);// 正常处理...return 0;
}
宏定义的优缺点总结
优点:
提高代码可读性:用有意义的名称代替魔术数字
便于修改:只需修改宏定义,所有使用处自动更新
避免函数调用开销:适合简单的频繁操作
实现条件编译:根据不同平台或配置编译不同代码
类型无关:可以用于各种数据类型
缺点:
无类型检查:容易出错,调试困难
可能产生副作用:参数可能被多次求值
代码膨胀:每次使用都会展开,增加代码大小
作用域问题:宏没有作用域概念
调试困难:调试器看到的是展开后的代码
最佳实践建议
多用括号:
#define SQUARE(x) ((x) * (x))
避免副作用的参数:不要使用
MAX(a++, b++)
复杂的逻辑用函数:不要用宏实现复杂算法
使用大写字母:便于区分宏和其他标识符
及时
#undef
:不再使用的宏及时取消定义