问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C语言内存管理深度解析:从基础概念到实战应用

创作时间:
作者:
@小白创作中心

C语言内存管理深度解析:从基础概念到实战应用

引用
CSDN
1.
https://blog.csdn.net/F1781936/article/details/145662373

引言

内存管理是计算机科学中的核心概念之一,尤其是在C语言这种编程语言中。内存与数组、指针等概念密切相关,掌握内存的使用和布局对于编写高效、稳定的程序至关重要。
本文旨在深入探讨内存管理的各个方面,包括内存布局、动态内存分配、虚拟内存以及内存管理等方面,讲的知识比较多,建议收藏慢慢学习。

1. 内存管理基础

内存(Memory)是计算机系统中用于存储数据和指令的硬件设备。它是程序运行的基石,所有的变量、常量、函数代码等都需要存储在内存中。内存通常分为物理内存和虚拟内存。
在操作系统的管理中,虚拟内存通常被划分成内核空间和用户空间,我们平时使用的内存空间都是位于用户空间,而用户空间有分成栈区、堆区、数据段、代码段以及内存映射段。

2. 物理内存 && 虚拟内存

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究其内存布局,逐个了解不同内存区域的特性。
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

  • PM:Physical Memory,物理内存。
  • VM:Virtual Memory,虚拟内存。
    将其中一个C语言进程的虚拟内存放大来看,会发现其内部包下区域:

    虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。
    虚拟内存的各段详细情况:

3. C语言中的内存布局

内存局中最常用和最需要掌握的是堆区,栈区,数据段。

3.1 栈区(Stack)

  • 栈空间是有限的,一般为8M,项目所需的内存的较大时,尤其是在嵌入式的环境下,因此栈不能存放尺寸大的变量。
  • 当执行、调用函数时,栈会自动向下增长一段,用来存放函数内的局部变量;当退出函数时,栈会自动向上缩减一段,将该函数的局部变量所占内存归还给系统,导致退出函数时,地址会自动销毁,不能够返回给调用的地方。

3.2 堆区(Heap)

  • 相比栈内存,堆内存由程序员自己来申请内存,使用完后也得自己释放内存。
  • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。(你的内存条越大,系统越放开,那么你能申请的堆内存也就越大)
  • 堆内存是匿名的,访问需要通过指针。

3.3 数据段

  • 数据段包含了.bss段,.data段,.rodata段。
  • .bss (Block Started bySymbol)段: 存放未初始化的静态数据和全局变量,它们将被系统自动初始化为0
  • .data段:存放已初始化的静态数据和全局变量
  • .rodata段:存放常量数据

3.4 代码段

  • .text段:存放用户代码
  • .init段:存放系统初始化代码(执行main函数之前,栈和堆的初始化信息会被先执行)
    示例代码(数据段和代码段解析):
  
#include <stdio.h>
// 全局变量(即没有被花括号{}包含的)
int num1;               // 数据段中.bss区域
int num3 = 100;         // 数据段中.data区域
// 主函数
int main(int argc, char const *argv[])
{
    // (1)、代码段:
    /*
        .text: 用户代码
        .inti: 系统初始化堆栈代码
        说明:代码段中的.init存放了栈和堆的初始化信息,这些信息会在
        执行main函数之前,完成初始化,将其内存从硬盘中加载到内存条中,形成
        内存中的堆栈区域
    */
    // (2)、数据段:
    // 1、.bss (Block Started bySymbol)段:  存放未初始化的静态数据和全局变量,它们将被系统自动初始化为0
    // a、未初始化的全局变量
    // b、未初始化的static修饰的局部变量
    static int num2;               
    // 2、.data段:存放已初始化的静态数据和全局变量
    // a、已初始化的全局变量
    // b、已初始化的static修饰的局部变量    
    static int num4 = 200;
    // 3、.rodata段:存放常量数据
    // 1、整型常量:100(整型int), 200L(长整型long int), 300LL(长长整型long long int), 400ULL(无符号长长整型unsigned long long int)
    // 2、浮点型常量:3.14(双精度浮点型), 6.18L(长双精度浮点型)
    // 3、字符常量:'a', '8'
    // 4、字符串常量:"nihao"
    // 5、科学计数法常量:e
    
    //  栈区       数据段中rodata段(常量区)
    char buf[128] = "haoyangdefantuan";
    *(buf+0) = 'a'; // 可以的,因为修改的是buf数组里面的内存(已经将常量区的内存复制于此),而不是常量区的内存
    //  栈区       数据段中rodata段(常量区)
    char *p = "haoyangdefantua";
    *(p+0) = 'a';   // 不可以, 因为p指针只存放地址(这个地址在常量区,所以不能够修改数据),不存放内存
    return 0;
}  

4. 静态数据

静态数据有两种:

  • (1) 全局变量
  • (2) static修饰的变量
    作用:
    (1) 在工程中多个文件同时需要使用同一变量的时候,为了不用多次定义,采用全局变量的写法,在要使用该变量的其他文件中使用 extern 关键字进行声明,使得机器在工程里面寻找这个变量。
    文件1:
  
// File1.c
#include <stdio.h>
int global_var = 10;  // 定义全局变量
int main()
{
    printf("%d\n", global_var);
}  

文件2:

  
// File2.c
#include <stdio.h>
extern int global_var;  // 声明全局变量
int main()
{
    printf("%d\n", global_var);
}  

(2) static修饰的全局变量:为了保证代码的安全性,不被工程内别的.c文件使用 extern 访问其数据,在定义全局变量的位置使用 static 关键字。
(3) static修饰的局部变量:在调用完函数,退出函数时,希望这个值保留,下次再调用时不会重新初始化,就可以在定义这个局部变量时使用 static 关键字,简单来说就是,将这个局部变量的生命周期延长到程序结束。

  
void counter() 
{
    static int count = 0;  // 只初始化一次
    count++;
    printf("Count: %d\n", count);
    // 正常情况下,每次调用函数都会初始化一次
    // 但static修饰的局部变量会只用初始化一次,生命周期延长到程序结束
}
int main()
{    
    int i   = 0;
    while(i<5)
    {
        counter();
    }
    return 0;
}  

5. 动态内存管理

5.1 malloc && calloc

  • **所需头文件:

#include <stdlib.h>
**

  • 函数原型:
  • void *malloc(size_t size);
  • 用于申请一块指定大小的堆内存,参数
    size
    表示所申请的一块堆内存的大小,单位是字节。成功时返回指向分配好的堆内存的指针,失败则返回
    NULL
  • void *calloc(size_t num, size_t size);
  • 用来申请
    count
    块连续分布、无间隔的堆内存,每块大小为
    size
    字节。成功返回指向分配好的堆内存的指针,失败返回
    NULL

5.2 bzero

  • **所需头文件:

#include <strings.h>
**

  • 函数原型:
  • void bzero(void *s, size_t n);
  • 参数
    s
    是指向要清零的内存的指针,
    n
    是要清零的内存大小,单位是字节,该函数无返回值。

5.3 free

  • **所需头文件:

#include <stdlib.h>
**

  • 函数原型:
  • void free(void ptr)**;*
  • 参数
    ptr
    是堆内存指针,函数无返回值,用于释放之前通过
    malloc

    calloc
    等函数申请的堆内存。

5.4 realloc

  • 所需头文件:**

#include <stdlib.h>
**

  • 函数原型
  • void *realloc(void *ptr, size_t size);
  • 功能:用于重新分配堆内存。它可以改变之前通过
    malloc

    calloc
    等函数分配的内存块的大小。参数
    ptr
    是指向原来已分配的堆内存的指针,如果
    ptr

    NULL
    ,则相当于执行
    malloc(size)
    ;参数
    size
    表示重新分配后的内存块大小。
  • 返回值:成功时返回指向重新分配后的内存块的指针(可能与原来的指针地址相同,也可能不同);如果分配失败,返回
    NULL
    ,并且原来
    ptr
    所指向的内存块不会被释放;如果
    size
    为 0 且
    ptr
    不为
    NULL
    ,则释放
    ptr
    指向的内存块,返回
    NULL。
    示例代码:
  
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main(int argc, char const *argv[])
{
    // (1)、使用malloc函数申请内存空间
    // 1、定义并初始化
    char *p_buf = (char *)malloc(sizeof(char)*5);       // 申请定义个堆空间
    if (p_buf == NULL) 
    {
        fprintf(stderr, "Malloc failed\n");     // 输出错误信息
        return 1;
    }
    /*
        一般使用sizeof(数据类型)*数量来确定申请的堆空间大小,因为会更加精准,不浪费空间
        也可以使用malloc(100)来申请空间,但是可能会存放浪费空间的现象
    */
    bzero(p_buf, sizeof(char)*5);               // 通过p_buf指针去到堆空间,将其内存全部清零(malloc申请的空间里面没有初始化,可能有乱码)
    // 2、使用
    // a、指针样式的赋值操作
    for (int i = 0; i < 5; i++)
    {
       *(p_buf+i) = i;
        printf("*(p_buf+%d) == %d\n", i, *(p_buf+i));
    }
    // b、数组样式的赋值操作
    for (int i = 0; i < 5; i++)
    {
        p_buf[i] = i*10;
        printf("p_buf[%d] == %d\n", i, p_buf[i]);
    }
    
    // 3、释放堆空间
    free(p_buf);   
    /*
        说明:
- 释放堆内存意味着将堆内存的使用权归还给系统。
- 释放堆内存并不会改变指针的指向。
- 释放堆内存并不会对堆内存做任何修改,更不会将内存清零。
    */     
    for (int i = 0; i < 5; i++)
    {
        p_buf[i] = i*5;
        printf("p_buf[%d] == %d\n", i, p_buf[i]);
    }
    
    // (2)、calloc函数
    char *p2_buf = (char *)calloc(5, sizeof(char));  // 等同于malloc(sizeof(char)*5);以及已经初始化了
    if (p2_buf == NULL) 
    {
        fprintf(stderr, "Calloc failed\n");
        return 1;
    }
    for (int i = 0; i < 5; i++)
    {
       *(p2_buf+i) = i;
        printf("*(p2_buf+%d) == %d\n", i, *(p2_buf+i));
    }
    
    // (3)、realloc函数
    char *tmp = (char *)realloc(p2_buf, 10 * sizeof(char)); // 重新申请内存空间
    if (tmp == NULL) 
    {
        fprintf(stderr, "Realloc failed\n");
        return 1;
    }
    p2_buf = tmp; // 更新原指针
    for (int i = 0; i < 10; i++)
    {
        *(p2_buf+i) = i;
        printf("*(p2_buf+%d) == %d\n", i, *(p2_buf+i));
    }
    // 释放重新分配的内存
    free(p2_buf);
    return 0;
}
  
  • 释放堆内存的含义:
  • 释放堆内存意味着将堆内存的使用权归还给系统。
  • 释放堆内存并不会改变指针的指向。
  • 释放堆内存并不会对堆内存做任何修改,更不会将内存清零。

6.内存泄露以及解决方案

内存泄漏的基本概念:内存泄漏指的是在程序中动态分配的内存没有被正确释放,导致这部分内存无法再被使用,最终可能耗尽系统内存资源,影响程序性能甚至导致崩溃。
常见的内存泄漏的原因:
1. 忘记释放动态分配的内存:使用 malloc 、 calloc、realloc 分配内存后,没有调用 free 释放内存。
2. 指针丢失:重新赋值指针前没有释放原有内存,导致无法再访问原内存块。
3. 异常路径未释放内存:在程序执行过程中,如果存在异常或错误退出的分支(选择语句,提前退出函数),可能跳过内存释放的代码。
4. 递归释放:在递归函数中分配内存,没有对应正确的释放机制,导致内存泄露。
示例代码:

  
int *arr = malloc(100 * sizeof(int)); 
// 忘记 free(arr);  
  
int *ptr = malloc(100);
ptr = malloc(200); // 原内存块丢失,无法释放  
  
void func() 
{
    int *data = malloc(100);
    if (error) return; // 内存泄漏
    free(data);
}  

解决方法:
1. 使用完动态分配的内存后,及时使用free释放。
2. 释放完内存后,将对应的指针指向NULL,避免指针悬空,指向不可访问区域。
3. 使用检测工具,例如:Valgrind(Linux):检测内存泄漏和非法访问。 Linux系统重安装命令:sudo apt-get install valgrind 编译C程序:gcc -g -o test test.c 使用Valgrind检测内存泄漏: valgrind --leak-check=full ./test

7.结束语

内存管理是 C 语言编程中的关键内容,从基础概念到物理与虚拟内存的区别,再到内存布局、静态与动态内存管理,以及内存泄露问题,都对程序的稳定性和性能有着重要影响。掌握这些知识,能帮助开发者编写出更高效、健壮的代码,在 C 语言编程之路上走得更稳更远 。
每次写C语言笔记时,我都是想了又想,改了又改,因为我认为对于一些问题是比较严谨的,希望能够给读者和自己带来最好的解答,同时也希望看完的朋友,要是有疑问可以在评论区回答,看到后我会马上回答。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号