动态内存分配之 malloc、calloc、realloc 和 free 以及常见动态分配内存错误

发布时间:2024年01月22日

1.一般内存开辟方法

int a = 20; // 在栈空间上分配一个 int 类型的变量,开辟四个字节,并将其初始化为 20
char arr[10] = {0}; // 在占空间开辟10个字节的连续空间

上述方法特性:

  • 开辟的空间大小固定
  • 数组申明必须指定数组长度,数组空间大小一旦确定不能调整,在实际运用中,容易出现开辟空间不够的情况

2.动态开辟内存

动态分配内存意味着在程序运行时,根据需要分配一块内存空间,而不是在编译时静态地分配。这为程序提供了更大的灵活性,允许根据实际需求分配和释放内存

① malloc

语法:

void* malloc(size_t size);

这里的 size 是要分配的内存块的字节数,函数返回一个指向新分配内存起始位置的指针。这个指针的类型是 void*,因此通常需要将其强制转换为所需的类型。

size_t 是一种用于表示内存块大小、数组索引和对象大小等的无符号整数类型。在C语言中,它是标准库 <stddef.h> 中定义的。通常,size_t 被用作与内存相关的函数的参数和返回类型,以确保能够表示所需的内存大小。
size_t 在malloc语法中表示要分配的内存块的字节数。由于内存的大小通常不能为负数,因此使用无符号类型 size_t 是合适的选择。这样可以确保能够表示大于等于零的整数值,以表示内存的大小。
你可以使用 %zu 格式说明符来打印 size_t 类型的值

  • 若开辟成功,则返回一个指向开辟的这块空间的指针
  • 若开辟失败,则返回一个NULL指针
  • 返回类型是 void*,所以malloc函数并没有规定开辟空间的类型,需要在使用的场景由使用者自己决定
  • 如果参数size为0,C语言标准并没有规定 malloc 应该如何处理这种情况。因此,编译器可以根据实际情况来决定如何处理,这可能会导致不同编译器之间的行为差异。(应该避免将 malloc 的参数设为0,因为这可能导致不可预测的结果。如果确实需要动态分配内存,并且大小可能为0,可以在调用 malloc 之前进行检查,以确保 size 大于零。)

②calloc

calloc也用来动态内存分配,但 calloc 提供了一个额外的参数,用于指定要分配的元素的数量和每个元素的大小,而malloc只有分配的内存块的字节数。语法:

void* calloc (size_t num,size_t size);

calloc 的功能是在内存中分配 num * size 字节的空间,并将分配的每个字节初始化为零(与malloc的区别),返回的指针指向新分配的内存块的起始位置,或者如果分配失败,则返回 NULL。
所以如果我们对申请的内存空间的内容要求初始化,那么可以很?便的使?calloc函数来完成任务

③realloc

有时会我们发现过去申请的空间太小了,有时候我们?会觉得申请的空间过大了,那为了合理的分配内存,我们?定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。用于更改之前由 malloc、calloc 或 realloc 分配的内存块的大小。它可以用于调整内存块的大小,以便适应更大或更小的需求。
语法:

void* realloc(void* ptr,size_t size);
  • ptr是要调整的内存地址
  • size是调整以后的新的空间大小
  • 返回值为调整后的内存起始位置

realloc在调整内存空间的时候存在两种情况

<1>原空间地址后边有足够的扩容空间
这种情况可以直接扩展内存,会在原有内存之后直接追加空间,原来的数据也不会发生变化(包括数据和数据地址),返回的还是原地址

<2>原空间地址后边没有足够的需要的空间
这种情况,会比第一种情况耗损更多(两者相比,但是效率还是较快的),会在堆空间上找一个合适大小的连续空间来使用,将原有的数据全部复制到新的更大的空间,并释放掉原空间,最后返回新的空间的内存地址(函数会自动处理释放原空间,不需要再去free)

需要注意的是,realloc 的返回值可能与之前的指针相同,也可能是一个新的指针。因此,在使用 realloc 后,最好将其返回值赋给原指针,以确保在调整大小时不会丢失原始指针。

3. 空间的释放free函数

free函数是专门用来做动态内存的释放和回收的,函数原型:

void free (void* ptr);
  • ptr:是指向待释放内存块的指针。如果参数 ptr 指向的空间不是动态开辟的,那free函数的?为是未定义的。如果参数 ptr 是NULL指针,则函数什么事都不做。
  • free 的功能是将之前动态分配的内存块释放,使得该内存块变为可用状态,可以供系统重新分配给其他需要内存的部分。

malloc和free都声明在 stdlib.h 头?件中。

注:在每次释放以后,将ptr指针置空,即 ptr = NULL;,这一步可以预防悬垂指针的问题。悬垂指针是指在释放了内存块后,仍然保留对该内存块的指针,这样可能导致一些未定义行为或错误的操作。
当你使用 free 释放内存块后,该内存块不再属于你的程序,而是被系统回收。如果你保留对已经释放的内存的指针,并尝试在之后的代码中访问或修改它,可能会导致以下问题:

  • 悬垂指针引发未定义行为: 尝试使用已经释放的内存块可能导致未定义行为,因为系统可以随时重新分配或修改该内存块。
  • 难以调试和定位问题: 悬垂指针问题可能不会立即导致程序崩溃,但可能在后续的代码中引发难以预测的错误。这样的问题可能很难调试和定位。
  • 通过将指针 ptr 设置为 NULL,你可以确保在释放内存后,任何对指针的操作都会导致空指针引用,而不是悬垂指针引用。这样可以避免一些潜在的问题,并使代码更加健壮。

4.常见的动态内存的错误

①对NULL指针解引用

void test()
 {
 	int *p = (int *)malloc(INT_MAX/4);
 	*p = 20;   // 如果内存开辟失败,p的值是NULL,就会有问题 
	 free(p);
 }

内存分配失败的处理: 在调用 malloc 后,应该检查分配是否成功。如果内存分配失败,malloc 返回 NULL,而在代码中并没有检查 p 是否为 NULL。在分配大量内存时,可能会导致分配失败,因此最好在分配后进行检查。

②对动态开辟空间的越界访问

 void test()
 {
	 int i = 0;
	 int *p = (int *)malloc(10*sizeof(int));
	 if(NULL == p)
	 {
	 	exit(EXIT_FAILURE);
	 }
	 for(i=0; i<=10; i++)
	 {
	 	*(p+i) = i;   // 当i是10的时候越界访问 ,下标为10是第11个数,但是只开辟了10个int的空间大小
	 }
	 free(p);
 }

循环条件 i <= 10 会导致在第11次循环时访问数组越界,这可能导致不可预测的结果,包括程序崩溃或数据损坏。改为 i < 10

③ 对非动态开辟内存使用free函数

void test() {
	 int a = 10;
	 int *p = &a;
	 free(p);
 }

这段代码试图释放一个不是由 malloc、calloc 或 realloc 分配的内存块。在这里,变量 a 是在栈上分配的局部变量,不应该使用 free 函数进行释放。

free 函数的目的是释放由动态内存分配函数(如 malloc、calloc、realloc)分配的内存块。这些内存块的生命周期延伸到程序员通过 free 明确释放它们。对于栈上分配的局部变量,它们的生命周期由程序的执行上下文和作用域管理,而不需要显式地使用 free 进行释放。

变量 a 在栈上分配,而动态分配的内存空间通常位于堆上。
栈区和堆区的区别:

  • 栈(Stack):
    由编译器自动管理。
    存储局部变量、函数参数和函数调用信息。
    具有快速的分配和释放内存的特性,但生命周期受到函数的调用和返回的影响。
    变量在栈上分配时,会在离开其作用域时自动释放。
  • 堆(Heap):
    由程序员手动管理或通过垃圾回收器进行管理。
    用于存储动态分配的内存,需要显式调用函数(如 malloc、calloc、realloc)来分配和释放内存。
    具有更灵活的生命周期,内存可以在程序的任何地方分配和释放,生存周期不受函数的调用和返回的限制。

④使用free释放一块动态开辟内存的一部分

 void test()
 {
	 int *p = (int *)malloc(100);
	 p++;
	 free(p);   // p不再指向动态内存的起始位置 
 }

⑤对同一块动态内存多次释放

void test()
 {
	 int *p = (int *)malloc(100);
	 free(p);
	 free(p);   // 重复释放 
 }

⑥动态开辟内存忘记释放(内存泄漏问题)

 void test()
 {
	 int *p = (int *)malloc(100);
	 if(NULL != p)
	 {
		 *p = 20;
	 }
 }
 
 int main()
 {
	 test();
	 while(1);
 }

在 test 函数中,通过 malloc 分配了一块内存,但没有在函数结束时调用 free 来释放这块内存。这意味着程序在运行时无法再访问该内存块的指针,导致内存泄漏。如果这个问题在一个循环或长时间运行的程序中出现,内存泄漏可能会导致程序占用越来越多的内存,最终耗尽系统的可用内存。

5.内存分配区域

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元?动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运?函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):?般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分方式类似于链表。
  3. 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的?进制代码。
    C/C++中程序内存区域划分
文章来源:https://blog.csdn.net/m0_63596031/article/details/135755761
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。