C语言:动态内存分配

目录

 一、 动态内存的相关函数:

1. malloc  的相关细节

1) 函数参数

2)函数类型

3) 实例

5)注:

2. free 的相关细节

1)函数部分

2)使用时的细节

调用free(NULL)不会产生任何效果。根据C语言标准,对free函数传递空指针参数是安全的,且不会引发任何错误或异常。

free函数在释放内存空间时,不需要显式指定要释放的空间大小。这是因为在动态内存分配过程中,系统会记录每个分配的内存块的大小信息。

什么叫做内存泄漏

3. calloc 的相关细节

1)函数的细节

2)calloc 和 malloc 函数的区别

 4. realloc 的相关细节 的相关细节

二、常见的动态内存错误

1、对NULL指针的解引用操作

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

3、对非动态开辟内存使用free释放

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

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

6、动态开辟内存忘记释放(内存泄漏)

三、柔性数组

概念:

柔性数组的大小:

柔性数组的基本用法:

柔性数组的模拟实现:


当我们在创建一个变量时,这个变量的字节大小(存储空间)是固定的:比如,我们创建一个整型变量a, 它的字节大小就是4个字节。

int a = 0;

而当我们想要自主管理我们的内存,当我们需要多大的空间我们就可以开辟多大的空间,这样就具有了灵活性。  如果想要实习这样的功能,就需要学习今天我要分享的博客:动态内存分配

 一、 动态内存的相关函数:

1. malloc  的相关细节

malloc 函数是C语言标准库中的一个动态内存分配函数,用于在运行时分配指定大小的内存空间。它的函数原型如下:

void* malloc(size_t size);

malloc 函数接收一个参数,即需要分配的内存空间的大小size,以字节为单位。它会在(heap)中分配一块大小为size字节的内存空间,并返回该内存空间的起始地址(指针)。

malloc 函数的工作原理如下:

  • malloc 函数会在堆中搜索足够大的连续空闲内存块,以满足请求的大小。
  • 如果找到了足够大的空闲内存块,则将其标记为已分配,并返回其起始地址(指针)。
  • 如果无法找到足够大的空闲内存块,则返回NULL,表示分配失败。

首先 malloc 的头文件为 <stdlib.h>

从上面的图片中可以看到  malloc  有一个参数且参数的类型为 size_t  ,这种类型其实是一种无符号整形类型(可以通过VS编译器转到定义进行查看)

1) 函数参数

 而这个参数 size 表示的是:在内存中要开辟空间的字节大小。

当然我们可以不直接写出字节数,为了方便我们可以这样写:

开辟4个整形变量大小的空间  ----->malloc(40)---->malloc(sizeof(int)*4)。  

2)函数类型

malloc  函数的返回类型是 void* , 你可能会有疑问:为什么返回类型是void* 类型,而不是其他类型。

其实,当我们在开辟一块内存空间时,我们是知道这块空间将要存放什么类型的内容,比如我们将开辟的空间中存放一个整形,但是对于其他人来说,他可能是想要在开辟的空间中存放一个字符类型的内容。为了同时满足不同人的需求,返回类型必须是 void* ,因为 void* 是一个无确定类型的指针,它可以指向任何类型,它可以指向整形,字符类型等等,这样就能够满足不同人的需求了。

也正因为 malloc  函数返回的是void*类型的指针,所以使用时需要进行类型转换,以便与具体的数据类型相匹配。

3) 实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	int* p = (int*)malloc(40);
	int a = 0;
	if (p == NULL)
	{
		printf("%s", strerror(errno));
	}
	for (a = 0; a < 10; a++)
	{
		*(p + a) = a + 1;
	}
	for (a = 0; a < 10; a++)
	{
		printf("%d ", *(p + a));
	}
	return 0;
}

 

当开辟空间过大时,可能会开辟失败,这时它会返回一个NULL

 

5)注:

2. free 的相关细节

1)函数部分

free 函数是C语言中的一个库函数,用于释放动态分配的内存空间。它位于stdlib.h头文件中。

当我们使用malloccallocrealloc函数动态分配内存空间时,这些函数会返回一个指向分配内存的指针。在使用完这些内存空间后,为了避免内存泄漏,我们需要使用free函数将其释放,以便系统可以重新利用这块内存。

free 函数的语法如下:

void free(void* ptr);

其中,ptr是一个指向要释放的内存空间的指针。调用free 函数时,它会将这块内存空间返回给系统,以便后续的内存分配使用。

例如:

#include <stdlib.h>   

int main ()
{
  int * buffer1, * buffer2, * buffer3;
  buffer1 = (int*) malloc (100*sizeof(int));
  buffer2 = (int*) calloc (100,sizeof(int));
  buffer3 = (int*) realloc (buffer2,500*sizeof(int));
  free (buffer1);
  free (buffer3);
  return 0;
}

通过例子就可以看到,我所用malloc, calloc, realloc 开辟的空间都可以使用free 函数进行释放。

这里要注意,free 函数只能释放上述函数所开辟的空间,而不能释放其他情况下的空间

例如:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int n = 0;
	int* p = &n;
	free(p);
	return 0;
}

2)使用时的细节

  • 调用free(NULL)不会产生任何效果。根据C语言标准,对free函数传递空指针参数是安全的,且不会引发任何错误或异常。

当调用free函数时,它会释放由malloccallocrealloc函数分配的动态内存空间。但是,如果将空指针(即NULL)传递给free函数,NULL会被视为一个有效的参数,但不会执行任何操作。

这意味着,free(NULL)语句不会释放任何内存,也不会引发错误。因此,可以安全地在代码中使用free(NULL)来进行内存释放操作,而无需担心任何副作用。

以下是一个示例代码,演示了free(NULL)的使用:

#include <stdlib.h>
#include <stdio.h> 

int main() 
{ 
    int* ptr = NULL; // 尝试释放空指针 
    free(ptr); 
    printf("Memory freed successfully\n"); 
    return 0; 
}

在上面的示例中,我们将空指针ptr传递给free函数进行内存释放操作。由于ptr是空指针,free(NULL)不会执行任何操作,因此程序会继续正常执行,并打印出"Memory freed successfully"的消息。

  • free函数在释放内存空间时,不需要显式指定要释放的空间大小。这是因为在动态内存分配过程中,系统会记录每个分配的内存块的大小信息。

当你使用malloccallocrealloc函数分配内存时,系统会在内存块的头部存储额外的信息,包括分配的内存块的大小。这个额外的信息通常被称为"内存管理块""堆管理块"。这个管理块包含了分配的内存块的大小以及其他一些内存管理的信息。

当调用free函数时,它会根据传递给它的指针参数,找到对应的内存管理块,并根据其中的大小信息,将整个内存块(包括管理块)返回给系统的内存池,以便后续的内存分配使用。

因此,free函数能够正确释放内存空间,而不需要显式指定要释放的空间大小。它通过查找内存管理块来确定要释放的内存块的大小,并将整个内存块返回给系统。

  • 什么叫做内存泄漏

内存空间开辟后要用 free 进行释放,否者这块空间可能导致内存泄漏.

内存泄漏是指在程序运行过程中,由于错误的内存管理导致一些已经不再使用的内存无法被释放,从而导致内存的消耗持续增加的现象。这些未释放的内存会一直占用系统资源,最终可能导致程序崩溃或系统变慢。

内存泄漏的常见情况包括:

  1. 动态分配的内存没有被正确释放:例如使用malloc或new等函数分配内存后,没有使用free或delete等函数释放内存。

  2. 对象之间的循环引用:当两个或多个对象相互引用,但没有正确解除引用关系时,这些对象就会一直存在于内存中,无法被垃圾回收机制回收。

  3. 未关闭的资源:例如打开文件、数据库连接、网络连接等,在使用完毕后没有关闭,导致资源一直被占用。

其实就相当于,我们向图书馆里借书,当我们看过之后不及时还书,导致别人想看这本书而看不到。

(其实,当我们整个程序结束后,操作系统会自动收回这块空间防止内存泄漏;但是当我们的程序一次运行好长时间,我们一直申请内存还不归还,此时内存不不断变小,这时就会出现一定的问题,所以我们用malloc开辟过空间后,要记得用free进行释放空间)

3. calloc 的相关细节

1)函数的细节

calloc 函数是C语言标准库中的一个动态内存分配函数,用于在运行时分配指定数量的连续内存空间,并将其初始化为零。它的函数原型如下:

void* calloc(size_t num, size_t size);

calloc 函数接收两个参数,即需要分配的内存块的数量num和每个内存块的大小size,以字节为单位。它会在堆(heap)中分配num * size字节的内存空间,并将其初始化为零。最后,它返回该内存空间的起始地址(指针)。

calloc 函数的工作原理如下:

  • calloc 函数会在堆中搜索足够大的连续空闲内存块,以满足请求的大小。
  • 如果找到了足够大的空闲内存块,则将其标记为已分配,并将其内容初始化为零。
  • 如果无法找到足够大的空闲内存块,则返回NULL,表示分配失败。

2)calloc 和 malloc 函数的区别

两者都是开辟空间的函数;函数返回的是void*类型的指针,需要进行类型转换,以便与具体的数据类型相匹配;开辟的空间使用后都需要用free函数进行释放,避免造成内存泄漏。

只是 calloc 在开辟空间后还能再对这块空间进行初始化(初始化的值为0)

calloc == malloc + memset

 

 我们发现结果是10个随机值

我们发现结果是10个0

 4. realloc 的相关细节 的相关细节

realloc 函数是C语言中的一个内存管理函数,用于重新分配已分配内存块的大小(包括减少内存空间的大小)。它可以用于调整已经分配的堆内存块的大小,以便满足程序的需求。

realloc 函数的原型如下:

void* realloc(void* ptr, size_t size);

其中,ptr是指向已分配内存块的指针,size是需要重新分配的内存块的大小。

realloc 函数的工作原理如下:

  1. 如果ptr为NULL,则realloc的行为等同于malloc,即分配一个新的内存块。
  2. 如果size为0,则realloc的行为等同于free,即释放ptr指向的内存块。
  3. 如果ptr不为NULL且size不为0,则realloc会尝试重新分配内存块的大小。
    • 如果新的大小小于等于原来的大小,realloc会保留原来的内存块,并返回ptr
    • 如果新的大小大于原来的大小,realloc会尝试扩大内存块的大小。如果扩大成功,会返回一个指向新内存块的指针,并且原来的内存块会被释放。如果扩大失败,ptr仍然是有效的,并且原来的内存块保持不变。

需要注意的是,realloc函数可能会将已分配的内存块移动到新的位置,因此在使用realloc重新分配内存块后,原来的指针可能会失效。因此,在使用realloc后,应该使用返回的新指针来访问重新分配后的内存块

二、常见的动态内存错误

1、对NULL指针的解引用操作

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

malloc 可能会出现内存开辟失败,然后函数返回NULL,因此在动态申请空间后一定要判断空间是否开辟成功。

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

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的时候越界访问
 }
 free(p);
}

这跟数组的空间相同,都不能进行越界访问。(数组越界访问可能造成的后果,可以看一下我的这一篇博客:写文章-CSDN创作中心https://mp.csdn.net/mp_blog/creation/editor/130806058

3、对非动态开辟内存使用free释放

这个在前面讲解 free 函数相关细节时,已经提到过了。

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

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

所以开辟空间返回的指针,一定不要移动,当我们需要移动时,可以再定义一个指针进行移动。

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

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

所以,我们一定要养成一个良好的写代码习惯:在free(p)后,p=NULL;这样就不会出现这种情况了。

6、动态开辟内存忘记释放(内存泄漏)

动态分配的内存空间在超出其作用域后不会自动销毁。在C语言中,动态分配的内存空间需要手动释放,否则会导致内存泄漏。

当使用malloccallocrealloc函数动态分配内存时,内存空间的生命周期不会受限于其作用域。即使离开了分配内存的代码块或函数,该内存空间仍然存在,直到显式调用free函数进行释放。

这是因为C语言中的内存管理是手动的,程序员需要负责分配和释放内存空间。动态分配的内存空间会一直存在,直到被显式释放,或者程序终止运行

以下是一个示例代码,演示了动态分配的内存空间在超出作用域后仍然存在的情况:

#include <stdio.h>
#include <stdlib.h>

void function() {
    int* ptr = (int*)malloc(sizeof(int));
    *ptr = 10;
    printf("Inside function: %d\n", *ptr);
}

int main() {
    function();
    printf("Outside function\n");

    return 0;
}

在上面的示例中,function函数内部使用malloc函数动态分配了一个整数的内存空间,并将其值设置为10。然后,在function函数结束后,我们在main函数中打印了"Outside function"的消息。

即使function函数结束,但是动态分配的内存空间仍然存在。如果我们不在适当的地方调用free函数释放该内存空间,将会导致内存泄漏。

因此,确保在不再需要动态分配的内存空间时,通过调用free函数来显式释放它们,以避免内存泄漏问题。

三、柔性数组

概念:

柔性数组允许在结构体的末尾定义一个长度可变的数组,这样可以在运行时动态地调整结构体的大小。这种特性在动态内存管理和数据结构设计中非常有用,特别是在需要处理变长数据的情况下。

要注意的几点:

  1. 柔性数组只能定义在结构体的末尾,而且结构体不能有其他成员后跟柔性数组。
  2. 柔性数组不能作为单独的类型,它必须作为结构体的成员(在数组前面至少有一个变量)。
  3. 结构体中的柔性数组可以在动态内存分配时分配更多的空间

柔性数组的大小:

它是根据结构体定义中的其他成员和柔性数组中元素的数量来计算的。柔性数组的大小在结构体的大小中是不计算在内的。

(注:在C语言中,结构体的大小是所有成员大小的总和,并且通常还要考虑对齐要求。)

柔性数组的大小计算遵循以下规则:

  1. 结构体的大小由所有非柔性数组成员的大小总和决定。
  2. 柔性数组的大小不计算在结构体的大小中。
  3. 柔性数组的大小取决于在运行时动态分配的数组元素数量。

柔性数组的基本用法:

#include <stdio.h>
#include <stdlib.h>
struct test
{
	int n;
	int arr[];
};
int main()
{
	struct test* s = (struct test*)malloc(sizeof(struct test) + 40);
	if (s == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	s->n = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		s->arr[i] = i + 1;
	}
	printf("扩容前:\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", s->arr[i]);
	}
	// 增容,增加数组的大小
	struct test* temp = (struct test*)realloc(s ,sizeof(struct test) + 60);
	if (temp == NULL)
	{
		perror("realloc");
		exit(-1);
	}
	s = temp;
	printf("\n扩容后:\n");
	for (i = 0; i < 15; i++)
	{
		printf("%d ", s->arr[i]);
	}
    free(s);
    s = NULL;
	return 0;
}

运行结果为:

扩容前:
1 2 3 4 5 6 7 8 9 10
扩容后:
1 2 3 4 5 6 7 8 9 10 -842150451 -842150451 -842150451 -842150451 -842150451

新扩容的空间没有进行赋值,所以数据为随机值。

柔性数组的模拟实现:

#include <stdio.h>
#include <stdlib.h>

struct test
{
	int n;
	int* arr;
};
int main()
{
	struct test* pa = (struct test*)malloc(sizeof(struct test));//此时开辟的只是int n 的空间
	if (pa == NULL)
	{
		perror("pa_malloc");
		exit(-1);
	}
	pa->n = 100;
	pa->arr = (int*)malloc(40);
	if (pa->arr == NULL)
	{
		perror("pa->arr_malloc");
		exit(-1);
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		pa->arr[i] = i + 1;
	}
	printf("扩容前:\n");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", pa->arr[i]) ;
	}
	int* temp = (int*)realloc(pa->arr, sizeof(int) * 15);
	printf("\n扩容后:\n");
	for (i = 0; i < 15; i++)
	{
		printf("%d ", pa->arr[i]);
	}
    free(pa->arr);
    pa->arr = NULL;
    free(pa);
    pa = NULL;
	return 0;
}

对比图解:

第一个好处是:方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。

内存碎片(Memory Fragmentation)是指在内存管理过程中出现的不连续、分散的小块未被使用的内存区域。它是由于频繁的内存分配和释放操作导致内存空间不连续而产生的现象。内存碎片会降低内存的有效利用率,并可能影响系统性能和稳定性。

内存碎片可以分为两种类型:

  1. 外部碎片(External Fragmentation): 外部碎片是指内存中有足够的总空闲内存,但由于空闲内存被分割成多个不连续的小块,无法满足大内存块的分配请求。即使总空闲内存足够,由于碎片化,系统无法满足较大内存需求。这种情况下,即使没有内存空间的真正不足,也可能无法分配所需大小的连续内存空间。

  2. 内部碎片(Internal Fragmentation): 内部碎片是指已经被分配给进程或应用程序的内存块中,有部分未被使用的空间。这种未使用的空间是由于内存分配策略和对齐要求导致的。虽然进程已经获得了一定大小的内存,但它只使用其中的一部分,导致浪费。

注意:malloc 开辟空间后,要记得 free 释放空间。


如果你觉得这篇博客对你有帮助的话 ,希望你能够给我点个赞,鼓励一下我。感谢感谢……

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值