【C语言】动态内存分配

C语言动态内存分配

0. 目录

1. 动态内存分配的意义

首先请问下面这段代码有什么缺点吗?

int a = 4;
char arr[10] = { 0 };

​ 上述代码中,我们定义一个占用4字节空间的整型变量a和一个占用10字节连续内存空间的字符数组arr,这种定义方式我们称之为静态分配,区别于动态分配,静态定义变量的方式在程序编译阶段就决定了变量占用空间的大小,但是我们经常在实际开发中遇到下面这种情况。

​ 需求:我们现在需要设计一个学生管理系统,但是学生人数暂时未知,并且会随着录入学生人数的增加而不断扩充容量,那么我们使用C语言编写这样一个学生管理系统时,采用结构体数组存储数据,数组的每一个元素都是一个结构体用于存放一个学生的信息,如何解决动态扩容的问题呢?换句话说只有在程序运行的过程中我们才可以确定数组开辟的空间大小。这个时候我们就需要用到C语言的动态内存分配

2. 动态内存分配函数介绍

2.1 malloc与free函数

C语言提供了一个用于动态开辟内存空间的函数:malloc

网站链接:https://legacy.cplusplus.com/reference/cstdlib/malloc/?kw=malloc

语法格式:void* malloc(size_t size)

函数作用:在内存中动态开辟size字节的连续空间,并且返回指向该空间的指针

函数特点:

  • 如果函数开辟成功,就返回指向这段内存空间的指针
  • 如果函数开辟失败,返回NULL指针,因此在使用malloc函数一定要做好返回值检查
  • 函数返回值类型为void*,函数使用时更加灵活,可以分配给不同类型的指针变量,但是一定要进行强制类型转换后才可以使用
  • 如果size的值设置为0,该种行为C语言并无明确规定如何处理,由各自实现的编译器决定

C语言为动态内存分配管理还提供了一个释放内存函数:free

网站链接:https://legacy.cplusplus.com/reference/cstdlib/free/?kw=free

语法格式:void free(void* ptr)

函数作用:释放ptr指针指向的内存空间

函数特点:

  • 如果free函数的参数不是动态分配的,C语言无明确规定如何处理,由各自编译器决定
  • 如果free函数的参数是NULL空指针,那么编译器不进行任何处理

注意:动态分配的空间使用完后一定要手动调用free函数释放,并将ptr指针置为空指针,这是一个良好习惯,否则容易造成内存泄漏等危害!

下面将对malloc与free函数的基本使用进行举例

int main() {
    int* arr = (int*)malloc(sizeof(int) * 10);
    if (arr == NULL) {
        perror("malloc");
        return -1;
    }
    // 使用
    for (int i = 0; i < 10; ++i) {
        arr[i] = i;
    }
    for (int i = 0; i < 10; ++i) {
        printf("%d ", arr[i]);
    }
    // 使用完后记得手动释放并置空指针
   	free(arr);
    arr = NULL;
    return 0;
}

​ 代码分析:在上述代码中,我们通过int* arr = (int*)malloc(sizeof(int) * 10)在内存中动态开辟10个整型即40字节的空间,并强制类型转换为int*赋值给一个整型指针变量arr,并且对arr类型进行检查,如果为NULL说明申请空间失败!若申请成功我们进行简单的赋值打印数组各个元素,最后切记使用完要手动调用free函数释放动态分配的内存空间,并且将指针置为空指针。

2.2 calloc函数

C语言还提供了另一个动态内存分配函数:calloc

网站链接:https://legacy.cplusplus.com/reference/cstdlib/calloc/?kw=calloc

语法格式:void* calloc(size_t num, size_t size)

函数作用:在内存中动态开辟num个大小为size字节的连续空间,并返回指向该段内存空间的指针

函数特点:

  • calloc函数与malloc函数不同,calloc函数会初始化该段内存空间的值全为0
  • 使用calloc函数进行动态分配的空间也需要使用free函数进行手动释放内存空间

下面将使用calloc函数进行举例说明

int main() {
    int* arr = (int*)calloc(10, sizeof(int));
    // 返回值检查
    if (arr == NULL) {
        perror("calloc");
        return -1;
    }
    // 使用
    for (int i = 0; i < 10; ++i) {
        printf("%d ", arr[0]);
    }
    // 释放内存
    free(arr);
    arr = NULL;
    return 0;
}

在这里插入图片描述

​ 代码分析:程序执行结果如果所示,变向证明了calloc函数除了开辟内存空间之外还会初始化内容为0,calloc函数使用时也有可能申请空间失败!因此一定要对返回值进行检查,最后使用完后记得调用free函数进行内存空间的释放并将指针置为空指针,保持良好的编码习惯!

2.3 realloc函数

除了malloc与calloc函数以外,C语言还提供了另外一个动态内存分配函数:realloc

网站链接:https://legacy.cplusplus.com/reference/cstdlib/realloc/?kw=realloc

语法格式:void* realloc(void* ptr, size_t size)

函数作用:对ptr指针指向的内存空间进行重新分配调整大小为size,返回指向新内存空间大小为size的指针

函数特点:

  • realloc函数可以灵活调整动态分配的内存大小

  • 参数中ptr指针为想要调整的内存地址,指向空间一定为动态内存分配的指针(由malloc、calloc、realloc等函数返回)

  • 参数中size为调整之后的新内存空间大小

  • 这个函数会自动将原ptr指针指向内存空间的数据完整拷贝到新内存空间中

  • realloc函数的返回值可以分为以下两种情况

    1. 当原内存空间仍有剩余位置可以扩容时,返回原空间内存地址
    2. 当原内存空间无法满足新空间大小时,就再开辟另外一块内存空间,并将原内存空间中的数据拷贝到新内存空间中并返回指向新空间的指针

在这里插入图片描述

下面对realloc函数进行举例说明

int main() {
    int* arr = (int*)malloc(sizeof(int) * 10);
    if (arr == NULL) {
        perror("malloc");
        return -1;
    }
    for (int i = 0; i < 10; ++i) {
        arr[i] = i;
    }
    // 对arr空间进行扩容
    int* tmp = (int*)realloc(arr, sizeof(int) * 20);
    if (tmp == NULL) {
        perror("realloc");
        return -1;
    }
    for (int i = 10; i < 20; ++i) {
        arr[i] = i;
    }
    for (int i = 0; i < 20; ++i) {
        printf("%d ", arr[i]);
    }
    // 释放内存空间
    free(arr);
    arr = NULL;
    return 0;
}

​ 代码分析:上述代码中,我们先使用malloc函数为arr分配40个字节的大小,但是后续我们需要对内存空间进行动态扩容,我们就是用realloc函数重新分配80个字节大小的内存空间,realloc函数将原内存空间的数据进行拷贝然后返回新内存空间地址,需要注意我们在使用realloc函数接收返回值时首先用tmp变量进行接收,这是因为如果我们直接用arr进行接收,若重新分配内存大小失败就会导致arr置为NULL,但是原先内存空间丢失无法访问,也会导致内存泄漏等问题,因此我们需要先用临时变量tmp进行接收。最后,realloc函数开辟的内存空间也是需要手动调用free函数进行释放的,并且指针也需要置为NULL。

3. 常见的动态内存错误

3. 1 对空指针进行解引用

// 对NULL解引用
void test1() {
    int* p = (int*)malloc(INT_MAX / 4);
    *p = 20;
    free(p);
    p = NULL;
}

​ 代码分析:上述代码中使用malloc函数申请INT_MAX / 4个字节的内存空间,但是极有可能申请失败!但是并未对返回值做检查当p为NULL时*p = 20;对空指针进行解引用操作就会导致程序报错!因此要养成良好的习惯,每次动态开辟内存都要对返回值进行检查后再使用。

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

// 越界访问错误
void test2 () {
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL) {
        perror("malloc");
        return -1;
    }
    for (int i = 0; i <= 10; ++i) {
        p[i] = i; // 当i为10越界访问
    }
    free(p);
    p = NULL;
}

​ 代码分析:上述代码中,我们开辟了10个int类型即40字节的连续内存空间,并将起始地址返回赋值给指针变量p,for循环执行11次当i == 10时进行循环但是p[i] = i;已经超出40字节内存空间的范围,这也是一种错误的使用方法!

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

// 错误使用free
void test3() {
    int a = 10;
    int* p = &a;
    free(p);
    p = NULL;
}

​ 代码分析:上述代码中我们定义一个整型a,将它的地址赋值给一个指针变量p,此时我们使用free函数释放内存空间并置空,但是事实上free函数只针对采用动态开辟的内存空间(使用malloc、calloc、realloc申请的内存空间),若对非动态开辟的内存空间是由free,这种使用方式是错误的!

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

// 使用free释放一部分
void test4() {
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL) {
        perror("malloc");
        return -1;
    }
    for (int i = 0; i < 5; ++i) {
        *p = i;
        p++;
    }
    free(p); // 此时p已经不指向原先位置
    p = NULL;
}

​ 代码分析:上述代码中,我们开辟了10个int类型即40字节的连续内存空间,并将起始地址返回赋值给指针变量p,在for循环中我们使用语句p++,这会导致指针变量p不指向原先内存空间位置,此时再使用free函数释放局部内存空间就会报错。

3.5 使用free函数连续释放同一块内存空间

// 连续释放同一块内存空间
void test5() {
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL) {
        perror("malloc");
        return -1;
    }
    free(p);
    free(p);
    p = NULL;
}

​ 代码分析:上述代码中我们已经释放p所指向的内存空间后如果再使用free函数连续释放同一块内存空间就会导致程序报错,这是一种错误行为!

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

// 内存泄漏
void test6() {
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL) {
        perror("malloc");
        return;
    }
  	// 忘记free
}

int main() {
    test6();
    while (1);
    return 0;
}

​ 代码分析:上述代码中,我们进入main函数首先调用test6函数,在test6函数中我们动态开辟内存空间大小为40字节并返回起始地址赋值给指针变量p,函数调用结束由于p为局部变量开辟在栈上所以p会被销毁,但是动态开辟的空间申请在堆上,所以p所指向的空间不会被释放回收!但是我们已经无法访问到那部分内存空间了,此时主函数执行while(1)死循环,程序一直运行,就会导致内存泄漏!

下面为内存泄漏示意图:

在这里插入图片描述

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函数会有什么样的结果?

题目分析:首先调用Test函数,创建变量str初始化为NULL,然后调用GetMemory函数并传递str作为参数,在GetMemory函数中首先创建局部变量p,并值拷贝str的值为NULL,然后在堆上申请100字节内存空间并将起始地址返回给指针变量p,然后GetMemory函数调用结束,局部变量p销毁。注意p和str的值并不一样,str仍指向NULL空指针,调用strcpy函数会对NULL进行解引用,程序报错!

这段代码存在两个问题:

  1. GetMemory函数中没有释放申请的资源,会存在内存泄漏隐患!
  2. Test函数中调用strcpy()函数,内部会对NULL指针进行解引用,程序报错!

在这里插入图片描述

面试题2:

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

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

题目分析:首先进入Test函数创建局部变量str并初始化为NULL,然后调用GetMemory函数创建局部变量字符数组arr并初始化为[hello world\0],然后将p的地址返回赋值给str,最后打印str,理论上这段代码最终能够打印出“hello world”,但是实际上在GetMemory函数中"hello world"用于初始化字符数组,本质上"hello world"字符数组开辟在栈上,当GetMemory函数调用结束,空间全部归还操作系统,哪怕最后返回该段空间的地址使用printf函数访问也是随机值!

这段代码存在的问题可以用一个成语概括:“物是人非”

面试题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);
}

题目分析:这段代码是与面试题1进行对比的,这段代码在Test函数中调用GetMemory传参时传递的是二级指针,所以在GetMemory函数中动态开辟内存空间num字节返回起始地址赋值给*p,此时str变量也指向动态开辟的内存空间,所以strcpy函数能够正常调用,最后成功打印"hello",这段代码唯一错误的地方就是没有进行内存释放,会发生内存泄漏的问题!

面试题4:

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

题目分析:本段代码在堆中开辟100字节的内存空间并将起始地址返回给str,调用strcpy函数修改内存空间中值为"hello",然后就调用free释放了内存空间,但是并没有给str指针置为NULL,随后进入if条件语句再次调用strcpy这时str为野指针,访问会报错!这段代码出错的直接原因是访问野指针,但是根本原因是没有养成free释放内存空间之后就将指针立即置为空指针的良好习惯!

5. 柔性数组

5.1 柔性数组概念

柔性数组:在C99当中,如果结构的最后一个成员是大小未确定的数组,该成员就被称为柔型数组(flexible array)

例如:

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

有时候编译器版本不同,上面定义方式若报错则换成一下定义方式

struct A {
   int a;
   int arr[]; // 柔性数组成员
};

5.2 柔性数组特点

  • 结构内部柔性数组成员之前至少有一个成员

  • sizeof(结构体)返回的值不包括柔性数组的大小

  • 包含柔性数组成员的结构在使用malloc函数动态开辟空间时应该预留柔性数组的空间

    柔性数组的使用方式如下

    // 代码一
    struct S {
        int i;
        int arr[0];
    };
    
    int main() {
        struct S s = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
        if (s == NULL) {
            perror("malloc");
            return -1;
        }
        for (int i = 0; i < 10; ++i) {
            s.arr[i] = i;
        }
        // 释放内存
        free(s);
        s = NULL;
        return 0;
    }
    

    上述代码中,我们为结构体类型变量s动态申请内存空间,并且预留柔性数组大小为40字节

5.3 柔性数组的优势

上述结构体也可以设计成以下形式

// 代码二
struct S {
  	int i;
    int* arr;
};
int main() {
    struct S s = (struct S*)malloc(sizeof(struct S));
    if (s == NULL) {
        perror("malloc struct s");
        return -1;
    }
    s->arr = (int*)malloc(sizeof(int) * 10);
    if (s->arr == NULL) {
        perror("malloc int* arr");
        return -1;
    }
    for (int i = 0; i < 10; ++i) {
        s->arr[i] = i;
    }
    // 释放内存
    free(s->arr);
    s->arr = NULL;
    free(s);
    s = NULL;
    return 0;
}

上述代码1与代码2能完成相同的功能,但是使用柔性数组(代码1)有两个好处:

  1. 方便内存释放

    使用柔性数组,我们只需要给结构体分配内存空间并预留柔性数组的大小,这种情况下我们释放内存只需要释放一次,而如果使用第二种方式,我们需要先为结构体内部整型指针分配内存空间,然后再为整个结构体进行内存分配,才能达到与代码一相同的效果。无疑使用柔性数组更加安全、高效、便捷。

  2. 方便减少内存碎片

    使用柔性数组的方式,使内存空间连续,有利于提高内部访存速度,减少内存碎片。

6. 总结

C语言的动态内存分配方式相较于静态分配更加灵活,能够适应随程序运行时才确定内存大小的需求,动态内存分配的空间均开辟在堆上,而静态定义申请的空间开辟在栈上。故使用动态内存分配的空间一定要手动调用free函数进行回收和释放!以防造成内存泄漏的危害。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值