15 C语言进阶动态内存管理

动态内存管理

  • 动态内存分配的意义

  • 动态内存函数介绍

    • malloc
    • free
    • calloc
    • realloc
  • 常见的动态内存错误

  • 几个经典的笔试题

  • 柔性数组


动态内存分配的意义

我们已经指到的内存开辟的方式:

//开辟四个字节的空间
int a = 5;
//开辟连续的一个字节的十个空间
char arr[10] = {0};

但是这两种方式开辟的空间有两个特点:

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

但是对空间的需求,不仅仅是上述的情况,有的时候我们需要多少的空间是不能直接知道的,需要程序运行开始,或者是用户使用的时才能知道,这时数组在编译时开辟空间的方式就不能满足了。

这时候就可以试试动态内存开辟了。

存储空间的使用

image-20220214095959271

动态内存函数介绍

malloc和free

C语言提供的开辟内存的函数:malloc

void* malloc(size_t size);//malloc函数的声明

函数作用:

这个函数向内存申请一块连续可用的空间,并返回这块空间的指针。

1. 如果开辟成功,返回一个指向开辟好空间的指针。
2. 如果开辟失败,则返回一个NULL指针,所以使用malloc,要对返回值进行检查。
3. 返回值类型是void\* ,即malloc函数不知道开辟空间的类型,在具体使用的时候,强制转化一下指针类型。
4. 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。

开辟空间后不能让他一直存在吧,这样内存就一直存放着。这时我们还有一个free函数用来释放或回收动态开辟的内存。

void free(void* ptr);

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

1. 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是没有定义的。
2. 如果参数ptr是NULL指针,则函数什么事都不做。

malloc和free都声明在头文件stdlib.h中。

函数使用实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(){
    //向内存申请10个整型的空间
    int* p = (int *) malloc(10* sizeof(int ));//改成INT_MAX测试就可以执行if部分
    if (p == NULL){
        //打印错误的原因
        printf("%s\n", strerror(errno));
    }
    else{
        //正常使用空间
        int i = 0;
        for (i = 0; i < 10; ++i) {
            *(p+i) = i;
        }
        for (i = 0; i < 10; ++i) {
            printf("%d",*(p+i));
        }
    }
    //当动态申请的空间不再使用的时候就应该还给操作系统。
    //回收空间
    free(p);
    //free掉之后仍然p仍然指向那个内存地址。
    p = NULL;//这样更加安全。
    return 0;
}

calloc

C语言除了malloc还提供了一个函数叫calloc,calloc函数也用来动态内存分配。

void* calloc(size_t num,size_ size);
  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。

具体使用同malloc和free的使用方式。

int main() {
    int *p = (int *) calloc(10, sizeof(int));
    if (NULL != p) {
        //使用空间
    }else{
        //打印错误信息
        printf("%s\n",strerror(errno));
    }
    free(p);
    p = NULL;
    return 0;
}

realloc

realloc函数让动态内存管理更加灵活。

有时我们觉得内存申请小了或者大了,就可以使用realloc函数进行调整。

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

参数解释:

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

realloc使用注意事项:

  1. 如果p指向的空间后面有足够的空间可以追加,则直接追加,返回p
  2. 如果p指向的空间后面没有足够的内存空间可以追加,则realloc函数会重新找一个新的区域开辟一块满足需求的空间,并且把原来内存中的数据拷贝回来,释放旧的空间,最后返回新开辟的内存空间地址。
  3. 追加失败,会返回空指针。–不能直接赋值到原来的指针上,防止数据丢失。

内存中情况:

image-20220214160357780

int main() {
    int *p = (int *) malloc(20);
    if (p == NULL) {
        printf("%s\n", strerror(errno));
    } else {
        int i = 0;
        for (i = 0; i < 5; i++) {
            *(p + i) = i;
        }
    }
    int *ptr = realloc(p, 4000);
    if (ptr != NULL) {
        int i = 0;
        for (i = 5; i < 10; i++) {
            *(ptr + i) = i;
        }
        for (i = 0; i < 10; i++) {
            printf("%d\n", *(p + i));
        }
    }
    //释放空间 
    //-- 如果出现不够用的情况,另起空间ptr原来的空间在realloc的时候就被释放了
    free(ptr);
    ptr = NULL;
    return 0;
}

常见的动态内存错误

对NULL指针的解引用操作

访问空指针,就是非法访问。

在动态内存分配之后一定要进行返回值的判断。

int main(){
    int *p = (int*)malloc(40);
    //万一malloc失败了,p就被赋值为NULL
    *p = 0;
    free(p);
    return 0;
}

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

程序崩溃

int main() {
    int a = 10;
    int *p = &a;
    free(p);//可以吗?
}

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

程序崩溃

int main() {
    int i = 0;
    int *p = (int *) malloc(10 * sizeof(int));
    if (NULL == p) {
        exit(EXIT_FAILURE);
    }
    for (i = 0; i <= 10; i++) {
        *(p + i) = i;//当i是10的时候越界访问
    }
    free(p);
}

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

释放的时候必须从申请空间的起始位置开始释放。

程序崩溃。

所以一般不对这个起始地址的指针p进行修改性质的运算。

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

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

int main() {
    int *p = (int *) malloc(100);
    free(p);
    free(p);//重复释放
}

避免方式:free完之后,将p置空。

int main() {
    int *p = (int *) malloc(100);
    free(p);
    p = NULL;
    free(p);//无效释放,程序不会崩溃。
}

动态开辟内存后没有释放(内存泄漏)

int main() {
    while (1){
        malloc(1);
    }
}

下面的这种情况就是,出了test函数想释放都没法释放了。

void test() {
    int *p = (int *) malloc(100);
    if (NULL != p) {
        *p = 20;
    }
}

int main() {
    test();
    while (1);
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

有些语言就有自己的内存回收机制。

申请动态内存或者使用动态内存时出现的错误,大都会导致程序直接崩溃。

free之后,记得将指针置空。

几个经典的笔试题

题目一

测试函数的执行结果是什么。

void GetMemory(char *p) {
    p = (char *) malloc(100);
}

void Test(void) {
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
  1. 运行代码程序会崩溃
  2. 程序存在内存泄漏的问题

str以值传递的形式给了p。p是GetMemory函数的形参,只能函数内部有效,等GetMemory函数返回之后,动态开辟内存尚未释放并且无法找到,所以会内存泄漏。

void GetMemory(char *p) {
    p = (char *) malloc(100);//2. 内存未释放
}

void Test(void) {
    char *str = NULL;
    GetMemory(str);//1. 这里传的是str的地址,并不是*str的地址。
    strcpy(str, "hello world");//3. 程序是在这里崩溃的,非法访问内存
    printf(str);//这个printf没有问题
}

int main(){
    Test();
    return 0;
}

提示:str是一个变量,向函数传变量,在函数运行的时候是变量的临时拷贝,要想对变量操作必须传地址。但是由于str本身就是一个地址,那在函数GetMemory中必须使用二级指针来接受。

这样是可以运行的。不至于崩溃,但是仍然存在内存泄漏的问题。

image-20220214181342732

修改方式2

image-20220214184854015

题目二

下面的测试函数运行结果是什么

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

void Test(void) {
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
int main() {
    Test();
    return 0;
}

程序可能会崩溃,可能是随机值。(补充:局部变量在栈区存储。)

产生原因:在GetMemory中创建的变量p在结束函数后就被销毁了,之后由函数返回的内存地址发生了什么是不知道的(有点非法访问的意思)。

解决方式:一、因为函数中变量在函数结束后被销毁,那么延长该变量的生命周期就可以了,也就是在函数中定义变量的时候用static修饰一下。(将变量存在静态区)

二、使用动态内存分配进行定义空间,然后不要在函数中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);
}
int main() {
    Test();
    return 0;
}

程序可以运行出结果,但是程序的问题是内存泄漏,在使用完str后没有将空间释放掉。

修改错误:

void Test(void) {
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf("%s", str);
    //代码修改
    free(str);
    str = NULL;
}

题目四

下面的测试函数的运行结果是什么?

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

int main() {
    Test();
    return 0;
}

可以运行出预想的结果,但是代码仍然存在问题。free之后继续使用之前开辟的空间的地址,就是非法访问。

注:free释放空间后并不会将内容置空。

修改:

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

int main() {
    Test();
    return 0;
}

C程序的内存开辟

image-20220214224432183

(图片来自于视频截图)

内存分配区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

通过这个图,我们就可以更好的理解static关键字修饰局部变量的例子了。

实际上一般的局部变量在栈区分配空间,栈区的特点是在上面创建的变量出了作用域就销毁了。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在这里创建的变量,直到程序结束才销毁。

所以生命周期变长。

柔型数组

柔型数组:在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做“柔性数组”成员。

举例说明:

// struct S{
//     int n;
//     int arr[0];
// };
struct S{
    int n;
    int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
    struct S s;
}

柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员
  2. sizeof返回的这种结构大小是不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔型数组的预期的大小。
struct S{
    int n;
    int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
    struct S s;
    printf("%d\n", sizeof(s));//结果是4
}

柔性数组的使用

struct S{
    int n;
    int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
    //创建空间
    struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int));
    ps->n=100;
    int i = 0;
    for (i = 0;i<5;i++){
        ps->arr[i] = i;
    }
    //空间不够了继续开辟
    struct S* ps2 = realloc(ps,sizeof(struct S)+10*sizeof(int));
    if(ps2!=NULL){
        ps = ps2;
    }
    for (i = 5; i < 10; ++i) {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; ++i) {
        printf("%d ",ps->arr[i]);
    }
    free(ps);
    ps = NULL;
    return 0;
}

柔性数组的优势

实现上面同样的功能但是不使用柔性数组

struct S {
    int n;
    int *arr;//这个指针指向一个动态开辟的空间存放数组
};

int main() {
    //申请空间,申请到的是一个int和一个指针的空间
    struct S *ps = (struct S *) malloc(sizeof(struct S));
    ps->arr = malloc(5 * sizeof(int));
    int i = 0;
    for (i = 0; i < 5; i++) {
        ps->arr[i] = i;
    }
    for (i = 0; i < 5; ++i) {
        printf("%d ", ps->arr[i]);
    }
    // 调整大小
    int *ps2 = realloc(ps->arr, 10 * sizeof(int));
    if (ps2 != NULL) {
        ps->arr = ps2;
    }
    for (i = 5; i < 10; ++i) {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; ++i) {
        printf("%d ", ps->arr[i]);
    }
    //释放空间  z
    free(ps->arr);
    ps->arr = NULL;
    free(ps);
    ps = NULL;
    return 0;
}

上面的代码我认为可以将ps创建为结构体变量,ps.arr创建为动态申请空间的数组变量。

小笔记:指针变量分配动态空间后,其指向的变量可以直接当数组使用。

对比两种实现方式

image-20220215112216243

第一种的好处:方便内存的释放

  1. 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

  2. 连续的内存有益于提高访问的速度,也有益于减少内存碎片。

第二种的好处:有利于访问速度

​ 连续的内存有益于提高访问的速度,也有益于减少内存碎片。

小结

柔性数组是结构体中的最后一个元素。(首先有结构体,然后最后一个0长度的数组),并且这个结构体至少有两个成员。

在结构体中柔性数组成员是不指定数组大小的。

使用柔性数组的目的:

  1. 想操控一块连续的空间,被当成数组使用。
  2. 内存释放的时候只需要释放一次。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黎丶辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值