一、动态内存管理引入
1、为什么要动态内存管理
静态分配的内存大小在编译时就固定了,不论是否使用都会占用这部分资源。动态内存管理可以根据实际需要来分配内存,这有助于避免浪费和更有效地利用有限的内存资源。
许多复杂的数据结构,如树、图和各种链式结构,通常需要在运行时动态地创建和销毁节点,这也需要动态内存管理。
二、相关函数
C语言的动态内存管理涉及几个关键函数,这些函数通常包含在C标准库的stdlib.h
头文件中。动态内存管理允许程序在运行时分配和释放内存,这对于处理大小不确定或数据量大的情况非常有用。以下是C语言中动态内存管理的几个关键函数:
1、malloc
1)函数作用
在堆区(heap)分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。通常用于分配单一大块内存,例如数组或结构体。
2)函数原型:
3)函数参数:
size是需要初始化内存块的大小,单位是字节。
如果size是0,这时malloc的行为是标准未定义的,取决于编译器。
4)返回值:
如果分配成功,返回指向这块内存的指针。如果分配失败,比如内存不足,返回NULL
指针。
malloc的返回值是void*类型的,是因为函数并不知道开辟空间的用途,所以使用泛型指针。
5)示例:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int n = 0;
printf("请输入数组大小>:");
scanf("%d", &n);
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL)
{
fprintf(stderr, "内存分配失败\n");
return EXIT_FAILURE;//内存分配失败,主函数以失败状态退出
}
//分配成功后的操作
int i = 0;
printf("请依次输入数组元素>:");
for (i = 0; i < n; i++)
{
scanf("%d", &arr[i]);
}
for (i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
运行结果:
虽然当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。
在释放内存后,将指针置为NULL。
2、free
1)函数作用:
用于释放不再需要的动态分配的内存,是防止内存泄漏的关键步骤。释放之前通过malloc
、calloc
或realloc
分配的内存块。free
后的指针应该设置为NULL
,避免悬挂指针(即指针指向的内存已被释放或不可用)。
2)函数原型:
3)函数参数:
如果memblock指针指向的空间不是动态内存开辟的,则free的行为是未定义的。
如果memblock是空指针(NULL),则free函数什么也不做。
4)示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* p = 0;
p = (int*)malloc(sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));//开辟失败
return EXIT_FAILURE;
}
//开辟完成后的操作
*p = 6;
printf("%d\n", *p);
free(p);//释放开辟的内存块
p = NULL;//将指针置空
return 0;
}
3、calloc
1)函数作用:
contiguous allocation,意思是连续分配。calloc
函数分配多个连续的内存块,每块的大小相同,并且将所有位自动初始化为零。
也就是在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是 0。
通常用于分配一定数量相同类型的对象,并需要其初始化为0的情况。
2)函数原型:
3)函数参数:
它接受两个参数,即需要分配的内存块数 num 和每个内存块的大小 size (以字节为单位),也就是分配num
个对象的内存,每个对象的大小都是size
字节。
4)函数返回值:
如果分配成功,返回指向这块内存的指针,如果分配失败,返回NULL
。
5)示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* arr = (int*)calloc(10,sizeof(int));
if (arr == NULL)
{
printf("%s\n", strerror(errno));//开辟失败
return EXIT_FAILURE;
}
//开辟完成后的操作
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
free(arr);//释放开辟的内存块
arr = NULL;//将指针置空
return 0;
}
运行结果:
可以发现,calloc 函数开辟的空间都被初始化为0。
4、realloc
1)函数作用:
尝试重新调整先前通过malloc
、calloc
或realloc
函数分配的内存块的大小。
2)函数原型:
3)函数参数:
i.参数具体情况:
它接受两个参数,即一个先前分配的内存块的指针 memblock 和一个新的内存大小 size ,然后尝试重新调整先前分配的内存块的大小到 size 大小。也就是说:memblock 是需要调整的内存块的指针,size 是调整后的新大小。
ii.realloc 的具体实现:
如果新大小比原来小,realloc
可能会释放多余的内存。
如果新大小比原来大,realloc
会尝试扩展现有的内存区域。如果不能扩展,它将分配一块新的内存区域,将原数据复制过去,然后释放原内存区域。
也就是说在新空间比原来空间大的情况下,realloc 的行为有两种情况:
第一种,现有的内存区域后面又充足的空间用来扩展:
第二种,现有的内存区域后面没有充足的空间用来扩展:
iii.参数的特殊情况:
如果 memblock 指针为 NULL ,则 realloc 的行为与 malloc 的行为相同,开辟一个大小为 size 字节的内存块,然后返回这个内存块的指针。
如果size
为0,且 memblock 不为NULL
,则 memblock 指向的内存块可能会被释放,并返回NULL
。
对于 realloc 这个函数,在开辟“新”空间时,还会把原来的数据复制到新空间中,所以会出现以下问题:如果新块比原来的小,数据可能会丢失;如果新块比原来的大,新部分不会被初始化。
4)函数返回值:
如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。
5)注意事项:
在使用 realloc 函数时,realloc 会返回扩展后的内存块的指针,这里的返回值不能用原来的指针变量来接收,因为,realloc 如果调整失败,则可能返回 NULL ,如果复制给原来的指针变量,就会导致原来内存块的地址丢失,导致内存泄漏,因为 realloc 调整失败不会释放原来的内存块。
为了避免这个问题,你应该将realloc
的返回值赋给一个临时指针变量,然后检查这个临时变量是否为NULL
。只有在它不为NULL
的情况下,你才更新原来的指针变量。
正确使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 简单示例,分配一个大小为10的int数组
int* ptr = malloc(10 * sizeof(int));
if (ptr == NULL)
{
// 处理分配失败
printf("%s\n",strerror(errno));
return -1;
}
// ... 使用ptr
// 重新分配内存,尝试扩展到20个int
int* temp = realloc(ptr, 20 * sizeof(int));
if (temp == NULL)
{
// 处理重新分配失败
printf("%s\n",strerror(errno));
free(ptr);
ptr = NULL;
return -1;
}
else
{
// 重新分配成功,更新原来的指针
ptr = temp;
}
// ... 使用新的ptr
// 最终释放内存
free(ptr);
// 指针置空
ptr = NULL;
return 0;
}
6)示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL)
{
printf("%s\n", strerror(errno));//开辟失败
return EXIT_FAILURE;
}
//开辟完成后的操作
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i + 1;
}
int* temp = (int*)realloc(arr, 20 * sizeof(int));//扩容
if (temp == NULL)//调整失败
{
printf("%s\n", strerror(errno));//抛出错误信息
free(arr);//释放先前的内存
arr = NULL;//指针置空
return EXIT_FAILURE;
}
else//调整成功
{
arr = temp;//更新指针
temp = NULL;//临时指针置空
}
for (; i < 20; i++)
{
arr[i] = i + 1;
}
for (i = 0; i < 20; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
free(arr);//释放开辟的内存块
arr = NULL;//将指针置空
return 0;
}
运行结果:
三、动态内存分配可能导致的一些问题
1、内存泄漏
这是动态内存分配中最常见的问题之一。内存泄漏发生在程序分配了内存但未能释放它,导致程序持续占用不再需要的内存。随着时间的推移,这可能导致程序和系统可用内存减少,严重时可能使系统变慢或崩溃。
解决方法:
这需要我们在使用完毕一块内存块时,要使用 free 函数来释放这个内存块。
2、内存碎片化
当我们多次使用 malloc 这类内存分配函数时,随着程序反复分配和释放不同大小的内存块,可用内存可能会分散为许多小块,这称为内存碎片。过度的内存碎片可能导致无法为大的内存请求分配连续的空间,即使有足够的总空闲内存,因为这时的总空闲内存是分散的,并不是连续的。
解决方法:
尽量减少频繁分配和释放小块内存的操作。
使用内存池。
3、重复释放一个内存块
如果尝试释放同一块内存两次或多次,可能会导致程序运行错误或崩溃。双重释放可能破坏内存分配器的数据结构,导致未定义行为。
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL)
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
free(arr);
free(arr);
解决方法:
再第一次释放内存后将指针置空,因为如果给free传空指针,free会什么也不做。
4、野指针
当内存被释放后,指向该内存的指针称为野指针,因为它指向的内存区域不再有效。继续使用这样的指针可能会导致不可预测的行为或程序崩溃。
解决方法:
在释放内存块后,将之前指向这块内存块的指针置空。
5、对NULL指针进行解引用
对于 malloc 这类内存分配函数,再内存分配不成功时,会返回NULL指针,这时如果没有进行对返回结果的检查,则可能发生对NULL指针的解引用操作,这时错误的。
解决方法:
对函数返回结果进行检查。
6、对非动态开辟空间使用free函数
对非动态开辟内存空间使用free函数释放空间可能导致者程序崩溃等等。
解决方法:
认真审视代码。
7、使用free释放部分动态开辟的内存块
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL)
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
arr++;//指针向后移了一个步长
free(arr);
arr = NULL;
return 0;
}
上面的示范代码就尝试只是放一部分动态开辟的空间,这时错误的,可能会导致程序崩溃。
解决方法:
尽量不要移动指向内存块的指针,如果要移动尽量定义一个备份指针保存这个内存块的地址。确保总是释放完整的内存块,而不是部分内存。
四、动态内存分配的通讯录
在了解了这些动态内存管理的函数后,我们可以对之前的静态通讯录稍微改动一下,实现对内存的更高效的使用。
该项目包含三个文件:
contacts_list.c/cpp
#include "contacts_list.h"
void menu()
{
printf("*************************************\n");
printf("***** 1.add 2.del ****\n");
printf("***** 3.search 4.modify ****\n");
printf("***** 5.display 6.sort ****\n");
printf("***** 0.exit ****\n");
printf("*************************************\n");
printf("please choose a option>:");
}
int InitContact(Contacts_list* list)
{
list->capacity = DEFUALT_CONTACT;//容量设为DEFUALT_CONTACT
list->num = 0;//联系人个数设为0
list->dataPtr = (PeopleInfo*)malloc(DEFUALT_CONTACT * sizeof(PeopleInfo));//开辟DEFUALT_CONTACT个联系人大小的内存块
if (list->dataPtr == NULL)//分配失败
{
printf("InitContact:%s\n", strerror(errno));//抛出错误信息
return 1;
}
//分配成功
return 0;
}
int IncreaseCapacity(Contacts_list* list, int num)
{
PeopleInfo* temp = (PeopleInfo*)realloc(list->dataPtr, (list->capacity + num) * sizeof(PeopleInfo));
if (temp == NULL)//分配失败
{
printf("IncreaseCapacity:%s\n", strerror(errno));//抛出错误信息
return 1;
}
else//分配成功
{
list->dataPtr = temp;//更新内存块的指针
temp = NULL;//将临时指针置空
list->capacity += num;//更新容量
}
return 0;
}
void AddContact(Contacts_list* list)
{
//增容
if (list->num == list->capacity)
{
IncreaseCapacity(list, DEFUALT_ALLOC_CONTACT);//默认增容DEFUALT_ALLOC_CONTACT大小
}
//录入信息
printf("please type in name(maximum 20)>:");
scanf("%s", list->dataPtr[list->num].name);
printf("please type in gender(maximum 8)>:");
scanf("%s", list->dataPtr[list->num].gender);
printf("please type in age>:");
scanf("%d", &(list->dataPtr[list->num].age));
list->num++;
printf("AddContact successfully\n");
}
void DisplayContact(const Contacts_list* list)
{
int i = 0;
printf("------------------------------------------------\n");
printf("%-20s%-8s%-s\n", "name", "gender", "age");
for (i = 0; i < list->num; i++)
{
printf("%-20s%-8s%d\n", list->dataPtr[i].name, list->dataPtr[i].gender, list->dataPtr[i].age);
}
printf("------------------------------------------------\n");
printf("display successfully\n");
}
void DestroyContact(Contacts_list* list)
{
free(list->dataPtr);//释放开辟的空间
list->dataPtr = NULL;//将指向内存块指针置空
}
void DeleteContact(Contacts_list* list)
{
char name[21];
if (list->num == 0)
{
printf("there is no contact can be deleted\n");//联系人为零,无需删除
return;
}
printf("please type in the name who you want to delete>:");
scanf("%s", name);
int res = SearchContact(list, name);
if (res != -1)
{
int i = 0;
for (i = res; i < list->num - 1; i++)
{
list->dataPtr[i] = list->dataPtr[i + 1];
}
list->num--;
printf("delete successfully\n");
}
else
{
printf("cannot find\n");//没找到要删除的联系人
return;
}
}
int SearchContact(Contacts_list* list, const char* string)
{
int i = 0;
for (i = 0; i < list->num; i++)
{
if (strcmp(list->dataPtr[i].name, string) == 0)
{
return i;//查找到返回下标
}
}
return -1;//未查找到返回-1
}
void ModifyContact(Contacts_list* list, const char* string)
{
int res = SearchContact(list, string);
if (res == -1)
{
printf("cannot find\n");
return;
}
printf("please type in name(maximum 20)>:");
scanf("%s", list->dataPtr[res].name);
printf("please type in gender(maximum 8)>:");
scanf("%s", list->dataPtr[res].gender);
printf("please type in age>:");
scanf("%d", &(list->dataPtr[res].age));
printf("modification successfully\n");
}
int cmp_by_name(const void* element1, const void* element2)
{
PeopleInfo* ele1 = (PeopleInfo*)element1;//强制转换为联系人结构体类型,方便访问name变量
PeopleInfo* ele2 = (PeopleInfo*)element2;
return strcmp(ele1->name, ele2->name);
}
int cmp_by_age(const void* element1, const void* element2)
{
int res = ((PeopleInfo*)element1)->age - ((PeopleInfo*)element2)->age;
return ((-res < 0) - (res < 0));
}
void SortContact(Contacts_list* list, int num)
{
switch (num)
{
case 1:
qsort(list->dataPtr, list->num, sizeof(PeopleInfo), cmp_by_name);
printf("sort successfully\n");
break;
case 2:
qsort(list->dataPtr, list->num, sizeof(PeopleInfo), cmp_by_age);
printf("sort successfully\n");
break;
default:
printf("error\n");
break;
}
}
contacts_list.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//默认联系人个数
#define DEFUALT_CONTACT 3
//联系人空间不够时,默认分配空间个数
#define DEFUALT_ALLOC_CONTACT 3
//打印菜单函数
void menu();
//声明联系人信息结构体变量
typedef struct PeopleInfo
{
char name[21];
char gender[9];
int age;
}PeopleInfo;
//声明通讯录结构体变量
typedef struct Contacts_list
{
PeopleInfo* dataPtr;//存放联系人数据,指向内存块的指针
int num;//记录通讯录联系人个数
int capacity;//记录通讯录容量
}Contacts_list;
//初始化函数,默认初始化DEFUALT_CONTACT个联系人的空间,分配成功返回0,失败返回1
int InitContact(Contacts_list* list);
//增容函数,增加num大小,成功返回0,失败返回1
int IncreaseCapacity(Contacts_list* list,int num);
//添加联系人,如果空间不足,默认再开辟DEFUALT_ALLOC_CONTACT个联系人的空间,分配成功返回0,失败返回1
void AddContact(Contacts_list* list);
//摧毁通讯录,释放开辟的空间
void DestroyContact(Contacts_list* list);
//显示通讯录
void DisplayContact(const Contacts_list* list);
//删除联系人
void DeleteContact(Contacts_list* list);
//查找联系人
int SearchContact(Contacts_list* list, const char* string);
//修改联系人
void ModifyContact(Contacts_list* list, const char* string);
//排序联系人
void SortContact(Contacts_list* list, int num);
//按名字排序
int cmp_by_name(const void* element1, const void* element2);
//按年龄排序
int cmp_by_age(const void* element1, const void* element2);
test.c/cpp
#include "contacts_list.h"
int main()
{
int status = 0;
Contacts_list contacts_list;//通讯录结构体变量
if (InitContact(&contacts_list))//初始化失败会返回1,如果初始化失败则程序以1返回
{
printf("Initialization Error\n");
return 1;
}
int res = 0;
int num = 0;
do
{
menu();//打印菜单
scanf("%d", &status);
system("cls");
switch (status)
{
case 1:
AddContact(&contacts_list);
break;
case 2:
DeleteContact(&contacts_list);
break;
case 3:
char name1[21];
printf("please type in the name>:");
scanf("%s", name1);
res = SearchContact(&contacts_list, name1);
if (res != -1)
{
printf("%d\n", res);
}
else
{
printf("cannot find\n");
}
break;
case 4:
char name2[21];
printf("please type in the name>:");
scanf("%s", name2);
ModifyContact(&contacts_list, name2);
break;
case 5:
DisplayContact(&contacts_list);
break;
case 6:
printf("1.sort by name\n");
printf("2.sort by age\n");
scanf("%d", &num);
SortContact(&contacts_list, num);
break;
case 0:
DestroyContact(&contacts_list);//释放空间,摧毁通讯录
printf("exit\n");
break;
default:
printf("error\n");
break;
}
} while (status);
return 0;
}