目录

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

2. 动态内存函数的介绍

2.1 malloc和free✅

①申请:1️⃣

②使用:2️⃣

③释放:3️⃣

2.2 calloc

与malloc的区别:

2.3 realloc

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

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

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

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

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

解决方法

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

情况1:如果不使用free释放:程序结束之后,也会由操作系统回收

情况2:如果不使用free释放,程序也不结束,内存泄露‍

4. 几个经典的笔试题

4.1 题目1: 对NULL指针追加字符串

4.2 题目2:数组在栈空间创建的销毁问题

4.3 题目3:程序正常执行但内存泄露

4.4 题目4:野指针使用问题


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

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

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

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

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

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

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

2. 动态内存函数的介绍

2.1 mallocfree✅

C语言提供了一个动态内存开辟的函数:void* malloc (size_t size);

假设有人在内存中申请20个字节的空间写成这样是肯定没错的:

void*p=malloc (20);

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

void*指针需要注意的特点有:

1.可以接收任意类型的地址(也就是无具体类型的指针)

2.不能+-*/,不能解引用操作,非常麻烦

并且如果在这20个byte里面存放1,2,3,4,5五个整型,那就应该用整型指针接收

int*p=(int*)malloc (20);

这里再提一下内存:

关于malloc的使用的整体的过程:①申请②使用③释放

整体呈现的代码:

int main(){//申请int* p = (int*)malloc(20);//使用if (p == NULL){printf("%s\n", strerror(errno));}int i = 0;//初始化:for (i = 0; i < 5; i++){*(p + i) = i + 1;}for (i = 0; i < 5; i++){printf("%d ", *(p + i));} //释放free(p);//置为空指针 p = NULL; return 0;}

①申请:1️⃣

int* p = (int*)malloc(20);

  • 这个函数向内存申请一块连续可用的空间,如果开辟成功,并返回指向这块空间的指针。

if (p == NULL)
{
printf(“%s\n”, strerror(errno));
}

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 由于malloc是可能申请失败的,失败的原因有:

1.申请开辟的内存空间过大

2.可能内存不足(设计到linux,不便阐述)

虽然申请了空间,但是操作系统并没有真正的给使用者,只是一个虚拟空间的映射,当使用者真正用到的时候,才会使用内存

3.不确定的地方⁉❎

  • malloc申请0个字节的空间” />如果参数 size 0malloc的行为是标准是未定义的,取决于编译器

    ②使用:2️⃣

    写法一:指针解引用

    int i = 0;
    for (i = 0; i < 5; i++)
    {
    *(p + i) = i + 1;
    }
    for (i = 0; i < 5; i++)
    {
    printf(“%d “, *(p + i));
    }

    在内存窗口地址处输入p即可监视值的变化

    写法二:想象成数组

    for (i = 0; i < 5; i++)
    {
    p[i] = i + 1;
    }
    for (i = 0; i < 5; i++)
    {
    printf(“%d “,p[i]);
    }

    两种写法的运行结果都是:

    需要注意的点是:malloc申请的空间不初始化,打印是随机值

    #include#includeint main(){int* p = (int*)malloc(20);if (p == NULL){printf("%s\n", strerror(errno));}int i = 0;for (i = 0; i < 5; i++){printf("%d ", *(p + i));}free(p);return 0;}

    换成16进制打印:

    printf(“%x”, *(p + i));

    ③释放:3️⃣

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

    void free (void* ptr);

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

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

    意思是ptr指向的那块空间一定是动态内存开辟到的一块空间,要是ptr随便指向一块空间,让它free一下,那是不行的

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

    要是ptr本来就是一个空指针,那free之后什么事情都不会发生

    free之前,可以看到p的地址

    free之后,地址没有改变!

    如果有使用者去使用p的话,那它就是一个野指针了,free这个函数不会帮指针p置空的⭕

    • 要避免野指针问题:free完要主动置为空指针, 这两步不要忘记了:

    free(p);//1

    p=NULL;//2

    2.2 calloc

    C语言还提供了一个函数叫 calloc calloc 函数也用来动态内存分配。原型如下:

    void* calloc (size_t num, size_t size);

    #includeint main(){int* p = (int*)calloc(5, sizeof(int));if (p == NULL){printf("calloc()-->%s\n", strerror(errno));return 1;}//使用int i = 0;for (i = 0; i < 5; i++){printf("%d ", p[i]);}//释放free(p);p = NULL;return 0;}

    注意:这段代码是没有初始化任何元素的

    执行:

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

    可以看到里面20个字节全部初始化为0了

    与malloc的区别:

    因为calloc要初始化元素,所以效率是没有malloc高的,根据实际情况去选择吧。

    2.3 realloc

    realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整 函数原型如下:

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

    • ptr 是要调整的内存地址
    • size 调整之后新大小
    • 返回值为调整之后的内存起始位置。
    • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

    realloc 在调整内存空间的是存在两种情况: ⇨ 情况 1 :原有空间之后有 足够大的空间

    本地扩容:

    realloc不会释放空间,free的就是旧的地址(p指向的那块的空间),这块空间地址是连续的,所以会根据地址的顺序连续释放掉

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

    异地扩容:

    1.realloc会找更大的空间
    2.将原来的数据拷贝到新的空间

    3.释放旧的空间 –>realloc函数会自己处理,不用我们手动释放(不用手动free)

    重新找了一片空间(ptr指向那块空间)进行扩容,那realloc会先将数据拷贝到(ptr指针指向的)新地址处,再将旧(p指向)的空间释放掉

    4.返回新空间的地址

    realloc返回的是新的地址–>(ptr指针存的地址)

    3.常见的动态内存错误

    3.1 NULL指针的解引用操作

    #includeint main() {int* p = (int*)malloc(20);//可能出现对NULL指针的解引用操作//所以malloc函数的返回值要判断int i = 0;for (i = 0; i < 5; i++){p[i] = i;}free(p);p = NULL; return 0;}

    malloc有可能申请失败,返回NULL指针,可能出现对NULL指针的解引用操作,所以malloc函数的返回值要判断

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

    #includeint main() {int* p = (int*)malloc(20);if (p == NULL){printf("%s\n", strerror(errno));return 1;}//可能出现对NULL指针的解引用操作//所以malloc函数的返回值要判断int i = 0;//越界访问 for (i = 0; i < 10; i++){p[i] = i;}free(p);p = NULL;return 0;}

    打开内存监视可以发现:

    ✨发现:

    这里越界了,等于甚至超过下标为5它依旧会初始化,但不会发生错误,只有到程序执行到free那一步才会发生错误

    个人理解:

    这个是因为vs对于越界的检查是不太严格的,可以理解为抽查,可能会检查到,也可能检查不到。

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

    arr创建这个变量在栈上,不在堆上

    注意:数组也是变量,可以理解为它是一种特殊的变量类型,是由同一类型的元素组成的

    #includeint main(){int arr[10] = { 1,2,3,4,5 };int* p = arr;free(p);p = NULL;return 0;}

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

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

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

    #includeint main(){int* p = (int*)malloc(40);if (p = NULL){printf("%s\n", strerror(errno));return 0; }int i = 0;for (i = 0; i < 5; i++){*p = i + 1;p++;}//释放free(p);return 0;}

    原因: 申请了40个字节,如果要释放整块空间,必须提供起始位置地址才能释放掉,如果提供中间位置地址,那程序会崩溃的。

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

    错误代码:

    int main(){int* p = (int*)malloc(20);if (p == NULL){return 1;}//使用free(p);free(p);p = NULL;return 1;}

    重复释放malloc申请开辟的空间会导致程序崩溃

    解决方法

    free(p);

    p=NULL;//这是解决的方法

    free(p); //置空之后,这里什么事情都不干

    p=NULL;

    3.6 动态开辟内存忘记释放(内存泄漏)” />写一下前面malloc,calloc,realloc的共同之处:

    malloc–calloc–realloc
    1️⃣所申请的空间,如果不想使用,需要free释放

    ❗❗

    2️⃣如果不使用free释放:程序结束之后,也会由操作系统回收

    注意:函数结束和程序结束不是一回事,整个程序的生命周期到了结束了,它会释放这个内存的。
    3️⃣如果不使用free释放,程序也不结束,
    内存泄露

    情况1:如果不使用free释放:程序结束之后,也会由操作系统回收

    #include
    test()
    {
    int* p = (int*)malloc(20);
    }
    int main()
    {
    test();
    return 0;
    }

    情况2:如果不使用free释放,程序也不结束,内存泄露‍

    问:什么时候程序不结束?

    答:死循环

    #includetest(){int* p = (int*)malloc(20);}int main(){test(); while (1);return 0;}

    main函数中,死循环 程序没有结束 那么那段空间也就一直没有释放

    死循环那是因为:
    while(1);相当于while(1){};

    条件是1永远是真,while(1)一直运行,那就是死循环了

    4. 几个经典的笔试题

    4.1 题目1: 对NULL指针追加字符串

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

    注意点:

    1.str是值传递,不是地址,可以理解为一个特殊的变量

    2.如果在GetMemory函数里面free的话,又违背了这题的初衷,因为它就想返回动态内存开辟的空间,然后让strcpy函数将字符串追加到首地址上去

    解决问题:

    写法一:通过返回值的方式带回100个字节的地址

    char* GetMemory(char* p){p = (char*)malloc(100);return p;//修改位置}void Test(void){char* str = NULL;str=GetMemory(str);//修改位置strcpy(str, "hello world");printf(str);free(str);//修改位置str = NULL; //修改位置}int main(){Test();return 0;}

    把申请的100个字节的起始地址返回来,由指针变量str接收

    ⚡简洁写法:不需要传参

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

    写法二:通过二级指针的方式

    插叙一条重要的知识点:

    #include
    int main()
    {
    char* p = “hehe\n”;//这里是把”hehe\n”放到p里面去了?不是,把这个表达式(字符串)首字符h的地址,放到p里面去了

    printf(“hehe\n”);//这里其实是把字符串首字符的地址传给了printf

    return 0;
    }

    回到原题:

    这题的问题出现在哪” />void* GetMemory(char**p){* p = (char*)malloc(100);//这里*p就是str}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 题目2:数组在栈空间创建的销毁问题

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

    执行结果:

    返回栈空间地址的问题:

    GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址给了str,但是数组的内存出了GetMemory函数就被回收了,而str依然保存了数组的起始地址,这时如果使用str,str就是野指针

    看到这是不是觉得跟某一题很眼熟

    关于左右两段代码,区别是一个在堆区,一个在栈区,左边位于堆区的那个GetMemory函数出了函数,那段空间还保留着,没有销毁,而右边位于栈区的那段空间,出了GetMemory函数就由操作系统回收了

    简单提一下函数栈帧的现象:

    int* test(){int a = 10;return &a;}int main(){int* p = test();printf("%d\n", *p);return 0;}

    由上面提到的知识我们可知这个p是野指针,但是依然记得这块空间的地址,并且打印该值

    可是加上了这一段之后,p的值就改变了

    int* test()
    {
    int a = 10;
    return &a;
    }

    int main()
    {
    int* p = test();
    printf(“hehe\n”);//添加的
    printf(“%d\n”, *p);
    return 0;
    }

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

    执行结果:

    错误:

    内存泄露

    该注意的点:

    这题是可以正常执行的,内存泄露看的是:空间有没有正常释放,不看程序结束之后的结果

    申请的内存由于某种原因没有被及时释放,导致内存空间被占用但无法再被程序使用或回收,导致内存泄露

    4.4 题目4:野指针使用问题

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

    执行:

    free是将空间还给操作系统了,但是地址是没有销毁的,这个str还是保存的之前的地址,但

    是继续这样访问是不对的,因为空间已经不属于str了

    简单来说就是:空间是销毁还给操作系统了,地址还记着,依旧使用

    图解:

    代码修改:

    void Test(void)
    {
    char *str = (char *) malloc(100);
    strcpy(str, “hello”);
    free(str);

    str=NULL;//修改位置
    if(str != NULL)
    {
    strcpy(str, “world”);
    printf(str);
    }
    }

    接下来还有柔性数组等的知识,欢迎大佬补充