动态内存分配
目前开辟空间的方法
int a = 1;//在栈空间上开辟一个int类型 (四个字节)
char arr[10] = {0};//在栈空间上开辟char类型的 10个字节的连续空间
这两种开辟空间的方式的特点
空间开辟⼤⼩是固定的。
数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整。
缺点也很明显
编译时无法确定内存大小
读取用户输入的字符串,其长度在编译时是未知的,只能在运行时根据实际输入分配内存。
高效利用内存资源
静态分配的内存(如数组)在程序运行期间始终占用固定空间,即使不再使用。
对于空间的需求,我们在编程中的需要更灵活的方式。
有时候我们需要的空间⼤⼩在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。
动态内存分配特点
在编程中,我们经常需要根据程序运行时的需求来分配内存,而不是在编译时就固定内存大小。
特点:
处理大型数据结构
对于需要大量内存的结构,动态分配可以避免栈溢出,因为动态内存位于堆中,而不是栈上。
动态分配可以在需要时申请,不需要时释放,提高内存利用率。
malloc 和 free:动态内存的基础操作
malloc
函数
定义:用于在堆上分配指定大小的内存块,返回指向该内存的指针。
函数原型:
void* malloc(size_t size);
参数:size
表示需要分配的字节数。
成功:开辟成功,则返回⼀个指向开辟好空间的指针。
失败:则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
void* :所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 分配一个整数大小的内存
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
peorr(“malloc”)
return 1;
}
// 使用内存
*ptr = 100;
printf(" %d\n", *ptr); // 输出:100
// 释放内存
free(ptr);
ptr = NULL; // 避免野指针
return 0;
}
free
函数
定义:free
用于释放 malloc
、calloc
或 realloc
分配的内存,使其可被后续程序再次使用。
void free(void* ptr);
参数:ptr 是指向要释放内存的指针。
注意事项:必须传入由动态分配函数返回的指针,否则行为未定义。
释放后的指针应立即赋值为 NULL,避免成为野指针。
calloc 和 realloc:进阶内存管理
calloc
函数
定义:calloc
用于分配内存并初始化为零,适合需要初始化的场景(如数组、结构体)。
void* calloc(size_t num, size_t size);
参数:num:元素个数。
size:每个元素的大小(字节)。
返回值: 成功:返回指向分配并初始化内存的指针。
失败:返回 NULL。
// 分配包含5个整数的数组并初始化为0
int* arr = (int*)calloc(5, sizeof(int));
if (arr != NULL) {
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 输出:0 0 0 0 0
}
free(arr);
}
和malloc函数相比,calloc函数会将动态分配的内存全部初始化,有利于编程的运用
realloc
函数
定义:realloc
用于调整已分配内存块的大小,可能涉及内存搬迁。
void* realloc(void* ptr, size_t new_size);
参数:
ptr:指向现有内存块的指针(可为 NULL,此时等价于 malloc)。
new_size:新的内存大小(字节)。
返回值:
成功:返回指向新内存块的指针(可能与原指针相同或不同)。
失败:返回 NULL,原内存块保持不变。
int* ptr = (int*)malloc(3 * sizeof(int)); // 初始分配3个整数
ptr = (int*)realloc(ptr, 5 * sizeof(int)); // 调整为5个整数
if (ptr != NULL) {
// 内存已扩展,可以安全访问5个元素
free(ptr);
}
注意事项:
new_size
小于原大小,内存会被截断,数据可能丢失。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有⾜够⼤的空间
如果此时用realloc函数进行扩容,
要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
情况2:原有空间之后没有⾜够⼤的空间
如图如果要扩展的内存空间不够,
扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。
到一个新的空间进行内存分配
由于上述的两种情况,realloc函数的使⽤就要注意⼀些。
常见的动态内存错误
动态内存管理容易出错,以下是几种典型错误
内存泄漏
原因:分配的内存未被释放,导致可用内存逐渐减少。
void func() {
int* ptr = malloc(100);
// 忘记调用 free(ptr)
} // 每次调用func都会泄漏100字节
解决方案:
确保每个 malloc/calloc/realloc 都有对应的 free。
使用 RAII(资源获取即初始化)思想,在函数退出前释放资源。
野指针
原因:释放内存后仍使用该指针。
int* ptr = malloc(sizeof(int));
free(ptr);
*ptr = 10; // 错误:访问已释放的内存
解决方案:
释放内存后立即将指针置为 NULL。
避免返回指向局部变量的指针(栈内存会在函数返回后释放)。
重复释放
原因:多次释放同一块内存。
int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // 错误:重复释放
解决方案: 释放后将指针置为 NULL,因为 free(NULL) 无操作。
越界访问
原因:访问超出分配内存范围的数据。
int* arr = malloc(3 * sizeof(int));
arr[3] = 10; // 错误:越界访问(有效索引为0~2)
解决方案: 始终检查数组索引范围。
使用安全的内存操作函数(如 snprintf 替代 sprintf)。
柔性数组
定义:C99 标准引入了柔性数组,允许结构体的最后一个成员为不指定大小的数组,用于动态扩展内存。
定义规则:
结构体至少包含一个其他成员。
柔性数组成员必须是最后一个成员。
声明时不指定大小,用 [] 表示。
事例
struct Buffer {
int length; // 必须有其他成员
char data[]; // 柔性数组(无大小)
};
// 动态分配内存
struct Buffer* buf = (struct Buffer*)malloc(
sizeof(struct Buffer) + 100 * sizeof(char) // 额外分配100字节
);
buf->length = 100;
strcpy(buf->data, "Hello");
优势
内存连续,提高访问效率。
一次性分配和释放,避免内存碎片。
注意事项:
必须通过 malloc
分配内存,且大小需包含柔性数组的预期空间。
释放时只需一次 free
。
C/C++ 程序内存区域划分
C/C++ 程序的内存通常分为以下几个区域:
栈区(Stack)
存储内容:局部变量、函数参数、返回地址等。
特点: 由编译器自动管理,函数调用时分配,返回时释放。
内存分配和释放速度快,但空间有限(通常为几 MB)。
按后进先出(LIFO)顺序管理内存。
堆区(Heap)
存储内容:动态分配的内存(如 malloc
、new
)。
特点:手动管理,需显式释放(free
、delete
)。
空间较大(受限于物理内存和虚拟内存),但分配和释放较慢。
容易出现内存碎片和泄漏。
全局 / 静态区(
存储内容:全局变量、静态变量(static
)。
特点: 程序启动时分配,程序结束时释放。
初始化的全局变量和静态变量存储在.data 段。
未初始化的存储在.bss 段(自动初始化为 0)
。
常量区
存储内容:字符串常量、const
变量等。
特点:只读,修改会导致运行时错误。
通常与代码段共享物理内存,提高缓存效率。
代码段
存储内容:程序的可执行代码。
特点:只读,防止程序被意外修改。
可共享(多个进程可同时运行同一程序)。