动态内存管理

目录

引言

[一. malloc和free](#一. malloc和free)

[1.1 malloc函数](#1.1 malloc函数)

函数作用

函数原型

使用方式

使用示例

注意事项

[1.2 free函数](#1.2 free函数)

函数作用

函数原型

使用方式

使用示例

注意事项

[二. calloc和realloc](#二. calloc和realloc)

[2.1 calloc函数](#2.1 calloc函数)

函数作用

函数原型

使用方式

使用示例

注意事项

[2.2 realloc函数](#2.2 realloc函数)

函数作用

函数原型

使用方式

使用示例

注意事项

[三. 常见的动态内存的错误](#三. 常见的动态内存的错误)

[3.1 对NULL指针的解引用操作](#3.1 对NULL指针的解引用操作)

[3.2 对动态开辟空间的越界访问](#3.2 对动态开辟空间的越界访问)

[3.3 对非动态开辟内存使用free释放](#3.3 对非动态开辟内存使用free释放)

[3.4 使用free释放一块动态开辟内存的一部分](#3.4 使用free释放一块动态开辟内存的一部分)

[3.5 对同一块动态内存多次释放](#3.5 对同一块动态内存多次释放)

[3.6 动态开辟内存忘记释放(内存泄漏)](#3.6 动态开辟内存忘记释放(内存泄漏))

[四. 动态内存笔试题分析](#四. 动态内存笔试题分析)

[4.1 题目1](#4.1 题目1)

[4.2 题目2](#4.2 题目2)

[4.3 题目3](#4.3 题目3)

[4.4 题目4](#4.4 题目4)

后记


引言

在之前的学习中,我们已经掌握的内存开辟方式有:

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

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

• 空间开辟大小是固定的。

• 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整

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

那么怎么解决这个问题呢?

C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

下面让我们来具体学习动态内存开辟。

一. malloc和free

1.1malloc函数

函数作用

malloc 函数是 C 语言标准库中的一个非常重要的函数,用于动态内存分配。它定义在 stdlib.h 头文件中。使用 malloc,程序可以在运行时请求一定大小的内存块,这在处理大小未知或变化的数据结构时非常有用。

函数原型

cs 复制代码
void* malloc(size_t size);
  • 参数size 是一个无符号整数,表示要分配的字节数。
  • 返回值 :如果分配成功,返回一个指向分配的内存块的指针;如果分配失败,返回 NULL

使用方式

使用 malloc 时,需要包含 stdlib.h头文件,并在使用完分配的内存后,通过free 函数释放它,以避免内存泄漏。

使用示例

cs 复制代码
#include <stdio.h>  
#include <stdlib.h>  
  
int main() 
{  
    int *ptr;  
    int n = 5; // 假设我们要存储5个整数  
  
    // 分配内存以存储 n 个整数  
    ptr = (int*)malloc(n * sizeof(int));  
  
    if (ptr == NULL) 
    {  
        printf("Memory allocation failed\n");  
        return 1;  
    }  
  
    // 使用分配的内存  
    for (int i = 0; i < n; i++) 
    {  
        ptr[i] = i * i; // 存储平方数  
    }  
  
    // 打印结果  
    for (int i = 0; i < n; i++) 
    {  
        printf("%d ", ptr[i]);  
    }  
  
    // 释放内存  
    free(ptr);
    ptr = NULL;  
  
    return 0;  
}

注意事项

  1. 类型转换 :虽然 C11 标准及以后版本允许省略 malloc 返回值的类型转换((int*)),但在旧代码或为了明确性,有时仍会看到类型转换。

  2. 内存泄漏 :使用 malloc 分配的内存必须显式地用 free 函数释放,否则会导致内存泄漏。

  3. 初始化malloc 分配的内存不会自动初始化为0。如果需要,可以使用 calloc(它会将内存初始化为0)或手动初始化。

  4. 内存对齐malloc 分配的内存块会根据平台的要求进行对齐,以确保访问效率。

  5. 失败处理 :总是检查 malloc 的返回值是否为 NULL,以处理内存分配失败的情况。

  6. 越界访问 :确保不要访问 **malloc**分配的内存块之外的内存,这可能导致未定义行为,包括程序崩溃。

  7. 重复释放 :不要对同一块内存使用 free 多次,这会导致未定义行为。

1.2 free函数

函数作用

free 函数是 C 语言标准库中的一个函数,用于释放之前通过 malloccallocrealloc 等函数分配的内存块。它定义在 stdlib.h 头文件中,是动态内存管理的重要部分。使用 free 可以帮助避免内存泄漏,即程序不再需要的内存未被正确释放,导致内存持续占用。

函数原型

cs 复制代码
void free(void *ptr);
  • 参数ptr 是一个指向之前分配的内存块的指针。如果 ptrNULL,则 free 函数什么也不做。
  • 返回值free 函数没有返回值(即返回类型为 void)。

使用方式

当你使用 malloccallocrealloc 成功分配内存后,应该在适当的时候使用 free 释放这些内存。通常,在内存块不再需要时调用 free

使用示例

cs 复制代码
#include <stdio.h>  
#include <stdlib.h>  
  
int main() 
{  
    int *ptr = (int*)malloc(sizeof(int) * 10); // 分配内存以存储10个整数  
  
    if (ptr == NULL) 
    {  
        printf("Memory allocation failed\n");  
        return 1;  
    }  
  
    // 使用分配的内存...  
  
    // 释放内存  
    free(ptr);  
  
    // 注意:释放内存后,ptr 仍然是之前那个地址,但它指向的内存已经不再是有效的了  
    // 因此,不应该再对 ptr 进行解引用操作  
  
    // 为了避免潜在的错误,可以将 ptr 设置为 NULL  
    ptr = NULL;  
  
    return 0;  
}

注意事项

  1. 只能释放动态分配的内存free 只能用于释放通过 malloccallocrealloc 分配的内存。尝试释放栈内存(如局部变量)或静态内存(如全局变量或静态局部变量)会导致未定义行为。

  2. 避免重复释放 :不要对同一块内存使用 free 多次。一旦内存被释放,指向它的指针就变成了"悬空指针",再次释放它将导致未定义行为。

  3. 内存泄漏 :即使使用了 malloc,如果忘记使用 free,也会导致内存泄漏。因此,良好的编程习惯是在分配内存后立即将其初始化,并在不再需要时立即释放。

  4. 释放后清零指针 :虽然 free 会释放内存,但它不会修改指针本身的值。因此,为了安全起见,通常在释放内存后将指针设置为 NULL,以防止后续对悬空指针的误操作。

  5. 跨函数使用:在函数内部分配的内存,如果需要在函数外部释放,则需要通过参数将指针传递给能够释放它的函数,或者将指针作为返回值返回给调用者。

二. calloc和realloc

2.1 calloc函数

函数作用

calloc 函数是 C 语言标准库中的一个函数,用于动态内存分配,并自动将分配的内存初始化为零。它定义在 stdlib.h 头文件中,是处理动态内存时非常有用的工具之一。与 malloc 函数相比,calloc 提供了一种更加安全的方式来分配和初始化内存,因为它保证了分配的内存块中的所有位都被清零。

函数原型

cs 复制代码
void* calloc(size_t num, size_t size);
  • 参数
    • num:表示要分配的元素数量。
    • size:表示每个元素的大小(以字节为单位)。
  • 返回值 :如果分配成功,返回一个指向分配的内存块的指针;如果分配失败,返回 NULL

使用方式

calloc 函数接收两个参数:需要分配的元素数量和每个元素的大小。它会自动计算所需的总内存大小(num * size),并分配这个大小的内存块,同时将所有位初始化为零。

使用示例

cs 复制代码
#include <stdio.h>  
#include <stdlib.h>  
  
int main() 
{  
    int *array = (int*)calloc(5, sizeof(int)); // 分配一个可以存储5个整数的数组,并初始化为0  
  
    if (array == NULL) 
    {  
        printf("Memory allocation failed\n");  
        return 1;  
    }  
  
    // 使用分配并初始化的内存...  
    for (int i = 0; i < 5; i++) 
    {  
        printf("%d ", array[i]); // 应该打印出5个0  
    }  
  
    // 释放内存  
    free(array);
    array = NULL;  
  
    return 0;  
}

注意事项

自动初始化 :与 malloc 不同,calloc 会自动将分配的内存初始化为零。这对于需要初始状态为"空"或"清零"的数据结构非常有用。

其余注意事项与其他的内存函数类似

2.2 realloc函数

函数作用

realloc 函数是 C 语言标准库中的一个非常有用的函数,它用于重新调整之前通过 malloccallocrealloc 函数分配的内存块的大小。这个函数在 **<stdlib.h>**头文件中声明。使用 realloc 可以避免在内存大小需要改变时手动释放旧内存并分配新内存的繁琐过程,同时也减少了内存泄漏的风险。

函数原型

cs 复制代码
void* realloc(void* ptr, size_t size);
  • ptr :指向之前通过 malloccallocrealloc 分配的内存块的指针。如果 ptrNULL,则 realloc 的行为就像 malloc,分配一个大小为 size 的新内存块。
  • size :新的内存块大小,以字节为单位。

使用方式

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

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

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

情况2:原有空间之后没有足够大的空间,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。

使用示例

cs 复制代码
#include <stdio.h>  
#include <stdlib.h>  
  
int main() 
{  
    int *ptr = malloc(10 * sizeof(int)); // 分配初始内存  
    if (ptr == NULL) 
    {  
        // 处理内存分配失败的情况  
        return 1;  
    }  
  
    // ... 使用ptr指向的内存 ...  
  
    // 需要更多空间  
    int *tmp = realloc(ptr, 20 * sizeof(int)); // 尝试重新分配内存  
    if (tmp == NULL) 
    {  
        // realloc失败,释放原内存并处理错误  
        free(ptr);
        ptr = NULL;  
        // ... 处理错误 ...  
        return 1;  
    }  
  
    // realloc成功,更新ptr指向新内存块  
    ptr = tmp;  
  
    // ... 继续使用ptr指向的内存 ...  
  
    // 释放内存  
    free(ptr);
    ptr = NULL;  
  
    return 0;  
}

注意事项

  1. 使用临时指针 :为了避免在**realloc失败时丢失对原内存块的引用,建议使用一个临时指针来接收realloc的返回值。如果realloc**成功,则更新原指针以指向新内存块;如果失败,则保持原指针不变,并可以安全地释放它(如果需要的话)。

  2. 数据完整性 :**realloc**只保证在内存块大小增加时保留原内存块的内容(直到新旧大小中较小的一个)。如果新大小小于原大小,则只有新大小部分的数据会被保留。

  3. 内存泄漏 :如果**realloc失败且你决定不释放原内存块(例如,因为程序即将退出),则必须确保没有其他指针指向该内存块,以避免潜在的内存泄漏。然而,在大多数情况下,如果realloc**失败,你应该释放原内存块并处理错误。

三. 常见的动态内存的错误

3.1 对NULL指针的解引用操作

cs 复制代码
void test()
 {
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;
 free(p);
 }

上述代码有什么问题呢?

该代码忘记检验动态内存开辟是否成功,这会导致可能对NULL指针进行解引用操作

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

cs 复制代码
void test()
{
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));
    if(NULL == p)
    {
     exit(EXIT_FAILURE);
    }
    for(i=0; i<=10; i++)
    {
     *(p+i) = i;//当i是10的时候越界访问
    }
    free(p);
} 

该代码越界访问,只动态开辟了10个int类型的空间,但却使用了11个。

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

cs 复制代码
int main()
{
	int a = 10;
	int* p = &a;

	free(p);
	p = NULL;
	return 0;
}

该代码对非动态开辟的内存使用free释放,这是错误的,只有用内存函数动态开辟出的内存才能用free释放。

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

cs 复制代码
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;//这样操作的话,p就不指向这块动态开辟空间的起始位置了
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

该代码对动态内存开辟的起始指针进行自增,这会导致它不再指向这块动态开辟空间的起始位置了,所以在后续用free进行释放的时候只能释放一部分,这会导致内存泄漏。

3.5 对同一块动态内存多次释放

cs 复制代码
int main()
{
	int* p = (int*)malloc(40);
	free(p);
	free(p);//多次释放
	return 0;
}

该代码对同一块动态开辟的空间进行多次释放,这是错误的。

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

cs 复制代码
void test()
{
	int* p = (int*)malloc(40);
	//......
	int flag = 0;
	scanf("%d", &flag);//5
	if (flag == 5)
		return;//内存泄露
	//free(p);
	//p = NULL;
}
int main()
{
	test();
	return 0;
}

该代码忘记用free释放动态开辟的内存,这会导致内存泄露。

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

切记:动态开辟的空间⼀定要释放,并且正确释放。

四. 动态内存笔试题分析

4.1 题目1

cs 复制代码
void GetMemory(char *p)
{
   p = (char *)malloc(100);
}
void Test(void)
{
   char *str = NULL;
   GetMemory(str);
   strcpy(str, "hello world");
   printf(str);
}

该代码存在哪些问题呢?

下面我用图片和注释的方式来解释

该代码存在两个问题:

1.GetMemory函数中的形参申请动态内存后被销毁,但是它所申请的空间并未被释放,导致内存泄漏

2.Test函数中虽然调用了GetMemory函数,但是它并没有返回值,也没有用变量来存储,这导致str 依旧是空指针,后续又对空指针进行解引用操作,这也是错误的。

下面给出正确的写法:

cs 复制代码
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	//此时str存放的就是动态开辟的100字节的地址
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
	return 0;
}

该代码使用传址调用确保str指针不会被销毁,且用二级指针来接收它的地址,并用解引用操作来实现动态内存的申请。

4.2 题目2

cs 复制代码
char *GetMemory(void)
{
   char p[] = "hello world";
   return p;
}
void Test(void)
{
   char *str = NULL;
   str = GetMemory();
   printf(str);
}

该代码的错误与第一题类似,p是函数内部的局部变量,这里仅仅只是将p首元素的地址传递给str但p在函数调用完成后已经销毁,str无法再找到p数组后面的内容,此时的str是野指针

4.3 题目3

cs 复制代码
void GetMemory(char **p, int num)
{
   *p = (char *)malloc(num);
}
void Test(void)
{
   char *str = NULL;
   GetMemory(&str, 100);
   strcpy(str, "hello");
   printf(str);
}

该代码的函数调用部分是正确的,所以可以打印出"hello",但是后续并没有用free函数释放内存这会导致内存泄漏。

下面给出正确的代码:

cs 复制代码
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;
	//修改将动态内存释放即可
}

4.4 题目4

cs 复制代码
void Test(void)
{
   char *str = (char *) malloc(100);
   strcpy(str, "hello");
   free(str);
   if(str != NULL)
   {
      strcpy(str, "world");
      printf(str);
   }
}

该代码在用free函数释放内存空间的时候并没有将str指针设置为NULL,这会导致str变成野指针,可能会在后续的使用中出现问题。

下面给出正确的代码:

cs 复制代码
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);//野指针,str并未设置成NULL
	str = NULL;这样修改即可
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

后记

偷个懒,明天给大家更后面的柔性数组,嘿嘿。

喜欢这篇文章的小伙伴们点点赞,点点关注哈,你们的支持就是我更新的最大动力,感谢各位大佬们的支持哈,谢谢大家,向大家学习!!!

共勉!!!

相关推荐
KBDYD10101 小时前
数据结构--单链表创建、增删改查功能以及与结构体合用
c语言·开发语言·数据结构·算法
为更好遇见5 小时前
C:内存函数
c语言·开发语言
武昌库里写JAVA7 小时前
人工智能不是人工“制”能
c语言·开发语言·数据结构·算法·二维数组
凌肖战9 小时前
力扣上刷题之C语言实现-Day2
c语言·算法·leetcode
Beginner_bml12 小时前
C语言编译四大阶段
c语言·开发语言
梁辰兴13 小时前
C语言 使用scanf函数时出现错误代码C4996
c语言·开发语言·scanf
凛冬将至__15 小时前
【C语言】__attribute__((constructor)) 和 __attribute__((destructor))详细解析
c语言·constructor·attribute
闲晨15 小时前
谈对象第二弹: C++类和对象(中)
c语言·数据结构·c++·算法
蟹至之16 小时前
字符串函数的使用与模拟(2)——C语言内存函数
c语言·字符串·指针·内存函数