我应该如何对付内存泄漏?
写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了 new 操作、
delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,
以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂
性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于
将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型
之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自
己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有 string 和 vector 的
帮助,写出这个:
#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
int main() // small program messing around with strings
{
cout << "enter some whitespace-separated words:\n";
vector<string> v;
string s;
while (cin>>s) v.push_back(s);
sort(v.begin(),v.end());
string cat;
typedef vector<string>::const_iterator Iter;
for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";
cout << cat << '\n';
}
你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢?
注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过
使用函数对象和标准算法(standard algorithm),我可以避免使用指针——例如使用
迭代子(iterator),不过对于一个这么小的程序来说有点小题大作了。
这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人
的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容
易被跟踪。早在 1981 年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少
到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,
甚至更加简单了。
如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确
运行的话,最快的途径也许就是先建立一个这样的库。
模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异
常的使用使之更加完善。
如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句
柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过
一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我
们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了
标准库中的 auto_ptr,使需要为之负责的地方变得明确了。
#include<memory>
#include<iostream>
using namespace std;
struct S {
S() { cout << "make an S\n"; }
~S() { cout << "destroy an S\n"; }
S(const S&) { cout << "copy initialize an S\n"; }
S& operator=(const S&) { cout << "copy assign an S\n"; }
};
S* f()
{
return new S; // 谁该负责释放这个 S?
};
auto_ptr<S> g()
{
return auto_ptr<S>(new S); // 显式传递负责释放这个S
}
int main()
{
cout << "start main\n";
S* p = f();
cout << "after f() before g()\n";
// S* q = g(); // 将被编译器捕捉
auto_ptr<S> q = g();
cout << "exit main\n";
// *p产生了内存泄漏
// *q被自动释放
}
在更一般的意义上考虑资源,而不仅仅是内存。
如果在你的环境中不能系统地应用这些技巧(例如,你必须使用别的地方的代码,或者你的
程序的另一部分简直是原始人类(译注:原文是 Neanderthals,尼安德特人,旧石器时
代广泛分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器作为开发
过程的一部分,或者插入一个垃圾收集器(garbage collector)。
写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了 new 操作、
delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,
以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂
性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于
将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型
之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自
己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有 string 和 vector 的
帮助,写出这个:
#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
int main() // small program messing around with strings
{
cout << "enter some whitespace-separated words:\n";
vector<string> v;
string s;
while (cin>>s) v.push_back(s);
sort(v.begin(),v.end());
string cat;
typedef vector<string>::const_iterator Iter;
for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";
cout << cat << '\n';
}
你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢?
注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过
使用函数对象和标准算法(standard algorithm),我可以避免使用指针——例如使用
迭代子(iterator),不过对于一个这么小的程序来说有点小题大作了。
这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人
的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容
易被跟踪。早在 1981 年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少
到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,
甚至更加简单了。
如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确
运行的话,最快的途径也许就是先建立一个这样的库。
模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异
常的使用使之更加完善。
如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句
柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过
一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我
们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了
标准库中的 auto_ptr,使需要为之负责的地方变得明确了。
#include<memory>
#include<iostream>
using namespace std;
struct S {
S() { cout << "make an S\n"; }
~S() { cout << "destroy an S\n"; }
S(const S&) { cout << "copy initialize an S\n"; }
S& operator=(const S&) { cout << "copy assign an S\n"; }
};
S* f()
{
return new S; // 谁该负责释放这个 S?
};
auto_ptr<S> g()
{
return auto_ptr<S>(new S); // 显式传递负责释放这个S
}
int main()
{
cout << "start main\n";
S* p = f();
cout << "after f() before g()\n";
// S* q = g(); // 将被编译器捕捉
auto_ptr<S> q = g();
cout << "exit main\n";
// *p产生了内存泄漏
// *q被自动释放
}
在更一般的意义上考虑资源,而不仅仅是内存。
如果在你的环境中不能系统地应用这些技巧(例如,你必须使用别的地方的代码,或者你的
程序的另一部分简直是原始人类(译注:原文是 Neanderthals,尼安德特人,旧石器时
代广泛分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器作为开发
过程的一部分,或者插入一个垃圾收集器(garbage collector)。