c语言动态内存管理

1. 为什么要有动态内存分配

动态内存分配为我们提供了很大的便利,如果我们想要一块自定的内村大小,可以通过动态内存管理来实现,从而提升了代码的灵活性,之前我们学习的空间开辟一共两种,

int a = 1;
int arr[] = {1,2,3};

这两种方式并不能根据我们自己的需求来修改需要的内存,这两种内存已经是固定的,无法进行修改了的,数组大小是在一开始就申明好的,无法进行修改数组大小的操作。

2. malloc和free

c语言提供了一个开辟内存的函数叫malloc,具体怎么用呢看一段代码,

void* malloc (size_t size);

这是malloc的使用,参数是一个size_t类型的,返回类型是void*,参数传入要开辟空间的大小,接下来使用一下malloc

int*a = (int*)malloc(4*sizeof(int));

这段代码是使用malloc,首先创建一个int*a的指针来接收,因为malloc返回的开辟空间的地址值,是一个void*型,我们要用int*来强转,然后再创建一个int*的指针来接收,a指向的便是开辟的空间,类似于数组,但是这种内存空间是可控的.

如果开辟开辟失败则返回空指针NULL,如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

2.2 free

c语言提供了另外一个函数free,是专门用来回收内存空间的 ,

void free (void* ptr);

free如果参数不是动态空间,那free的行为是未定义的,如果参数是空指针,那free是不进行任何操作的,malloc和free都在<stdlib.h>里的,举个例子

#include <stdio.h>
#include <stdlib.h>
int main()
{
 int num = 0;
 scanf("%d", &num);
 int arr[num] = {0};
 int* ptr = NULL;
 ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
 {
 int i = 0;
 for(i=0; i<num; i++)
 {
 *(ptr+i) = 0;
 }
 }
 free(ptr);//释放ptr所指向的动态内存
 ptr = NULL;//是否有必要?
 return 0;
}

图中的ptr=NULL是否有必要呢,仔细想来,ptr指向的空间被free回收了,但是ptr依然是指向那个空间,因此ptr就成为了一个野指针,野指针是很危险的,所以把ptr置为空指针是很有必要的。

3. calloc和realloc

3.1 calloc

c语言还提供了一个函数叫calloc,和malloc类似的。

void* calloc (size_t num, size_t size);

calloc两个参数,第一个参数是开辟个数,第二个是开辟类型,所以和calloc类似,但是不同的是,malloc会把所有开辟空间的值都设置为0 ,举个例子

#include <stdio.h>
#include <stdlib.h>
int main()
{
 int *p = (int*)calloc(10, sizeof(int));
 if(NULL != p)
 {
 int i = 0;
 for(i=0; i<10; i++)
 {
 printf("%d ", *(p+i));
 }
 }
 free(p);
 p = NULL;
 return 0;
}

输出结果:0 0 0 0 0 0 0 0 0 0

所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。

3.2 realloc

realloc从名字上面理解一下,re是又的意思,所以relloc是又开辟一块新的空间,并且把原先的值赋值到新的空间里面去,• 有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时
候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。

函数原型如下:

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

ptr是需要修改的地址,size为修改后的内存

realloc分为两种情况,

第一种是原先有足够大的空间地址,会在后面继续开辟剩下的地址

第二种是原先没有足够大的空间,会重新申请一块新的空间来存放,并且把原先的数据都复制到这个空间内。

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

4.1 对NULL指针的解引⽤操作

void test()
 {
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题
 free(p);
 }
  • INT_MAX 是一个非常大的值,通常是 2^31 - 1(在32位系统中),即 2147483647。
  • INT_MAX / 4 的值大约是 536870911。
  • malloc 函数尝试分配约 2 GB 的内存,这在大多数系统上是不合理的,因为大多数用户程序无法分配这么多内存。这个分配很可能会失败,返回一个空指针(NULL)。

所以我们要进行检测,p是否为空指针。

4.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);
 }

毫无疑问,程序会直接崩溃。

4.3 对⾮动态开辟内存使⽤free释放

free是专门用来释放动态内存的,如果对非动态内存来进行释放,同样程序会崩溃掉的。

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

4.4 使⽤free释放⼀块动态开辟内存的⼀部分

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

如果p指向的不是动态内存的起始地址,同样,程序会直接崩溃的。

4.5 对同⼀块动态内存多次释放

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

对动态空间的重复释放,也会导致程序的崩溃。

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

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

在主函数中,调用完test函数后,函数创建的动态内存没有被释放掉,而程序也没有停下的意思,就造成了内存泄漏。

总计一下,动态内存空间的释放一共就两种方式,第一种就是使用free函数,第二种就是等待整个程序的结束吗,整个程序结束后,动态内存的空间就被释放掉了,在一些服务器中,需要24小时不停止的运行,因此动态空间忘记释放之后,并不能通过程序结束来释放,因此在使用完动态空间之后,一定要进行空间的释放,并且把指向动态空间的指针置为空指针,防止出现野指针。     

5. 动态内存经典笔试题分析

5.1 题⽬1:

void GetMemory(char *p)
 {
 p = (char *)malloc(100);
 }
void Test(void)
 {
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
 }

分析一下,程序能打印出来Hello world吗。

答案是不能的,因为在传入str中,p只是一个副本,p指向的空间也只是一个副本,离开了这个函数并不会对str有什么影响。

5.2 题⽬2:

char *GetMemory(void)
 {
 char p[] = "hello world";
 return p;
 }
void Test(void)
 {
 char *str = NULL;
 str = GetMemory();
 printf(str);
 }

还是不会输出hello world,虽然用str接收了返回值,但是数组p在函数运行之后,内存就已经释放掉了。

5.3 题⽬3:

void GetMemory(char **p, int num)
 {
 *p = (char *)malloc(num);
 }
void Test(void)
 {
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
 }

这个程序是可以打印出hello world的,因为传入的是str的地址,修改的也是str值指向的内容

5.4 题⽬4:

void Test(void)
 {
 char *str = (char *) malloc(100);
 strcpy(str, "hello");
 free(str);
 if(str != NULL)
 {
 strcpy(str, "world");
 printf(str);
 }
 }

 这个也是不行的,str在释放之后,指向的内存空间已经不复存在,但是str依然指向一块内存,所以str已经成为了一个野指针,再次拷贝字符串,程序会崩溃。

6. 柔性数组

柔性数组根据名字我们可以大概来猜一下,这个数组应该是可变数组。

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
typedef struct st_type
{
 int i;
 int a[];//柔性数组成员
}type_a;

a可以写0也可以不写,但是有些编译器会报错,此时就可以把0删除即可。

6.1 柔性数组的特点:

柔性数组是结构体最后一个成员,并且他的前面一定有其他成员变量,sizeof计算结构体大小时是不会计算柔性数组大小的,包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。

例如:

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
int main()
{
 printf("%d\n", sizeof(type_a));//输出的是4
 return 0;
}

6.2 柔性数组的使⽤

struct S
{
	int i;
	int a[];
}a;
int main()
{
	
	
	struct S*a = (struct S*)malloc(4 + 40);
	a->i = 0;

}

因此a[]就有了40个字节大小的空间。  

6.3 柔性数组的优势

上述的代码通过这个代码也可以实现

struct S
{
	int i;
	int* a;
}a;
int main()
{
	a.i = 0;
	int* p = (int*)malloc(40);
	if (p != NULL)
	{
		a.a = p;
	}
	

}

两种代码是不同风格的代码,第一种是直接在结构体里开辟40个字节的空间,而第二个是在结构体里创建一个指针,指针指向一个40字节的空间,二者各有利弊。

第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)

7. 总结C/C++中程序内存区域划分

C/C++程序内存分配的⼏个区域:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时
这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅
式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝莓星冰乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值