C语言:动态内存管理

C语言:动态内存管理,第1张

目录
  • 为什么存在动态内存分配
  • 动态内存函数的介绍
    • 1.malloc
    • 2.free
    • 3.calloc
    • 4.realloc
  • 常见的动态内存错误
    • 1. 对NULL指针的解引用 *** 作
    • 2.对动态开辟空间的越界访问
    • 3. 对非动态开辟内存使用free释放
    • 4.使用free释放一块动态开辟内存的一部分
    • 5.同一块动态内存多次释放
    • 6.动态开辟内存忘记释放(内存泄漏)
  • 经典笔试题
    • 题目1
    • 题目2
    • 题目3
    • 题目4
  • C/C++程序的内存开辟

为什么存在动态内存分配

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

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

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

  1. 空间开辟大小是固定的。


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


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


有时候我们需要的空间大小在程序运行的时候才能知道,有时候我们需要内存空间扩大或者缩小,这时候我们只能使用动态内存开辟了

比如说,之前我写的通讯录一次开辟了1000个数据空间,但我可能只需要3个人,所以其他的空间就会浪费,我们要是能随时改变开辟空间的大小,就可以让空间不再浪费,而我们开辟的动态内存,是在堆区上进行的








动态内存函数的介绍 1.malloc

void* malloc (size_t size);

头文件:

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

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






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



这里利用INT_MAX值超大的特性,向系统申请开辟一块超大的内存,系统发现没有这么大的空余空间,就会开辟失败,返回NULL



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






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



size为0,可以返回地址,但是地址不能解引用去使用,即使编译器可以去使用,也不要去使用,因为该地址中的内存空间为0,你去使用会出现非法访问的问题




2.free

void free (void* ptr);

头文件:

功能:free函数用来释放动态开辟的内存

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



free经常配合malloc去使用,当malloc开辟玩内存并进行完 *** 作后,就要去释放开辟的动态内存



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







3.如果我们不释放动态内存申请的内存的时候,如果程序结束,动态申请的内存由 *** 作系统回收,如果程序不结束,动态内存是不会自动回收的,就会造成内存泄漏

假如我们有一个程序7*24小时跑在服务器里,如果有程序会慢慢吃内存,内存早晚会被侵蚀完,早晚会被泄漏完,到时候服务器里没有内存可用,服务器就会崩掉






3.calloc

void* calloc (size_t num, size_t size);

头文件:

功能:这个函数向内存申请 num 个大小为 size 的元素个内存空间,并且把空间的每个字节初始化为0,返回指向这块空间的指针

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



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




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





4.realloc

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

头文件:

功能:增加或减少内存空间原有的大小,使内存管理更加灵活
realloc函数的出现让动态内存管理更加灵活有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。


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


ptr 是要调整的内存地址
size 调整之后新大小,也就是整块内存增加后的总大小
返回值为调整之后的内存起始位置。



这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。



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


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



1.找到足够大的空间
2.拷贝原有内容
3.free掉原空间
4.返回新空间地址
在这里插入图片描述" />

realloc有可能找不到合适的空间来调整大小,这是就会返回空指针
如果出现了这种情况,又用的原有指针去接受的realloc返回的空指针,原有空间就会出现泄漏,你无法再找回原有空间了,为了避免出现这种情况,我们可以创建一个临时的指针去接受,检验一下realloc是否找到了合适的空间

realloc单独使用也会有malloc的效果
这里的realloc的功能类似于malloc,就是直接在堆区开辟40个字节






常见的动态内存错误 1. 对NULL指针的解引用 *** 作

解决办法:对malloc函数返回值进行判空 *** 作
用INT_MAX开辟动态内存,malloc会返回NULL,这样我们解引用ptr就相当于对NULL指针进行解引用,这样就会出现问题,所以我们应该先进行判空 *** 作再去解引用

修改:




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

当i=10时会访问第41-44个字节空间,会出现越界访问,内存空间会出现问题




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

a是在栈上开辟的,free无法释放栈上的空间,运行后会崩




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

下面的p++是int类型,一次跳过4个字节,这里p++后跳过4个字节,然后再去free释放内存后,会释放p指向地址的100个字节的内存,结果这里释放的和开辟的内存位置不一样,会出现非法访问的问题




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

对同一快动态内存多次释放会出现问题




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

一直运行test函数每次都会吃掉100字节内存,每当结束一次test函数,地址p就会无法找到,而且test函数在堆上开辟的内存,函数结束后无法销毁,所以不在test函数中及时释放内存,每调用一次test函数就会出现这100个字节内存无法释放的问题






经典笔试题 题目1
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;
}

这个代码首先调用Test函数,然后创建一个为NULL的空指针ptr,调用GetMemory函数,将ptr传值调用,因为是传值,p是一个临时拷贝,p和ptr一样都是空指针,在GetMemory函数内开辟了100个字节的动态内存,因为是传值调用所以p的地址无法将开辟的动态内存返回到ptr指针中,函数结束后销毁指针p,p销毁后无法得知动态内存的起始地址,动态内存空间有没有释放会丢失100个字节,出现内存泄漏,回到Test函数后,str还是空指针,strcpy会先解引用传来的目标地址,因为解引用NULL,会出现问题,属于非法访问内存,所以程序到了strcpy会崩掉,

可以修改一下让整体没有错误

这里的printf(str),可能会有异议,其实是可以打印输出的,因为printf接受的都是字符串首地址




题目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虽然能接受到字符串的首地址,但是只要一printf解引用就会出现非法访问内存的问题

还有个类似的例子:
下面这个代码虽然能输出10,但是a这个变量是创建在栈上的,当到函数结束后,就会被销毁,所以虽然传过去a的地址但是要是使用的话就是非法访问




题目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);
}
int main()
{
	Test();
	return 0;
}

虽然也能打印hello,但是忘记去释放创建的动态内存
修改:




题目4
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;
}

free(str)后再去strcpy会进行非法访问,因为原有的动态内存被释放掉了,可以在free(str),后面将指针变为空,这样后面的判空 *** 作就可以不再去追加字符串了
修改:






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

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
    束时这些存储单元自动被释放。


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


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


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



    配方式类似于链表。


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


    程序结束后由系统释放。


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


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



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


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

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

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

发表评论

登录后才能评论

评论列表(0条)

保存