嵌入式小知识2_内存
1、内存
1.1 内存分配方式
1.1.1 栈
栈(Stack)用来保存临时变量,包括函数参数,函数内部定义的临时变量;
每个线程都有自己独立的栈空间,操作系统在创建线程时,会为其分配一块内存作为该线程的栈,保证了多线程编程的实现。
-
分配/释放方式:由编译器自动管理。当函数被调用时,其参数、返回地址和
局部变量(非静态的)
在栈上分配空间(“压栈”)。函数执行结束时,这些空间被自动释放
(“弹栈”)。 -
存储内容:函数参数、返回地址、非静态的局部变量(自动变量)。
-
特点:
速度快:分配和释放只是指针的移动。
空间有限:栈大小通常较小(如几MB),过度使用(如深度递归、大数组)会导致栈溢出。
生命周期:与函数调用周期一致。
1.1.2 静态存储区
-
分配/释放方式:在程序编译期就已确定大小和相对地址,
在程序启动时就被分配,直到程序结束时才被释放。
-
存储内容:全局变量、静态变量
-
特点:
生命周期与整个程序的生命周期相同。 -
数据初始化和未初始化:通常分为.data段(已初始化的全局/静态变量)和.bss段(未初始化的全局/静态变量,程序启动时由系统初始化为0或空指针)。
1.1.3 堆
-
分配/释放方式:由程序员
手动
控制。使用malloc/calloc/realloc(C)或new(C++)申请内存;使用free(C)或delete(C++)释放内存。 -
存储内容:几乎所有动态创建的对象和数据。
-
特点:
空间巨大:理论上只受限于系统的可用虚拟内存大小。
分配速度慢:需要在运行时寻找合适的内存块。
手动管理:由程序员分配和释放。如果只分配不释放,会导致内存泄漏;如果释放后再次使用或重复释放,会导致未定义行为(程序崩溃最常见)。
特性 | 栈 (Stack) | 静态存储区 (Static) | 堆 (Heap) |
---|---|---|---|
管理方式 | 编译器自动 | 编译器/系统自动 | 程序员手动 |
分配/释放时机 | 函数调用/返回时 | 程序开始/结束时 | 随时(new/malloc时) |
生命周期 | 函数作用域 | 整个程序运行期 | 从分配直到被释放 |
速度 | 非常快 | 快(编译期确定) | 慢(运行时查找) |
空间大小 | 小(默认几MB) | 由程序内容决定 | 大(受系统限制) |
主要问题 | 栈溢出 | - | 内存泄漏、碎片化 |
1.2堆栈区别
1.2.1 申请方式不同
栈是自动管理,而堆由程序员手动管理
void function() {
int a; // 自动在栈上分配
char b[100]; // 自动在栈上分配
// 函数结束时自动释放
int *a = malloc(sizeof(int)); // 手动申请
char *b = malloc(100 * sizeof(char)); // 手动申请
// 必须手动释放!
free(a);
free(b);
}
1.2.2 内存布局和方向
栈是向低地址申请的空间,而堆的申请是向高地址扩张的
1.2.3 效率差异
栈使用的是一级缓存, 在被调用时只需移动栈指针,后进先出,调用完毕立即释放;堆则是存放在二级缓存中,速度更慢,管理更复杂。
特性 | 栈(Stack) | 堆(Heap) |
---|---|---|
管理方式 | 编译器自动管理,效率极高 | 程序员手动管理(malloc/free , new/delete ) |
分配速度 | 快(只需移动栈指针) | 慢(需要寻找合适的内存块) |
生命周期 | 函数调用开始到结束 | 从malloc 到free ,完全由程序员控制 |
大小限制 | 较小(如几MB),OS预设 | 很大(受限于系统虚拟内存) |
碎片问题 | 无(后进先出保证了顺序) | 有(频繁分配释放会产生碎片) |
主要用途 | 函数调用、局部变量 | 动态分配、大小可变的数据结构 |
灵活性 | 大小在编译期确定(静态数组是硬伤) | 大小可在运行时决定 |
1.3 堆栈溢出
堆栈溢出的原因一般有:递归,动态申请内存,数组访问越界,指针非法访问
- 函数调用层次太深。函数递归调用时,系统要在栈中不断保存函数调用时的现场和产生的变量,如果递归调用太深,就会造成栈溢出,这时递归无法返回。再有,当函数调用层次过深时也可能导致栈无法容纳这些调用的返回地址而造成栈溢出。
void recursive_function(int n) {
int large_array[1000]; // 每次递归都在栈上分配
if (n > 0) {
recursive_function(n - 1); // 深度递归会导致栈溢出
}
}
// 调用导致栈溢出
// recursive_function(10000);
- 动态申请空间使用之后没有释放。由于C语言中没有垃圾资源自动回收机制,因此,需要程序主动释放已经不再使用的动态地址空间。申请的动态空间使用的是堆空间,动态空间使用不当造成堆溢出。
void memory_leak() {
int *data = malloc(100 * sizeof(int));
// 使用data...
// 忘记free(data); ← 内存泄漏!
}
- 数组访问越界。C语言没有提供数组下标越界检查,如果在程序中出现数组下标访问超出数组范围,在运行过程中可能会内存访问错误。
- 指针非法访问。指针保存了一个非法的地址,通过这样的指针访问所指向的地址时会产生内存访问错误
int *create_number() {
int num = 42;
return # // ❌ 错误:返回栈内存地址
} // 函数结束num被释放,返回的指针无效
void use_dangling_pointer() {
int *ptr = create_number();
printf("%d\n", *ptr); // ❌ 未定义行为
}
1.4 内存泄漏
内存泄漏就是申请了内存,①不使用之后并没有释放内存,或者说,②指向申请的内存的指针突然又去指向别的地方,导致找不到申请的内存。随着程序运行时间越长,占用内存越多,最终用完内存,导致系统崩溃。上述第二个例子就是第①种情况。
例②:
char *p = (char*)malloc(sizeof(int)*25);
if( p == NULL)
{
return -1;
}
printf("malloc %p\n",p); //malloc 0x5573503ee2a0
p = "hello";
printf("hello %p\n",p); //hello 0x55734f5eb00f
//可以看出这里出现内存泄漏:
//原本p指向使用malloc申请内存的地址0x5573503ee2a0,后来又让p指向字符串常量的地址0x55734f5eb00f,此时就找不到malloc的地址了
内存模型:
2、字节对齐
字节对齐就是各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放。需要字节对齐的根本原因在于CPU访问数据的效率问题,例如:
如果地址0x02 ~ 0x05存储了一个int类型的数据,读取这个int就需要先读取0x00 ~ 0x03地址范围的数据,提取其中的0x02 ~ 0x03部分,然后再读取0x04 ~ 0x07地址范围的数据,提取其中的0x04部分,最后将这两部分拼接起来才能得到完整的int值。这样读取一个int就需要两次内存访问操作,效率较低。
3、C语言函数参数压栈顺序
压栈顺序:从右向左 (Right-to-left)
栈增长方向:从高地址向低地址 (High-to-low)
结果:先压入的参数(最右边的参数)位于更高的内存地址。
例如:
int a = 1, b = 2, c = 3;
printf("%d %d %d", a, b, c);
根据从右向左的压栈规则,参数入栈的顺序是:
c (值为 3)
b (值为 2)
a (值为 1)
格式字符串 “%d %d %d” 的地址
同时,栈指针 (SP) 每次压栈后都会向低地址移动。
4、内存申请函数
4.1 malloc- 内存分配 (Memory Allocation)
- 原型: void *malloc(size_t size);
- 功能:向系统申请一块连续的、大小为 size 字节的内存。新分配的内存中的内容是
未初始化
的(内容是随机的、不确定的垃圾值)。 - 参数:size:需要分配的
字节
数。 - 返回值:
成功:返回一个指向这块内存起始地址的 void* 类型指针,需要将其转换为所需的指针类型。
失败:如果申请失败(如内存不足),则返回 NULL。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 申请存放 10 个 int 的内存空间
int *arr = (int*)malloc(10 * sizeof(int));
// 必须检查是否分配成功!
if (arr == NULL) {
printf("Memory allocation failed!\n");
exit(1); // 退出程序
}
// 使用这块内存(此时里面的值是垃圾值)
for (int i = 0; i < 10; i++) {
arr[i] = i * 2;
}
// 不再使用时,必须释放!
free(arr);
return 0;
}
4.1 calloc- 清零分配 (Cleared Allocation)
-
原型: void *calloc(size_t num, size_t size);
-
功能:分配一块足够容纳 num 个长度为 size 字节的内存空间。
-
关键区别:
它会将分配到的内存中的每一位都初始化为 0
。对于整数来说,就是初始化为 0;对于指针来说,就是初始化为 NULL;对于浮点数来说,就是初始化为 0.0。 -
参数:num:元素的个数;size:每个元素的大小(字节)。
-
返回值:与 malloc 相同。成功返回 void* 指针,失败返回 NULL。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 申请存放 10 个 int 的内存空间,并全部初始化为0
int *arr = (int*)calloc(10, sizeof(int));
if (arr == NULL) {
printf("Memory allocation failed!\n");
exit(1);
}
// 使用这块内存(此时里面的值全是0)
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]); // 将会输出 ten 0
}
free(arr);
return 0;
}
4.1 realloc- 重新分配 (Reallocation)
- 原型: void *realloc(void *ptr, size_t new_size);
- 功能:调整之前由 malloc, calloc, 或 realloc 分配的内存块的大小。它
尝试在原有内存块的基础上扩大或缩小
。如果原内存块后面有足够的空闲空间,则直接扩展原内存块,原有数据保留,返回的指针和 ptr 相同。如果后面空间不够,它会:寻找一块新的足够大的内存,将原内存块的数据复制到新内存块,释放原内存块,返回一个指向新内存块的指针。 - 参数:ptr:指向原先内存块的指针。如果 ptr 是 NULL,则 realloc 的行为和 malloc(new_size) 一样;new_size:新的目标大小(字节)。
- 返回值:
成功:返回一个指向新内存块的 void* 指针。这个指针很可能和 ptr 不同,所以必须用返回值更新指针。
失败:返回 NULL,并且原内存块不会被释放,保持原样。
void *realloc(void *ptr, size_t size);//申请内存,重新申请size_t字节内存
如果size_t>原来的s申请的空间大小,比如原来是100个字节,现在是150个字节,那么就有以下两种情况:
- 原来100个字节后面还能放的下50个字节,那么就在原来地址上增加50个字节,返回的还是原来地址
- 如果100个字节后面放不下50个字节,那么就会重新找个地址开辟150个字节空间,把原来的地址数据拷贝过来,释放掉原来地址空间,返回一个新的地址
如果size_t<原来的s申请的空间大小,比如原来是100个字节,现在是50个字节,那么就会释放掉后50个字节
int *p = (int*)malloc(sizeof(int)*25);
if( p == NULL)
{
return -1;
}
printf("malloc %p\n",p);
//p = (int *)realloc(p,10);//重新分配10个字节大小,删除原来后面的90个字节
p = (int *)realloc(p,200);//重新分配200个字节大小,可能是在原来基础上加100,也可能是重新开辟200
printf("malloc %p\n",p);
```