动态内存管理

动态内存管理,第1张

为什么动态内存管理

我们目前已知的开辟内存的方法

int val = 20 ; // 在栈空间上开辟四个字节 char arr [ 10 ] = { 0 }; // 在栈空间上开辟 10 个字节的连续空间 但是上述的开辟空间的方式有两个特点: 1. 空间开辟大小是固定的。

2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。

有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。

这时候就只能试试动态存开辟了

动态内存函数的介绍  malloc和free malloc free 都声明在 stdlib.h 头文件中。

-

malloc

 void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

如果开辟成功,则返回一个指向开辟好空间的指针。

如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

如果参数 size 0malloc的行为是标准是未定义的,取决于编译器。

下面举例 

#include 
#include 

int main()
{
	//申请空间
	int* ptr = (int*)malloc(40); //返回类型强转为 int*
	int* p = ptr;                 //记录起始地址

	if (p == NULL)        //判断是否为空指针
	{
		perror("malloc");
		return 1;
	}
	//打印新开辟的空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}

}

 free

void free (void* ptr);

释放时必须是在内存空间的起始位置

free函数用来释放动态开辟的内存。

如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

如果参数 ptr NULL指针,则函数什么事都不做

当我们不释放动态申请的内存的时候
如果程序结束,动态申请的内存由 *** 作系统自动回收
但是如果程序不结束,动态内存是不会自动回收的,就会形成内存泄露的问题,free空指针啥也不发生
//释放空间
	free(ptr);
	ptr = NULL;

	if (ptr != NULL)
	{
		*ptr = 100;
	}
	*ptr = 100;//err
	return 0;
calloc
void* calloc ( size_t num , size_t size ); 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0

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

                

#include 
#include 
int main()
{
 int *p = (int*)calloc(10, sizeof(int));
 if(NULL != p)
 {
 //使用空间}
 free(p);
 p = NULL;
 return 0; }
realloc
void* realloc ( void* ptr , size_t size ); realloc 函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。

那 realloc 函数就可以做到对动态开辟内存大小的调整。

函数原型如下: ptr 是要调整的内存地址 size 调整之后新大小 返回值为调整之后的内存起始位置。

这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新 的空间 realloc 在调整内存空间的是存在两种情况: 情况 1 :原有空间之后有足够大的空间 情况 2 :原有空间之后没有足够大的空间 情况 1 当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

情况 2 当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。

这样函数返回的是一个新的内存地址

int main()
{
	int*p = (int*)malloc(40);
	
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;//0 1 2 3 4 5 6 7 8 9
	}

	//空间不够,希望能放20个元素,考虑扩容
	int*ptr = (int*)realloc(p, 80);
	if (ptr != NULL)
	{
		p = ptr;                 //我们希望还是p来掌管这块新空间,先用ptr做个周转。

因为扩容失败也会返回空指针 } //扩容成功了,开始使用 //不再使用,就释放 free(p); p = NULL; return 0; }

常见的动态内存错误 对空指针解引用 *** 作
void test () { int * p = ( int * ) malloc ( INT_MAX / 4 ); * p = 20 ;            // 如果 p 的值是 NULL ,就会有问题,造成空指针从而访问失败 free ( p ); } 如何避免: 记得对malloc返回值进行判空 *** 作。

记得最后进行free free(p); p = NULL;

对动态内存开辟的越界访问 
void test () { int i = 0 ; int * p = ( int * ) malloc ( 100 ); if ( NULL == p ) { exit ( EXIT_FAILURE ); } for ( i = 0 ; i <= 25 ; i ++ ) { * ( p + i ) = i ;                // i是25的时候越界访问 } free ( p ); } 对内存边界进行检查
对非动态内存进行释放 
void test () { int a = 10 ; int * p = & a ; free ( p ); } 编译器会对非动态内存释放
使用free释放一块动态开辟内存的一部分 
void test () { int * p = ( int * ) malloc ( 100 ); if(p == NULL) {
return 1
}
for(int i=0;i<10;i++)
{ *p=i; p ++ ; } free ( p ); p = NULL; } 此时的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 ); } p是个局部变量,出了之后就被销毁,没有人记得申请空间的地址,无法释放。

造成内存泄漏,之后想释放也释放不了

笔试题目
void GetMemory ( char * p ) { p = ( char * ) malloc ( 100 );   //本身p是str的临时拷贝,p也是空指针,出了作用域,p被销毁。

} void Test ( void ) { char * str = NULL ; GetMemory ( str ); strcpy ( str , "hello world" );   //对空指针进行解引用,造成程序崩溃,内存泄漏 printf ( str );                    //这种写法是将str首地址传过去进行打印 } int main() {    Test()    return 0;
}
输出:打印不出来


char * GetMemory ( void ) { char p [] = "hello world" ;  //栈上开辟的空间,出了这个函数就被 *** 作系统回收 return p ; } void Test ( void ) { char * str = NULL ; str = GetMemory (); printf ( str ); } int main() {    Test()    return 0;
}
输出:烫烫烫 原因:str先是空指针,然后进入GetMemory函数,p创建了字符串,有了地址,出来作用域p所创建的字符串被销毁,空间已经不属于我们了,但p的地址被返回str,str通过这个地址再去访问的时候,就变成了野指针

void GetMemory ( char ** p , int num ) { * p = ( char * ) malloc ( num ); } void Test ( void ) { char * str = NULL ; GetMemory ( & str , 100 ); strcpy ( str , "hello" ); printf ( str ); } int main() {    Test()    return 0;
}
输出:hello 申请的空间忘记释放

void Test ( void ) { char * str = ( char * ) malloc ( 100 ); strcpy ( str , "hello" ); free ( str ); if ( str != NULL ) { strcpy ( str , "world" ); printf ( str );              //野指针。

free后空间已经回收了,但str还记得空间的地址。

想在复制上world造成非法访问 } } int main() {    Test()    return 0;
}

C/C++程序的内存开辟
C/C++ 程序内存分配的几个区域: 1. 栈区( stack ):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。

栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。

栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。

2. 堆区( heap ):一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS( *** 作系统) 回收 。

分配方式类似于链表。

3. 数据段(静态区)( static )存放全局变量、静态数据。

程序结束后由系统释放。

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

有了这幅图,我们就可以更好的理解在《 C 语言初识》中讲的 static 关键字修饰局部变量的例子了。

实际上普通的局部变量是在 栈区 分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被 static 修饰的变量存放在 数据段(静态区) ,数据段的特点是在上面创建的变量,直到程序结束才销毁 所以生命周期变长。

 柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员

struct S1
{
    int num;
    double d;
    int arr[];//柔性数组成员
};


struct S2
{
    int num;
    double d;
    int arr[0];//柔性数组成员

柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员。

sizeof 返回的这种结构大小不包括柔性数组的内存。

包含柔性数组成员的结构用 malloc () 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

上图是预留了10个元素给柔性数组 

struct S3
{
	int num;//4
	int arr[];//柔性数组成员
};


int main()
{
	struct S3* ps = (struct S3*)malloc(sizeof(struct S3)+40); //“+40”预留十个整型数组大小
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}

	ps->num = 100;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
柔性数组的优势 

上述还可以不用柔性数组来实现

 
typedef struct st_type
{
int i;int *p_a;
}type_a;
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));
//业务处理
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
}

这个是是malloc开辟两次空间从而实现。

但是第一种代码更优。

原因如下:

第一个好处是:方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。

用户调用free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free ,所以你不能指望用户来发现这个事。

所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free 就可以把所有的内存也给释放掉。

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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/674294.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存