c语言-动态内存管理

在 C 语言编程中,内存管理是绕不开的核心知识点。我们一开始学的变量、数组,都是在栈上开辟内存,比如int val = 20或者char arr[10] = {0}。但这种方式有个明显的局限:空间大小固定,数组声明时必须指定长度,运行中还不能调整。可实际开发里,很多时候我们要到程序跑起来才知道需要多少内存 ------ 比如用户输入数据的数量、读取文件的大小。这时候,动态内存分配就该登场了,它能让我们灵活申请和释放内存,完美解决固定内存的痛点。今天就从基础到实战,把动态内存管理讲明白!

一、动态内存的 "核心工具":4 个关键函数

动态内存操作主要靠mallocfreecallocrealloc这四个函数,它们都声明在stdlib.h头文件里,缺一不可

1. malloc:最基础的 "内存申请器"

函数原型:void* malloc (size_t size);

  • 功能:向内存(堆区)申请一块连续可用的空间,返回指向这块空间的指针。
  • 三个关键注意点:
    • 申请成功:返回有效指针,需要自己强转成对应类型(比如int*char*
    • 申请失败:返回**NULL指针,所以一定要检查返回值**,不然会踩坑!
    • 参数为 0:行为不确定,取决于编译器,尽量别这么用。
cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		perror("malllc");
		return 1;
	}
	int i = 0;
	for (i; i < 5; i++)
	{
		(*(p + i)) = i + 1;


	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

注意:开辟空间后,未赋值的空间是随机值

2. free:内存的 "回收工"

函数原型:void free (void* ptr);

  • 功能:专门释放动态开辟的内存,把空间还给系统
  • 两个关键注意点:
    • 只能释放堆区 内存:如果给它传栈区变量的地址(比如int a=10; free(&a);),行为未定义,程序可能崩溃。
    • NULL函数啥也不做,所以释放后把指针设为NULL很安全
    • 传入要释放内存的起始位置

这里要强调:动态内存申请后一定要释放,不然会造成内存泄漏,程序运行越久占内存越多,最后可能卡死。

上面代码后面加上:

cpp 复制代码
free(p);
p = NULL;

避免p成为野指针

3. calloc:带 "初始化" 的内存申请

函数原型:void* calloc (size_t num, size_t size);

  • 功能:和malloc类似,也是申请动态内存,但多了个 "初始化" 功能 ------ 会把申请的每个字节都设为 0。
  • 参数含义:num是元素个数,size是每个元素的大小。比如calloc(10, sizeof(int))就是申请 10 个 int 的空间,每个都初始化为 0
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

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

4. realloc:内存的 "伸缩器"

函数原型:void* realloc (void* ptr, size_t size);

  • 功能:调整已动态开辟内存的大小,比如之前申请的空间不够用了,或者太大了想缩小。
  • 两个关键注意点。
    • 两种调整情况:
      • 情况 1:原内存后面有足够空间,直接在后面追加,返回原地址。
      • 情况 2:原内存后面空间不够,会在堆区找一**块新的合适空间,返回新地址,**会把原来的数据复制到新空间,释放旧的内存空间。
      • 情况3:调整失败,返回空指针
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int* p = (int*)calloc(5, sizeof(int));
    if (p == NULL) {
       
        perror("mallloc");
        return 1;
    }

    for (int i = 0; i < 5; i++)
    {
        *(p + i) = i + 1;
    }
    //空间不够,realloc申请内存40个字节
  int *ptr=realloc(p, 40);
  if (ptr != NULL)
  {
      p = ptr;

  }
  else
  {
      //失败使用原来的内存
      perror("realloc");
  }

  //可以使用40个字节的内存
  for (int i = 5; i < 10; i++)
  {
      *(p + i)= i+1;
  }

  //打印内存中的内容
  for (int i = 0; i < 10; i++)
  {
      printf("%d ", *(p + i));
  }
    free(p);
    p = NULL;
    return 0;
}

调试:

二、动态内存的 "避坑指南":6 个常见错误

动态内存用不好容易出问题,下面这 6 个错误一定要避开!

1. 对 NULL 指针解引用

malloccallocrealloc都可能返回 NULL,如果直接用*p = 20,程序会崩溃。

cpp 复制代码
// 错误示范
void test() {
    int* p = (int*)malloc(INT_MAX / 4); // 申请超大空间,大概率失败
    *p = 20; // 如果p是NULL,这里直接报错
    free(p);
}
// 正确做法:先检查p是否为NULL

2. 越界访问动态内存

和数组越界一样,动态内存也不能超范围访问,会触发未定义行为。

cpp 复制代码
void test() {
    int* p = (int*)malloc(10 * sizeof(int)); // 10个int,索引0-9
    if (NULL == p) exit(EXIT_FAILURE);
    for (int i = 0; i <= 10; i++) {
        *(p + i) = i; // i=10时越界,错误!
    }
    free(p);
}

3. 用 free 释放非动态内存

free只能释放堆区内存,栈区的局部变量不能用 free 释放。

cpp 复制代码
void test() {
    int a = 10;
    int* p = &a;
    free(p); // 错误!a在栈上,不是动态内存
}

5. 多次释放同一块内存

一块动态内存只能 free 一次,多次 free 会导致程序崩溃。

cpp 复制代码
void test() {
    int* p = (int*)malloc(100);
    free(p);
    free(p); // 错误!重复释放
}

6. 忘记释放内存(内存泄漏)

这是最常见的错误!申请的动态内存不用了不释放,程序运行期间内存会一直被占用,直到程序结束才会被系统回收(但长期运行的程序比如服务器,会越跑越卡)。

cpp 复制代码
void test() {
    int* p = (int*)malloc(100);
    if (p != NULL) *p = 20;
    // 没有free(p),内存泄漏!
}

7.不再内存的起始位置释放内存(易错)

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		perror("malllc");
		return 1;
	}
	int i = 0;
	for (i; i < 5; i++)
	{
		*p = i+ 1;
		p++;


	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;

	return 0;
}

p指针不指向内存的起始位置,释放会发生错误。

三、动态内存经典笔试题分析(高频考点)

笔试题是检验动态内存掌握程度的核心,下面这几道经典题几乎是面试 / 考试必出,我们逐题拆解。

笔试题 1:函数传参错误导致内存泄漏 + 野指针

cpp 复制代码
// 题目:想通过函数给指针p分配内存,运行后会出什么问题?
void GetMemory(char* p) {
    p = (char*)malloc(100); // 申请100字节内存
}

void Test(void) {
    char* str = NULL;
    GetMemory(str); // 调用函数
    strcpy(str, "hello world"); // 拷贝字符串
    printf(str);
}

问题分析:

  1. 传参错误GetMemory的参数是char* p(值传递),函数内的pstr的临时拷贝,修改p的指向不会影响外部的str
  2. 野指针访问 :函数执行后,str依然是NULLstrcpy(str, ...)对 NULL 指针解引用,程序崩溃。
  3. 内存泄漏GetMemorymalloc的 100 字节没有free,且指针丢失,内存永远无法释放。

正确写法(两种方案):

cpp 复制代码
// 方案1:传二级指针(推荐)
void GetMemory(char** p) {
    *p = (char*)malloc(100); // 修改外部指针的指向
}

void Test(void) {
    char* str = NULL;
    GetMemory(&str); // 传str的地址
    if (str != NULL) { // 检查是否申请成功
        strcpy(str, "hello world");
        printf(str);
        free(str); // 释放内存
        str = NULL; // 避免野指针
    }
}

// 方案2:函数返回指针
char* GetMemory() {
    char* p = (char*)malloc(100);
    return p;
}

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

笔试题 2:返回栈区指针导致野指针

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
// 题目:这个函数返回的指针能用吗?
char* GetString(void) {
    char p[] = "hello world"; // 栈区局部数组
    return p; // 返回数组首地址
}

void Test(void) {
    char* str = NULL;
    str = GetString(); // 接收返回值
    printf(str); // 打印结果?
}
int main()
{
    Test();
    return 0;
}

问题分析:

  1. char p[] = "hello world"是栈区局部变量,函数执行结束后,栈区空间会被系统回收
  2. GetString返回的是p的地址,但该地址对应的空间已失效,str成为野指针
  3. 调用printf(str)时,访问的是已释放的栈区空间,结果是随机的(未定义行为)

修改:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 方案1:用静态变量(数据段,程序结束才释放)
char* GetString(void) {
    static char p[] = "hello world"; // static修饰,存放在数据段
    return p;
}

// 方案2:用动态内存(堆区,手动释放)
//char* GetString(void) {
//    char* p = (char*)malloc(12); // 12字节:hello world + 结束符'\0'
//    strcpy(p, "hello world");
//    return p;
//}


void Test(void) {
    char* str = NULL;
    str = GetString(); // 接收返回值
    printf(str); // 打印结果?
}
int main()
{
    Test();
    return 0;
}

笔试题 3:重复释放 + 内存泄漏

cpp 复制代码
// 题目:这个函数有几个问题?
void Test(void) {
    char* str = (char*)malloc(100); // 申请内存
    strcpy(str, "hello"); // 拷贝字符串
    free(str); // 释放内存
    if (str != NULL) { // 检查指针是否为空
        strcpy(str, "world"); // 再次拷贝
        printf(str);
        free(str); // 释放内存
    }
}
int main()
{
    Test();
    return 0;
}

问题分析:

  1. 野指针访问free(str)后,str的指向的内存已释放,str本身没有置 NULL,依然指向原地址(野指针)。
  2. 非法内存操作strcpy(str, "world")访问已释放的堆区内存,程序可能崩溃。
  3. (进阶)如果在strcpy后再次free(str),会导致重复释放,同样触发未定义行为。

修改:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

void Test(void) {
    char* str = (char*)malloc(100);
    if (str != NULL) { // 先检查再使用
        strcpy(str, "hello");
        free(str);
        str = NULL; // 释放后立即置NULL,关键!
    }
    if (str != NULL) { // 此时str是NULL,不会执行内部代码
        strcpy(str, "world");
        printf(str);
    }
}

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

笔试题 4:realloc 使用不当导致内存泄漏

cpp 复制代码
// 题目:想扩容内存,哪里错了?
void Test(void) {
    char* p = (char*)malloc(100);
    strcpy(p, "hello");
    p = (char*)realloc(p, 200); // 扩容到200字节
    strcpy(p + 5, "world");
    printf(p);
    free(p);
}

问题分析:

  1. realloc(p, 200)如果扩容失败,会返回NULL此时p被赋值为NULL,原来malloc的 100 字节指针丢失,导致内存泄漏。
  2. 即使扩容成功,代码本身能运行,但存在隐藏风险(失败时崩溃 + 泄漏)。

修改:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

void Test(void) {
    char* p = (char*)malloc(100);
    if (p != NULL) {
        strcpy(p, "hello");
        // 用临时指针接收realloc返回值
        char* temp = (char*)realloc(p, 200);
        if (temp != NULL) { // 检查扩容是否成功
            p = temp;
            strcpy(p + 5, "world");
            printf(p);
        }
        free(p); // 无论扩容是否成功,都要释放原内存
        p = NULL;
    }
}

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

四、柔性数组:动态内存的 "进阶用法"

你可能没听过柔性数组,但它确实是 C99 标准里的特性,用好了很方便。

1. 什么是柔性数组?

结构体的最后一个元素可以是未知大小的数组,这就是柔性数组。

cpp 复制代码
// 两种写法(有些编译器不支持int a[0],可以用int a[])
struct st_type {
    int i;
    int a[0]; // 柔性数组成员
};

2. 柔性数组的特点

  • 前面必须有至少一个其他成员(比如上面的 int i)。
  • sizeof计算结构体大小时,不包含柔性数组的内存(比如sizeof(struct st_type)是 4,只算 int i 的大小)。
  • 如:
  • 必须用malloc动态分配内存,且分配的空间要大于结构体本身大小,给柔性数组留空间

3. 怎么用柔性数组?

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

struct s {
    char c;
    int i;
    int a[0];  // 柔性数组(C99 也可写成 int a[])
};

int main()
{
    // 初始分配:结构体固定部分 + 5个int的柔性数组空间
    struct s* ps = (struct s*)malloc(sizeof(struct s) + 5 * sizeof(int));
    if (ps == NULL)
    {
        perror("malloc");
        return 1;
    }

    // 初始化结构体成员和柔性数组
    ps->c = 'A';  // 补充初始化字符成员
    ps->i = 100;
    for (int i = 0; i < 5; i++)
    {
        ps->a[i] = i;  // 柔性数组赋值:0 1 2 3 4
    }

    // 打印初始柔性数组内容
    printf("初始柔性数组:");
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", ps->a[i]);
    }
    printf("\n");

    // 关键修正:realloc 传递内存块起始地址 ps,且扩大空间到 10 个int
    struct s* ptr = (struct s*)realloc(ps, sizeof(struct s) + 10 * sizeof(int));
    if (ptr != NULL)
    {
        ps = ptr;  // 调整成功,更新指针
        printf("内存调整成功!\n");

        // 给新增的柔性数组元素赋值
        for (int i = 5; i < 10; i++)
        {
            ps->a[i] = i;  // 新增元素:5 6 7 8 9
        }

        // 打印调整后的完整柔性数组
        printf("调整后柔性数组:");
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", ps->a[i]);
        }
        printf("\n");
    }
    else
    {
        perror("realloc");  // 调整失败,保留原内存
    }

    // 释放内存(避免内存泄漏)
    free(ps);
    ps = NULL;  // 野指针置空

    return 0;
}

方法二:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

struct s
{
    int n;      // 表示数组的元素个数
    int* arr;   // 指向动态分配的数组
};

int main()
{
    // 1. 为结构体本身分配内存
    struct s* p = (struct s*)malloc(sizeof(struct s));
    if (p == NULL)  // 检查内存分配是否成功
    {
        perror("malloc for struct");
        return 1;
    }

    // 2. 初始化结构体成员,并为arr指针分配数组内存
    p->n = 5;  // 初始数组有5个int元素
    p->arr = (int*)malloc(p->n * sizeof(int));
    if (p->arr == NULL)  // 检查数组内存分配是否成功
    {
        perror("malloc for arr");
        free(p);  // 分配arr失败时,先释放已分配的结构体内存
        p = NULL;
        return 1;
    }

    // 3. 使用数组:给arr赋值并打印初始内容
    for (int i = 0; i < p->n; i++)
    {
        p->arr[i] = i + 1;  // 赋值:1,2,3,4,5
    }
    printf("初始数组内容:");
    for (int i = 0; i < p->n; i++)
    {
        printf("%d ", p->arr[i]);
    }
    printf("\n");

    // 4. 调整数组大小:用realloc把arr的空间扩大到10个int
    int* ptr = (int*)realloc(p->arr, 10 * sizeof(int));
    if (ptr != NULL)  // 检查realloc是否成功
    {
        p->arr = ptr;    // 调整成功,更新arr指针
        p->n = 10;      // 同步更新数组元素个数为10
        ptr = NULL;     // 临时指针置空,避免野指针

        // 给新增的5个元素赋值(索引5~9)
        for (int i = 5; i < p->n; i++)
        {
            p->arr[i] = i + 1;  // 赋值:6,7,8,9,10
        }

        // 打印调整后的数组内容
        printf("调整后数组内容:");
        for (int i = 0; i < p->n; i++)
        {
            printf("%d ", p->arr[i]);
        }
        printf("\n");
    }
    else
    {
        perror("realloc for arr");  // 调整失败,保留原数组
        // 失败时p->arr仍有效,无需额外处理,后续正常释放即可
    }

    // 5. 释放内存(关键:先释放子指针arr,再释放结构体p)
    free(p->arr);  // 释放数组内存
    p->arr = NULL; // 野指针置空
    free(p);       // 释放结构体内存
    p = NULL;

    return 0;
}

核心差异对比表:

特性 结构体 + 柔性数组(int a [0]) 结构体 + 普通指针成员(int* arr)
内存布局 结构体固定部分 + 柔性数组成员在同一块连续内存 结构体固定部分(含指针)和数组内存是两块独立的内存(指针指向另一块)
内存分配次数 只需 1 次 malloc(整体分配) 需要 2 次 malloc(先分配结构体,再分配数组)
realloc 使用 结构体起始地址 realloc(整体调整) 指针指向的数组地址 realloc(仅调整数组)
内存释放 只需 1 次 free(释放结构体地址即可) 需要 2 次 free(先释放数组,再释放结构体)
内存碎片 碎片少(连续内存) 碎片多(两块独立内存,释放顺序错易泄漏)
可移植性 C99 标准支持(部分编译器需用 int a []) 完全兼容所有 C 编译器,无兼容性问题
使用场景 适合数组和结构体生命周期一致的场景 适合数组需要独立管理(如单独替换、释放)的场景

4. 柔性数组的优势

对比用指针实现的类似功能,柔性数组有两个明显好处:

  • 方便释放内存:一次 free 就能释放结构体和数组的所有内存,不用手动释放成员指针。
  • 访问速度更快:内存是连续的,减少内存碎片,CPU 缓存效率更高。

五、总结:C/C++ 程序的内存区域划分

最后我们梳理一下程序的内存布局,帮你更好理解动态内存的位置:

  1. 栈区(stack):存放局部变量、函数参数、返回值等,自动分配释放,效率高但空间小(向下增长)。
  2. 堆区(heap):动态内存分配的区域,由程序员申请释放,空间较大(向上增长)。
  3. 数据段(静态区):存放全局变量、静态变量(static 修饰),程序结束后系统释放。
  4. 代码段:存放函数二进制代码、只读常量(比如字符串常量),不可修改。
  5. 内核空间:用户代码不能读写,操作系统使用。

六、关键点回顾

  1. 动态内存核心函数:malloc(申请)、free(释放)、calloc(申请 + 初始化)、realloc(扩容 / 缩容),使用前必须检查返回值是否为 NULL。
  2. 常见坑点:NULL 解引用、越界访问、释放非堆内存、释放内存一部分、重复释放、内存泄漏,其中 "释放后置 NULL" 是避免野指针的关键。
  3. 笔试题核心考点:值传递导致指针无效、返回栈区指针、realloc 直接赋值、释放后未置 NULL,解决思路是 "传二级指针 / 返回值""用静态 / 堆内存""临时指针接收 realloc 结果"。
相关推荐
Lution Young2 小时前
Qt隐式共享产生的问题
开发语言·qt
9稳2 小时前
基于单片机的家庭安全系统设计
开发语言·网络·数据库·单片机·嵌入式硬件
JQLvopkk2 小时前
C#调用Unity实现设备仿真开发浅述
开发语言·unity·c#
每天吃饭的羊2 小时前
hash结构
开发语言·前端·javascript
一路往蓝-Anbo2 小时前
第37期:启动流程(二):C Runtime (CRT) 初始化与重定位
c语言·开发语言·网络·stm32·单片机·嵌入式硬件
Jackson@ML2 小时前
2026最新版Python 3.14.2安装使用指南
开发语言·python
橘子师兄2 小时前
C++AI大模型接入SDK—ChatSDK使用手册
开发语言·c++·人工智能
txinyu的博客2 小时前
STL string 源码深度解析
开发语言·c++
Channing Lewis2 小时前
正则灾难性回溯(catastrophic backtracking)
开发语言·python