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

简介: 本章主要介绍C语言动态内存管理,主要内容为:malloc、calloc、realloc三个动态内存函数的使用。动态内存管理的核心也就是这三个函数的使用。以及柔性数组。

本章主要介绍C语言动态内存管理,主要内容为:malloc、calloc、realloc三个动态内存函数的使用。动态内存管理的核心也就是这三个函数的使用。以及柔性数组。

1、为什么存在动态内存分配?

我们已经掌握的内存开辟方式有:

int a = 20;        //在栈空间上开辟四个字节。
char arr[20];      //在栈空间上开辟10个字节的连续空间。

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

  • 开辟空间大小是固定的
  • 数组在申请的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小在程序运行的时候才能知道,这个时候就只能试试动态内存开辟了。

2、动态内存函数的介绍

2.1、malloc(申请内存空间)和free(释放/回收内存空间)

1、C语言提供了一个动态内存开辟的函数------malloc:

<stdlib.h>      
void* malloc (size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针(返回这块空间的起始地址)。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数size的单位字节为0,malloc的行为是标准是未定义的,取决于编译器。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
    int arr[10] = { 0 };
    //动态内存开辟
    int* p = (int*)malloc(40);
    int i = 0;
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }
    for (i = 0; i < 10; i++)
    {
        printf("%d ",* (p + i));
    }
    return 0;
    //没有free,并不是说内存空间就不回收了,当程序退出的时候,系统会自动回收内存空间。
}

输出:

image.png

使用数组申请的内存空间和使用malloc申请的空间在不同的区域上:

image.png

2、free------释放/回收内存空间。

void free(void* ptr);
  • ptr为NULL,则什么事都不做。

  • ptr必须是动态分配的空间。

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

int main()
{
    int arr[10] = { 0 };
    //动态内存开辟
    int* p = (int*)malloc(INT_MAX);
    int i = 0;
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    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;
}

【注:】free释放的必须是动态内存的空间,也就是说释放的需要是在堆区中的空间,而不应该去释放栈区中的空间。

如下是错误的:

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

int main()
{
    int a = 0; 
    int* p = &a;         //p是栈区里面的空间,不用free来释放
    free(p);
    p = NULL;
    return 0;
}

2.2、calloc

C语言也提供了一个函数叫calloccalloc函数也用来动态内存分配,原型如下:

void* calloc(size_t num,size_t size);
  • num是代表要开辟多少个元素
  • size代表开辟的每个元素是多少字节。

比如:想要开辟40字节的内存,num=10,size=4即可。

  • 返回值是开辟的那块空间的起始地址。

  • 这个函数还有一个特殊的地方:它在返回之前会把将要开辟的内存空间初始化一下,并初始化为全0。

代码验证:在使用calloc开辟好空间之后,我们来打印,看是不是全部初始化为0。

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

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    int i = 0;
    for (i=0; i < 10; i++)
    {
        printf("%d ", *(p + i));
    }
    free(p);
    p = NULL;
    return 0;
}

输出:

image.png

malloc和calloc如何选择呢?

如果想要初始化使用calloc,如果不初始化,两个都可以。

calloc相当于malloc+memset。

2.3、realloc

  • realloc函数的出现让动态内存管理更加灵活
  • 有时我们会发现去申请的空间太小,有时候我们又觉的申请的空间过大,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整,那realloc函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc(void* ptr,size_t size);
  • ptr是要调整的内存地址。
  • size是调整后的内存大小。希望要调整为多大的空间。
  • 返回值为调整之后的内存起始位置。
  • 这个函数在调整原内存空间大小的基础上,还会将原内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间。
    • 情况2:原有空间之后没有足够大的空间。

下面先来说下realloc的两种情况:

比如现在有个使用malloc分配的动态内存,大小为40字节。然后现在想要扩容到80字节。

已存在40个字节了,需要扩容为80字节,所以还需要在原有的内存上在使用realloc追加40个字节。

那主要问题就在于这新追加的40字节的内存位置在那。

1、原有空间之后有足够大的空间:

这种情况是直接追加在原有40字节的后面:这个实现很简单就是直接追加就行了。

image.png

2、原有空间之后没有足够大的空间:

这个就是如果在原有的40字节的后面直接在追加40个字节的内存后,由于原有空间之后没有足够大的空间,强行追加40个字节,会占用其它数据的内存地址。所以这样肯定是不行的。那如何解决呢?
答案:realloc会找到一个80字节大小的内存空间,然后先把原有的(使用malloc)动态分配的40字节移动到这个80个字节的前40个字节处,然后还剩40个字节,这个算是扩容后的内存地址。

并且,旧的原40个字节内存,会被realloc自动释放回收。

image.png

代码示例:

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

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    int i = 0;
    for (i=0; i < 10; i++)
    {
        *(p + i) = i+1;
    }
    //将p处的内存地址,扩容到80字节
    int* ptr = realloc(p, 80);
    if (ptr != NULL)
        p = ptr;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", *(p + i));
    }
    free(p);
    p = NULL;
    return 0;
}

输出:

image.png

2.4、realloc充当malloc

realloc(NULL,40);    ==========       malloc(40);

3、常见的动态内存错误

3.1、对NULL指针的解引用操作

//不进行NULL的判断,这样是存在安全隐患的。
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = (int*)malloc(40);
    *p = 20;
    return 0;
}


//对指针进行NULL的判断
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    *p = 20;
    free(p);
    p = NULL;
    return 0;
}

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

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

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    int i = 0;
    //当i=10时,就越界访问了。
    for (i = 0; i <= 10; i++)
    {
        p[i] = i;
    }
    free(p);
    p = NULL;
    return 0;
}

3.3、对非动态开辟内存使用free释放

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

int main()
{
    int a = 10;
    //p是非动态开辟内存,是不能用free释放的。
    int* p = &a;
    free(p);
    p = NULL;
    return 0;
}

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

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

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *p = i;
        p++;             //p在++之后,p已经不在是起始位置了,所以下面free释放只是释放了一部分,所以不对。
    }
    free(p);
    p = NULL;
    return 0;
}

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

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

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 1;
    }
    //多次释放,会报错
    free(p);
    free(p);
    return 0;
}

//改进:要么free一次,要么添加p = NULL;

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

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

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 1;
    }
    return 0;
}

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

切记:动态开辟的空间一定要释放,并且正确释放。

4、几个经典的笔试题

4.1、题目1:野指针---返回栈区空间地址问题

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

void GetMemory(char* p)
{
    //p是形参,在栈区里面存放
    p = (char*)malloc(100);
}

void Test(void)
{
    char* str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}

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

问:运行结果?

传值调用,str是实参,p是形参,所以说GetMemory运行后,对str没啥影响,str还是空指针。并且p没有内存释放,导致内存泄漏。

所以说运行结果:

  • 内存泄漏
  • str是NULL,在strcpy时,需要传目标内存地址,而不是NULL,所以会导致内存崩溃。

正确修改:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

void Test(void)
{
    char* str = NULL;
    GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);
  //这个打印相当于:因为即便printf("hello world");,那传给print函数的也是字符'h'的地址,起始是和直接传str地址是一样的道理。
    printf("hello world");
    free(str);
    str = NULL;
}

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

输出:

image.png

4.2、题目2:野指针---返回栈区空间地址问题

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}

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

输出:

image.png

分析:数组p在GetMemory里面,且p存放的是字符'h'的地址,但是当函数GetMemory运行完毕,p数组就会销毁。然后str = GetMemory(),当GetMemory返回值为p,但是p已销毁。所以str是野指针。str指向的那块地址已经被销毁了,所以结果如上。

总结:以上两题都是返回栈区空间地址问题。让一个函数返回函数体里面的变量的地址时,用个变量接收,这个是非常危险的。

4.3、题目3:

int* f1(void)
{
    int x = 10;
    return (&x);
}


//判断下列代码的问题:野指针问题。
//x在函数f1内部,return &x,说明此函数返回个指针,但是这个函数在运行完毕后,x变量会销毁,所以&x就是野指针。
int* f2(void)
{
    int* ptr;
    *ptr = 10;
    return ptr;
}

//也是野指针问题。
//ptr没有初始化,然后*ptr相当于随便找了地址来解引用,相当于随机访问,野指针问题。

4.4、野指针

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

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;
}

分析错误:使用malloc动态内存分配100字节大小的空间。然后拷贝,str里面存放了首字符'h'的地址。当free(str)后,动态分配的100字节大小的空间就交给操作系统回收了。但是因为没有进行str = NULL这一步操作,所以str的值,也就是存放的首字符'h'的地址并没有变。然后str != NULL为真,然后在进行拷贝,然后现在str已经时野指针了。虽然将"world"传给str,但是str指向的地址,已经不归我们使用了,所以在访问有可能时访问其它的数据的空间,所以此程序不对。

image.png

5、C/C++程序的内存开辟

image.png

内核空间是用来运行操作系统的。我们写的代码不可以运行在此处。

数据段又是静态区。

代码段:存放我们写的代码进行编译、链接后为可执行程序的二进制指令。

6、柔性数组

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

1、必须在结构体中。

2、必须是最后一个成员。

3、必须是大小未知的数组。

eg:

typdef struct st_type
{
    int i;
    int a[0];    //柔性数组成员
    int b[];     //这个写法也行
}type_a;

6.1、柔性数组的特点

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

例如:

typdef struct st_type
{
    int i;
    int a[0];    //柔性数组成员
}type_a;
prinf("%d\n,sizeof(type_a)");        //输出的是4。

6.2、柔性数组的使用

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

struct S
{
    int i;
    int arr[];   //打算给此柔性数组10个元素的大小。
};

int main()
{
    //包含柔性数组成员的结构用malloc()函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
    //sizeof(struct S)是结构体大小,40就是柔性数组的大小。
    struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
    if (ps == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; i++)
    {
        printf("%d ", ps->arr[i]);
    }
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
    if (ptr != NULL)
    {
        ps = ptr;
    }
    free(ps);
    ps = NULL;
    return 0;
}

以后采用柔性数组的方法可以对结构体数组进行动态内存分配。

除此以上使用柔性数组的方法,其实我们也有第二种方法来对结构体中的数组进行动态内存分配:

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

struct S
{
    int i;
    int* arr;
};

int main()
{
    struct S* ps = (struct S*)malloc(sizeof(struct S));
    if (ps == NULL)
    {
        printf("%s", strerror(errno));
        return 1;
    }
    //这里为什么需要对结构体进行malloc呢?将结构体malloc是为将结构体中的成员变量i也放在堆区。
    //因为下面我们要将arr进行malloc,为了将结构体中的每个成员一致,所以先也将结构体malloc,这样以来i就放在了堆区里面了。
    ps->i = 100;
    //给指向arr的地址动态分配40个字节大小的空间。
    ps->arr = (int*)malloc(40);
    int  i = 0;
    for (i = 0; i < 10; i++)
    {
        ps->arr[i] = i;
    }
    for (i = 0; i < 10; i++)
    {
        printf("%d ", ps->arr[i]);
    }
    int* ptr = (int*)realloc(ps->arr, 80);
    if (ptr != NULL)
    {
        ps->arr = ptr;
    }
    free(ps->arr);
    free(ps);
    //这里直接一步到位把ps置为NULL,那ps->arr自然而然的就为NULL了。
    ps = NULL;
    return 0;
}

那以上两种方法如何选择呢?

  • 采用柔性数组的方法,只需要一次malloc,后续不够在使用realloc。
  • 而第二种方法,需要两次malloc,后续不够在使用realloc

注意:使用malloc越多,就越需要free,而且还会产生内存碎片。

总结:

  • 采用柔性数组的好处是:方便内存释放。
  • 第二个的好处是:有利用访问速度。
相关文章
|
3天前
|
C语言
C语言—内存函数的实现和模拟实现(内存函数的丝绸之路)
C语言—内存函数的实现和模拟实现(内存函数的丝绸之路)
17 0
|
4天前
|
程序员 编译器 C语言
C语言----动态内存分配(malloc calloc relloc free)超全知识点
C语言----动态内存分配(malloc calloc relloc free)超全知识点
14 6
|
4天前
|
存储 程序员 编译器
C语言:动态内存管理
C语言:动态内存管理
11 1
|
4天前
|
存储 编译器 程序员
C语言:数据在内存中的存储
C语言:数据在内存中的存储
15 2
|
4天前
|
存储 编译器 C语言
C语言:字符函数 & 字符串函数 & 内存函数
C语言:字符函数 & 字符串函数 & 内存函数
16 2
|
4天前
|
存储 C语言 开发者
【C言专栏】C 语言实现动态内存分配
【4月更文挑战第30天】C语言中的动态内存分配允许程序运行时按需分配内存,提供处理未知数据量的灵活性。这涉及`malloc()`, `calloc()`, `realloc()`, 和 `free()`四个标准库函数。`malloc()`分配指定大小的内存,`calloc()`同时初始化为零,`realloc()`调整内存大小,而`free()`释放内存。开发者需谨慎处理内存泄漏和指针使用,确保程序的稳定性和性能。动态内存分配是C语言中的重要技能,但也需要良好的内存管理实践。
|
4天前
|
存储 C语言
C语言进阶---------作业复习
C语言进阶---------作业复习
|
4天前
|
存储 Linux C语言
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)-2
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
|
4天前
|
自然语言处理 Linux 编译器
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)-1
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
|
4天前
|
存储 编译器 C语言
C语言进阶第十课 --------文件的操作-1
C语言进阶第十课 --------文件的操作