十三.动态内存管理

目录

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

二.动态内存函数的介绍

1.malloc函数

2.free函数

3.calloc函数

4.reallco函数

三.常见的动态内存错误

1.对NULL空指针的解引用操作

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

3.对非动态开辟的内存使用free释放

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

5.对同一动态内存多次释放

6.动态开辟内存忘记释放导致内存泄露

四.柔性数组

1.柔性数组的定义及特点

2.柔性数组的使用

3.柔性数组的优势


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

目前我们已经掌握了以下两种开辟内存的方式:

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

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

  • 空间开辟的大小是固定的。
  • 数组在声明时必须指定数组的长度,在编译时会开辟并分配其所需要的内存空间。

动态内存分配的定义:

所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

十三.动态内存管理_第1张图片 

为什么会存在动态内存开辟?

  • 有时我们需要的空间大小在程序运行的时候才能知道,这时在数组编译时开辟空间的方式就不能满足了,这时我们就需要动态内存开辟来解决问题。

二.动态内存函数的介绍

1.malloc函数

头文件:stdlib.h

void *malloc(size_t size)

malloc 是C语言提供的一个动态内存开辟的函数,该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。具体情况如下:

  1. 如果开辟成功,则返回一个指向开辟好空间的指针。
  2. 如果开辟失败,则返回一个 NULL 指针。
  3. 返回值的类型为 void* ,malloc 函数并不知道开辟空间的类型,由使用者自己决定。
  4. 如果 size 为 0(开辟0个字节),malloc 的行为是标准未定义的,结果将取决于编译器。 

2.free函数

头文件:stdlib.h

void free(void *ptr)

free 函数用来释放动态开辟的内存空间。具体情况如下:

  1. 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的。
  2. 如果参数 ptr 是 NULL 指针,那么 free 将不会执行任何动作。
  3. 使用完之后一定要记得使用 free 函数释放所开辟的内存空间。
  4. 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为NULL空指针。

用法演示: 动态内存开辟10个整型空间

#include 
#include 
 
int main(void) 
{
    // 假设开辟10个整型空间
    int arr[10]; // 在栈区上开辟
 
    // 动态内存开辟
    int* p = (int*)malloc(10*sizeof(int)); // 开辟10个大小为int的空间
 
    // 使用时先判断是否开辟成功
    if (p == NULL) {
        perror("main"); // main: 错误信息
        return 0;
    }
    
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i;
    }
    for (i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
 
    // 回收空间
    free(p);
    p = NULL; // 需要手动置为空指针
 
    return 0;
}

运行结果:0 1 2 3 4 5 6 7 8 9 

那么问题来了,为什么 free 之后,一定要把 p 置为空指针?

因为 free 之后那块开辟的内存空间已经不在了,它的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,为了防止后续再对这块空间执行操作,我们需要及时使用 p = NULL 把他置成空指针。

3.calloc函数

头文件:stdlib.h

void *calloc(size_t num, size_t size)    

calloc 函数的功能实为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0,返回一个指向它的指针。 

malloc函数做对比:

  • malloc 只有一个参数,而 calloc 有两个参数,分别为元素的个数和元素的大小。
  • 与函数 malloc 的区别在于 calloc 会在返回地址前把申请的空间的每个字节初始化为 0 

用法演示:

#include 
#include 
 
int main()
{
    //malloc
    int* p1 = (int*)malloc(40);  //开辟40个空间
    //calloc
    int* p2 = (int*)calloc(10, sizeof(int));  //开辟10个大小为int的空间,40
   
    if (p1 == NULL)
        return 1;
    if (p2 == NULL)
        return 1;
   
    int i = 0;
    for (i = 0; i < 10; i++)
        printf("%d ", *(p1 + i));
    printf("\n");
    for (i = 0; i < 10; i++)
        printf("%d ", *(p2 + i));
   
    free(p1);
    p1 = NULL;
    free(p2);
    p2 = NULL;
    
    return 0;
}

 运行结果:10个随机值       0 0 0 0 0 0 0 0 0 0 

说明 calloc 会对内存进行初始化,把空间的每个字节初始化为 0 。如果我们对于申请的内存空间的内容,要求其初始化,我们就可以使用 calloc 函数来轻松实现。

4.reallco函数

头文件:stdlib.h

void *realloc(void *ptr, size_t size)

realloc 函数,让动态内存管理更加灵活。用于重新调整之前调用 malloccalloc 所分配的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。具体介绍如下: 

  1. ptr 为指针要调整的内存地址。
  2. size 为调整之后的新大小。
  3. 返回值为调整之后的内存起始位置,请求失败则返回空指针。
  4. realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc 函数在调整内存空间时存在的三种情况:

  1. 当原有空间之后没有足够大的空间时,直接在原有内存之后直接追加空间,原来空间的数组不发生变化。
  2. 当原有空间之后没有足够大的空间时,会在堆空间上另找一个合适大小的连续的空间来使用。函数的返回值将是一个新的内存地址。
  3. 如果找不到合适的空间,就会返回一个空指针。

用法演示:如果 realloc 找不到合适的空间,就会返回空指针,所以realloc在开辟空间时存在返回空指针的危险,我们不要拿指针直接接收 realloc,可以使用临时指针判断一下realloc是否为空

#include 
#include 
 
int main() 
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL) {
        perror("main");
        return 1;
    }
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i)  = 5;
    }
    
    //此时,这里需要 p 指向的空间更大,需要 20 个int的空间
    //realloc 调整空间
    int* ptmp = (int*)realloc(p, 20*sizeof(int));
    
    //如果ptmp不等于空指针,再把p交付给它
    if (ptmp != NULL) {
        p = ptmp;
    }
 
    //释放
    free(p);
    p = NULL;
}

三.常见的动态内存错误

1.对NULL空指针的解引用操作

错误代码演示:

#include 
#include 
 
int main()
{
    int* p = (int*)malloc(9999999999);
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }
 
    return 0;
}

解决方案:对 malloc 函数的返回值做判空处理

#include 
#include 
 
int main()
{
    int* p = (int*)malloc(9999999999);
    // 对malloc函数的返回值做判空处理
    if (p == NULL) {
        perror("main");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }
 
    return 0;
}

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

错误代码演示:

#include 
#include 
 
int main()
{
    int* p = (int*)malloc(10*sizeof(int));  //申请10个整型的空间
    if (p == NULL) {
        perror("main");
        return 1;
    }
    int i = 0;
    
    //越界访问 - 指针p只管理10个整型的空间,根本无法访问40个
    for (i = 0; i < 40; i++) {
        *(p + i) = i;
    }
 
    free(p);
    p = NULL;
 
    return 0;
}

为了防止越界访问,使用空间时一定要注意开辟的空间大小。 

3.对非动态开辟的内存使用free释放

错误代码演示:

#include 
#include 
 
int main()
{
    int arr[10] = {0};  //在栈区上开辟
    int* p = arr;
 
    free(p);  //使用free释放非动态开辟的空间
    p = NULL;
 
    return 0;   
}

运行结果:

十三.动态内存管理_第2张图片 

所以,不要对非动态开辟的内存使用 free,否则会出现难以意料的错误。

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

错误代码演示:

#include 
#include 
 
int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 5; i++) {
        *p++ = i;  //p指向的空间被改变了
    }
 
    free(p);
    p = NULL;
  
    return 0;
}

p++ 会使p指向p后面的空间,原本开辟给p的动态内存的起始地址发生改变,free时会导致 p 只释放了后面的空间,存在内存泄漏问题。释放内存空间的时候一定要从头开始释放。

5.对同一动态内存多次释放

错误代码演示:

#include 
#include 
 
int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        p[i] = i;
    }
 
    //释放
    free(p);
    //再释放
    free(p);
  
    return 0;
}
 

解决方案:在第一次释放后紧接着将 p 置为空指针

// 释放
free(p);
p = NULL;
 
free(p); // 此时p为空,free什么也不做

6.动态开辟内存忘记释放导致内存泄露

错误代码演示:

#include 
#include 
 
void test()
{
    int* p = (int*)malloc(100);
    if (p == NULL) {
        return;
    }

    // 此时忘记释放了
}
 
int main()
{
    test();
    
}

动态开辟的内存空间有两种回收方式:  1. 主动释放(free)      2. 程序结束 

malloc 这一系列函数 和 free 一定要成对使用,记得及时释放。

四.柔性数组

1.柔性数组的定义及特点

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

例如:

struct S {
    int n;
    int arr[0];   //柔性数组成员
};

部分编译器可能会报错,可以试着将 a [ 0 ] 改为 a [ ] : 

struct S {
    int n;
    int arr[];   //柔性数组成员
};

特点:

  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

代码演示:

#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 

struct S {
    int n;
    int arr[0];  //前面至少有一个成员
};

int main() {

    printf("%d", sizeof(struct S));   //大小为4,不包含柔性数组
    //后面+的大小就是给柔性数组准备的
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));

    return 0;
}

2.柔性数组的使用

用法演示:

#include 
#include 
 
struct S {
    int n;
    int arr[0];
};
 
int main() {
    //期望arr的大小是10个整型
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
    ps->n = 10;
 
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    //增容
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if (ptr != NULL) {
        ps = ptr;
    }
 
    // 再次使用
    ………………
    …………
    ……

    // 释放
    free(ps);
    ps = NULL;
 
    return 0;
}

3.柔性数组的优势

对比下面两片代码:

1.使用柔性数组:

#include 
#include 
 
struct S {
    int n;
    int arr[0];
};
 
int main() {
    //期望arr的大小是10个整型
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
    ps->n = 10;
 
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    //增容
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if (ptr != NULL) {
        ps = ptr;
    }
 
    // 再次使用
    ………………
    …………
    ……

    // 释放
    free(ps);
    ps = NULL;
 
    return 0;
}

2.使用指针

#include 
#include 
 
struct S {
    int n;
    int* arr;
};
 
int main() {
    struct S* ps = (struct S*)malloc(sizeof(struct S));
    if (ps == NULL)
        return 1;
    ps->n = 10;
    ps->arr = (int*)malloc(10 * sizeof(int));
    if (ps->arr == NULL)
        return 1;
 
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    // 增容
    int* ptr = (struct S*)realloc(ps->arr, 20 * sizeof(int));
    if (ptr != NULL) {
        ps->arr = ptr;
    }
 
    // 再次使用 
    ………………
    …………
    ……
 
    // 释放
    free(ps->arr); // 先free第二块空间
    ps->arr = NULL;
    free(ps);
    ps = NULL;
 
    return 0;
}

 上述 代码1 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

(1)第一个好处:有利于内存释放

代码1只需要释放一次内存,代码2需要释放两次,防止了内存释放不干净的疏忽

(2) 第二个好处:有利于访问速度

连续内存多多少少有益于提高访问速度,还能减少内存碎片。

malloc 的次数越多,产生的内存碎片就越多,这些内存碎片不大不小,再次被利用的可能性很低。内存碎片越多,内存的利用率就会降低,频繁的开辟空间效率会变低,碎片也会增加。

 


本篇到此结束,码文不易,还请多多支持哦!

你可能感兴趣的:(c语言学习,算法,c语言)