嵌入式(C语言篇)Day11
嵌入式Day11
一、动态内存分配核心函数
(一)函数列表
函数名 | 功能 | 头文件 | 返回值 |
---|---|---|---|
malloc | 分配连续的size 字节堆内存 | stdlib.h | 成功返回首地址(void* ),失败返回NULL |
calloc | 分配num 个元素×size 字节/元素的堆内存,自动初始化为0 | stdlib.h | 成功返回首地址(void* ),失败返回NULL |
realloc | 调整已分配内存块的大小 | stdlib.h | 成功返回新首地址(void* ),失败返回NULL |
free | 释放堆内存块 | stdlib.h | 无 |
(二)通用注意事项
- 返回值处理
- 必须用指针接收返回值,推荐显式强转(如
(int*)malloc(...)
)。 - 严格平台(如C++)必须显式强转,普通C环境可隐式转换。
- 必须用指针接收返回值,推荐显式强转(如
- 错误处理
- 分配函数(
malloc/calloc/realloc
)调用后必须判断返回值是否为NULL
,失败时需进行错误处理(如打印日志、退出程序)。
- 分配函数(
- 内存释放
- 不再使用的内存块必须用
free
释放,否则导致内存泄漏。 free
参数必须是分配函数返回的原始指针,不可是移动后的指针。- 释放后建议将指针置为
NULL
,避免野指针问题。
- 不再使用的内存块必须用
二、核心函数详解
(一)malloc
:基础内存分配
void* malloc(size_t size); // 分配size字节的连续堆内存
特点
- 参数:仅一个参数
size
,表示目标内存块的字节大小。 - 返回值:成功返回首地址,失败返回
NULL
。 - 初始化:分配的内存块未初始化,包含随机值,使用前需手动初始化。
示例
int* p = (int*)malloc(5 * sizeof(int)); // 分配5个int的数组
if (p == NULL) {perror("malloc failed");exit(1);
}
// 使用后释放
free(p);
p = NULL; // 置空避免野指针
(二)free
:内存释放
void free(void* ptr); // 释放ptr指向的堆内存块
注意事项
- 参数要求:必须传入分配函数返回的原始指针,否则程序可能崩溃(如传入移动后的指针)。
- 释放后处理:释放后指针变为野指针,需手动置
NULL
。 - 重复释放:同一块内存不可释放两次,否则导致程序崩溃。
扩展思考
- free是否清理数据:不清理,仅告知操作系统内存可重用,原有数据变为垃圾数据。
- MSVC平台特殊值:
- 未初始化的
malloc
内存填充0xCD
(标记“已分配未初始化”)。 - 未初始化的局部变量填充
0xCC
。 free
后的内存填充0xDD
(标记“已释放”)。
- 未初始化的
(三)calloc
:清零内存分配
void* calloc(size_t num, size_t size); // 分配num个元素×size字节/元素的内存,自动初始化为0
特点
- 参数:
num
为元素数量,size
为单个元素大小,总分配大小为num×size
字节。 - 初始化:分配的内存块自动填充为0,无需手动初始化。
- 适用场景:需要初始化为0的数组或结构体,如二叉树节点(指针自动置
NULL
)。
与malloc
对比
特性 | malloc | calloc |
---|---|---|
初始化 | 无,需手动处理 | 自动初始化为0 |
参数数量 | 1个(总字节数) | 2个(元素数×单元素大小) |
性能 | 较高(无初始化) | 较低(需清零内存) |
安全性 | 可能因未初始化导致未定义行为 | 更安全,避免随机值问题 |
(四)realloc
:内存重分配
void* realloc(void* ptr, size_t new_size); // 调整ptr指向的内存块大小为new_size字节
特殊行为
ptr=NULL
:等价于malloc(new_size)
。new_size=0
:等价于free(ptr)
,但ptr
不可为NULL
(否则无意义)。
正常行为(ptr≠NULL
且new_size≠0
)
new_size=原大小
:不操作,直接返回ptr
。new_size<原大小
:从高地址截断内存块,截断部分自动释放。new_size>原大小
:- 优先原地扩容:若原内存块后方有足够空间,直接扩展(未初始化新增区域)。
- 异地扩容:若空间不足,分配新内存块,复制原数据,释放旧块(程序员无需手动处理旧块)。
调用规范
int* p = (int*)malloc(5 * sizeof(int));
int new_size = 10;
int* temp = (int*)realloc(p, new_size); // 用临时指针接收新地址
if (temp == NULL) {free(p); // 若扩容失败,释放原内存exit(1);
}
p = temp; // 更新原始指针
// 扩容后新增区域(5~9号元素)未初始化,需手动处理
三、内存问题与优化
(一)内存泄漏 vs 内存溢出
问题 | 定义 | 后果 |
---|---|---|
内存泄漏 | 分配的内存未释放,长期积累导致可用内存减少 | 短期无明显影响,长期可能引发内存溢出 |
内存溢出 | 申请内存超过系统可用空间,或访问越界 | 程序崩溃、数据损坏 |
(二)避免内存泄漏的关键措施
- 正确调用
free
:确保传入原始指针,释放前检查指针是否被移动。 - 释放后置空指针:
free(p); p = NULL;
,避免“二次释放”和野指针。 - 谨慎修改堆指针:若需移动指针(如遍历数组),使用临时指针(如
int* temp = p;
)。 - 单一职责原则:同一内存块的分配与释放由同一函数管理,避免多函数交叉操作。
(三)性能与安全权衡
- 优先
malloc
场景:需手动赋值非0值,或对性能敏感的场景。 - 优先
calloc
场景:需初始化为0值,或对安全性要求高(如避免随机值导致的bug)。 realloc
使用建议:扩容后需手动初始化新增区域,避免使用未定义值。
四、代码示例:动态数组操作
#include <stdio.h>
#include <stdlib.h>int main() {// 分配5个int的数组(malloc)int arr_len = 5;int* p = (int*)malloc(arr_len * sizeof(int));if (p == NULL) {perror("malloc failed");exit(1);}// 手动初始化for (int i = 0; i < arr_len; i++) {p[i] = i + 1;}// 扩容至10个元素(realloc)int new_len = 10;int* temp = (int*)realloc(p, new_len * sizeof(int));if (temp == NULL) {free(p);exit(1);}p = temp;// 初始化新增区域for (int i = arr_len; i < new_len; i++) {p[i] = 0;}// 释放内存free(p);p = NULL;return 0;
}
五、动态数组Vector核心概念
(一)基本定义
-
动态数组特性:具有初始容量,可在容量不足时自动扩容,存储元素数量(
size
)随数据增减变化,实际内存容量(capacity
)按需扩展。 -
内存模型:通过结构体管理,包含以下成员:
typedef struct {ElementType *data; // 指向堆内存的数组指针int size; // 当前已存储元素数量int capacity; // 动态数组总容量 } Vector;
(二)核心操作场景
- 初始化:创建空动态数组,分配初始容量(如默认
DEFAULT_CAPACITY
)。 - 扩容:当
size == capacity
时,自动扩展容量以容纳新元素。 - 元素操作:支持末尾添加、头部插入、指定位置插入等操作,涉及元素移位和内存重分配。
六、模块化编程与头文件设计
(一)模块化编程原则
- 分模块实现:
.h
头文件:声明对外接口(函数原型、结构体定义、宏定义等)。.c
源文件:实现具体功能,包含头文件以暴露接口。
- 设计目标:低耦合、高内聚,模块间通过明确接口交互,隐藏内部实现细节。
(二)头文件内容规范
- 必含内容:
- 函数声明(如
Vector* vector_create();
)。 - 结构体类型定义(如
Vector
结构体)。 - 类型别名(如
typedef int ElementType;
)。 - 宏定义(如
#define DEFAULT_CAPACITY 10
)。
- 函数声明(如
- 禁止内容:
- 函数具体实现代码(应放在
.c
文件中)。 - 非公开的内部函数(用
static
修饰,仅限当前文件使用)。
- 函数具体实现代码(应放在
(三)头文件保护语法
-
作用:避免头文件被重复包含,防止编译错误。
-
标准写法:
#ifndef VECTOR_H // 宏名通常与头文件同名(全大写,下划线分隔) #define VECTOR_H // 头文件内容 #endif
-
替代方案:
#pragma once
(非C标准,但现代编译器普遍支持,写法更简洁)。
七、动态数组Vector实现细节
(一)关键函数实现
1. vector_create
:初始化动态数组
Vector* vector_create() {// 1. 分配Vector结构体内存(自动初始化为0)Vector* v = (Vector*)calloc(1, sizeof(Vector));if (v == NULL) {perror("calloc Vector failed");return NULL;}// 2. 分配初始数组内存(默认容量,自动清零)v->data = (ElementType*)calloc(DEFAULT_CAPACITY, sizeof(ElementType));if (v->data == NULL) {free(v); // 分配失败时释放结构体perror("calloc array failed");return NULL;}v->capacity = DEFAULT_CAPACITY; // 初始化容量return v;
}
2. vector_destroy
:销毁动态数组
void vector_destroy(Vector* v) {free(v->data); // 释放数组内存free(v); // 释放结构体内存
}
3. vector_print
:遍历打印数组
void vector_print(const Vector* v) {printf("[");for (size_t i = 0; i < v->size; i++) {printf("%d, ", v->data[i]);}printf("\b\b]\n"); // 去除末尾多余逗号
}
(二)扩容机制
1. 扩容策略
-
阈值判断:若当前容量小于扩展阈值(
EXPAND_THRESHOLD
),按2倍扩容;否则按1.5倍扩容(避免大数组过度扩容)。 -
代码实现:
static void grow_capacity(Vector* v) {int old_capacity = v->capacity;int new_capacity = (old_capacity < EXPAND_THRESHOLD) ?(old_capacity << 1) : // 左移1位等价于×2(old_capacity + (old_capacity >> 1)); // 右移1位等价于÷2,即1.5倍// 调用realloc调整内存大小ElementType* temp = (ElementType*)realloc(v->data, new_capacity * sizeof(ElementType));if (temp == NULL) {perror("realloc failed");exit(1); // 扩容失败时终止程序}v->data = temp; // 更新数组指针v->capacity = new_capacity; // 更新容量 }
2. 触发场景
- 当调用
vector_push_back
/vector_insert
等函数时,若size == capacity
,自动触发扩容。
(三)元素添加操作
1. vector_push_back
:末尾添加元素
void vector_push_back(Vector* v, ElementType element) {if (v->size == v->capacity) { // 容量不足时扩容grow_capacity(v);}v->data[v->size++] = element; // 直接追加到末尾
}
2. vector_insert
:指定位置插入元素
void vector_insert(Vector* v, int idx, ElementType val) {// 参数校验:确保索引合法(0 ≤ idx ≤ size)if (idx < 0 || idx > v->size) {fprintf(stderr, "Illegal index: %d, size=%d\n", idx, v->size);exit(1);}if (v->size == v->capacity) { // 容量不足时扩容grow_capacity(v);}// 元素后移:从末尾开始,将[idx, size-1]的元素右移一位for (int i = v->size - 1; i >= idx; i--) {v->data[i + 1] = v->data[i];}v->data[idx] = val; // 插入新元素v->size++; // 更新元素数量
}
八、注意事项与优化点
(一)内存管理要点
- 初始化与销毁配对:
vector_create
与vector_destroy
必须成对调用,避免内存泄漏。 - 扩容时的临时指针:使用
realloc
时先用临时指针接收返回值,确保扩容失败时不丢失原始指针。
(二)性能优化
- 扩容策略选择:小容量时2倍扩容(快速增长),大容量时1.5倍扩容(减少内存浪费)。
- 元素移位方向:插入操作从后往前移位,避免覆盖未处理的元素(如
vector_insert
中倒序遍历)。
(三)接口设计建议
- 隐藏内部函数:扩容函数
grow_capacity
用static
修饰,不暴露给头文件,符合模块化封装原则。 - 参数校验:关键函数(如
vector_insert
)需校验输入合法性,增强程序鲁棒性。
九、代码示例:动态数组基本操作
#include "vector.h" // 假设头文件已包含相关声明int main() {Vector* v = vector_create(); // 创建动态数组if (v == NULL) return 1;// 末尾添加元素vector_push_back(v, 10);vector_push_back(v, 20);vector_push_back(v, 30);// 插入元素到索引1的位置vector_insert(v, 1, 15); // 数组变为[10, 15, 20, 30]vector_print(v); // 输出: [10, 15, 20, 30]vector_destroy(v); // 销毁数组,释放内存return 0;
}
十、二级指针基础概念
(一)定义与语法
-
二级指针:指向一级指针的指针,用于间接操作原始数据或修改一级指针的指向。
int a = 10; // 原始数据 int *p = &a; // 一级指针,指向a的地址 int **pp = &p; // 二级指针,指向p的地址
-
解引用操作:
*pp
:获取一级指针p
的值(即a
的地址)。**pp
:获取a
的值(即10
)。
(二)核心作用
-
修改一级指针的指向:当函数需要改变实参指针的指向时,需传递二级指针。
void modify_ptr(int **pp) {int b = 20;*pp = &b; // 通过二级指针修改一级指针的指向 }
-
对比一级指针:
- 一级指针可修改所指内容,但无法修改自身指向(实参指针的指向不变)。
- 二级指针可通过解引用
*pp
修改一级指针的指向,或通过**pp
修改所指内容。
十一、单链表基本概念与结构
(一)核心术语
-
结点(Node):
- 组成链表的基本单元,包含数据域(
data
)和指针域(next
,指向下一结点)。
typedef struct Node {int data; // 数据域struct Node *next; // 指针域 } Node;
- 组成链表的基本单元,包含数据域(
-
头结点(Head Node):
- 可选的虚拟结点,位于链表头部,不存储实际数据,用于简化头部操作(如插入、删除)。
-
头指针(Head Pointer):
- 必有的指针,指向链表第一个结点(若有头结点则指向头结点,否则指向首数据结点)。
-
尾结点(Tail Node):
- 链表最后一个结点,其指针域为
NULL
。
- 链表最后一个结点,其指针域为
-
尾指针(Tail Pointer):
- 可选指针,指向尾结点,用于优化尾部操作性能(如尾插法)。
(二)链表分类
- 带头结点链表:头指针指向头结点,头结点
next
指向首数据结点。 - 不带头结点链表:头指针直接指向首数据结点,空链表时头指针为
NULL
。
(三)构建方式
- 头插法:
- 新结点插入到链表头部,成为新的首结点。
- 特点:插入速度快(无需遍历),但结点顺序与插入顺序相反。
- 尾插法:
- 新结点插入到链表尾部,需遍历至尾结点或借助尾指针。
- 特点:结点顺序与插入顺序一致,适合顺序构建链表。
十二、二级指针在链表操作中的应用
(一)头插法中的二级指针
场景:修改头指针的指向
-
不使用二级指针的实现(返回新头指针):
Node* insert_head(Node* head, int data) {Node* new_node = malloc(sizeof(Node));new_node->data = data;new_node->next = head;return new_node; // 返回新头指针,需手动更新实参 }
-
使用二级指针的实现(直接修改实参头指针):
void insert_head2(Node** phead, int data) { // phead指向实参头指针Node* new_node = malloc(sizeof(Node));new_node->data = data;new_node->next = *phead; // 新结点指向原头结点*phead = new_node; // 头指针指向新结点(修改实参) }
-
优势:无需返回值,直接通过二级指针修改头指针,代码更简洁安全。
(二)尾插法中的二级指针
场景:空链表时需修改头指针
-
实现逻辑:
- 若链表为空(头指针为
NULL
),新结点即为头结点,需通过二级指针更新头指针。 - 若链表非空,遍历至尾结点,修改其
next
指针。
void insert_tail(Node** phead, int data) {Node* new_node = malloc(sizeof(Node));new_node->data = data;new_node->next = NULL;if (*phead == NULL) { // 空链表时更新头指针*phead = new_node;return;}Node* tail = *phead;while (tail->next != NULL) tail = tail->next; // 遍历至尾结点tail->next = new_node; // 尾结点指向新结点 }
- 若链表为空(头指针为
十三、单链表核心操作:遍历与查找
(一)遍历链表
-
目标:依次访问每个结点的数据域。
-
实现步骤:
- 创建临时指针
curr
,初始化为头指针。 - 循环条件:
curr != NULL
(处理当前结点后,curr
指向下一结点)。
- 创建临时指针
-
代码示例:
void print_list(Node* head) {Node* curr = head;while (curr != NULL) {printf("%d -> ", curr->data);curr = curr->next;}printf("NULL\n"); }
(二)查找尾结点
-
目标:获取链表最后一个结点。
-
实现逻辑:
- 临时指针
tail
从头部开始遍历。 - 循环条件:
tail->next != NULL
(当tail->next
为NULL
时,tail
即为尾结点)。
- 临时指针
-
代码示例:
void find_tail(Node* head) {if (head == NULL) return; // 空链表处理Node* tail = head;while (tail->next != NULL) tail = tail->next;printf("Tail data: %d\n", tail->data); }
十四、关键对比与注意事项
(一)头插法 vs 尾插法
特性 | 头插法 | 尾插法 |
---|---|---|
插入位置 | 头部 | 尾部 |
是否需要遍历 | 否 | 是(无尾指针时) |
头指针修改 | 必改 | 空链表时需改 |
结点顺序 | 逆序 | 顺序 |
适用场景 | 快速插入、逆序构建 | 顺序构建、需要保持插入顺序 |
(二)二级指针使用场景
- 必须使用二级指针:
- 函数需要修改头指针的指向(如头插法、空链表尾插法)。
- 避免滥用:
- 仅操作结点内容(不修改指针指向)时,使用一级指针即可。
(三)内存管理要点
- 结点分配与释放:
- 插入新结点时需用
malloc
分配内存,并在删除时用free
释放,避免内存泄漏。
- 插入新结点时需用
- 头指针置空:
- 销毁链表时,需将头指针置为
NULL
,避免野指针。
- 销毁链表时,需将头指针置为
十五、代码示例:基于二级指针的单链表操作
#include <stdio.h>
#include <stdlib.h>// 结点定义
typedef struct Node {int data;struct Node *next;
} Node;// 头插法(二级指针版)
void insert_head(Node** phead, int data) {Node* new_node = (Node*)malloc(sizeof(Node));new_node->data = data;new_node->next = *phead;*phead = new_node;
}// 尾插法(二级指针版)
void insert_tail(Node** phead, int data) {Node* new_node = (Node*)malloc(sizeof(Node));new_node->data = data;new_node->next = NULL;if (*phead == NULL) {*phead = new_node;return;}Node* tail = *phead;while (tail->next != NULL) tail = tail->next;tail->next = new_node;
}// 遍历打印
void print_list(Node* head) {Node* curr = head;while (curr != NULL) {printf("%d -> ", curr->data);curr = curr->next;}printf("NULL\n");
}int main() {Node* head = NULL; // 空链表头指针// 头插法添加结点insert_head(&head, 3); // 链表:3 -> NULLinsert_head(&head, 2); // 链表:2 -> 3 -> NULLinsert_head(&head, 1); // 链表:1 -> 2 -> 3 -> NULL// 尾插法添加结点insert_tail(&head, 4); // 链表:1 -> 2 -> 3 -> 4 -> NULLprint_list(head); // 输出:1 -> 2 -> 3 -> 4 -> NULL// 释放内存(示例省略,实际需遍历释放所有结点)return 0;
}
十六、函数指针基础概念
(一)本质与定义
- 函数指针:指向函数的指针变量,存储函数在代码段中的入口地址(首字节地址)。
- 函数地址:函数编译后生成的机器指令存储区域的起始地址,函数名即代表该地址。
(二)声明与初始化
1. 变量声明格式
返回值类型 (*指针变量名)(参数列表);
// 示例:指向无参无返回值函数的指针
void (*ptr)();
// 示例:指向带两个int参数、返回int的函数的指针
int (*calc_ptr)(int, int);
2. 类型别名定义(推荐写法)
typedef 返回值类型 (*别名)(参数列表);
// 示例:定义计算器函数指针类型
typedef int (*CalculatePtr)(int, int);
3. 初始化方式
CalculatePtr add = add_func; // 直接用函数名赋值
// 或
CalculatePtr add = &add_func; // 取地址符可选(函数名隐式转换为地址)
(三)调用方式
int add(int a, int b) { return a + b; }
CalculatePtr ptr = add; // 初始化// 方式1:直接通过函数指针调用(最常用)
int result = ptr(3, 5); // 等价于 add(3,5)// 方式2:解引用后调用(不推荐)
int result = (*ptr)(3, 5); // 方式3:通过函数名解引用调用(极少用)
int result = (*add)(3, 5);
十七、函数指针匹配规则
(一)必须匹配的要素
- 返回值类型:需与函数指针声明完全一致(包括是否为指针、const修饰等)。
- 参数列表:参数个数、顺序、类型必须完全一致(参数名可忽略)。
(二)无需匹配的要素
- 函数名:无关,只要地址正确即可指向。
- 函数形参名:仅需类型匹配,名称任意。
- 函数实现:与指针指向无关,可指向任意符合规则的函数。
十八、函数指针的核心作用:回调函数
(一)回调函数概念
- 定义:通过函数指针传递的函数,作为参数传入其他函数(如
qsort
、自定义过滤函数等),在适当的时候被调用以完成特定逻辑。 - 作用:解耦算法与规则,提高代码灵活性和可扩展性。
(二)经典场景:排序函数qsort
1. qsort
函数原型
void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
- 参数:
base
:待排序数组首地址。num
:元素个数。size
:单个元素大小(字节)。compar
:比较函数指针,返回值决定元素顺序:<0
:第一个参数应排在第二个之前。=0
:两者相等。>0
:第一个参数应排在第二个之后。
2. 自定义比较函数示例(整数升序)
int int_cmp(const void* a, const void* b) {return *(int*)a - *(int*)b; // 升序排列
}// 使用qsort排序整数数组
int arr[] = {3, 1, 4, 2};
qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_cmp);
(三)自定义回调函数示例:计算器
1. 定义函数指针类型
typedef int (*OpFunc)(int, int); // 操作函数指针类型
2. 实现具体运算函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; } // 简化处理除零情况
3. 通用计算函数(接收回调函数)
int calculate(int a, int b, OpFunc op) {return op(a, b); // 通过函数指针调用回调函数
}// 使用示例
int result = calculate(10, 3, add); // 调用加法,结果13
result = calculate(10, 3, subtract); // 调用减法,结果7
十九、函数指针的注意事项
(一)空指针检查
- 调用函数指针前需确保其非
NULL
,避免程序崩溃。
OpFunc op = NULL;
if (op != NULL) { // 关键检查op(10, 5);
}
(二)类型安全
- 确保函数指针与目标函数的参数、返回值严格匹配,避免未定义行为(如传入不匹配的函数导致栈溢出)。
(三)与数据指针的区别
- 函数指针:指向代码段,存储函数入口地址,调用时执行函数逻辑。
- 数据指针(如
int*
):指向数据段或堆/栈空间,存储数据地址,解引用时操作数据。
(四)回调函数的内存管理
- 回调函数若为全局函数或静态函数,无需担心生命周期;若为局部函数(如嵌套函数),需确保其在调用期间有效(C语言不支持局部函数作为回调,会编译报错)。
二十、函数指针与闭包(扩展)
- 闭包概念:在支持嵌套函数的语言(如Python、JavaScript)中,闭包允许函数捕获外部作用域的变量。
- C语言的局限性:C语言不直接支持闭包,但可通过函数指针结合结构体(存储上下文数据)模拟类似功能。
模拟闭包示例:带状态的计数器
typedef struct {int count; // 状态变量int (*increment)(struct Counter*); // 函数指针
} Counter;int increment(Counter* c) {return ++c->count;
}Counter* create_counter() {Counter* c = malloc(sizeof(Counter));c->count = 0;c->increment = increment; // 函数指针指向增量函数return c;
}// 使用
Counter* cnt = create_counter();
printf("%d\n", cnt->increment(cnt)); // 输出1
printf("%d\n", cnt->increment(cnt)); // 输出2
二十一、总结:函数指针的价值
- 灵活性:通过传递不同的回调函数,同一函数可实现多种逻辑(如排序规则、过滤条件)。
- 可扩展性:无需修改现有代码,通过新增回调函数即可扩展功能,符合开闭原则。
- 底层开发必备:在嵌入式、系统编程中常用于实现驱动接口、事件回调等场景。
关键记忆点:
- 函数指针声明需匹配返回值和参数列表。
- 回调函数是函数指针的核心应用,用于解耦算法与规则。
- 调用前检查指针非空,确保类型安全。