C语言----动态内存管理,malloc,free,calloc,realloc

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

我们已经掌握的内存开辟方式有:

int val = 20;  //在栈空间上开辟四个字节
char arr[10] = {0};  //在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。


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


二,动态内存函数的介绍

2,1 malloc和free

2.1.1,C语言提供了一个动态内存开辟的函数

#include<stdlib.h>
#include<malloc.h>//两个都行,但是第一个较为常用

void* malloc (size_t size);

//1, 这里的参数(size)是要开辟空间的字节数。

//2, 这里是向内存堆区申请空间,不是栈区。

//3, 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
//5, 如果开辟成功,则返回一个指向开辟好空间的指针。
//5, 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
//6, 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
//7, 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

举例:

int main()
{
	//int arr[40];//向栈区申请了40个字节,用来存放arr数组。
	//char arr[40];//向栈区申请了40个字节,用来存放arr数组。

	int* ptr = (int*)malloc(40);//向堆区申请40个字节 把他的首地址传给 p ,并且强制类型转换为(int*)类型的
	
    int* p = ptr;//一般未来防止ptr地址丢失或者不知道ptr在运行之后指向哪里
                 //一般会创建另一个变量p来代替ptr参与修改。
    
	if (p == NULL) //如果开辟错误他就会打印错误信息。
	{
		perror("malloc");
		return 1;

	}

	for (int i = 0; i < 10; i++)//给这块空间赋值
	{
		*(p+i) = i;
	}
	

	for (int i = 0; i < 10; i++)//打印这块空间
	{
		printf("%d", *(p + i));
	}
    //打印结果为:0123456789
        
    return 0;
}

2.1.2,C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的

#include<stdlib.h>
#include<malloc.h>//两个都行,但是第一个较为常用

void free (void* ptr);

//ptr是一块动态内存开辟的空间的起始地址。

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

//1, 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
//2, 如果参数 ptr 是NULL指针,则函数什么事都不做。

//3, 一般在使用完后还会把刚刚创建的ptr变量置空。防止非法访问。

//4, 在释放完后 ptr 就变成一个野指针了。

//5,free释放的空间必须是动态开辟的。

在malloc开辟空间后可以不free释放吗?

//1, 当我们不释放动态申请的空间的时候,如果程序结束,动态申请的内存由操作系统自动回收。

//2, 如果程序不结束,动态内存是不会自动回收的,就会形成内存泄漏的问题。

//3, 内存泄漏就会一直吃电脑的内存,内存被消耗完后电脑就会死机。

//3, 一般都是要用free释放自己申请的动态内存,并把指针变量置空。

举例:


#include<stdlib.h>

int main()
{
	//申请一块空间
	int* ptr = (int*)malloc(40);
	//检验是否开辟成功
	if (ptr == NULL)
	{
		perror("malloc");//打印错误原因
		return 1;
	}

	//使用这块空间----
	//----


	//释放ptr所指向的动态内存
	free(ptr);
	//参数是开辟的起始地址。
    //之后ptr已经不属于这个程序了再去使用的话就变为一个野指针了。
	ptr = NULL;//一般在释放空间后会再一次给ptr置空
	//因为ptr还记得上层开辟的空间的地址。
	//为了避免非法访问,需要给ptr置空。
    //以后在使用的时候判断一下。
    if(ptr != NULL)
    {    
        //使用就可以了。
    }

    //*ptr = 100;  //但是直接使用就可能构成非法访问。

	return 0;

}

2,2 calloc

在内存中申请数组(动态内存),并且初始化为0

#include<stdlib.h>
#include<malloc.h>

void* calloc (size_t num, size_t size);
//参数 num 是元素的个数
//参数 size 是每一个元素的大小,单位是字节。

说明

//1, 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

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

//3, 这里也是向内存堆区申请空间,不是栈区。

//3, 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
//5, 如果开辟成功,则返回一个指向开辟好空间的指针。
//5, 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
//6, 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
//7, 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

举例:


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

int main()
{
	//申请40个字节的动态内存
	int* p1 = (int*)malloc(40);//未初始化 值为随机数


	//申请10个整型的空间的动态内存。
	int* p2 = (int*)calloc(10, sizeof(int));//已初始化为0.

	//检查是否申请成功
	if (p2 == NULL)
	{
		perror("calloc");
		return 1;
	}

	//使用
	for (int i = 0; i < 10; i++)
	{
		printf("%d", *(p2 + i));
	}


	free(p2);//使用完成后也是需要释放的
	p2 = NULL; //也是需要置空的。

	return 0;
}

调试起来我们就能看到初始化为0的结果。

2,3 realloc

重新开辟 (不够用了再次开辟)(调大调小)

#include<stdlib.h>
#include<malloc.h>

void* realloc (void* ptr, size_t size);
//参数 ptr 是要调整的内存地址
//参数 size 是调整之后新大小。 
//返回的时候会分为两种情况
//1 如果想要扩容的空间后面有足够的未使用的空间的时候,直接扩容后返回输入的ptr地址。
//本来的数据不会发生改变。

//2 如果想要扩容的空间后面没有足够的未使用的空间的时候,
//会在内存中重新找一块符合条件的区域,返回这块区域的首地址(新的地址),
//并把数据拷贝到新的区域,并free释放本来想要扩容的空间。

说明:

//1, 返回值为调整之后的内存起始位置。
//2, 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

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

情况1:原有空间之后有足够大的空间

情况2:原有空间之后没有足够大的空间

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

举例:

#include<limits.h>
int main()
{
	int* p1 = (int*)malloc(40);

	if (p1 == NULL)
	{
		perror("malloc");
		return 1;
	}

	使用
	//for (int i = 0; i < 10; i++)
	//{
	//	*(p1 + i) = i;//0,1,2,3,4----
	//	//放了是10个元素
	//}

	//假如空间不够了, 需要20个元素(int)类型
	//p1 = realloc(p1, 80);//这样写是不合理的 ,这样会有一种情况,如果扩容失败,他会返回一个NULL
	//这个时候p1就被置空了。p1==NULL
	//建议不要直接用p1来接受扩容后的空间地址,应该做一步周转。
	int* ptr = realloc(p1, 80);
	if (ptr != NULL)
	{
		p1 = ptr;
	}//这样就会更加合理一点。

	if (ptr == NULL)//检验是否扩容成功
	{
		perror("realloc");
		return 1;
	}

	for (int i = 0; i < 20; i++)
	{
		*(p1 + i) = i;//0,1,2,3,4----
		//放了是20个元素
	}



	free(p1);
	p1 = NULL;

	return 0;
}


三, 常见的动态内存错误

3,1 对NULL指针的解引用操作

void test()
{
    int *p = (int *)malloc(INT_MAX/4);

    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
    p = NULL;
}


//改进

void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    //检验
    if(p == NULL)
    {
        perror("malloc");//打印错误信息
        return 1; //注意这里一定要返回,不然后面的代码就可能会构成非法访问
    }

    *p = 20;//如果p的值是NULL,就会有问题


    free(p);
    p = NULL;
}

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


void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	//if (NULL == p)
	//{
	//	exit(EXIT_FAILURE);//结束进程
	//}
	//也可以这样写

	if (p == NULL)
	{
		perror("malloc");//打印错误信息。
		return 1;//返回
	}



	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候越界访问
		//构成非法访问。
		//要自己好好检查
	}
	free(p);
}

3,3对非动态内存的释放



void test()
{
	int a = 10;
	int* p = &a;//非动态内存空间的地址

	free(p);//ok?
	p = NULL;
	//这样写法是不行的,不是动态内存空间就不能用free释放。
	//这种局部变量的内存空间是不需要我们自己释放的
	//编译器会自己回收。

	return 0;


}

3,4 使用free释放一块动态开辟的内存空间的一部分


int test()
{

	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return 1;
	}
	//p在使用的时候被改变。
	p++;
    //所以建议创建一个零时的来代替。
    //注意一定要记住初始位置。

	free(p);//p不再指向动态内存的起始位置,
	//这里会报错的,
	p = NULL;

	return 0;

}

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


int test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return 1;
	}

	free(p);
	free(p);//重复释放,这也是不允许的
	p = NULL;

	return 0;
}

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


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

	//内存未释放
	//free(p);
	//p = NULL;

}

int main()
{
	test();//出函数以后就无法释放了

	//假设这里写了一个死循环程序无法结束
	while (1);
	
	//假如一个项目中这样的内存很多的话
	//内存就会被吃完了
	//这样机器就会死机。
	//假如这样一个程序一天24小时都在运行的话
	//每天都在吃内存,这是很可怕的。
	//而且这样的错误很难查找

}

四:经典例题

4,1  例题一:

 讲解:

#include<string.h>

void GetMemory(char* p)//这个p是str的零时拷贝传过来的时候p里面放的是空指针
{
	p = (char*)malloc(100);//注意这里没有改变str  只是改变了p
	//未使用free释放,也是不对的。(内存泄漏了)
}

void Test(void)
{
	char* str = NULL;
	GetMemory(str);//出了函数指针p就被销毁了
	//所以开辟的动态内存空间就找不到了,

	strcpy(str, "hello world");
	//str现在是空指针,程序就会崩溃

	printf(str);//这里是没有问题的
	printf("haha\n");//这个时候就是把常量字符串的首地址传给printf函数。
	//并不是把整个字符串都传过去了。
	//同样上面也是一样的原理。是可以的。没有错误
}

int main()
{

	Test();
	return 0;
}


正确版本
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;
}

4,2 例题二:(返回栈空间地址的问题)

讲解:

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;//这里虽然返回了数组的地址
	//但是数组p[] 已经被编译器回收了
	//地址被记住了,但是可以通过地址找到那块空间,
	//但是那块空间已经还给操作系统了,值也变为随机数了
	//返回的地址已经是一个野指针了,

}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();//那块空间的地址被记住了,但是那块空间已经被释放了
	//str就是一个野指针了,会构成非法访问。那块空间已经不是该函数的了
	//已经还给操作系统了。
	printf(str);
}

int main()
{
	Test();
	return 0;
}

4,3例题三:


#include<stdio.h>


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

讲解:


void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);//申请了100字节的空间,
	//把地址通过解引用传给了指针变量str
	//注意看到malloc后要注意,释放申请的空间。
}

void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);

	strcpy(str, "hello");
	
	printf(str);
	//这里有个释放空间
	//free(str);
	//str = NULL;

}

int main()
{
	Test();

	return 0;
}

4,4例题四:


void Test(void)
{
	char* str = (char*)malloc(100);

	strcpy(str, "hello");

	free(str);//到这,空间已经被释放了。
	//但是str没有置空,还存放着原来的地址。
	//str已经是野指针了(使用个不属于自己的空间)
    //应该在这里把str置空

	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}//这里使用就是非法访问了

}


int main()
{
	Test();

	return 0;
}

5,5 例题五:


int* Test(void)
{
	int a = 10;
	return (&a);
}//这个代码就是返回栈空间地址的问题了
//返回变量的地址就是不对的,这块栈空间已经被释放了
//再次通过这个地址去访问的时候就会出现问题了


int Test(void)
{
	int a = 10;
	return a;
}//这个代码是没有任何问题的
//返回这个变量是没有问题的。


int main()
{
	Test();

	return 0;
}

五,C/C++程序的内存开辟

 c程序内存分配的几个区域:

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

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

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

六,柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构体中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

举例:

typedef struct s
{
	int numn;
	double a;
	int arr[];//柔性数组成员

}s;


或者

typedef struct s
{
	int numn;
	double a;
	int arr[0];//柔性数组成员

}s;

柔性数组的特点:

  • 结构体中柔性数组前面必须有其他成员(至少一个)。
  • sizeof计算结构体大小的时候,不计算柔性数组的内存。
  • 但是在确定最大对齐数的时候会参与确定。
  • 包含柔性数组的结构体用malloc()函数进行内存动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。

使用:



#include<stdlib.h>
struct s1
{
	int a;
	int arr[];//给柔性数组成员开辟10个整型的大小
};



int main()
{
	//分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
	struct s1* ps = (struct s1*)malloc(sizeof(struct s1) + 40);//说的就是这个意思。
	
	//访问a的时候
	ps->a = 10;
	//访问arr的时候
	for (int i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}


	for (int i = 0; i < 10; i++)
	{
		printf("%d", ps->arr[i]);
	}//0123456789

	free(ps);
	ps = NULL;

	return 0;
}

柔性数组的优势:

我们来观察一下下面代码:

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


typedef struct sss
{
	int a;
	int* arr;
}sss;

int main()
{

	sss ret;
	ret.a = 10;
	ret.arr = (int*)malloc(40);
	if (ret.arr == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(ret.arr + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d", *(ret.arr + i));
	}
	free(ret.arr);
	ret.arr = NULL;

	return 0;
}

这个代码和柔性数组所达到的目的相同。但是柔性数组比他好:

1,如果我们把数据开辟在堆区,就需要连续两次free,这样不好。

2,第二种内存不连续,连续的内存有利于提高访问速度,第一种也有利于减少内存碎片。

拓展

1,内存的结构:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小峰同学&&&

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

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

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

打赏作者

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

抵扣说明:

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

余额充值