嵌入式软件知识点汇总(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;  // 危险!访问已释放内存
    
    // 正确做法:释放后立即置为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. 程序崩溃:访问受保护的内存区域导致段错误

  2. 数据损坏:错误地修改了其他变量或系统数据

  3. 安全漏洞:可能被利用进行恶意代码执行

  4. 难以调试:问题可能随机出现,难以复现和定位

预防野指针的最佳实践

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;

选择指南

选择数组的情况:

  • 数据量固定或可预测上限

  • 需要频繁随机访问

  • 内存空间紧张(链表有指针开销)

  • 缓存性能要求高

  • 嵌入式资源受限环境

选择链表的情况:

  • 数据量动态变化,频繁插入删除

  • 主要进行顺序访问

  • 内存碎片化严重

  • 需要动态增长的数据结构

  • 实现队列、栈等动态结构

嵌入式开发特别考虑

  1. 内存限制:链表每个节点有指针开销(通常4-8字节)

  2. 实时性:数组随机访问快,链表插入删除快

  3. 内存碎片:数组连续分配,链表可能产生碎片

  4. 缓存效率:数组对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;
}

宏定义的优缺点总结

优点:

  1. 提高代码可读性:用有意义的名称代替魔术数字

  2. 便于修改:只需修改宏定义,所有使用处自动更新

  3. 避免函数调用开销:适合简单的频繁操作

  4. 实现条件编译:根据不同平台或配置编译不同代码

  5. 类型无关:可以用于各种数据类型

缺点:

  1. 无类型检查:容易出错,调试困难

  2. 可能产生副作用:参数可能被多次求值

  3. 代码膨胀:每次使用都会展开,增加代码大小

  4. 作用域问题:宏没有作用域概念

  5. 调试困难:调试器看到的是展开后的代码

最佳实践建议

  1. 多用括号#define SQUARE(x) ((x) * (x))

  2. 避免副作用的参数:不要使用MAX(a++, b++)

  3. 复杂的逻辑用函数:不要用宏实现复杂算法

  4. 使用大写字母:便于区分宏和其他标识符

  5. 及时#undef:不再使用的宏及时取消定义

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值