学而时习之:C语言中的内存管理

一、C 程序的内存布局

程序的内存布局指的是程序在运行期间其数据在计算机内存中的存储方式。理解这一布局有助于开发者更高效地管理内存,并避免出现段错误(segmentation faults)和内存泄漏(memory leaks)等问题。

如下图所示,C 程序的内存被划分为若干特定的区域(段),每个区域在程序执行过程中承担着不同的职责。

1.代码段(Text Segment)

代码段(text segment 又称文本段)用于存放程序的可执行指令。它包含了程序各函数经编译后生成的机器码。该段通常是只读的,并被放置在内存的较低地址区域,以防止程序运行期间代码被意外修改。 代码段的大小由指令数量和程序的复杂程度决定。

2.数据段(Data Segment)

数据段用来存放由程序员定义的全局变量和静态变量,位于代码段之上。它可进一步细分为两部分:

A. 已初始化数据段(Initialized Data Segment)

顾名思义,该部分包含已由程序员显式初始化的全局变量和静态变量。例如:

c 复制代码
// 全局变量
int a = 10;

// 静态变量
static int b = 20;

变量 ab 将被存储在已初始化数据段中。

B. 未初始化数据段(BSS)

未初始化数据段常被称为"BSS"段,名字源自早期汇编操作符"Block Started by Symbol"。该段存放未被程序员显式初始化的全局变量和静态变量。程序加载时,操作系统会自动将这些变量清零。例如:

c 复制代码
// 全局变量
int x;

// 静态变量
static int y;

上述变量 xy 会被存储在 BSS 段中。

3.堆段(Heap Segment)

堆段是动态内存分配通常发生的区域。它从 BSS 段的末尾开始,并向高地址方向增长。其大小由 malloc()realloc()free() 等函数管理,这些函数内部可能通过 brksbrk 系统调用来调整堆的大小。

堆会被进程内的所有共享库和动态加载模块共同使用。例如,下面指针 ptr 所指向的变量就存放在堆段中:

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

int main() {
    // 创建一个整型指针,并分配 10 个 int 大小的空间
    int *ptr = (int*) malloc(sizeof(int) * 10);
    return 0;
}

4.栈段(Stack Segment)

栈是用于存放局部变量管理函数调用的内存区域。每次函数被调用时,都会创建一个栈帧,用来保存局部变量、函数参数以及返回地址,这些栈帧就存放在栈段中。

栈段通常位于内存的高地址区域,其增长方向与堆相反。二者相向而生,当栈指针与堆指针相遇时,就认为程序的可用内存已耗尽。

存放在栈段的数据示例:

c 复制代码
#include <stdio.h>

void func() {
    // 存放在栈中
    int local_var = 10;
}

int main() {
    func();
    return 0;
}
  1. 我们将全局变量初始化,这样它就会存放到数据段(DS):
c 复制代码
#include <stdio.h>

// 已初始化的全局变量,存放在数据段
int global = 10;

int main() {
    // 已初始化的静态变量,也存放在数据段
    static int i = 100;
    return 0;
}

编译并查看各段大小:

arduino 复制代码
gcc memory-layout.c -o memory-layout
size memory-layout

输出:

arduino 复制代码
   text    data     bss     dec     hex  filename
    960     256       8    1224     4c8  memory-layout

二、C 语言中的动态内存分配

动态内存分配技术让程序员能够自主决定:

  • 何时分配内存
  • 分配多少内存
  • 何时释放内存

主要特点:

  1. 在程序运行时按需申请内存,可灵活处理大小变化的数据
  2. 所申请的空间位于堆(heap),而非栈(stack)
  3. 若后续需要更多元素,可扩大已分配块;若元素减少,也可缩小
  4. 只要程序员未主动释放,这块内存在函数返回后依旧有效,因此函数可以返回指向该内存的指针。相比之下,普通栈变量在函数结束即失效

C 语言通过 <stdlib.h> 提供的 4 个库函数实现动态内存分配:

1.malloc() 函数

malloc()(memory allocation 的缩写)用于在运行时从堆中申请一块连续的内存 。这块内存未被初始化,也就是说里面保存的是"垃圾值"。

语法: 该函数返回一个 void 类型的指针,指向已分配的内存。为了使其可用,需要将其转换为所需类型的指针。如果分配失败,则返回 NULL 指针。

c 复制代码
malloc(size); //其中,`size` 是要分配的字节数。

假设我们要存放 5 个整数。已知 int 占 4 字节,共需 5×4 = 20 字节,可像下面这样申请: C语言中 int 的实际大小与平台有关,因此更推荐用 sizeof 计算。 此外,如果操作系统无法满足申请,malloc 会返回 NULL。因此务必检查返回值

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(sizeof(int) * 5);
    if (ptr == NULL) { // 申请失败
        printf("Allocation Failed");
        exit(0);
    }
    // 赋值并打印
    for (int i = 0; i < 5; i++){
        ptr[i] = i + 1;
    }
    for (int i = 0; i < 5; i++){
        printf("%d ", ptr[i]);
    }
    return 0;
}

输出:

复制代码
1 2 3 4 5

2.calloc() 函数

calloc()(代表连续分配)函数与 malloc() 类似,但它会将分配的内存初始化为零。当你需要默认值为零的内存时使用它。

语法: 该函数同样返回一个 void 类型的指针,指向已分配的内存。为了使其可用,需要将其转换为所需类型的指针。如果分配失败,则返回 NULL 指针。

c 复制代码
calloc(n, size); 其中,`n` 是元素的数量,`size` 是每个元素的大小(以字节为单位)。
c 复制代码
#include <stdio.h>
#include <stdlib.h>
​
int main() {
    int *ptr = (int *)calloc(5, sizeof(int));
    // 检查是否分配成功
    if (ptr == NULL) {
        printf("分配失败");
        exit(0);
    }
    // 无需手动初始化,因为已经自动设置为 0 ,   打印数组
    for (int i = 0; i < 5; i++){
        printf("%d ", ptr[i]);
    }
    return 0;
}

输出:

复制代码
0 0 0 0 0

3.free() 函数

使用 malloc()calloc() 函数分配的内存不会自动释放。free() 函数用于将动态分配的内存释放回操作系统。及时释放不再使用的内存是防止内存泄漏的关键。

语法: 释放内存块后,该指针将变为无效,不再指向任何可用的内存位置。

c 复制代码
free(ptr); 其中,`ptr` 是指向已分配内存的指针。
c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)calloc(5, sizeof(int));
    // 执行一些操作......
    for (int i = 0; i < 5; i++){
        printf("%d ", ptr[i]);
    }    
    free(ptr); // 操作完成后释放内存
    ptr = NULL;
    return 0;
}

输出:

复制代码
0 0 0 0 0

注意: 调用 free() 后,建议将指针设为 NULL,以避免使用"悬空指针"------即指向已释放内存的指针。

c 复制代码
ptr = NULL;

4.realloc() 函数

realloc() 函数用于调整之前已分配内存块的大小。它允许你在无需释放旧内存并重新分配新块的情况下,改变已有内存的大小。

假设我们最初为 5 个整数分配了内存,但后来需要把数组扩容到 10 个整数,就可以用 realloc() 来调整内存块的大小:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(5 * sizeof(int));
    // 将内存块扩容,以容纳 10 个整数
    ptr = (int *)realloc(ptr, 10 * sizeof(int));
    // 检查是否重新分配成功
    if (ptr == NULL) {
        printf("内存重新分配失败");
        exit(0);
    }
    return 0;
}

注意: 需要注意的是,如果 realloc() 失败并返回 NULL原来的内存块不会被释放 。因此,在确认新内存块分配成功之前,不要直接用原指针接收返回值 ,否则将导致原内存地址丢失,造成内存泄漏。为防止这种情况,应谨慎处理 NULL 返回值:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(5 * sizeof(int));
    // 重新分配内存
    int *temp = (int *)realloc(ptr, 10 * sizeof(int));
    
    // 仅当重新分配成功时才更新指针
    if (temp == NULL){
        printf("内存重新分配失败\n");
    }else{
        ptr = temp;
    }
    return 0;
}

5.malloc()calloc() 的区别

两者功能几乎一样,为何还要提供两个接口? 关键在于是否初始化

  • malloc() 不会清零,内存内容是随机的。
  • calloc() 会把分配的内存全部初始化为 0。

因此,当你需要"默认值为 0"的缓冲区时,用 calloc() 更方便;否则用 malloc() 即可。

三、动态内存分配带来的问题

动态内存分配虽然好用,但也极易出错,一旦处理不当就会造成内存暴涨甚至系统崩溃。下面列出几种常见错误:

  1. 内存泄漏

    分配后忘了 free,内存再也收不回来,系统资源会被慢慢耗尽。

  2. 悬空指针

    内存已经被 free 了,还继续使用原指针,会导致未定义行为或直接崩溃。

  3. 内存碎片

    频繁地申请、释放不同大小的块,会把堆空间"剁"得七零八落,利用率下降。

  4. 分配失败

    内存申请失败时若不做检查,后续解引用就会段错误;程序必须对返回的 NULL 做保护。


(A) 什么是内存泄漏?如何避免?

数据可以存放在栈(stack)堆(heap)。 栈保存函数的局部变量和参数;堆用于运行时的动态内存分配。

内存泄漏 :程序在堆上动态申请了内存,但用完之后忘记释放,导致这块内存永远无法再被回收利用。

  • C 中,用 malloc() / calloc() 申请,用 free() 释放。
  • C++ 中,用 new / new[] 申请,用 delete / delete[] 释放。

内存泄漏示例

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int *ptr = new int;  // 申请堆内存
    *ptr = 20;
    cout << *ptr << endl;
    // 忘记释放:delete ptr;
    return 0;    // 程序结束,内存却未归还
}

如何避免内存泄漏?

  1. 每次申请后,都在不再使用时立即 free/delete
  2. 覆盖指针前,先释放它原来指向的内存
  3. C++ 优先使用智能指针unique_ptrshared_ptr),让对象生命周期自动管理内存。
  4. 借助工具检测:Valgrind、AddressSanitizer 等可在测试阶段找出泄漏点。
相关推荐
。。。9045 天前
mit6s081 lab8 locks
操作系统·c
CAU界编程小白5 天前
数据结构系列之堆
数据结构·c
煤球王子7 天前
学而时习之:C语言中文件操作Error处理
c
煤球王子7 天前
学而时习之:C语言中的Exception处理
c
BlackQid10 天前
深入理解指针Part3——指针与数组
c
要做朋鱼燕10 天前
【AES加密专题】1.AES的原理详解和加密过程
运维·网络·密码学·c·加密·aes·嵌入式工具
煤球王子11 天前
学而时习之:C语言中的Error处理
c
qq_4378964315 天前
unsigned 是等于 unsigned int
开发语言·c++·算法·c
Lonble16 天前
C语言篇:预处理
c语言·c