嵌入式知识点总结(二)-C/C++ 内存
针对于嵌入式软件杂乱的知识点总结起来,提供给读者学习复习对下述内容的强化。
目录
1.C语言中内存分配的方式有几种?
1.静态存储区分配 内存分配在程序编译之前完成,且在程序的整个运行期间都存在,例如全局变量、静态变量等。
2. 栈上分配 在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束时这些存储单元自动释放。
3.堆上分配。
序号 | 分配方式 | 特点和用途 |
---|---|---|
1 | 静态存储区分配 | - 分配时机: 在程序编译时完成分配。 - 生存周期: 程序运行期间始终存在。 - 典型用途: 全局变量、静态变量和常量存储在此区域。 - 示例: static int x; |
2 | 栈上分配 | - 分配时机: 在函数调用时分配。 - 生存周期: 随函数调用结束自动释放。 - 典型用途: 函数的局部变量、函数参数。 - 示例: int x = 10; |
3 | 堆上分配 | - 分配时机: 在程序运行时动态分配,需显式分配和释放内存。 - 生存周期: 程序员控制,直到显式释放为止。 - 典型用途: 动态内存分配。 - 示例: 使用 malloc 、calloc 或 realloc 分配内存,释放用 free 。 |
属性 | 静态存储区 | 栈 | 堆 |
---|---|---|---|
分配时机 | 编译时 | 运行时,函数调用时 | 运行时,显式调用动态分配函数 |
生存周期 | 程序整个生命周期 | 函数作用域 | 程序员控制 |
优点 | 高效,无需手动管理 | 分配与释放自动化,高效 | 灵活,适合动态需求 |
缺点 | 占用固定内存,不灵活 | 容量有限(受栈空间限制) | 分配和释放管理复杂,可能引发内存泄漏 |
典型用途 | 全局变量、静态变量 | 局部变量、函数参数 | 动态数组、动态数据结构 |
2.堆和栈有什么区别?
1. 申请方式 栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放。
2. 申请大小的限制 栈空间有限。在Windows下,栈是向低地址扩展的数据结 构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
堆是很大的自由存储区。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3. 申请效率 栈由系统自动分配,速度较快。但程序员是无法控制的。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
区别维度 | 栈 | 堆 |
---|---|---|
申请方式 | - 系统自动分配和释放。 - 无需程序员干预,简单高效。 | - 由程序员手动分配和释放(使用 malloc 、calloc 、realloc 等)。- 若程序员未释放,则会导致内存泄漏。 |
空间大小限制 | - 容量有限:通常由操作系统设置,典型大小为 1M 或 2M(Windows)。 - 超过栈大小会引发 栈溢出(stack overflow)。 | - 容量较大:取决于系统的可用虚拟内存,通常远大于栈。 - 适合大对象或动态分配的需求。 |
地址方向 | - 向低地址扩展:内存从高地址向低地址增长。 | - 向高地址扩展:内存从低地址向高地址增长。 |
内存结构 | - 连续的内存区域,分配效率高。 | - 不连续的内存区域,系统通过链表管理自由存储区,分配效率相对较低。 |
存储内容 | - 存储局部变量、函数调用信息(返回地址、参数、临时变量等)。 | - 存储动态分配的内存块,如动态数组、动态对象等。 |
生存周期 | - 由函数的生命周期决定,函数退出时自动释放。 | - 由程序员控制生存周期,需手动释放内存。 |
申请效率 | - 分配速度快,开销小。 | - 分配速度慢,开销大,容易产生 内存碎片。 |
典型用途 | - 局部变量、函数参数。 | - 动态数组、动态数据结构(如链表、树)。 |
-
栈内存分配的特点:
- 因为栈是连续的内存块,所以访问速度快,开销低。
- 栈的生命周期由作用域控制,当作用域结束时,栈上的内存会被自动回收。
- 栈适用于临时变量或较小的数据结构的存储。
-
堆内存分配的特点:
- 堆是由程序员控制分配和释放的,因此灵活性高。
- 堆适用于动态分配的大型数据结构或需要跨函数共享的数据。
- 如果程序员未能及时释放内存,可能引发内存泄漏。
引申一下:RTOS 中,任务(Task)是一个可以在嵌入式系统中独立调度和执行的实体。任务在 FreeRTOS 中通常是一个 函数,但与普通的函数不同,它有特定的调度机制和上下文切换机制。在深入了解任务的机制之前,先简单回顾 FreeRTOS 的一些概念:
任务TASK是一个运行的函数,为什么这么说?
RTOS 中的任务是运行在 多任务环境 中的,每个任务就是一个线程。任务的运行和调度由 RTOS 内核管理,任务可以拥有不同的优先级。
在 RTOS 中,任务本质上是一个 C 函数。这个函数通常是由 xTaskCreate()
创建任务时指定的。RTOS 会为任务分配一块内存,用来保存任务的上下文信息,包括其堆栈、寄存器状态等。
为什么任务要保存寄存器?
任务是一个独立的执行单元,在多任务操作系统中,多个任务共享一个处理器。为了保证任务切换后能够恢复到正确的执行状态,需要保存当前任务的 CPU 寄存器值,包括:
- 程序计数器 (PC):指向当前执行位置。
- 堆栈指针 (SP):指向任务栈的当前位置。
- 通用寄存器:用于存储任务执行中的临时数据。
- 状态寄存器:保存中断和任务的状态信息。
任务的栈中不仅保存了任务函数的局部变量,还保存了任务切换时的寄存器值。因此,不同于普通函数调用,任务函数的栈不仅用于存储局部变量,还要保存任务切换时的上下文。
从汇编的角度来看,任务的上下文切换涉及到保存和恢复 CPU 寄存器的过程:
- 任务切换过程:当任务切换时,FreeRTOS 会通过汇编指令(通常是
push
和pop
指令)保存当前任务的寄存器状态到任务的堆栈上,并恢复下一个任务的寄存器状态。任务的堆栈空间因此在上下文切换过程中发挥了重要作用。
典型的任务上下文切换流程:
- 在任务 A 执行时,任务 A 的寄存器值会被保存在任务 A 的栈中。
- 如果任务 A 被挂起,FreeRTOS 会选择任务 B 来执行,在切换到任务 B 时,FreeRTOS 会从任务 B 的栈中恢复其寄存器值,从而恢复任务 B 的执行状态。
- 任务 A 的寄存器状态被恢复时,任务 A 继续从它的上次执行位置恢复执行。
这种上下文切换是由 FreeRTOS 内核的调度程序管理的。内核提供了对上下文切换的支持,具体实现依赖于目标架构的汇编代码。
3.栈在C语言中有什么作用?
1.C语言中栈用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数调用中和函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景。
2.多线程编程的基础是栈,栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。 操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石。
4.C语言函数参数压栈顺序是怎么样的?
从右至左。
C语言参数入栈顺序的好处就是可以动态变化参数个数。自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。
为什么是从右至左?
-
支持可变长参数:C 语言中,函数可以接受不同数量的参数(例如
printf
函数)。如果从右至左入栈,最右边的参数会被首先压入栈中,这样可以更容易处理可变参数列表。因为可变长参数的处理通常需要从最后一个参数开始,逐步向左处理,这种方式与栈的操作顺序相匹配。 -
便于参数访问:压栈顺序决定了如何通过栈指针(SP)访问函数参数。在从右至左压栈时,最后一个参数会位于栈的最低地址,首先入栈。调用者可以通过调整栈指针来正确地访问各个参数。
-
递归和多层函数调用支持:右至左的压栈顺序使得在递归调用中能够正确地管理栈空间,避免了在栈中动态变化的参数个数带来的混乱。
void exampleFunction(int a, float b, double c);
如果按右至左的顺序压栈,那么调用 exampleFunction(1, 2.5, 3.14)
时,栈的压栈顺序将是:
- 首先压入
c
(类型为double
,栈地址最低)。 - 然后压入
b
(类型为float
)。 - 最后压入
a
(类型为int
,栈地址最高)。
栈底 ——> a (int)
b (float)
c (double) <—— 栈顶
5.C++如何处理返回值?
C++函数返回可以按值返回和按常量引用返回,偶尔也可以按引址返回。多数情况下不要使用引址返回。
- 适合返回临时值或简单的数据类型。
- 不需要考虑对象的生命周期和所有权问题。
- 返回值的独立副本,调用者可以放心修改返回值。
- 对于大对象,拷贝成本较高,可能会影响性能。
int add(int a, int b) {
return a + b; // 按值返回
}
- 避免了拷贝大对象的开销。
- 保证返回的对象在函数外部不可修改。
- 返回的引用必须指向一个有效的对象,不能返回指向局部变量的引用,否则会导致悬挂引用(dangling reference)。
- 一般不能直接返回临时对象的常量引用,除非返回的是静态对象。
const std::string& getName() {
static std::string name = "John";
return name; // 返回常量引用
}
- 可直接修改返回的对象。
- 必须确保返回的引用指向一个有效且存在的对象,避免返回局部变量的引用。
- 使用不当可能导致悬挂引用或未定义行为。
int& getElement(std::vector<int>& vec, size_t index) {
return vec[index]; // 返回元素的引用
}
6.C++中拷贝赋值函数的形参能否进行值传递?
不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数(aa=ex.aa; //此处调用拷贝构造函数)。如此循环,无法完成拷贝,栈也会满。
class Example {
public:
Example(int a) : aa(a) {} // 构造函数
Example(const Example& ex) : aa(ex.aa) {} // 拷贝构造函数,使用常量引用
private:
int aa;
};
int main() {
Example e1(10); // 创建一个对象 e1
Example e2 = e1; // 使用拷贝构造函数来创建 e2
return 0;
}
7.C++内存管理是怎么样的?
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
BSS 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
8.什么是内存泄漏?
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。 它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。
内存泄漏是指程序在运行过程中动态申请了内存(通常使用 malloc()
、calloc()
、realloc()
或 new
等函数),但是在不再使用这些内存时,没有及时释放(通常通过 free()
或 delete
),导致这块内存无法被再次使用或回收,进而浪费系统资源。
9.如何判断内存泄漏?
1.良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。
- 每次分配内存后,确保使用相应的释放函数进行内存释放,例如
free()
(C)或delete
/delete[]
(C++)。 - 使用 RAII(Resource Acquisition Is Initialization)理念,确保资源的获取和释放通过对象的生命周期进行管理,减少手动管理内存的机会。
- 可以使用链表等数据结构来跟踪内存分配和释放情况,确保内存资源的回收。
2.将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
// 简单示例
struct MemoryNode {
void* ptr;
MemoryNode* next;
};
void addMemoryNode(void* ptr) {
// 将内存指针添加到链表中
}
void freeMemoryNode(void* ptr) {
// 在程序结束时检查链表并释放内存
}
3. Boost 中的smart pointer.
智能指针(如 std::unique_ptr
和 std::shared_ptr
)是 C++ 提供的一种资源管理工具,能够自动管理动态分配的内存,避免手动释放内存,从而减少内存泄漏的风险。智能指针的生命周期与其作用域相关,作用域结束时,智能指针会自动释放内存。
std::unique_ptr
:保证同一内存只能有一个所有者,适用于严格控制内存生命周期的场景。std::shared_ptr
:允许多个指针共享内存,引用计数为 0 时自动释放内存。
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// 内存会在 ptr 离开作用域时自动释放
4.一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等
-
Valgrind:一个强大的动态分析工具,可以帮助检测程序中的内存泄漏、未初始化的内存访问等问题。它能够报告程序中分配的内存和未释放的内存块。
示例命令:
valgrind --leak-check=full ./your_program
-
AddressSanitizer:GCC 和 Clang 提供的内存错误检测工具,能够检测内存泄漏、堆溢出等错误。
示例命令:
clang++ -fsanitize=address your_program.cpp -o your_program
-
Dmalloc:一个内存调试工具,可以跟踪内存分配和释放,检测内存泄漏。
-
Leaky:一个专门的内存泄漏检测工具,可以帮助开发者检测并定位内存泄漏。
-
CCmalloc:一个基于 malloc 的工具,可以检测和报告内存泄漏。
原文地址:https://blog.csdn.net/weixin_64593595/article/details/145225579
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!