内存泄漏是 C++ 中常见的问题之一,可能导致程序运行时资源消耗过大、性能下降,甚至程序崩溃。
内存泄漏的原因
1. 未释放动态分配的内存
在 C++ 中,通过 new
操作符分配的内存需要手动使用 delete
操作符进行释放。如果忘记或者由于某种原因未释放内存,就会导致内存泄漏。
cppCopy code
int* myArray = new int[10];
// 漏掉 delete 操作
2. 循环引用
在使用智能指针时,如果存在循环引用,指针之间的引用计数可能永远不会降为零,导致内存泄漏。
cppCopy code
class Node {
public:std::shared_ptr<Node> next;
};
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1; // 循环引用
3. 异常导致的资源泄漏
在发生异常的情况下,如果没有适当地释放资源,就可能发生内存泄漏。在异常发生时,确保已经分配的资源得到正确释放是很重要的。
cppCopy code
try {int* myData = new int[100];// 可能会发生异常// ...delete[] myData; // 在发生异常时可能不会执行到这里
} catch (...) {// 处理异常
}
4.虚析构
请问:在STL中std::string等容器能否被继承,为什么?
回答:不能,因为继承需要父类析构函数为virtual
,而string类它的析构函数没有提供虚函数。事实上**std::vector
、std::list
、std::deque
、std::set
、std::map
等容器都不是设计为可继承的。它们的设计目标是提供高效、通用的数据结构,而不是作为基类供其他类进行继承。
如果继承了string类会怎样?
答案:这样会引起内存泄漏
。
例如:
class Base {public:Base(){buffer_ = new char[10];}~Base() {std::cout << "in Base::~Base" << std::endl;delete []buffer_;}
private:char *buffer_;
};
class Derived : public Base {public:Derived(){}~Derived() {std::cout << "int Derived::~Derived" << std::endl;}
};
int main() {Base *base = new Derived;delete base;return 0;
}
上面代码输出如下:
in Base::~Base
可见,上述代码并没有调用派生类Derived的析构函数,如果派生类中在堆上申请了资源,那么就会产生内存泄漏
。
为了避免因为继承导致的内存泄漏,我们需要将父类的析构函数声明为virtual
,代码如下(只列了部分修改代码,其他不变):
~Base() {std::cout << "in Base::~Base" << std::endl;delete []buffer_;}
然后重新执行代码,输出结果如下:
int Derived::~Derived
in Base::~Base
总结下存在继承情况下,构造函数和析构函数的调用顺序。
派生类对象在创建时构造函数调用顺序:
- 调用父类的构造函数
- 调用父类成员变量的构造函数
- 调用派生类本身的构造函数
派生类对象在析构时的析构函数调用顺序:
- 执行派生类自身的析构函数
- 执行派生类成员变量的析构函数
- 执行父类的析构函数
为了避免存在继承关系时候的内存泄漏,请遵守一条规则:无论派生类有没有申请堆上的资源,请将父类的析构函数声明为virtual
。
如何避免内存泄漏
1. 使用智能指针
C++11 引入的智能指针(如 std::shared_ptr
和 std::unique_ptr
)可以在对象生命周期结束时自动释放内存,减少手动内存管理的负担。
std::shared_ptr<int> myInt = std::make_shared<int>(42);
2. RAII(资源获取即初始化)原则
通过使用 RAII,可以确保资源在对象构造时获得,并在对象析构时释放。这种方法减少了手动管理资源的需要,降低了内存泄漏的风险。
class FileHandler {
public:FileHandler(const std::string& fileName) : file(std::fopen(fileName.c_str(), "r")) {if (!file) {throw std::runtime_error("Failed to open file");}}~FileHandler() {if (file) {std::fclose(file);}}// 其他成员函数...
private:FILE* file;
};
3. 使用工具检测内存泄漏
使用工具如 Valgrind、cppcheck等来检测和分析内存泄漏问题。或者一些商用的代码静态分析工具(例如SonarQube)。这些工具能够在运行时提供详细的内存分析报告,帮助找到潜在的问题。
内存泄漏的定位方法
1. 静态分析工具
使用静态分析工具(如cppcheck)可以在编译阶段检测到一些潜在的内存泄漏问题。这些工具通过分析源代码来查找可能导致内存泄漏的模式。
cppcheck --enable=all --inconclusive --std=posix source.cpp
cppcheck还可以搭配jenkins使用,实现自动编译分析,并进行图形化显示。在Jenkins中已经有cppcheck的插件,所以它可以配合jenkins使用。Jenkins可以对cppcheck检测后的结果进行处理,并且可以将结果图形化的显示。
2. 动态分析工具
使用动态分析工具(如 Valgrind)来运行程序,检查其内存使用情况。Valgrind 特别适用于检测未释放的内存。
valgrind --leak-check=full ./your_program
在Linux上比较常用的内存泄漏检测工具是valgrind
,所以咱们就以valgrind为工具,进行检测。
我们首先看一段代码:
#include <stdlib.h>void func (void){char *buff = (char*)malloc(10);
}int main (void){func(); // 产生内存泄漏return 0;
}
- 通过
gcc -g leak.c -o leak
命令进行编译 - 执行
valgrind --leak-check=full ./leak
在上述的命令执行后,会输出如下:
==9652== Memcheck, a memory error detector
==9652== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9652== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==9652== Command: ./leak
==9652==
==9652==
==9652== HEAP SUMMARY:
==9652== in use at exit: 10 bytes in 1 blocks
==9652== total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==9652==
==9652== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==9652== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==9652== by 0x40052E: func (leak.c:4)
==9652== by 0x40053D: main (leak.c:8)
==9652==
==9652== LEAK SUMMARY:
==9652== definitely lost: 10 bytes in 1 blocks
==9652== indirectly lost: 0 bytes in 0 blocks
==9652== possibly lost: 0 bytes in 0 blocks
==9652== still reachable: 0 bytes in 0 blocks
==9652== suppressed: 0 bytes in 0 blocks
==9652==
==9652== For lists of detected and suppressed errors, rerun with: -s
==9652== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
valgrind的检测信息将内存泄漏分为如下几类:
- definitely lost:确定产生内存泄漏
- indirectly lost:间接产生内存泄漏
- possibly lost:可能存在内存泄漏
- still reachable:即使在程序结束时候,仍然有指针在指向该块内存,常见于全局变量
主要上面输出的下面几句:
==9652== by 0x40052E: func (leak.c:4)
==9652== by 0x40053D: main (leak.c:8)
提示在main函数(leak.c的第8行)fun函数(leak.c的第四行)产生了内存泄漏,通过分析代码,原因定位,问题解决。
valgrind不仅可以检测内存泄漏,还有其他很强大的功能,由于本文以内存泄漏为主,所以其他的功能就不在此赘述了,有兴趣的可以通过valgrind --help
来进行查看。
3. 编写自定义的内存分析工具
通过编写自定义的内存分析工具,可以在应用程序中插入代码来跟踪内存分配和释放的情况,从而帮助定位内存泄漏。
#define new DEBUG_NEWvoid* operator new(size_t size, const char* file, int line) {void* p = malloc(size);// 记录分配信息...return p;
}#define DEBUG_NEW new(__FILE__, __LINE__)
在编写代码时,结合使用上述方法,可以显著减少内存泄漏的风险。通过良好的代码设计、使用智能指针和工具的辅助,可以更容易地避免和解决内存泄漏问题。
参考C/C++ 内存泄漏-原因、避免以及定位