让你迷上动态内存的用法及管理

动态内存管理

  • 1.为什么存在动态内存分配
  • 2.动态内存函数的介绍
    • 2.1:malloc
    • 2.2:free
    • 2.3:calloc
    • 2.4:realloc
  • 3.常见的动态内存错误
    • 3.1:对NULL的解引用操作
    • 3.2:对动态开辟空间的越界访问
    • 3.3:对非动态内存使用free释放
    • 3.4:使用free释放一块动态内存的一部分
    • 3.5:对同一块内存释放多次
    • 3.6:动态内存忘记释放(内存泄露)
  • 4.几个经典的笔试题
  • 5.柔性数组

1.为什么存在动态内存分配

首先我们普及一下有关内存的知识:
内存分为三个区域:栈区,堆区,静态区。

1:堆区存放的是:局部变量,函数的形式参数。
2:堆区存放的是:动态开辟的内存:malloc,free,calloc,realloc。
3:静态区存放的是:全局变量,静态变量(static所修饰的)。

具体如图:
让你迷上动态内存的用法及管理_第1张图片
而我们现在已知的内存开辟方式主要有两种:

1:创建一个变量。
例如:int a=10;(假设是局部变量)在栈区空间开辟四个字节的空间。

2:创建一个数组。
例如:char arr[20];(假设是局部变量)在栈区空间开辟20个字节的就空间。

但是以上两种开辟方式都有相同的局限性:
1:开辟的空间大小是固定的,无法进行调节。
2:数组在声明的时候,必须指定数组的长度,它所需要的内存在编译的时候分配,但是对于空间的需求,不仅仅是上述的情况,有的时候我们需要空间的大小是在程序运行的时候才知道的,那数组在编译的时候开辟空间的方式就不满足了,这个时候只能试试动态内存开辟空间了。

2.动态内存函数的介绍

2.1:malloc

void* malloc(size_t size)

1:头文件 include
2:功能:在堆区开辟(申请)一块连续可用大小为size的空间,并返回指向这块空间的指针。
3:注意事项:

(1):如果开辟成功,则返回开辟好的空间的指针。
(2):如果开辟失败,则返回一个NULL,因此malloc的返回值要进行检查。
(3):返回类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候由自己来决定。
(4):如果参数size=0,malloc的行为是标准未定义的,取决于编译器。

4:函数的使用:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	char* p=(char*)malloc(40);
	if(p==NULL)
	{
		//打印错误的原因
		printf("%s\n",strerror(errno));
	}
	else
	{
		//开辟成功
		int i=0;
		for(i=0;i<10;i++)
		{
			*(p+i)=i;
			printf("%d ",*(p+i));
		}
	}
	return 0;
}

2.2:free

void* free(void* memoryblock)

1:头文件 include
2:功能:用来释放动态内存开辟的内存空间。
3:注意事项
(1):如果参数指向的空间不是动态内存开辟的,那free函数的行为未定义的。
(2):如果参数是NULL,则函数什么事情都不做。
4:函数的使用:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	//向内存申请10个整形空间大小
	char* p = (int*)malloc(40);//这里强制类型转换,因为malloc的返回类型时void*
	if (p == NULL)
	{
		//打印错误原因
		printf("%s\n", strerror(errno));//INT_MAX*10
	}
	else
	{
		//开辟成功
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

当动态申请的空间不再使用的时候,就应该把剩下的空间还回去,这里就用到了free函数,这里我们注意到了,我么free§后,还把p赋值成了NULL,因为虽然我们把p所指向的空间释放掉了,但是p还在,也就是说只要我使用了p我就能通过p找到一块内存空间,但是这块内存空间哪里的我们不知道,因为此时的p是野指针,所以在最后的时候,我们要把p赋值成空指针。

这里我们就知道malloc和free一般是成对出现的,成对使用的。

2.3:calloc

void*calloc(size_t num,size_t size)

1:头文件 include
2:功能:将num个大小为字节size的元素开辟一块空间,并且把这块空间的每个字节初始化为0。
3:注意事项:
(1):如果开辟成功,则返回开辟好的空间的指针。
(2):如果开辟失败,则返回一个NULL,因此malloc的返回值要进行检查。
(3):返回类型是void*,所以calloc函数并不知道开辟空间的类型,具体在使用的时候由自己来决定。
(4):如果参数size=0,calloc的行为是标准未定义的,取决于编译器。

与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
4:函数使用:

int main()
{
	int* p=(int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		//打印错误原因
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	//释放空间
	//free函数是用来释放动态内存村空间的
	free(p);
	p = NULL;
	return 0;
}

2.4:realloc

void* realloc(void* ptr,size_t size)

1:头文件 include
2:功能:有时候我们会发现我们之前申请的空间太大了,有时候我们又会发现我们申请的空间太小了,那位了合理的使用内存我们一定会对内存的大小做灵活的调增,这个时候realloc函数就可做到动态开辟内存的大小调整。
3:注意事项:
(1):参数ptr是要调整的内存地址。
(2):参数size是调整之后的新大小。
(3):返回值为调整之后的起始地址。
(4):realloc在调整内存空间的时候存在两种情况:
情况一:如果ptr指向的空间之后还有足够的空间可以追加,则直接追加啊,返回起始地址。
情况二:如果ptr指向的空间之后没有足够的靠空间可以追加,则realloc函数会重新找一个足够大的一块连续空间,并开辟空间,并且把原来的数据拷贝到这块空间并释放原来的内存空间。
所以基于上面的情况,我们得用一个新的变量来接受realloc的返回值,并进行判断;
情况一:
让你迷上动态内存的用法及管理_第2张图片
情况二:
让你迷上动态内存的用法及管理_第3张图片

4:函数的使用:

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;
		}
	}
	//这就是在使用malloc开辟的20个字节的空间
	//假设这里的20个字节不够用我们希望有40个空间
	//这里就可以使用realloc来调整动态开辟的内存
	int* ret = (int*)realloc(p, 30);
	//判断开辟是否成功
	if (ret != NULL)
	{
		p = ret;
		int i = 0;
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	//释放内存
	free(p);
	p = NULL;
	return 0;
} 

3.常见的动态内存错误

3.1:对NULL的解引用操作

int main()
{
	int* p = (int*)malloc(40);int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

上面这种情况是很容易犯的错误,就是我们么有对malloc进行合理判断,假如malloc开辟失败了,那么malloc返回的就是NULL,这个时候把NULL赋值给了p,后面有对p进行解引用,这个时候就会出问题了。
所以这里我们一定要对malloc的返回值进行一个判断,避免出错。

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 0;
	}
	else
	{
		int i = 0;
		for (i = 0; i < 20; i++)
		{
			*(p + i) = i;
		}
	}
	return 0;
}

上述代码malloc开辟了10个int类型大小的空间但是我们后面的for循环中遍历了20遍,这个时候就出问题了,业界访问了。

3.3:对非动态内存使用free释放

int main()
{

	int a = 10;//a是局部变量,是在栈区开辟空间的
	int* p = &a;
	*p = 20;
	free(p);//而free是对堆区进行释放的
	p = NULL;
	return 0;
}

上述的a是局部变量,是在栈区开辟空间的,而我们free函数释放的是堆区上的空间。

3.4:使用free释放一块动态内存的一部分

int main()
{
	int* p =(int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	else
	{
		int i = 0;
		for (i = 0;i < 10; i++)
		{
			*p++ = i;
		}
	}
	//回收空间
	free(p);//free只能从刚刚已经开辟好的空间的起始位置开始释放
	return 0;
}

让你迷上动态内存的用法及管理_第4张图片
上述代码for循环结束后p指向最后一块空间,但是我们的free是从返回的地址开始释放,那么p前面的内存空间就没有释放,而p又被赋值上了NULL,也就是说p被永远的销毁了,那p前面的空间就永远不会别找到了,那也就是说这块空间不会在被使用了,别浪费掉了,就会导致内存泄露。

3.5:对同一块内存释放多次

int main()
{

	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	//使用
	
	//释放空间
	free(p);
	//.......
	free(p);
	return 0;
}

这里用了多个free释放p,会导致错误。

3.6:动态内存忘记释放(内存泄露)

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

这里没有释放内存空间,而我们的电脑或者是其他的电子器材都是有有限的内存空间的,而这里我们我们只是一味地开辟空间而并没有释放空间,这就会导致内存被不断的开辟直至将内存填满,这就会导致我们的电脑新能下降,所以我们要明白的是我们开辟空间和释放空间是成对出现的。

4.几个经典的笔试题

实例1:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
void GetMemory(char* p)
{
	p=(char*)malloc(100);
}
void Test(void)
{
	char* str=NULL;
	GetMemory(str);
	strcpy(str,"hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

这里我们要知道的是传值和传址的区别在哪里,这里我们创建了指针变量str,并且把str传过另一个函数里面,那我们就要明白的是这里传过去的是一个什么,既然创建的是一个指针变量,我们可以类比一下int a;这里的a也是个变量,那我们在类比一下也是GetMemory(a)我们把a传过去的这个是传值,所以这里我们传str也是传值,我们搞清楚了这是个传值的过程,我们把str里面的NULL传给了p而str的类型是char* 所以p的类型也是char *,并且用p创建了100个字节的空间,但是我们要清楚的是这里的p是局部变量,局部变量的特点只在局部变量里面起作用,出了他的生命周期就会自动销毁。所以这里当我们运行到GetMemory函数的时候虽然p确确实实的创建了100个字节的大小,但是一旦运行过后p所创建的空间就被销毁了,而且它同时并没有改变str所指向的值,所以str还是等于NULL,所以到后面的strcpy字符拷贝函数就会出错了。
让你迷上动态内存的用法及管理_第5张图片
让你迷上动态内存的用法及管理_第6张图片

改进1:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
void GetMemory(char** p)
{
	*p=(char*)malloc(100);
}
void Test(void)
{
	char* str=NULL;
	GetMemory(&str);
	strcpy(str,"hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

之前我们学过了想要用一个函数里面的形参来改变实参,那我们就要通过传地址的方式,通过指向的地址来改变里面的值,这就是我们所说的传址操作。这里我们将str的地址传过去了,这个时候str的类型是char*又传过去的是str的地址,所以这里p是一个二级指针,而p其实存放的就是str的地址了,我们对p进行接应用操作就可以该改变str里面的值了。这个时候str就成功的开辟了100个字节的空间了。

改进2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
char* GetMemory(char* p)
{
	p=(char*)malloc(100);
	return p;
}
void Test(void)
{
	char* str=NULL;
	str=GetMemory(str);
	strcpy(str,"hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

上述代码之所以会失败,其主要原因是p在出生命周期会自动销毁,而销毁之后p里面存放的地址就不见了,自然而然str就没有被改变,那我们就想了想既然p被销毁了,那我们就想啊,如果我们可以将开辟好的地址记住,并且告诉str,这样str就知道了开辟空间的地址,有了地址我们就可以存放东西到地址里面去了。所以这里我们这里就返回p的地址就可以了。

实例2:返回栈空间地址的问题

char* GetMemorg()
{
	char p[] = "hello world";//创建局部变量,出了生命周期会销毁,而里面的值也没了
	return p;//返回栈空间的地址(栈空间上的地址不要随便返回)
}
void Test(void)
{
	char* str = NULL;
	str = GetMemorg();//这里str确实是p的地址,但是p里面的值没了
	printf(str);//所以打印随机值
 //存在非法访问内存
}
int main()
{
	Test();
	return 0;
}

这里其实也要考虑到局部变量的生命周期的问题,上面我们定义了一个函数GetMemory,函数里常见了数组p,最后还把数组的地址放回去了,但是我们要知道的是虽然我们知道了地址,而我们知道地址的目的是找到地址所指向的那块空间的内容,而这里数组p是局部变量出了生命周期被销毁,也就是说数组p里面的内容被销毁了还给操作系统了,最后我们打印str虽然有了地址,但是地址所指向的那块空间的内容没了,那还打印什么。

类似的题目:

int* test()
{
	int a = 10;//栈区开辟空间,出生命周期销毁
	return &a;
}
int main()
{
	int* p=test();
	*p = 20;
	return 0;
}

这里是同样的道理,a出生命周期内容被销毁返回给操作系统了,这样就会造成非法访问内存空间里。
但是如果把int a=10改成static int a=10;这样子就可以了,这里a别static后a就不是放在栈区了,而是放在静态区了,这个时候就不会被销毁了,就可以正常是用了。

我们在看一个例子:

int* f(void)
{
	int* ptr;
	*ptr = 100;
	return ptr;
}

这里ptr没有初始化也没有指向对象,那ptr就默认是随机值,而后面对ptr解引用,就是说ptr拿一个随机值做一个地址访问它所指向的空间。这个时候就会出问题了,就会形成非法访问了,这里ptr其实就是野指针。
实例3:

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

这个问题就比较简单了,忘记释放动态内存开辟的空间,导致内存泄露
只要在最后加上:

free(str);
str = NULL;

实例4:

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	
	str = NULL;//改进
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

free释放str指向的空间后,并不会把str置为NULL,会成为野指针。
造成的问题:
1:会篡改动态内存的内容
2:free(str)后str成为野指针,if(str!=NULL)语句不起作用
改进:在free后面加上str=NULL;

5.柔性数组

前言:
C/C++程序内存分配的几个区域
1:栈区(stack):在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束使这些存储单数自动销毁,栈内存分配运算内置处理器的指令集中,效率很高,但是分配的内存容量有限,栈区主要是存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等。
2:堆区(heap):一般由程序员分配释放,若程序员结束可能由OS回收,分配方式类似于链表。
3:数据段(静态区)(static)存放全局变量,静态数据,程序结束后由系统释放。
4:代码端:存放函数数体(类成员函数和全局便函数)的二进制代码。

实际上普通的局部变量在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁,但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

柔性数组(flexible array)
1:概念:在c99中,结构中(包含结构体)的最后一个元素允许是未知大小的数组,这个数组就叫做”柔性数组“成员。
2:柔性数组的特点:
(1):结构中的柔性数组成员前面必须至少有一个成员。
(2):sizeof返回的这种结构大小不包含柔性数组的内存。
(3):包含柔性数组的结构用malloc()函数进行内存的动态分配,并且分配的内存因该大于结构的大小,有以适应柔性数组的预期大小。

柔性数组的形式:

struct S
{
	int n;
	int arr[];//柔性成员
	//有些编译器不支持这种写法也可以向一下方法
	//int arr[0];未知大小的-柔性数组成员-数组的大小是可以调整的
};
int main()
{
	struct S s;
 printf("%d",sizeof(s));
	return 0;
}

这里我们看一下结果:
让你迷上动态内存的用法及管理_第7张图片
这里我们可以看到结构体的大小只有4个字节的大小,这就说明了结构体的大小并没有包括柔性数组,他只计算了n的大小。

柔性数组的使用:

struct S
{
	int n;
	int arr[0];
};
int main()
{
	//使用
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//调整空间
	struct S* ptr=realloc(ps, 44);//调整为数组个元素
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//访问
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//释放空间
    free(ps);
    ps=NULL;
	return 0;
}

上面的(struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));sizeof(struct S)是为结构体开辟空间的,但是因为结构体的大小不包括柔性数组,所以我们得为柔性数组单独开辟空间,也就是 5 * sizeof(int)。后面用realloc调整空间。

我们使用柔性数组的目的是想让结构体中的数组或者类似于数组的这样一块空间,你想让这个结构体能控制一块连续空间,而这个数组可大可小,可以控制的。
这里有个类似的写法:

struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	ps->arr = malloc(5 * sizeof(int));
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//调整大小
	int* ptr = realloc(ps->arr, 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//释放空间
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

我们可以看到这个写法可以做到和柔性数组相同的作用,那我们就会思考了,为什么要使用柔性数组呢?
我们可以给他们两个做一下对比,也可以说是柔性数组的优点:

1:从形式上看第二种使用了两次malloc开辟空间,一次同时是用了两次free释放空间,而我们知道malloc和free总是同时出现的,当malloc用的多的时候free也会使用的多,这个时候就越容易出现错误,比如使用malloc漏了free,而且这里的第二种写法还要确定free的先后顺序。如果是柔性数组的话,他是一次性开辟了一块连续的空间malloc只使用了一次,而free也使用了一次,这样我们的出错率就降低了。

2:malloc的开辟空间的特点是,就是他觉得内存里那块空间是空着的,他就开辟
而开辟的空间有大有小,这个时候有些空间就会使用不上,并且这些没有被使用上的空间是随机分布的,而这些空间在内存上是使用不上的,这些空间叫做内存碎片这些内存碎片越多空间使用率就越低,因为内存碎片不能很好的使用,而像第二种方法,他会使用个malloc就会在内存中开辟多个空间,这个时候 就很有可能在内存中导致内存碎片。而上面的柔性数组,它同时开辟了两块连续的内存空间,这样子它的内存碎片就会更少,内存利用率就更高。

内存池:让你迷上动态内存的用法及管理_第8张图片

你可能感兴趣的:(#,C语言,java,c++,算法)