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,而且还会产生内存碎片。

总结:

  • 采用柔性数组的好处是:方便内存释放。
  • 第二个的好处是:有利用访问速度。
相关文章
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
42 3
|
25天前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
39 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
25天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
54 6
|
28天前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
46 6
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
126 13
|
29天前
|
大数据 C语言
C 语言动态内存分配 —— 灵活掌控内存资源
C语言动态内存分配使程序在运行时灵活管理内存资源,通过malloc、calloc、realloc和free等函数实现内存的申请与释放,提高内存使用效率,适应不同应用场景需求。
|
1月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
62 11
|
29天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
1月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
58 11
|
27天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
61 1