c++内存管理

本文详细介绍了C++中的内存分区,包括栈、堆、全局区、常量区和代码区,并通过示例代码阐述了内存的分配与释放。同时,讨论了栈与堆的主要区别,如申请方式、系统响应、空间连续性和申请效率。针对内存泄漏问题,文章给出了预防措施,如内部封装、智能指针,并讲解了内存泄漏检测工具valgrind的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++ 内存分区

:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 栈 --> env

// 程序示例
#include <iostream>
using namespace std;

/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var;  // gp_var 在全局区(.bss 段)

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = "abc";         // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
    char *p_var1 = "123456";    // p_var1 在栈区;"123456"为字符串常量,存储在常量区
    static int s_var = 0;       // s_var 为静态变量,存在静态存储区(.data 段)
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}

栈和堆的区别

1. 申请方式:栈是系统自动分配的内存,堆由程序员控制。
2. 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出。分配堆空间,堆在内存中的呈现方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中,该块空间的首地址存放的是本块空间的大小,便于释放,将剩余的内存空间接回链表。
3. 栈在内存中是连续的一块空间(向低地址存储),最大容量是系统预定好的;而堆在内存中的空间是不连续的(向高地址扩展)。
4. 申请效率:栈是系统自动分配的,效率高,但程序员无法控制;堆由程序员分配,一般速度比较慢,使用起来方便但容易产生碎片。
5. 存储内容:栈存放的是局部变量,函数参数,返回地址和返回数据;堆存放的的内容由程序员控制。

内存泄漏

内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。

并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。

防止内存泄漏的方法:

  1. 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。
#include <iostream>
#include <cstring>

using namespace std;

class A
{
private:
    char *p;
    unsigned int p_size;

public:
    A(unsigned int n = 1) // 构造函数中分配内存空间
    {
        p = new char[n];
        p_size = n;
    };
    ~A() // 析构函数中释放内存空间
    {
        if (p != NULL)
        {
            delete[] p; // 删除字符数组
            p = NULL;   // 防止出现野指针
        }
    };
    char *GetPointer()
    {
        return p;
    };
};
void fun()
{
    A ex(100);
    char *p = ex.GetPointer();
    strcpy(p, "Test");
    cout << p << endl;
}
int main()
{
    fun();
    return 0;
}

说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况,请看如下程序:

void fun1()
{
    A ex(100);
    A ex1 = ex; 
    char *p = ex.GetPointer();
    strcpy(p, "Test");
    cout << p << endl;
}

简单解释:对于 fun1 这个函数中定义的两个类的对象而言,在离开该函数的作用域时,会两次调用析构函数来释放空间,但是这两个对象指向的是同一块内存空间,所以导致同一块内存空间被释放两次,可以通过增加计数机制来避免这种情况,看如下程序:

#include <iostream>
 #include <cstring>

 using namespace std;
 class A
 {
 private:
     char *p;
     unsigned int p_size;
     int *p_count; // 计数变量
 public:
     A(unsigned int n = 1) // 在构造函数中申请内存
     {
         p = new char[n];
         p_size = n;
         p_count = new int;
         *p_count = 1;
         cout << "count is : " << *p_count << endl;
     };
     A(const A &temp)
     {
         p = temp.p;
         p_size = temp.p_size;
         p_count = temp.p_count;
         (*p_count)++; // 复制时,计数变量 +1
         cout << "count is : " << *p_count << endl;
     }
     ~A()
     {
         (*p_count)--; // 析构时,计数变量 -1
         cout << "count is : " << *p_count << endl; 

         if (*p_count == 0) // 只有当计数变量为 0 的时候才会释放该块内存空间
         {
             cout << "buf is deleted" << endl;
             if (p != NULL) 
             {
                 delete[] p; // 删除字符数组
                 p = NULL;   // 防止出现野指针
                 if (p_count != NULL)
                 {
                     delete p_count;
                     p_count = NULL;
                 }
             }
         }
     };
     char *GetPointer()
     {
         return p;
     };
 };
 void fun()
 {
     A ex(100);
     char *p = ex.GetPointer();
     strcpy(p, "Test");
     cout << p << endl;

     A ex1 = ex; // 此时计数变量会 +1
     cout << "ex1.p = " << ex1.GetPointer() << endl;
 }
 int main()
 {
     fun();
     return 0;
 }

程序运行结果:

count is : 1
Test
count is : 2
ex1.p = Test
count is : 1
count is : 0
buf is deleted

  1. 智能指针:是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用。

内存泄漏检测工具的实现原理:
内存检测工具有很多,这里重点介绍下 valgrind

在这里插入图片描述
valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合,包括以下工具:

  • Memcheck:内存检查器(valgrind 应用最广泛的工具),能够发现开发中绝大多数内存错误的使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
  • Callgrind:检查程序中函数调用过程中出现的问题。
  • Cachegrind:检查程序中缓存使用出现的问题。
  • Helgrind:检查多线程程序中出现的竞争问题。
  • Massif:检查程序中堆栈使用中出现的问题。
  • Extension:可以利用 core 提供的功能,自己编写特定的内存调试工具。

Memcheck 能够检测出内存问题,关键在于其建立了两个全局表:

  • Valid-Value 表:对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits ;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
  • Valid-Address 表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。

检测原理:

  • 当要读写内存中某个字节时,首先检查这个字节对应的 Valid-Address 表中对应的 bit。如果该 bit 显示该位置是无效位置,Memcheck 则报告读写错误。
  • 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节在 Valid-Value 表对应的 bits 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 Memcheck 会检查 Valid-Value 表对应的 bits,如果该值尚未初始化,则会报告使用未初始化内存错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值