持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情
内存泄漏
在C++中,内存是由我们开发人员来分配和释放的,和Java、Golang等拥有GC的语言不同的是,C++中,若是发生了内存泄漏,则可能导致我们的程序crash掉。
内存泄漏可以大致分为 忘记释放和错误释放的情况 (并不严谨,这是我个人的分类方法
忘记释放
对于忘记释放来说,可能是 new 了内存,不释放,一点点的积累起来之后,慢慢的,我们的操作系统就无内存可分配了,还可能导致频繁的缺页,最后被OOM kill 掉我们的进程。
忘记释放内存
int main() {
while (true)
for (int i = 0; i < 10000; i ++) {
new int;
}
return 0;
}
错误释放
对于错误释放来说,可能是用 new 分配的内存,然后我们则用 delete[] 去释放掉了这块内存,反之亦然。new 和 delete的不同步使用会给我们带来大麻烦,或者说,也许你很小心,每一个new 都配对了对应的delete。可是,比如一个类没有定义虚析构函数,然后你却使用一个父类指针(子类的内存)去delete之后,这样释放显然并没有释放完内存,这都属于在错误释放第一类中。
new delete 不配对
int main() {
auto t = new int;
delete[] t;
t = new int[10];
delete t;
return 0;
}
未释放完
class Base {
public:
~Base() { std::cout << "delete base" << std::endl; }
};
class T : public Base {
int arr[100];
public:
~T() { std::cout << "T base" << std::endl; }
};
int main() {
Base *b = new T();
delete b;
return 0;
}
错误释放的第二类则是在于我们的重复释放一块内存,new了一次,结果却delete了两次;这种情况什么时候有呢?假设你的类中有一个成员有一块动态内存,是动态分配的,可是你并没有实现拷贝函数,结果导致了多个对象共享一块动态内存。然后在析构时,就发生的多次释放。
多次释放
class A {
int *p;
public:
A() : p(new int[100]) {}
~A() { std::cout << "delete to: " << p << std::endl; }
};
int main() {
A a;
A b = a;
return 0;
}
上述举的几个例子都是很基础的小例子,真实的内存泄漏,情况比这个复杂的多
那么,我们如何去检测有没有内存泄漏呢?
检测内存泄漏
自己动手
自己动手,丰衣足食(bushi
那么,作为一个C++程序员,当然想的是去造一遍已经有的轮子了(刻板印象
我们来想一想,如何才能做到知道我们分配了多少内存呢?
我们分配内存都是通过 new 和 delete 来分配的 ( c++ 中
c++ 亲切的为我们提供了 new 和 delete 关键字
它会调用 operator new(size) 和 operator delete(void*) 来对我们进行内存管理
我们可以使用重载的方式,重载全局的new 和delete来实现我们的操作
在c++中,如果我们没有提供 new 和 delete,那么,系统会内建帮我们生成一个new 和 delete的
但是,我们重载之后,就不会再有了
可以重载 new 和 delete 之后,我们就可以玩出许多花样了
void *operator new(std::size_t size) {
std::cout << "new\n";
return malloc(size);
}
void operator delete(void *ptr) {
std::cout << "delete\n";
return free(ptr);
}
比如说,我们可以 重载 new(size_t),那么在其后面加一点参数不过分吧!
void *operator new(size_t size, const char *file_name, long line) {
std::cout << file_name << " " << line << std::endl;
return malloc(size);
}
欸,现在我就可以通过调用 new (__FILE__, __LINE__)new (__FILE__, __LINE__) xxx 这样的方式,来实现调用的行号定位了
我们还可以通过 宏定义 #define new new(__FILE__, __LINE__)的形式,我们就可以使用少量的代码,去侵入式的统计我们的内存泄漏问题
当然,这个对于C++来说还是有些不够用,因为,比如你用某一个库、或者某一个其他的调用,其中的内存统计,你是统计不了的,因为malloc和free不是受你所控制的结果
那么,我们其实可以hook掉malloc和free来试试,这是一个思路,我们去动系统的动态链接库,然后搞一点我们自己的统计代码进去,这样,我们就可以实现无缝的统计一些信息了
上述的方式是在自己的角度来考虑的这一件事
工具
那么,其实,我们有一些好用的工具来帮助我们检测内存泄漏问题
就拿Linux 平台来说
valgrind 提供了一些 debug 和优化的工具的工具箱,可以使得你的程序减少内存泄漏或者错误访问
valgrind 会默认使用 memcheck 去检查内存问题。
valgrind 的使用也是比较简单的一个工具,就如下图使用一般,还是比较清晰明了的
> g++ -g -o test test.cpp
> valgrind --tool=memcheck ./test
还有一些其实的内存检测软件,比如 debug_new(Windows)、Visual Leak Detecter (Windows)、Bounds Checker(Windows)、mtrace (Linux)、memwatch(Linux)
防范内存泄漏
那么,我们如何去防止内存泄漏呢?
GC?
的确,垃圾回收是个好东西,让别人帮我操心内存回收这一回事
我们不管内存回收,这个语言一下子就简洁了不少(我没内涵Java!
其实rust的所有权机制也蛮不错的,可以保证内存安全,就是写链表有点麻烦(
那么,在C++中,C++11为我们提供了几个管理内存的工具
其核心是引用计数
对一块内存的引用计数为0了,那么这块内存就应该被回收
那么,C++中拥有RAII机制,可以方便快捷使我们不用在意栈上对象的析构
离开作用域会自动帮我们析构
那么,利用引用计数的思想,其实,就可以大致解决内存问题
目前,C++ 11从Boost库中提取了三个管理内存的类来用
分别是 shared_ptr, unique_ptr, weak_ptr
对于这三个来说,unique_ptr就像rust中的所有权一样,我只能移动转移出去,并且没有拷贝语义
shared_ptr的话,就是一个引用计数的例子,可以有多个对象拥有一个实例
直到最后才会释放掉内存
引用计数也是有所缺陷的,有循环计数的情况,即互相引用
struct B;
struct A {
std::shared_ptr<B> b;
~A() { std::cout << "delete" << std::endl; }
};
struct B {
std::shared_ptr<A> a;
~B() { std::cout << "delete" << std::endl; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;
return 0;
}
可以自己试试看,上述代码并不会在离开作用域后自己析构,内存并没有释放
所有,拥有了引用计数,但并不是完美的,也要时时刻刻注意
这个时候就引入了我们的weak_ptr,这个指针不会增加引用,不会出现问题