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; // 危险!访问已释放内存
// 正确做法:释放后立即置为NULL
p = 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); // 危险!未定义行为
// 解决方案:释放后立即置NULL
str = 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; // 或者使用malloc
return &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 : b
int 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()
#endif
int 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)); // 输出: 变量名: temperature
PRINT_VAR(temperature); // 输出: temperature = 25
return 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:不再使用的宏及时取消定义
419

被折叠的 条评论
为什么被折叠?



