C语言动态内存函数的理解和总结

作者: 51CTO博文  更新时间:2021-04-13 04:03:13  原文链接


第一:内存的使用

内存可以分为以下三个主要的部分:栈区、堆区、静态区
    栈区(stack):存放的是局部变量、函数的形参等都是在该区上存放的。
    堆区(heap):动态内存函数开辟的空间。比如malloc realloc等函数。
    静态区:存放的是全局变量或者静态变量(由static关键字修饰的变量)。

第二:动态内存函数

2.1 存在的价值

动态内存函数可以向内存申请大小可变的的空间,比如创建长度可以改变的数组时,就需要用到动态内存函数malloc和realloc函数来实现。
    但是C语言也是支持创建可变长度数组的(C99是支持的,以前是不支持的。如gcc编译器是支持的)
    在栈上开辟的空间大小是固定的,是不可改变的。
    数组在定义时,其大小必须指明大小,不能是变量,因为其所需的内存是在编译时分配。
    根据需求,我们所需要的的空间只有在运行时,才可知,则数组在编译时开辟空间的方式就没法满足,因此需要动态开辟内存空间。
    如:

但是VS2019是不支持的。

2.2 动态内存分配函数malloc和free

2.2.1 malloc函数介绍

作       用:向内存的堆区申请一块连续可用的空间:Allocates memory blocks.
    函数原型:void *malloc( size_t size );
    参       数:size:Bytes to allocate(申请空间大小,单位:字节)
    返  回 值:返回void类型的指针(指向所申请空间的地址)(申请成功)
                     返回NULL(空指针)(如果没有足够内存空间可用时,申请失败)
    注       意:返回类型是void类型的指针,因此使用的时候根据需求对其进行强制转化为所需要的类型的指针变量。
                      如果参数值为0,C对此没有做出明确解释,具体结果,依据编译器而定,因此,尽量不要出现这种情况。
                      需要对malloc的返回值进行检查,因为有可能申请失败的情况。
                      因为返回的是void类型的指针,因此需要对结果进行相应的强制类型转化。

2.2.2 free函数的介绍

作       用:释放和回收在堆区动态开辟的内存空间:Deallocates or frees a memory block.
    函数原型:void free( void *memblock );
    参       数:将要被释放的内存空间的地址
    返  回 值:无返回值
    注       意:只能释动在堆区上开辟的态内存的地址,否则报错。
                     只能释放一次,不能释放多次。
                     free函数没有将指针设为NULL的能力,即指针的值没有发生变化,因此需要手动将其设为NULL,这样避免后面不小心进行非法访问内存的情况发生。
                     如果没有free,则会发生内存泄露的情况,即申请的空间不释放,也不同,别人也没法用,
                     如果没有free,只有当程序结束时,系统自动还给内存,但是如果不结束,且会多次申请且不释放,则会发生严重的内存泄露。
                     因此要做到,谁申请,谁使用,谁释放,谁使其设为空的原则。

2.2.3 malloc和free函数的使用

开辟成功时:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
    int i;
    int len = 10;
    // 向内存开辟10个连续整型的空间,申请成功时,把空间的地址赋值给arr
    int* arr = (int*)malloc(len * sizeof(int));
    // 判断开辟内存空间是否成功,因为会开辟失败
    if (NULL == arr)
    {
        printf("%s\n", strerror(errno));
    }
    else 
    {
        for (i = 0; i < len; i++)
        {
            *(arr + i) = i * i;
        }
        for (i = 0; i < len; i++)
        {
            printf("%d ",*(arr + i));
        }
        // 动态开辟的内存,需要进行释放,否则会导致内存泄露
        free(arr);
        // 手动将指针设为空指针,因为free没有将其设为空指针的能力
        // 如果没有将其手动设为空指针,且别别人使用该指针,则引起非法访问内存的错误。
        arr = NULL;
    }
    return 0;
}

开辟失败时:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
    int* arr = (int*)malloc(INT_MAX);
    if (NULL == arr)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        // do something.
        free(arr);
        arr = NULL;
    }
    return 0;
}

2.3 动态内存分配函数calloc

2.3.1 calloc函数介绍

作       用:开始num个,每个大小为size个字节的一块内存空间,并将每个字节设为0。Allocates an array in memory with elements initialized to 0.
    函数原型:void *calloc( size_t num, size_t size );
    参       数:num:Number of elements(申请元素的个数)
                      size:Length in bytes of each element(每个元素所占字节空间大小)
    返 回  值:同malloc
    注       意:与malloc函数的区别:申请成功后,把空间的每个字节进行初始化为0,然后再讲空间的地址进行返回。

2.3.2 malloc和calloc函数的比较

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
    int len = 10;
    int* p1 = (int*)malloc(len * sizeof(int));
    int* p2 = (int*)calloc(len, sizeof(int));
    int i;
    if (NULL == p1)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        // do something
        printf("malloc 返回指针空间的内容:随机值:");
        for (i = 0; i < len; i++) { printf("%d ",*(p1+i)); }
        printf("\n");
        free(p1);
        p1 = NULL;
    }
    if (NULL == p2)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        // do something...
        printf("calloc 返回指针空间的内容:0: ");
        for (i = 0; i < len; i++) { printf("%d ", *(p2 + i)); }
        printf("\n");
        free(p2);
        p2 = NULL;
    }
    return 0;
}

2.4 动态内存分配函数realloc

2.4.1 realloc函数的说明

作       用:可以对动态申请的内存空间大小进行调整。
    函数原型:Reallocate memory blocks.
    参       数:memblock: Pointer to previously allocated memory block(将要修改的开辟内存块的地址)
    size: New size in bytes(调整后的内存块的大小,新的大小)
    返 回  值:同malloc和calloc,如果调整失败,返回NULL,否则返回重新分配内存块的空间地址(该地址有可能会发生变化)
    注       意:
        1. 会将原来内存空间上的数据移动到新的空间内存中。
        2. 调整内存时存在两种情况:
            2.1. 原来申请的内存空间后面有足够的空间用于调整内存。则会在原来内存空间的后面直接追加空间到指定大小,并返回原空间的地址。
                   原来空间的内容不发生变化。
            2.2. 原来的空间没有足够的空间来满足新的空间大小,则会在堆区重新找一块连续空间来存放新的内存空间。
                   原空间的内容拷贝到新的空间中,释放原来的内存空间,把新的内存空间的地址进行返回。                          
        3. 用于接收realloc返回值的指针名最好不要和realloc的第一个参数名相同,即不要把原空间的地址作为接收realloc返回值,如:
            建    议:p1 = (int*)realloc(p, newsize);p = p1;
            不建议:p = (int*)realloc(p, newsize);
        4. 释放内存时:如上述例子
            4.1. free(p1);p1 = NULL;
            4.2. free(p);p = NULL;(推荐)
            4.3. free(p1);p1 = NULL;free(p);p = NULL;(报错,因为他们指向同一个空间,只能释放一次,如注意2.1,
                    如果如注意2.2所示,realloc函数会把原来的内存块释放掉,所以不需要手动调用free(原来的内存地址))
            5. 如果第一个参数为NULL,第二个参数为size,则相当于直接开辟size个字节的大小
                此时realloc(NULL, size)等价于malloc(size);

2.4.2 realloc函数的使用

realloc的第一个参数不为NULL时:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
    int len = 5;
    int add1 = 5;
    int add2 = 50000;
    int i;
    // malloc函数申请空间
    int* p = (int*)malloc(len * sizeof(int));
    if (NULL == p)
    {
        printf("%s", strerror(errno));
        return 0;
    }
    printf("malloc: %p\n", p);
    for (i = 0; i < len; i++) { *(p + i) = i; }

    // realloc函数申请增加少量的空间,原来内存空间后面有足够的空间用于增加的空间
    int* p1 = (int*)realloc(p, (len+add1) * sizeof(int)); 
    if (NULL == p1)
    {
        printf("%s", strerror(errno));
        return 0;
    }
    else
    {
        p = p1;
    }
    printf("realloc 增加小的内存空间: %p\n", p);
    for (i = len; i < len + add1; i++) { *(p + i) = i; }
    printf("会把原来的值给复制过来:\n");
    for (i = 0; i < len + add1; i++) { printf("%d ", *(p + i)); }
    // realloc函数申请增加大量的的空间,原来内存空间后面没有足够的空间用于增加的空间
    // 需要重新找一块足够大的空间,足以存放原来和新增加的空间的大小,内存空间的地址则会变化
    int* p2 = (int*)realloc(p, (len + add1 + add2) * sizeof(int)); printf("\n");
    if (NULL == p2)
    {
        printf("%s", strerror(errno));
        return 0;
    }
    else
    {
        p = p2;
    }
    printf("realloc 增加大的内存空间: %p\n", p);
    for (i = len+add1; i < len + add1 + add2; i++) { *(p + i) = i; }
    free(p);
    p = NULL;
    return 0;
}

realloc的第一个参数为NULL时:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
    int i;
    int* p = (int*)realloc(NULL, 20);
    if (NULL != p)
    {
        for (i = 0; i < 5; i++) { printf("%d ", *(p + i)); }
        free(p);
        p = NULL;
    }
    return 0;
}

第三:常见的内存错误

3.1 对开辟内存失败且没有检查而进行解引用

错误写法:

正确写法:一定要对malloc、calloc、realloc函数的返回值进行判断

3.2 对开辟的内存进行越界访问

3.3 对非动态内存使用free函数

free函数只能对堆区上的空间进行释放

3.4 free开辟的部分空间

如果free释放的空间的地址不是开辟空间的地址,都属于free部分空间的范围

编译通过,运行报错,因为会释放不属于开辟空间的内存。

3.5 free同一开辟空间多次

为了避免这种问题,应该遵守谁开辟,谁释放,释放后立马将其设为NULL;

3.6 忘记free

如果对动态开辟的空间没有free,则该空间,只有在程序结束时,才被回收。但是如果程序为一直运行状态,且开辟空间为重复过程。则会持续开辟空间而不会被回收,因此就会导致可用的内存越来越少,即发生内存泄露的情况。

执行之前:

执行之后:

第四:经典易错的面试题

4.1 非法访问内存和内存泄露:

错误代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void runtest(char* p) // 为实参的一份临时拷贝
{
    p = (char*)malloc(40);
} // 当函数调用结束后,创建的局部变量会自动销毁

int main()
{   
    // 代码运行时,会发生错误,或者崩溃
    char* str = NULL;
    runtest(str); // 参数为变量本身,指针变量也是变量,str以值的形式传给形参p
    strcpy(str, "hello, C/C++"); // 将字符串拷贝到空指针中,非法访问内存
    printf(str); 
    // 动态开辟的内存没有释放,会有内存泄露的问题
    return 0;
}

正确代码1:传递变量的地址

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void runtest(char** p) 
{
    *p = (char*)malloc(40);
}

int main()
{
    char* str = NULL;
    runtest(&str); // 传递变量的地址
    if (NULL != str)
    {
        strcpy(str, "hello, C/CPP"); 
        printf(str);
        free(str);
        str = NULL;
    }
    return 0;
}

正确代码2:将动态内存的地址返回

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

char* runtest(char* p) // 为实参的一份临时拷贝
{
    p = (char*)malloc(40);
    return p;
} 

int main()
{
    char* str = NULL;
    str = runtest(str); // 参数为变量本身,指针变量也是变量,str以值的形式传给形参p
    if (NULL != str)
    {
        strcpy(str, "hello, C/C++");
        printf(str);
        free(str);
        str = NULL;
    }
    return 0;
}

4.2 返回栈区空间的地址

错误代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

char* runtest(void)
{
    // 问题因为这是个局部变量,当函数调用结束后
    // 虽然返回了该变量的指针,但是其所指向的空间的内容将会被销毁
    // 当函数调用后,对该地址进行访问,就属于非法访问内存
    // 数据经典的返回栈区空间的地址
    char p[] = "hello c/cpp"; // p位于栈区
    return p;
}
int main()
{
    char* str = NULL;
    str = runtest();
    printf(str);
    return 0;
}

正确代码:将局部变量用static修饰,即将栈区的变量,修改为静态区的变量