代码改变世界

深入解析:C++ 内存泄漏检测器设计

2025-10-07 08:11  tlnshuju  阅读(14)  评论(0)    收藏  举报


在这里插入图片描述


1. C++中的动态内存分配

动态内存分配不同于固定大小的静态数组,通过 new 和 delete让我们可以根据需要随时申请和释放内存

先来看段代码:

#
include <iostream>
  using
  namespace std;
  int main(
  ) {
  // 动态分配单个整型变量
  int* pInt =
  new
  int
  ;
  *pInt = 20
  ;
  cout <<
  "动态分配的整数数值为: " <<
  *pInt << endl;
  // 释放内存
  delete pInt;
  // 动态分配数组
  int arraySize;
  cout <<
  "请指定数组的大小: "
  ;
  cin >> arraySize;
  int* dynamicArray =
  new
  int[arraySize]
  ;
  for (
  int i = 0
  ; i < arraySize; i++
  ) {
  dynamicArray[i] = i * 3
  ;
  }
  cout <<
  "数组中的元素为: "
  ;
  for (
  int i = 0
  ; i < arraySize; i++
  ) {
  cout << dynamicArray[i] <<
  " "
  ;
  }
  cout << endl;
  // 释放数组内存
  delete[] dynamicArray;
  return 0
  ;
  }

上面的代码,做了如下事情:

动态分配单个整型变量:

  1. 内存分配:new int在堆上分配 int 类型的内存。
  2. 指针操作:通过*pInt解引用赋值和读取。
  3. 内存释放:使用delete释放单个对象内存。

动态分配数组:

  1. 内存分配:new int[arraySize]分配连续内存块。
  2. 指针类型:dynamicArray指向数组首元素(类型为int*)。
  3. 数组释放:使用delete[]释放数组内存。

注意事项:无论是单个变量还是数组,动态分配的内存都需要通过 delete 或 delete[] 进行释放。

2. 什么是内存泄漏

内存泄漏是指程序在运行过程中申请了内存空间却未及时释放,导致随着程序运行时间增长,占用的内存持续累积,最终可能耗尽系统可用资源。在 C++ 编程中,避免内存泄漏需牢记以下要点:

  1. 内存释放的配对原则:
    每次通过 new 分配的单个内存对象,必须使用 delete 进行释放;对于通过 new[] 动态分配的数组内存,则需用 delete[] 释放。
  2. 系统不会自动回收动态内存:
    与栈内存不同,堆内存的释放完全依赖开发者手动操作。若忘记释放已申请的动态内存,系统无法自动清理这些 “闲置” 内存,它们会一直驻留在内存中,形成内存泄漏。所以在动态内存使用完毕后,应立即执行释放操作。

3. 内存泄漏的代码案例

1. 基础堆内存未释放(Basic Heap Leak):

new分配的内存未通过delete释放。

int* p =
new
int
;
// 未释放

2. 数组未正确释放(Array Deallocation Mismatch):

new[]分配的数组必须用delete[]释放。若使用delete而非delete[],仅释放首元素内存,其余元素内存泄漏。

int* arr =
new
int[10]
;
// delete arr; // 错误写法
// 正确应使用 delete[] arr;

3. 异常路径未释放(Exception Safety Issue)

异常抛出后,delete语句被跳过,内存未释放。

int* data =
new
int
;
throw std::runtime_error("Oops!"
)
;
// 异常导致delete跳过
delete data;
// 永远不会执行

4. 类成员未释放(Class Member Leak)

动态分配的成员变量buf未在析构函数中释放。当对象销毁时,buf指向的内存仍被占用。

class LeakyClass {
public:
LeakyClass(
) : buf(
new
char[1024]
) {
}
~LeakyClass(
) {
} // 析构函数未释放buf
private:
char* buf;
}
;

5. 容器指针未清理(Container of Pointers)

vector存储的是原始指针,容器销毁时不会自动释放指针指向的内存。

std::vector<
int*> vec;
for (
int i = 0
; i <
5
;
++i) {
vec.push_back(
new
int(i)
)
;
// 未释放元素
}

4. 内存泄漏检查器的设计

原理:通过重载 new/new[] 和 delete/delete[] 运算符,在内存分配/释放时插入跟踪逻辑,在内存分配时:记录内存地址、大小、分配位置(FILELINE),在内存释放时:从跟踪表中移除记录。

模块1:位置信息捕获:

通过预处理器宏替换,在每次内存分配时自动捕获源代码位置信息:文件名和行号。

关键技术点:

  1. 利用__FILE__和__LINE__预定义宏获取当前位置。
  2. 通过宏替换将普通new操作转换为带位置信息的版本。
  3. 避免在实现文件中应用宏替换:防止递归定义。
// mem_leak_detector.hpp
#
ifndef MEM_LEAK_DETECTOR_IMPLEMENTATION
// 关键替换:所有new操作被转换为带位置信息的版本
#
define new new(__FILE__
, __LINE__
)
#
endif
// 位置感知的内存分配运算符声明
void*
operator
new(size_t size,
const
char* file,
int line)
;
void*
operator
new[](size_t size,
const
char* file,
int line)
;

模块2:内存分配跟踪:

重载全局内存分配函数,在分配时记录内存信息。

关键技术点:

  1. 重载operator new和operator new[]捕获分配请求。
  2. 使用malloc进行实际内存分配。
  3. 分配成功后记录指针、大小和位置信息。
  4. 处理分配失败情况返回nullptr。
// mem_leak_detector.cpp
// 重载的全局new运算符
void*
operator
new(size_t size,
const
char* file,
int line) {
void* p = malloc(size)
;
// 实际内存分配
if (p) {
// 记录分配信息:指针、大小、文件名、行号
MemLeakDetector::track(p, size, file, line)
;
}
return p;
}
// 数组版本转发给普通new
void*
operator
new[](size_t size,
const
char* file,
int line) {
return
operator
new(size, file, line)
;
}
// 跟踪函数实现
void MemLeakDetector::track(
void* p, size_t size,
const
char* file,
int line) {
char* file_copy = strdup(file)
;
// 复制文件名(确保长期有效)
allocations[p] = MemAllocRecord{
p, size, file_copy, line
}
;
}

模块3:内存释放跟踪:

重载全局内存释放函数,在释放时移除跟踪记录。

关键技术点:

  1. 重载operator delete和operator delete[]。
  2. 释放前从跟踪表中移除记录。
  3. 使用free进行实际内存释放。
// mem_leak_detector.cpp
// 重载的全局delete运算符
void
operator
delete(
void* p)
noexcept {
MemLeakDetector::untrack(p)
;
// 从跟踪表中移除
free(p)
;
// 实际内存释放
}
// 数组版本转发给普通delete
void
operator
delete[](
void* p)
noexcept {
operator
delete(p)
;
}
// 停止跟踪函数实现
void MemLeakDetector::untrack(
void* p) {
auto it = allocations.find(p)
;
if (it != allocations.end(
)
) {
free((
void*
)it->second.file)
;
// 释放复制的文件名
allocations.erase(it)
;
// 从映射表移除
}
}

模块4:泄漏记录存储:

使用全局数据结构存储所有未释放的内存分配记录。

关键技术点:

  1. 静态std::map存储分配记录—键:内存地址,值:分配信息。
  2. 使用strdup复制文件名,确保长期有效性。
  3. 在释放时清理复制的文件名。
// mem_leak_detector.hpp
// 内存分配记录结构
struct MemAllocRecord {
void* ptr;
// 内存地址
size_t size;
// 分配大小
const
char* file;
// 分配位置文件名
int line;
// 分配位置行号
}
;
// 静态存储定义
class MemLeakDetector {
private:
static std::map<
void*
, MemAllocRecord> allocations;
// 未释放内存记录表
// ...
}
;

模块5:报告生成:

程序退出时分析未释放记录,生成详细泄漏报告。

关键技术点:

  1. 通过atexit注册报告函数。
  2. 遍历所有未释放记录。
  3. 计算总泄漏字节数。
  4. 分类显示泄漏位置信息。
  5. 区分"无泄漏"和"有泄漏"情况。
// mem_leak_detector.cpp
// 生成泄漏报告:程序退出时自动调用
void MemLeakDetector::report(
) {
if (allocations.empty(
)
) {
std::cout <<
"\n[SUCCESS] No memory leaks detected\n"
;
}
else {
std::cout <<
"\n[MEMORY LEAKS] " << allocations.size(
)
<<
" leaks found:\n"
;
size_t total = 0
;
// 遍历所有未释放的记录
for (
const
auto& entry : allocations) {
const MemAllocRecord& record = entry.second;
std::cout <<
" Leak at " << record.ptr
<<
" (" << record.size <<
" bytes)"
<<
" allocated in " << record.file
<<
":" << record.line <<
"\n"
;
total += record.size;
// 累计泄漏字节数
}
std::cout <<
"Total leaked: " << total <<
" bytes\n"
;
}
}
// 初始化宏注册报告函数
#
define MEM_LEAK_DETECTOR_INIT(
) \
std::atexit(MemLeakDetector::report)

5. 测试案例

测试案例放在main.cpp文件中,内存泄漏检测文件由mem_leak_detector.cpp和mem_leak_detector.hpp组成,我使用的lab环境为 Ubuntu 系统。

执行以下命令看到结果:

g++ -std=c++11 -g mem_leak_detector.cpp main.cpp -o memtest
./memtest

在这里插入图片描述

main.cpp

#
include <vector>
  #
  include <stdexcept>
    #
    include "mem_leak_detector.hpp" // 引入内存泄漏检测器头文件
    // 基础内存泄漏示例:分配单个int未释放
    void basic_leak(
    ) {
    int* p =
    new
    int
    ;
    // 分配内存 (通过重载的new记录位置)
    }
    // 数组内存泄漏示例:分配int数组未释放
    void array_leak(
    ) {
    int* arr =
    new
    int[10]
    ;
    // 分配数组 (通过重载的new[]记录)
    }
    // 异常导致的内存泄漏:分配后抛出异常跳过delete
    void exception_leak(
    ) {
    int* data =
    new
    int
    ;
    throw std::runtime_error("Oops!"
    )
    ;
    // 抛出异常
    delete data;
    // 此句不会执行
    }
    // 类内泄漏示例:析构函数未释放成员指针
    class LeakyClass {
    public:
    LeakyClass(
    ) : buf(
    new
    char[1024]
    ) {
    } // 分配内存
    ~LeakyClass(
    ) {
    } // 析构函数未释放buf → 泄漏
    private:
    char* buf;
    }
    ;
    // 容器内存泄漏示例:vector存储指针未释放
    void container_leak(
    ) {
    std::vector<
    int*> vec;
    for (
    int i = 0
    ; i <
    5
    ;
    ++i) {
    vec.push_back(
    new
    int(i)
    )
    ;
    // 5次分配
    } // vector销毁时不会自动释放指针 → 5处泄漏
    }
    int main(
    ) {
    MEM_LEAK_DETECTOR_INIT(
    )
    ;
    // 注册退出时报告泄漏
    basic_leak(
    )
    ;
    // 产生1处泄漏
    array_leak(
    )
    ;
    // 产生1处泄漏 (数组)
    try {
    exception_leak(
    )
    ;
    // 抛出异常导致1处泄漏
    }
    catch (...
    ) {
    } // 捕获异常但不处理泄漏
    LeakyClass* cls =
    new LeakyClass(
    )
    ;
    // 类内泄漏 + 对象本身泄漏 → 共2处
    container_leak(
    )
    ;
    // 产生5处泄漏
    // 注意:未释放cls指针 → 额外泄漏LeakyClass对象
    return 0
    ;
    } // 程序退出时自动调用report()

mem_leak_detector.hpp

#
pragma once
#
ifndef MEM_LEAK_DETECTOR_HPP
#
define MEM_LEAK_DETECTOR_HPP
#
include <map>
  #
  include <string>
    #
    include <iostream>
      #
      include <cstdlib>
        // 内存分配记录结构体
        struct MemAllocRecord {
        void* ptr;
        // 分配的内存地址
        size_t size;
        // 分配的字节数
        const
        char* file;
        // 分配发生的源文件名
        int line;
        // 分配发生的代码行号
        }
        ;
        class MemLeakDetector {
        public:
        // 跟踪内存分配(由重载的new调用)
        static
        void track(
        void* p, size_t size,
        const
        char* file,
        int line)
        ;
        // 停止跟踪内存(由重载的delete调用)
        static
        void untrack(
        void* p)
        ;
        // 生成泄漏报告(程序退出时调用)
        static
        void report(
        )
        ;
        private:
        static std::map<
        void*
        , MemAllocRecord> allocations;
        // 未释放内存记录表
        }
        ;
        // 声明带位置信息的全局运算符重载
        void*
        operator
        new(size_t size,
        const
        char* file,
        int line)
        ;
        void*
        operator
        new[](size_t size,
        const
        char* file,
        int line)
        ;
        void
        operator
        delete(
        void* p)
        noexcept
        ;
        void
        operator
        delete[](
        void* p)
        noexcept
        ;
        // 初始化宏:注册报告函数到atexit
        #
        define MEM_LEAK_DETECTOR_INIT(
        ) \
        std::atexit(MemLeakDetector::report)
        // 在非实现文件中重定义new宏(捕获分配位置)
        #
        ifndef MEM_LEAK_DETECTOR_IMPLEMENTATION
        #
        define new new(__FILE__
        , __LINE__
        ) // 替换所有new为带位置信息的版本
        #
        endif
        #
        endif

mem_leak_detector.cpp

#
define MEM_LEAK_DETECTOR_IMPLEMENTATION // 启用实现模式
#
include "mem_leak_detector.hpp"
#
include <cstring>
  #
  include <cstdlib>
    #
    include <iostream>
      // 静态成员初始化:存储所有未释放的内存记录
      std::map<
      void*
      , MemAllocRecord> MemLeakDetector::allocations;
      // 跟踪内存分配:记录指针、大小、文件名和行号
      void MemLeakDetector::track(
      void* p, size_t size,
      const
      char* file,
      int line) {
      char* file_copy = strdup(file)
      ;
      // 复制文件名字符串
      allocations[p] = MemAllocRecord{
      p, size, file_copy, line
      }
      ;
      // 存入映射表
      }
      // 停止跟踪:内存释放时从映射表移除记录
      void MemLeakDetector::untrack(
      void* p) {
      auto it = allocations.find(p)
      ;
      if (it != allocations.end(
      )
      ) {
      free((
      void*
      )it->second.file)
      ;
      // 释放复制的文件名内存
      allocations.erase(it)
      ;
      // 移除记录
      }
      }
      // 生成泄漏报告:程序退出时自动调用
      void MemLeakDetector::report(
      ) {
      if (allocations.empty(
      )
      ) {
      std::cout <<
      "\n[SUCCESS] No memory leaks detected\n"
      ;
      }
      else {
      std::cout <<
      "\n[MEMORY LEAKS] " << allocations.size(
      )
      <<
      " leaks found:\n"
      ;
      size_t total = 0
      ;
      // 遍历所有未释放的记录
      for (
      const
      auto& entry : allocations) {
      const MemAllocRecord& record = entry.second;
      std::cout <<
      " Leak at " << record.ptr
      <<
      " (" << record.size <<
      " bytes)"
      <<
      " allocated in " << record.file
      <<
      ":" << record.line <<
      "\n"
      ;
      total += record.size;
      // 累计泄漏字节数
      }
      std::cout <<
      "Total leaked: " << total <<
      " bytes\n"
      ;
      }
      }
      // 重载全局new运算符:捕获分配位置信息
      void*
      operator
      new(size_t size,
      const
      char* file,
      int line) {
      void* p = malloc(size)
      ;
      // 实际内存分配
      if (p) {
      // 记录分配信息:指针、大小、文件名、行号
      MemLeakDetector::track(p, size, file, line)
      ;
      }
      return p;
      }
      // 重载全局new[]运算符:转发给带位置信息的new
      void*
      operator
      new[](size_t size,
      const
      char* file,
      int line) {
      return
      operator
      new(size, file, line)
      ;
      }
      // 重载全局delete运算符:释放内存并停止跟踪
      void
      operator
      delete(
      void* p)
      noexcept {
      MemLeakDetector::untrack(p)
      ;
      // 从跟踪表中移除
      free(p)
      ;
      // 实际释放内存
      }
      // 重载全局delete[]运算符:转发给delete
      void
      operator
      delete[](
      void* p)
      noexcept {
      operator
      delete(p)
      ;
      }