首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C语言基础:(二十二)动态内存管理

C语言基础:(二十二)动态内存管理

作者头像
_OP_CHEN
发布2026-01-14 09:26:10
发布2026-01-14 09:26:10
1430
举报
文章被收录于专栏:C++C++

前言

在C语言编程中,动态内存管理是构建高效、灵活程序的关键技术之一。通过动态分配和释放内存,开发者能够更精准地控制资源使用,适应复杂多变的运行时需求。然而,动态内存管理也带来了诸如内存泄漏、悬垂指针等风险,对开发者的技术能力提出了更高要求。本文将从基础的内存分配函数入手,逐步探讨动态内存管理的核心机制、常见问题及优化策略,下面就让我们开始吧!


一、为什么要有动态内存分配?

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

代码语言:javascript
复制
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

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

  • 空间开辟大小是固定的
  • 数组在申明的时候,必须制定数组的长度,数组空间一旦确定了大小不能调整

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那么数组的编译时开辟空间的方式就不能满足了。

C语言引入了动态内存开辟,让程序员可以自己申请和释放空间,那就比较灵活了。

下图为内存中三个区的分配情况:

二、malloc 和 free

2.1 malloc

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

代码语言:javascript
复制
void* malloc (size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

2.2 free

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

代码语言:javascript
复制
void free (void* ptr);

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

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

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

下面来看看这两个函数的使用示例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int num = 0;
    scanf("%d", &num);
    int arr[num] = {0};
    int* ptr = NULL;
    ptr = (int*)malloc(num*sizeof(int));
    if(NULL != ptr)//判断ptr指针是否为空
    {
        int i = 0;
        for(i=0; i<num; i++)
        {
            *(ptr+i) = 0;
        }
    } 
    free(ptr);//释放ptr所指向的动态内存
    ptr = NULL;//是否有必要?
    return 0;
}

三、calloc 和 realloc

3.1 calloc

C语言还提供了一个函数叫做calloc,calloc函数也是用于动态内存分配的。原型如下:

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

下面我们来举个例子:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *p = (int*)calloc(10, sizeof(int));
    if(NULL != p)
    {
        int i = 0;
        for(i=0; i<10; i++)
        {
            printf("%d ", *(p+i));
        }
    } 
    free(p);
    p = NULL;
    return 0;
}

输出结果如下:

代码语言:javascript
复制
0 0 0 0 0 0 0 0 0 0

所以如果我们对申请的空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

3.2 realloc

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

函数原型如下:

代码语言:javascript
复制
void* realloc (void* ptr, size_t size);
  • ptr :要调整的内存地址
  • siize :调整之后的新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间时存在两种情况,如下所示:

1. 原有空间之后有足够大的空间

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

情况一:

当为情况一的时候,要扩展内存就要直接在原有内存之后追加新空间,原来空间的数据不发生变化。

情况二:

当是情况二的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另外找一个合适大小的连续空间来使用。这样函数返回的就是一个新的内存地址。

由于上述的两种情况,在使用realloc函数时就得注意一些。代码演示如下所示:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *ptr = (int*)malloc(100);
    if(ptr != NULL)
    {
        //业务处理
    } 
    else
    {
        return 1;
    }      

    //扩展容量
    //代码1 - 直接将realloc的返回值放到ptr中
    ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)    
    //代码2 - 先将realloc函数的返回值放在p中,不为NULL,在放ptr中
    int*p = NULL;
    p = realloc(ptr, 1000);
    if(p != NULL)
    {
        ptr = p;
    } 
    //业务处理
    free(ptr);
    return 0;
}

四、常见的动态内存的错误

4.1 对NULL指针的解引用操作

代码语言:javascript
复制
void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
}

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

代码语言:javascript
复制
void test()
{
    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);
}

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

代码语言:javascript
复制
void test()
{
    int a = 10;
    int *p = &a;
    free(p);//ok?
}

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

代码语言:javascript
复制
void test()
{
    int *p = (int *)malloc(100);
    p++;
    free(p);//p不再指向动态内存的起始位置
}

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

代码语言:javascript
复制
void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放
}

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

代码语言:javascript
复制
void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }
} 

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

忘记释放不再使用的动态开辟的空间会导致内存泄漏。因此一定要切记:动态开辟的空间一定要释放,并且正确释放。

五、动态内存经典笔试题分析

5.1 题目一

代码语言:javascript
复制
void GetMemory(char *p)
{
    p = (char *)malloc(100);
} 

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

请问以上代码运行Test函数会有什么样的结果?答案是:会崩溃。

下面我们来逐步分析以上代码:

1. 指针传递问题(主要问题)
代码语言:javascript
复制
void GetMemory(char *p)  // 这里传递的是指针的值(地址),而不是指针的地址
{
    p = (char *)malloc(100);  // 这里修改的是局部副本,不影响外部的str
}

在C语言中,函数参数是按值传递的。当传递str给GerMemory时,传递的是NULL值的副本,而不是str变量的地址。因此,在函数内部对p的赋值就不会影响外部的str。

2. 内存泄漏

即使GerMemory正确分配了内存,由于没有保存返回的指针,分配的内存也无法被释放。

3. 空指针解引用
代码语言:javascript
复制
strcpy(str, "hello world");  // str仍然是NULL,这里会导致段错误
4. 内存未释放

程序没有释放分配的内存(虽然在这道题中由于前面的问题,实际上没有成功分配)。

5.2 题目二

代码语言:javascript
复制
char *GetMemory(void)
{
    char p[] = "hello world";
    return p;
} 

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

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

这段代码中存在着一个严重的悬空指针问题,会导致未定义行为。

主要问题是:返回局部数组的地址。

代码语言:javascript
复制
char *GetMemory(void)
{
    char p[] = "hello world";  // 局部数组,在栈上分配
    return p;                   // 返回局部变量的地址
} 
  1. p[ ] 是一个局部数组,在栈上分配内存。
  2. 当GetMemory函数返回时,局部变量p的内存空间会被自动释放(栈帧被销毁)。
  3. 返回的指针指向的是已经被释放的内存区域,成为空指针。
  4. 在Test函数中使用这个指针会导致未定义行为(可能打印乱码、程序崩溃等)。

在VS2022环境下输出结果如下:

我们要想消除这一错误,应该使用静态存储duration,如下所示:

代码语言:javascript
复制
char *GetMemory(void)
{
    static char p[] = "hello world";  // 静态存储,生命周期为整个程序
    return p;
}

5.3 题目三

代码语言:javascript
复制
void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
} 

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

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

代码的输出结果如下:

可见上述代码最后能正确打印我们需要的值,但仍存在一些问题:

1. 未检查malloc返回值
代码语言:javascript
复制
*p = (char *)malloc(num);  // 没有检查是否分配成功

如果malloc失败,返回了NULL,那么后续的strcpy会导致段错误。

2. 内存泄漏

程序在使用完内存之后没有调用 free() 释放内存。

3. printf 使用方式
代码语言:javascript
复制
printf(str);  // 虽然语法正确,但存在安全风险

如果str包含格式化字符(如 &s , &d等),会导致未定义行为。

因此我们可以对源代码做出改进如下:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
    if (*p == NULL) {
        // 处理内存分配失败
        fprintf(stderr, "Memory allocation failed\n");
        exit(EXIT_FAILURE);
    }
} 

void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    
    // 安全地复制字符串
    strncpy(str, "hello", 99);
    str[99] = '\0';  // 确保字符串以null结尾
    
    // 安全的printf用法
    printf("%s\n", str);
    
    // 释放内存
    free(str);
    str = NULL;  // 避免悬空指针
}

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

5.4 题目四

代码语言:javascript
复制
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

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

上述这段代码存在着几个严重的未定义行为的问题,同样会导致悬空指针问题,是非常危险的编程错误。

我们做出修改如下:

代码语言:javascript
复制
c
void Test(void)
{
    char *str = (char *)malloc(100);
    if (str == NULL) {
        // 处理分配失败
        return;
    }
    
    strcpy(str, "hello");
    printf("%s\n", str);
    
    free(str);
    str = NULL;  // 立即置空,避免误用
    
    // 现在正确的检查方式
    if(str != NULL) {  // 这里不会执行
        strcpy(str, "world");
        printf("%s\n", str);
    }
}

六、柔性数组

也许有的人从来没有听说过柔性数组(flexible array)这个概念,但他确实是存在的。

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

代码示例如下:

代码语言:javascript
复制
struct st_type
{
    int i;
    int a[0];//柔性数组成员
};

有些编译器会报错无法编译,可以改成如下形式:

代码语言:javascript
复制
struct st_type
{
    int i;
    int a[];//柔性数组成员
};

6.1 柔性数组的特点

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

代码示例如下:

代码语言:javascript
复制
typedef struct st_type
{
    int i;
    int a[0];//柔性数组成员
}type_a;
int main()
{
    printf("%d\n", sizeof(type_a));//输出的是4
    return 0;
}

6.2 柔性数组的使用

大家看下面这段示例代码:

代码语言:javascript
复制
//代码1
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int i = 0;
    type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
    //业务处理
    p->i = 100;
    for(i=0; i<100; i++)
    {
        p->a[i] = i;
    } 
    free(p);
    return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

6.3 柔性数组的优势

上述的type_a结构也可以设计为下面的结构,同样能完成一样的效果:

代码语言:javascript
复制
//代码2
#include <stdio.h>
#include <stdlib.h>

typedef struct st_type
{
    int i;
    int *p_a;
}type_a;

int main()
{
    type_a *p = (type_a *)malloc(sizeof(type_a));
    p->i = 100;
    p->p_a = (int *)malloc(p->i*sizeof(int));
    //业务处理
    for(i=0; i<100; i++)
    {
        p->p_a[i] = i;
    } 

    //释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
    return 0;
}

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

第一个好处是:方便内存释放

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

第二个好处是:有利于提高访问速度

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

下面给大家推荐一篇酷壳上陈皓大佬写的一篇关于结构体中的数组和指针的文章,供大家参考学习:C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell

七、C/C++中程序内存区域划分总结

如上图,C/C++程序内存分配有以下几个区域:

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

总结

以上就是本期博客的全部内容啦!C语言中的动态内存管理这部分的知识比较难理解,也比较抽象。但如果能充分掌握这部分的内容,会对后续学习数据结构有很大的帮助。下期见!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、为什么要有动态内存分配?
  • 二、malloc 和 free
    • 2.1 malloc
    • 2.2 free
  • 三、calloc 和 realloc
    • 3.1 calloc
    • 3.2 realloc
      • 情况一:
      • 情况二:
  • 四、常见的动态内存的错误
    • 4.1 对NULL指针的解引用操作
    • 4.2 对动态开辟空间的越界访问
    • 4.3 对非动态开辟内存使用free释放
    • 4.4 使用free释放一块动态开辟内存的一部分
    • 4.5 对同一块动态内存多次释放
    • 4.6 动态开辟内存忘记释放(内存泄漏)
  • 五、动态内存经典笔试题分析
    • 5.1 题目一
      • 1. 指针传递问题(主要问题)
      • 2. 内存泄漏
      • 3. 空指针解引用
      • 4. 内存未释放
    • 5.2 题目二
    • 5.3 题目三
      • 1. 未检查malloc返回值
      • 2. 内存泄漏
      • 3. printf 使用方式
    • 5.4 题目四
  • 六、柔性数组
    • 6.1 柔性数组的特点
    • 6.2 柔性数组的使用
    • 6.3 柔性数组的优势
  • 七、C/C++中程序内存区域划分总结
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档