013+limou+C语言深入知识——(5)动态内存管理以及分配

000、简易的C语言变量视角空间申请图示

  • 局部变量、函数形式参数在栈区申请内存空间
  • 动态内存在堆区申请内存空间
  • 静态变量、全局变量在静态区申请内存空间

013+limou+C语言深入知识——(5)动态内存管理以及分配_第1张图片

001、为什么存在动态内存分配

(1)之前创建空间的方法

int a = 10;//在栈空间开辟4个字节
int arr[10];//在栈空间开辟4*10个字节
  • 可以看到这样开辟的空间是无法修改大小的,数组也必须指定其长度,其所需内存在“编译时”分配
  • 而有的时候需要在程序运行的时候才能知道所需空间大小,这样就需要动态分配内存了

(2)动态内存分配的相关函数

  • malloc
  • free
  • calloc
  • realloc

002、动态内存函数

(1)malloc和free

void* malloc (size_t size);
void free (void* ptr);
  • malloc函数的功能是先向内存申请一块连续可用的空间,并且返回指向这块空间的地址

    • 如果开辟空间失败,就返回一个NULL,为了避免解引用空指针,所以一般都要对malloc的返回值做检查
    • malloc的返回值是void*,malloc并不知道使用者需要开辟空间的类型,在使用malloc的时候由使用者决定指针类型,通常使用强制类型转换来转化
    • 参数size的值为0,其结果是未知的(C标准没有对此进行定义),要看编译器的具体实现(另外这种需求也很奇怪:如果说要开辟空间,又想得到0个空间,这是很矛盾的)
    • 注意size是以字节为单位申请空间的
  • free函数的功能是将某个指针指向的动态内存释放掉

    • 通常释放掉某个指针指向的动态内存后,要将该指针置空,避免其成为空指针
//例子一
#include 
#include 
#include 
int main()
{
    //1.申请动态内存
    int* a = (int*)malloc(sizeof(int));
    if(a == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }

    //2.使用动态内存
    *a = 5;
    printf("在%p地址处存储了数值:%d\n", a, *a);

    //3.释放动态内存
    free(a);//释放动态内存
    a = NULL;//置空,避免野指针的出现
    return 0;
}
//例子二
#include 
#include 
#include 
#define NUMBER 10
int main()
{
    //1.申请动态内存
    int* arr = (int*)malloc(sizeof(int) * NUMBER);
    if(arr == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }

    //2.使用动态内存
    for(int i = 0; i < NUMBER; i++)
    {
        arr[i] = i + 1;
    }

    for(int i = 0; i < NUMBER; i++)
    {
        printf("%d ", arr[i]);
    }

    //3.释放动态内存
    free(arr);//释放动态内存
    arr = NULL;//置空,避免野指针的出现
    return 0;
}

(2)calloc

void* calloc (size_t num, size_t size);
  • 函数的功能是为num个大小为size的元素开辟一块连续可用的空间,并且把空间的每个字节都初始化为0
  • 与malloc的区别
    • calloc会在返回地址之前,把申请空间的每个字节都初始化为全0
    • 同时注意该函数参数的参数设计和malloc不太一样
    • 若是calloc的参数num==1,则后面参数就和malloc是一样的
#include 
#include 
#include 
#define NUMBER 10
int main()
{
    //1.申请动态内存
    int* arr = (int*)calloc(NUMBER, sizeof(int));//注意参数设计有点不太一样
    if (arr == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }

    //2.使用动态内存
    for (int i = 0; i < NUMBER; i++)
    {
        printf("%d ", arr[i]);
    }

    //3.释放动态内存
    free(arr);//释放动态内存
    arr = NULL;//置空,避免野指针的出现
    return 0;
}

(3)realloc

void* realloc (void* ptr, size_t size);
  • 该函数的功能是可以灵活调整“由malloc/calloc开辟的动态内存空间”的大小
    • ptr是要调整的内存地址,size是调整之后新大小,返回值为调整之后的内存起始位置。
    • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
    • realloc调整内存空间的时候存在两种情况:
      • 情况1:原有空间后面若有足够大的空间,要扩展内存就直接在原有内存后追加空间,原有空间的数据不发生变化
      • 情况2:原有空间后面若无足够大的空间,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的将会是一个新的内存地址
  • 因此对realloc函数的使用,其返回值是需要极其注意的
  • 如果realloc申请空间变小,则多余的空间就会自动还给操作系统
  • 另外realloc是可以替代malloc函数的,即:如果第一个参数是一个空指针的话,就可以跟malloc一个效果
#include 
#include 
#include 
#define NUMBER 10
int main()
{
    //1.申请动态内存
    int* arr = (int*)malloc(NUMBER * sizeof(int));//注意参数设计有点不太一样
    if (arr == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }

    //2.使用动态内存
    //①输入数据
    for (int i = 0; i < NUMBER; i++)
    {
        arr[i] = i * i;
    }
    //②打印数据
    for (int i = 0; i < NUMBER; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    //3.再次扩大动态内存
    int* arr1 = realloc(arr, sizeof(int) * (NUMBER + 10));
    if (arr1 == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }

    //4.使用扩大后的动态内存
    //①查看原数据是否还存在
    for (int i = 0; i < NUMBER; i++)
    {
        printf("%d ", arr1[i]);
    }
    //②查看是否可以在新申请的空间赋予新的数据
    for (int i = NUMBER; i < NUMBER + 10; i++)
    {
        arr1[i] = i * i;
    }
    printf("\n");
    for (int i = 0; i < NUMBER + 10; i++)
    {
        printf("%d ", arr1[i]);
    }

    //5.释放动态内存
    free(arr1);//释放动态内存
    arr1 = NULL;//置空,避免野指针的出现
    return 0;
}
//如果为了代码的变量统一性,也可以这么做
#include 
#include 
#include 
#define NUMBER 10
int main()
{
    //1.申请动态内存
    int* arr = (int*)malloc(NUMBER * sizeof(int));//注意参数设计有点不太一样
    if (arr == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }

    //2.使用动态内存
    //①输入数据
    for (int i = 0; i < NUMBER; i++)
    {
        arr[i] = i * i;
    }
    //②打印数据
    for (int i = 0; i < NUMBER; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    //3.再次扩大动态内存
    int* arr1 = realloc(arr, sizeof(int) * (NUMBER + 10));
    if (arr1 == NULL)//对返回值指针进行检查
    {
        printf("申请失败\n");
        printf("%s\n", strerror(errno));
        exit(-1);//或者return 1;
    }
    else
    {
        arr = arr1;//这样后面的代码就会统一一些,这其实没有太大问题,尽管编译器还是有可能报警告
    }

    //4.使用扩大后的动态内存
    for (int i = NUMBER; i < NUMBER + 10; i++)
    {
        arr[i] = i * i;
    }
    printf("\n");
    for (int i = 0; i < NUMBER + 10; i++)
    {
        printf("%d ", arr[i]);
    }

    //5.释放动态内存
    free(arr1);//释放动态内存
    arr1 = NULL;//置空,避免野指针的出现
    return 0;
}

013+limou+C语言深入知识——(5)动态内存管理以及分配_第2张图片

003、常见的动态内存错误

(1)对NULL指针的解引用操作

#include 
void test()
{
    int *p = (int*)malloc(4);//申请4个字节
    *p = 20;//如果malloc申请失败,则p的值是NULL,则这里就会出现对空指针的解引用
    free(p);
}
int main()
{
    test();
    return 0;
}

013+limou+C语言深入知识——(5)动态内存管理以及分配_第3张图片

(2)对动态开辟空间的越界访问

#include 
void test()
{
    int i = 0;
    int *p = (int*)malloc(10 * sizeof(int));
    if(NULL == p)
    {
        return 1;
    }
    for(i=0; i<=10; i++)
    {
        *(p+i) = i;//当i等于10的时候就发生越界访问
    }
    free(p);
}
int main()
{
    test();
    return 0;
}

013+limou+C语言深入知识——(5)动态内存管理以及分配_第4张图片

(3)对非动态开辟内存使用free释放

void test()
{
    int a = 10;
    int *p = &a;
    free(p);//释放了非动态内存
}
int main()
{
    test();
}

013+limou+C语言深入知识——(5)动态内存管理以及分配_第5张图片

(4)使用free释放一块动态内存的一部分

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

013+limou+C语言深入知识——(5)动态内存管理以及分配_第6张图片

(5)对同一块动态内存多次释放

void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放了该指针,但是有一个解决方案就是将已经释放的指针置空,free(NULL)不会产生任何效果,这样即使多次释放也没关系
}
int main()
{
    test();
    return 0;
}

013+limou+C语言深入知识——(5)动态内存管理以及分配_第7张图片

(6)动态开辟内存忘记释放(内存泄漏,在工程中属于比较严重的问题)

  • 注意以下代码不能运行太久,不然有可能你的计算机会变“卡”,不过现在大多数系统都会对此做保护
#include 
#include 
int main()
{
    while (1)
    {
        int* p = (int*)malloc(100);
        Sleep(1000);
    }
}

004、经典题目

(1)题目一:没有正确区分栈内存和堆内存

void GetMemory(char *p) 
{
    p = (char*)malloc(100);//这个p出了这个函数就再也找不到了,会一直内存泄漏,没有机会释放
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);//而且p没有被带回来,这里的str应该是传指针变量的地址,而不是传指针变量本身的值,因此strcpy会解引用空指针
    strcpy(str, "hello world");
    printf(str);
}
int main()
{
    Test();
    return 0;
}
//上述代码应该修改如下
#include 
#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;
}

(2)题目二:返回栈空间地址错误

char *GetMemory(void) 
{
    char p[] = "hello world";//局部变量除了出了该函数会被销毁,无法像动态内存一样被带回
    return p; 
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();//带回来了一个野指针
    printf(str);
}

(3)题目三:忘记使用free

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

005、C/C++程序的内存开辟(开头的三区是以下的简化版,两者是包含关系)

(1) 使用柔性数组的方法

//code1
#include 
#include 
#include 
#include 
typedef struct limou
{
    int n;
    char c;
    int arr[0];
}limou;
int main()
{
    //申请结构体内存
    limou* plimou = (limou*)malloc(sizeof(limou) + 5 * sizeof(int));
    if (plimou == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }

    //使用柔性数组的内存
    for (int i = 0; i < 5; i++)
    {
        plimou->arr[i] = i;
        printf("%d ", plimou->arr[i]);
    }
    printf("\n");

    //调增柔性数组的大小
    limou* plimou1 = realloc(plimou, sizeof(limou) + 10 * sizeof(int));
    if (plimou1 == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    for (int i = 5; i < 10; i++)
    {
        plimou1->arr[i] = i;
    }
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", plimou1->arr[i]);
    }

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

(2)柔性数组的另一种等价实现

柔性数组使得结构体的内存是可变的,当然,这种场景下,在C99之前还有一种解决方案

//code2
#include 
#include 
#include 
#include 
typedef struct limou
{
    int n;
    char c;
    int* arr;
}limou;
int main()
{
    limou* plimou = (limou*)malloc(sizeof(limou));
    if (plimou == NULL)
    {
        perror("malloc1");
        return 1;
    }
    int* ptr = (limou*)malloc(sizeof(int) * 10);
    if (ptr == NULL)
    {
        perror("malloc2");
        return 1;
    }
    else
    {
        plimou->arr = ptr;
    }

    //使用动态内存
    for (int i = 0; i < 10; i++)
    {
        plimou->arr[i] = i * i;
        printf("%d ", plimou->arr[i]);
    }

    //调整内存大小
    ptr = realloc(plimou->arr, 20 * sizeof(int));
    if (ptr == NULL)
    {
        perror("malloc3");
        return 1;
    }
    else
    {
        plimou->arr = ptr;
    }

    printf("\n");
    //使用调整后内存
    for (int i = 0; i < 20; i++)
    {
        plimou->arr[i] = i * i;
        printf("%d ", plimou->arr[i]);
    }

    //两次释放内存
    free(plimou->arr);
    plimou->arr = NULL;
    free(plimou);
    plimou = NULL;
    return 0;
}

(3)对比两种实现的优缺点

  • 出错概率:code2方法需要多次free,从次数上来讲,这种方法比较容易出错,code1柔性数组的实现更加简单
  • 使用频率:只可惜现在code1中的柔性数组的普及力还不够大,往往较为少用
  • 内存碎片:在code2中,多次malloc生成的空间大概率是不连续的,申请的越多,内存碎片就会增多,内存使用率就会降低,而code1就会提高内存使用效率
  • 访问速度:由于是code1中申请的内存是连续的,可以提高访问效率(不是特别明显)

(4)有关柔性数组拓展阅读

C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell

你可能感兴趣的:(C语言学习笔记,c语言,c++,开发语言)