动态内存管理 - malloc、calloc、realloc、柔性数组

目录

一、为什么存在动态内存分配

二、动态内存函数的介绍

1、1 malloc  

1、2 free

1) 动态开辟多少个字节的内存空间,返回该空间的起始地址;且开辟的空间使用方法,类似于数组,是一块连续的空间,使用时如数组一样

 2)内存申请是有限制的,当开辟空间过大,内存不够时,会返回空指针

3、calloc

4、realloc

关于realloc函数开辟空间的几种情况:

三、常见的动态内存错误

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

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

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

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

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

6.内存泄露

堆上的内存释放只有两种可能:1.free释放 2.程序结束否则会出现 内存泄露

可以结合一个故事来看:

四、几个经典的笔试题

1.

 2.

3.

4.

 5.


一、为什么存在动态内存分配

我们已经掌握的内存开辟方式有:
int val = 20 ; // 在栈空间上开辟四个字节
char arr [ 10 ] = { 0 }; // 在栈空间上开辟 10 个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态存开辟了。

二、动态内存函数的介绍

1、1 malloc  

1、2 free

1) 动态开辟多少个字节的内存空间,返回该空间的起始地址;且开辟的空间使用方法,类似于数组,是一块连续的空间,使用时如数组一样


//**********  malloc - 动态开辟多少个字节的内存空间,返回该空间的起始地址
//且开辟的空间类似于数组,是一块连续的空间,使用时如数组一样
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
	//申请开辟空间
	int* p = (int*)malloc(20);//动态开辟20个字节的内存空间,返回该空间的起始地址,用p保存下来
	if (p == NULL)//如果开辟空间失败会返回空指针,所以要判断一下
	{
		printf("%s", strerror(errno));
		return 1;
	}
	//使用空间
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
		//p[i] = i + 1; //都可以表示
		printf("%d ", p[i]);
	}//1 2 3 4 5

	//释放空间:主动申请,主动释放,释放后置为空指针
	free(p);//如果不释放,则为野指针,后期存在隐患
	p = NULL;

	return 0;
}

 

 2)内存申请是有限制的,当开辟空间过大,内存不够时,会返回空指针

//内存申请是有限制的,当开辟空间过大,内存不够时,会返回空指针
int main()
{
	//申请开辟空间
	int* p = (int*)malloc(2000000000);//动态开辟2000000000个字节的内存空间,空间不够返回空指针
	if (p == NULL)//如果开辟空间失败会返回空指针,所以要判断一下
	{
		printf("%s", strerror(errno));//此时返回报错信息:Not enough space
		return 1;
	}
	//使用空间

	//释放空间:主动申请,主动释放,释放后置为空指针
	free(p);
	p = NULL;//如果不置空,则为野指针,后期存在隐患

	return 0;
}

 

3、calloc

//*********  calloc - 开辟多少个什么类型的空间,并且初始化为0
int main()
{
	//申请开辟空间
	int* p = (int*)calloc(10, sizeof(int));//动态开辟10个整型类型的内存空间,返回该空间的起始地址,用p保存
	if (p == NULL)//同样要判断一下:如果开辟空间失败会返回空指针
	{
		printf("calloc-->%s", strerror(errno));
		return 1;
	}

	//使用空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}//没有赋值,打印发现,默认初始化为0了
	//0 0 0 0 0 0 0 0 0 0

	//释放空间:主动申请,主动释放,释放后置为空指针
	free(p);
	p = NULL;//如果不置空,则为野指针,后期存在隐患

	return 0;
}

//malloc 和 calloc 的区别:
/*
1.参数不一样
	1)malooc(要开辟的字节数)
	2)calloc(开辟几个,数据类型的大小)
2.虽然都是再堆区申请内存空间,但是malloc不初始化,而calloc会初始化为0
	1)如果需要初始化,就使用calloc
	2)如果不需要初始化,就使用malloc
*/

4、realloc

realloc 函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。
函数原型如下:
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。

realloc在调整内存空间的是存在两种情况:

情况 1
当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况 2
当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小
的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况, realloc 函数的使用就要注意一些。
举个例子:

1.realloc重新调整动态开辟的空间的大小 

int main()
{
	//开辟20字节空间
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i + 1;
		printf("%d ", p[i]);
	}//1 2 3 4 5

	//realloc
	//20个字节已经使用完了,如果此时,还想继续使用这一块空间,需要扩容,这时realloc可以发挥作用了
	int* str=(int*)realloc(p, 400000);//从动态开辟空间的起始位置,往后重新调整为40个字节的空间
	//int* str=(int*)realloc(p, 400000);//换成400000观察,开辟前后的地址变化

	if (str != NULL)
	{
		p = str;
	}
	else
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//继续使用该空间
	for (i = 5; i < 10; i++)
	{
		p[i] = i + 1;
		printf("%d ", p[i]);
	}//6 7 8 9 10

	//释放空间
	free(p);
	p = NULL;

	return 0;
}

 

关于realloc函数开辟空间的几种情况:


可能存在新开辟空间的地址与原有的地址不同的情况
一、开辟空间成功:
    1.第一种:如果原有的空间后面有足够的空间,可以直接接着开辟新的空间,然后返回原有的空间的地址。
(返回原有空间的地址)
    2.第二种:如果原有的空间后面没有足够的空间,那么会再内存里找一块更大的空间,新开辟这块空间,
然后将原有空间的数据拷贝到这块新空间里,释放原有的空间,并返回这个新空间的地址。
(返回新空间的地址)
二:开辟空间失败
    返回空指针

注意:新开辟的空间返回的指针,不能直接使用原始的指针p来接收,必须先用一个临时的指针接收看看是否为空指针。
    因为:如果开辟新空间失败,那么会返回NULL空指针,p原本指向一块空间,此时被置为空,那么原来空间里的数据就没人能找到了,数据就丢失了。
(开辟新空间不成,还把原有空间的内容搞丢了,俗话叫赔了夫人又折兵)
    所以,realloc之后,要判断一下指针是否为空指针,如果不为空,则再用p接收
        int* str=(int*)realloc(p, 40);
    if (str != NULL)
    {
        p = str;
    }
    else
    {
        printf("%s\n", strerror(errno));
        return -1;
    }

 2.realloc也可以像malloc一样申请空间

int main()
{
	//malloc
	//calloc
	//realloc - 调整申请的堆区内存的大小的
	
	//补充:realloc也可以像malloc一样申请空间
	int* p = (int*)realloc(NULL, 20);//malloc(20);
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i + 1;
		printf("%d ", p[i]);
	}
	free(p);
	p = NULL;
	return 0;
}

三、常见的动态内存错误

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


int main()
{
	int* p = (int*)malloc(20);
	//如果p的值是NULL,就会有问题
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i + 1;
	}
	free(p);
	p = NULL;
}

修改:

//修改:
int main()
{
	int* p = (int*)malloc(20);
	//对p做一个空指针NULL的判断
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i + 1;
	}

	free(p);
	p = NULL;
}

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

//2.对动态开辟空间的越界访问
int main()
{
	int* p = (int*)malloc(20);
	//对p做一个空指针NULL的判断
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//越界访问 - 程序崩溃了
	int i = 0;
	for (i = 0; i < 10; i++)//原本20个字节,只能访问5个整型,现在越界访问(从i=5开始越界)
	{
		p[i] = i + 1;
	}

	free(p);
	p = NULL;
}

修改:

int main()
{
	int* p = (int*)malloc(20);
	//对p做一个空指针NULL的判断
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//越界20字节以内的
	int i = 0;
	for (i = 0; i < 5; i++)//只访问5个整型
	{
		p[i] = i + 1;
	}

	free(p);
	p = NULL;
}

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

//3.对非动态开辟内存使用free释放
int main()
{
	//非动态开辟内存 - 不在堆上
	int arr[10] = { 1,2,3,4,5 };
	int* p = arr;

	//对非动态开辟内存使用free释放 - 程序崩溃了
	free(p);
	p = NULL;
	return 0;
}

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

//4.使用free释放一块动态开辟内存的一部分
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//使用 [1] [2] [3] [4] [5] [] [] [] [] []
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p = i + 1;
		p++;
	}
	//此时p不再指向内存的起始位置,p指向i=5的位置;
	//释放内存的一部分,程序崩溃
	free(p);
	p = NULL;

	return 0;
}

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

//5.同一块动态内存多次释放
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//释放一次
	free(p);
	//又释放一次,会崩溃
	free(p);//此时p是一块未初始化的指针,释放会出问题
	p = NULL;

	return 0;
}

 

 修改:

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//释放一次
	free(p);
	p = NULL;//第一次释放时,置空,后面再释放没问题

	//又释放一次
	free(p);//此时p是NULL空指针,释放没有任何作用,不会报错
	p = NULL;

	return 0;
}

6.内存泄露

堆上的内存释放只有两种可能:1.free释放 2.程序结束
否则会出现 内存泄露

//6.堆上的内存释放只有两种可能:1.free释放 2.程序结束
//否则会出现 内存泄露
test()
{
	int* p = (int*)malloc(20);
	//此时,函数调用使用栈空间,p临时变量也是再栈空间,调用结束回收栈空间
	//而malloc开辟的空间时堆空间,不会因为函数结束而释放
	//所以该函数回收之后,指针p被回收,malloc开辟空间还在,而此时没有人知道该空间的位置了
	//造成了内存泄露
}
int main()
{
	test();

	return 0;
}

可以结合一个故事来看:

        假设test函数时一个案子,要破这个案子,派卧底p监视malloc的情况,只有p知道这个malloc的位置。然后这个案子结束的时候,p死了,那么malloc的线索在这里
就断了,谁也不知道malloc的具体情况了,这个test回收之后,malloc还没被抓出来,没被释放一直占据着内存,内存泄露。

        所以malloc使用时:自己申请,尽量自己释放。自己不释放,告诉别人要别人释放。

内存泄露是一个很可怕的事情:
    假如有一个程序运行在服务器里,这个服务器时24小时运行的,程序永远不会结束,然后在里面运用了malloc开辟空间,却又不释放,然后这个malloc就会一直不断占内存,吃资源,直到内存没了程序崩溃,服务器卡死。

(一直占着内存,又不用,又不释放,程序又不结束。其他想用内存的人用不了,程序最终会崩溃。俗话:占着茅坑不拉屎,又不把坑让出来)

四、几个经典的笔试题

1.

//1.请问运行Test 函数会有什么样的结果?
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;
}

结果:无法打印。
问题:
1.GetMemory(str),传过去的参数是值,值传递(传过去的是指针变量,指针变量,也是变量也是值传递)p存放的malloc开辟空间的地址,不会影响到str,str依然是空指针NULL。
函数销毁之后,p没了,str为NULL,strcpy(str, "hello world")调用失败,原因是对NULL的解引用时出错,程序崩溃。
2.在GetMemory函数内malloc开辟的空间,GetMemory销毁之后,malloc空间依然存在,但是没有释放,内存泄露。

可以理解为:卧底p死了,没人知道malloc的信息

修改:

//方法1.函数里返回指针,主函数接收指针保存
char* GetMemory()
{
	char* p = (char*)malloc(100);
	return p;
}
void Test(void)
{
	char* str = NULL;
	str=GetMemory();
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

//方法2.对传过去的指针取地址(二级指针),这样可以改变到实参
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

 2.

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

结果:烫烫烫烫烫烫烫烫?(无法打印想要的结果)
问题:
    GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址,而且str接收了GetMemory函数返回的字符串"hello world"的起始地址,但是由于数组作为临时函数GetMemory的临时变量,出了函数就被回收了销毁了,内容已经不存在了。如果使用str,str就是野指针,打印出来就是个随机值。此时这里printf(str)是非法访问。

相当于:
    卧底p知道罪犯的地址"hello world",它告诉了str,但是str正准备抓犯人的时候,这个地址的罪犯人间蒸发了,不见了,找不到了。这个地址现在也不知道住了哪些人。硬是要抓里面的人,就是随机的。

也可以理解为:
    a定了酒店当天的房间203,(此时地址p相当于203,a为"hello world",p指向了"hello world"的起始地址;203指向了a的地址),a告诉b房间地址203,让他当天(在函数内)来见a。(str相等与b,str也保存了"hello world"的起始地址),但是b去找a的时候,a退房了(出了函数,被回收了),此时203房间该地址不指向a了(str指向的内容没了),里面可能住了别的客人。然后b(str现在为野指针),硬要访问此时203房间的人(非法访问),见到的就是随机的人。(随机值)
 

同理:这题也存在问题 

//同理:这题也存在问题
test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("hehe\n");//
	printf("%d\n", *p);

	return 0;
}
/*
问题:test函数里的整型a是临时的,虽然返回了a的地址,但是出了函数,a的内容就回收了
p(野指针)现在指向的位置是一个未知的值,打印出来是随机值。
打印会发现的确是10啊,这个场景下是巧合,刚好test回收的时候,a的地址还没被其他的栈帧空间
覆盖。如果在中间加入其他的东西,就不一样了。
	printf("hehe\n");//就覆盖了a的空间,导致a的地址存放的值变了。
结果:hehe
	 5

*/

3.

int* f2(void)
{
	int* ptr;//指针未初始化,没有任何指向,野指针,里面是个随机值。
	*ptr = 10;//野指针解引用,非法访问
	return ptr;
}

4.

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;
}

问题:
    malloc空间没有释放。内存泄露
 

修改:

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	//释放空间
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

 5.

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

问题:
    释放了malloc开辟的空间,但是str没置空,然后依然是非空指针,
    strcpy(str, "world");就非法访问了。
 

修改:

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

五、柔性数组(结构体成员)

1 柔性数组的特点:
  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

1.柔性数组的定义

柔性数组的[ ]里面,可以写0,也可以空着不写。

这两种都可以 

 2.柔性数组的使用

struct S
{
	int n;
	char c;
	int arr[0];//柔性数组成员
};
int main()
{
	//malloc开辟空间(结构体类型)
	struct S* ps = (struct S*) malloc(sizeof(struct S) + 10 * sizeof(int));
	//						结构体struct S的大小 + 自己定义柔性数组的大小(10个整型大小)
	//判断开辟之后空间的首地址
	if (ps == NULL)
	{
		perror("malloc->\n");
		return 1;
	}
	//使用
	ps->n = 6;
	ps->c = 'z';
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i + 1;
		printf("%d ", ps->arr[i]);
	}
	//假设空间不足,要扩大空间(柔性数组增加10个整型,原来的10个增加到20个整型)
	struct S* ptr = realloc(ps, sizeof(struct S) + 20 * sizeof(int));
	//判断ptr
	if (ptr == NULL)
	{
		perror("realloc->\n");
		return 1;
	}
	else
	{
		ps = ptr;
	}
	//再次使用扩容后的空间
	for (i = 10; i < 20; i++)
	{
		ps->arr[i] = i + 1;
		printf("%d ", ps->arr[i]);
	}

	//释放空间
	free(ps);
	ps = NULL;

	return 0;
}

 3.柔性数组的优势

实现同样的功能,可以不使用柔性数组,通过指针

两个方法比较:
上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

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

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

 

struct S
{
	int n;
	char c;
	int* arr;
};
int main()
{
	//malloc开辟空间
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	//判断空指针
	if (ps == NULL)
	{
		perror("malloc1(结构体空间)->\n");
		return 1;
	}
	//malloc开辟整型数组空间
	int* ptr = (int*)malloc(10 * sizeof(int));
	//判断空指针
	if (ptr == NULL)
	{
		perror("malloc2(数组空间)->\n");
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	//使用结构体空间
	ps->n = 100;
	ps->c = 'a';
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i + 1;
		printf("%d ", ps->arr[i]);
	}
	//假如空间不足,扩容
	ptr = (int*)realloc(ps->arr,20 * sizeof(int));
	//判断空指针
	if (ptr == NULL)
	{
		perror("realloc(数组)->\n");
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	//使用扩容的空间
	for (i = 10; i < 20; i++)
	{
		ps->arr[i] = i + 1;
		printf("%d ", ps->arr[i]);
	}

	//先释放里面的空间(数组),再释放外面的空间(结构体)
	free(ptr);
	ptr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值