动态内存管理

  • 1.为什么存在动态内存分配
  • 2.动态内存函数的介绍
    • 2.1malloc和free函数
    • 2.2calloc函数
    • 2.3realloc函数
  • 3.常见的动态内存错误
    • 3.1对NULL指针的解引用操作
    • 3.2对动态开辟空间的越界访问
    • 3.3对非动态开辟空间内存使用free释放
    • 3.4使用free释放一块动态开辟内存的一部分
    • 3.5对同一块动态内存多次释放
    • 3.6动态开辟内存忘记释放(内存泄漏)
  • 4.几个经典的笔试题
    • 4.1题目一
    • 4.2题目二
    • 4.3题目三
    • 4.4题目四
  • 5.C/C++程序的内存开辟
  • 6.使用动态内存相关的知识改进通讯录
  • 7.柔性数组
    • 7.1柔性数组的特点
    • 7.2柔性数组的使用
    • 7.3柔性数组的优势

1.为什么存在动态内存分配

✅截止目前,我们掌握的内存开辟的方式有:

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

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

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

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

2.动态内存函数的介绍

2.1malloc和free函数

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

void *malloc(size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者来决定。
  • 如果参数size0malloc的行为是标准是未定义的,取决于编译器。

int main(){//申请一块空间,用来存放10个整型int* p = (int*)malloc(10 * sizeof(int));return 0;}

✅内存的存储:

注意: mallocfree都声明在stdlib.h的头文件中。

✅运行示例:

#include #include int main(){//申请一块空间,用来存放10个整型int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return 1;}//使用int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", p[i]);}return 0;}


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

void free(void *ptr);

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

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

malloc函数申请的空间,是怎么释放的呢?

  1. free释放-主动释放
  2. 程序退出后,malloc申请的空间,也会被操作系统回收。-被动回收
free(ptr);//释放ptr所指向的动态内存ptr = NULL;

2.2calloc函数


malloccalloc函数除了参数的区别,calloc函数申请好空间后,会将空间初始化0,但是malloc函数不会初始化。

2.3realloc函数


✅该函数用于对我们已开辟动态空间大小的调整!

#include #include int main(){//申请一块空间,用来存放10个整型int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return 1;}//使用int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}//打印for (i = 0; i < 10; i++){printf("%d ", p[i]);}//空间不够,希望调整空间为20个整型的空间realloc(p, 20 * sizeof(int));//释放free(p);p = NULL;return 0;}

但是这种方法是有问题的,因为realloc开辟空间也可能会失败,失败的时候返回NULL!因此,如果这个时候,我们还用p来接收的话,那么我们原来有的10个字节的空间可能也丢失了。

//更改://空间不够,希望调整空间为20个整型的空间int* ptr = (int*)realloc(p, 20 * sizeof(int));if (ptr != NULL){p = ptr;}

realloc函数是如何工作的呢?

  1. 要扩展的内存就直接在原有内存之后追加空间,原来空间的数据不发生变化。
  2. 原有空间之后没有足够多的空间,扩展的方法就是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存空间。

3.常见的动态内存错误

3.1对NULL指针的解引用操作

void test(){int* p = (int*)malloc(INT_MAX / 4);*p = 20;//如果p的值是NULL,就会有问题free(p);}

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

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

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

void test(){int a = 10;int* p = &a;free(p);//no}

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

void test(){int* p = (int*)malloc(100);p++;free(p);//p不再指向动态内存的起始位置}

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

void test(){int* p = (int*)malloc(100);free(p);free(p);//重复释放}

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

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

❗**注:**动态开辟的空间一定要释放,并且正确释放。

4.几个经典的笔试题

4.1题目一

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

请问运行Test函数会有什么样的结果?

//当程序对NULL的进行解引用操作的时候,程序崩溃!后序代码也不会执行。strcpy(str, "hello world");//同时malloc开辟的空间没有释放,内存会泄露!p = (char*)malloc(100);
//更正:#include #include 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;}

✅运行:

//更正二:#include #include 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;}

4.2题目二

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

请问运行Test函数会有什么样的结果?

str = GetMemory();//返回栈空间地址的问题,野指针

✅可以这样进行修改:

4.3题目三

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

请问运行Test函数会有什么样的结果?

*p = (char*)malloc(num);//malloc申请的空间没有释放
//更正:#include #include 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;}

4.4题目四

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

请问运行Test函数会有什么样的结果?

strcpy(str, "world");//非法访问内存的情况,str为野指针

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


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

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

6.使用动态内存相关的知识改进通讯录

  1. 通讯录刚开始时,可以存放3个人的信息。
  2. 空间如果放满,每次可以增加2个信息的空间。
contact.h#pragma once//类型的声明#include #include #include #include #define Max 100#define NAME_MAX 10#define DEFAULT_SZ 3typedef struct PepInfo{char name[NAME_MAX];int age;char sex[5];char tele[12];char addr[20];}PInfo;//静态通讯录//typedef struct Contact//{//PInfo data[Max];//int sz;//用于记录当前通讯录中存放了多少个人的信息//}Contact;//动态通讯录的版本typedef struct Contact{PInfo* data;//存放数据int sz;//记录当前通讯录中存放的人的信息的个数int capacity;//记录通讯录的容量}Contact;//初始化通讯录void InitContact(Contact* c);//增加联系人void AddContact(Contact* c);//删除指定的联系人void DelContact(Contact* c);//查找指定的联系人void SearchContact(Contact* c);//修改指定联系人void ModifyContact(Contact* c);//按照年龄排序void AgeSortContact(Contact* c);//销毁通讯录void DestroyContact(Contact *c);contact.c#define _CRT_SECURE_NO_WARNINGS 1#include "contact.h"//静态的版本//void InitContact(Contact *c)//{//assert(c);//c->sz = 0;//memset(c->data, 0, sizeof(c->data));//}void InitContact(Contact* c){assert(c);c->sz = 0;c->capacity = DEFAULT_SZ;c->data = calloc(c->capacity, sizeof(PInfo));if (c->data == NULL){perror("InitContact->calloc");return;}}//增容的函数可以单独封装void CheckCapacity(Contact* c){if (c->sz == c->capacity){PInfo* ptr = (PInfo*)realloc(c->data, (c->capacity + 2) * sizeof(PInfo));if (ptr != NULL){c->data = ptr;c->capacity += 2;printf("增容成功!\n");}else{perror("AddContact->realloc");return;}}}//销毁通讯录void DestroyContact(Contact* c){free(c->data);c->data = NULL;c->sz = 0;c->capacity = 0;}void AddContact(Contact* c){//首先要判断该通讯录是否已经满了assert(c);//增加容量CheckCapacity(c);if (c->sz == Max){printf("通讯录已满,无法增加!\n");return;}printf("请输入姓名:");scanf("%s", c->data[c->sz].name);printf("请输入年龄:");scanf("%d", &c->data[c->sz].age);printf("请输入性别:");scanf("%s", c->data[c->sz].sex);printf("请输入电话:");scanf("%s", c->data[c->sz].tele);printf("请输入地址:");scanf("%s", c->data[c->sz].addr);c->sz++;printf("增加成功!\n");}void ShowContact(const Contact* c){assert(c);if (c->sz == 0){printf("通讯录为空,无需打印!\n");}int i = 0;printf("%-20s%-5s%-5s%-12s%-30s\n", "姓名", "年龄", "性别", "电话", "地址");for (int i = 0; i < c->sz; i++){printf("%-20s%-5d%-5s%-12s%-30s\n",c->data[i].name, c->data[i].age, c->data[i].sex, c->data[i].tele, c->data[i].addr);}}int FindByName(Contact* c, char name[]){assert(c);int i = 0;for (i = 0; i < c->sz; i++){if (strcmp(c->data[i].name, name) == 0){return i;}}return -1;//找不到}void DelContact(Contact* c){char name[NAME_MAX];assert(c);if (c->sz == 0){printf("通讯录为空,无法删除!\n");return;}printf("输入要删除人的姓名:");scanf("%s", name);//找到姓名为name的人int ret = FindByName(c, name);if (ret == -1){printf("要删除的人不存在!\n");return;}//删除这个人int i = 0;for (i = ret; i < c->sz - 1; i++){c->data[i] = c->data[i + 1];}c->sz--;printf("删除成功!\n");}void SearchContact(Contact* c){char name[NAME_MAX];assert(c);printf("请输入要查找人的姓名:");scanf("%s", name);int ret = FindByName(c, name);if (ret == -1){printf("要查找的人不存在!\n");return;}//若找到了,打印出相关信息printf("%-20s%-5s%-5s%-12s%-30s\n", "姓名", "年龄", "性别", "电话", "地址");printf("%-20s%-5d%-5s%-12s%-30s\n",c->data[ret].name, c->data[ret].age, c->data[ret].sex, c->data[ret].tele, c->data[ret].addr);}void ModifyContact(Contact* c){char name[NAME_MAX];assert(c);printf("请输入要修改人的姓名:");scanf("%s", name);int ret = FindByName(c, name);if (ret == -1){printf("要修改的人不存在!\n");return;}//修改printf("请输入姓名:");scanf("%s", c->data[ret].name);printf("请输入年龄:");scanf("%d", &c->data[ret].age);printf("请输入性别:");scanf("%s", c->data[ret].sex);printf("请输入电话:");scanf("%s", c->data[ret].tele);printf("请输入地址:");scanf("%s", c->data[ret].addr);printf("修改成功!\n");}int cmp(const void *a,const void *b){return strcmp((*(PInfo*)a).age, (*(PInfo*)b).age);}void AgeSortContact(Contact* c){assert(c);qsort(c->data, c->sz, sizeof(PInfo), cmp);printf("%-20s%-5s%-5s%-12s%-30s\n", "姓名", "年龄", "性别", "电话", "地址");for (int i = 0; i < c->sz; i++){printf("%-20s%-5d%-5s%-12s%-30s\n",c->data[i].name, c->data[i].age, c->data[i].sex, c->data[i].tele, c->data[i].addr);}}test.c//文件用于测试通讯录的基本功能。#define _CRT_SECURE_NO_WARNINGS 1#include "contact.h"//自己定义的头文件用""void menu(){printf("***********************************\n");printf("********1.增加联系人***************\n");printf(" \n");printf("********2.删除指定联系人的信息*****\n");printf(" \n");printf("********3.查找指定联系人的信息*****\n");printf(" \n");printf("********4.修改指定联系人的信息*****\n");printf(" \n");printf("********5.排序通讯录的信息*********\n");printf(" \n");printf("********6.显示所有联系人的信息*****\n");printf(" \n");printf("********0.退出程序*****************\n");printf("***********************************\n");}enum Option{EXIT,ADD,DEL,SEARCH,MODIFY,SHOW,SORT};int main(){int input = 0;Contact con;//初始化函数InitContact(&con);do{menu();printf("请输入你的选择:>");scanf("%d", &input);switch (input){case ADD:AddContact(&con);system("pause");system("cls");break;case DEL:DelContact(&con);system("pause");system("cls");break;case SEARCH:SearchContact(&con);system("pause");system("cls");break;case MODIFY:ModifyContact(&con);system("pause");system("cls");break;case SHOW:ShowContact(&con);system("pause");system("cls");break;case SORT:AgeSortContact(&con);system("pause");system("cls");break;case EXIT:DestroyContact(&con);printf("退出通讯录\n");break;default:break;}} while (input);return 0;}

7.柔性数组

C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做(柔性数组)成员。

✅注意点:

  1. 在结构体中
  2. 最后一个成员
  3. 未知大小的数组
//例如:typedef struct st_type{int i;int a[0];//柔性数组成员}type_a;typedef struct st_type{int i;int a[];//柔性数组成员}type_a;//两种写法是一样的

7.1柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员。
  • sizeof返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type{int i;int a[0];//柔性数组成员}type_a;printf("%d\n", sizeof(type_a));//输出的是4

7.2柔性数组的使用

#include #include struct S{int i;int a[];//柔性数组成员};int main(){struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);if (ps == NULL){perror("malloc");return 1;}ps->i = 100;int i = 0;for (i = 0; i < 5; i++){ps->a[i] = i;}//打印for (i = 0; i < 5; i++){printf("%d ", ps->a[i]);}//释放free(ps);ps = NULL;return 0;}

7.3柔性数组的优势

//代码2#include #include typedef struct st_type{int i;int* p_a;}type_a;int main(){type_a* p = (type_a*)malloc(sizeof(type_a));p->i = 100;p->p_a = (int*)malloc(p->i * sizeof(int));//业务处理int i = 0;for (i = 0; i < 100; i++){p->p_a[i] = i;}//释放空间free(p->p_a);p->p_a = NULL;free(p);p = NULL;return 0;}

好处:

  1. 方便内存释放:如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
  2. 这样有利于访问速度.:连续的内存有益于提高访问速度,也有益于减少内存碎片。

好啦,关于动态内存管理的知识到这里就先结束啦,后期会继续更新学习C语言的相关知识,欢迎大家持续关注、点赞和评论!❤️❤️❤️