删除 STL 序列容器中指针的 Functor






4.48/5 (16投票s)
一个基于策略的删除 Functor,可与 for_each 函数一起使用。
引言
STL 容器的一个优点是它们会自动为我们处理内存。通过使用 vector 或 deque 等标准顺序容器,而不是直接处理 new[]
和 delete[]
运算符,我们可以避免内存泄漏的可能性,并使我们的代码具有异常安全性。例如,如果在函数中需要一个整数的动态数组,如果我们使用 std::vector<int>
而不是 int*
,我们将不必担心在退出函数之前调用 delete 来释放内存。
vector<int> vectorArray (30); // the memory will be automatically freed ... int* oldStyleArray = new int[30]; // be sure to call delete[] oldStyleArray;
然而,如果我们把指针放入标准容器中,事情就会变得更加复杂。
struct C { char data_[50]; }; ... vector <C*> dataArray; for (int i = 0; i < 20 ; ++i) dataArray.push_back(new C());
在 dataArray
退出作用域后,只有指针本身占用的内存会被释放,但对象本身将保持未删除状态,除非我们这样做:
for (vector <C*>::iterator it = dataArray.begin(), it != dataArray.end(); ++it) delete *it;
在 dataArray
退出作用域之前。
处理此问题的最佳方法是永远不要将原始指针放入容器中,而是使用智能指针,例如 boost::shared_ptr
[1] 或 Loki::SmartPtr
[2]。
vector <boost::shared_ptr<C> > dataArray; for (int i = 0; i < 20 ; ++i) dataArray.push_back(boost::shared_ptr<C> (new C()));
现在,我们无需担心释放对象。
但是,在某些情况下,我们仍然希望在 STL 顺序容器中保留原始指针,在这种情况下,我们可以使用一种比手动编写循环更简单的方法来清理内存。一种常见的解决方案是编写一个 Functor,使我们能够使用诸如 for_each
[3] 之类的 STL 算法。
for_each(dataArray.begin(), dataArray.end(), del_fun<C>());
这种方法更好,但仍有许多不足之处。首先,它总是调用 operator delete
,这对于使用 new[]
或 malloc
创建的对象的情况并不适用。此外,在某些情况下,我们可能希望在删除指针后将其清零。
为了解决这些问题,我实现了一个基于策略的 Functor free_ptr
。
free_ptr
的设计
在使用 Functor 删除对象时,我们面临两个独立的选择:
- 使用哪个函数来删除对象?
- 删除后是否将指针清零?
为了让用户做出选择,我使用了两个单独的策略:CleanUpPolicy
和 ZeroPtrPolicy
。
template <typename T, class CleanUpPolicy = DeleteSingleObject, class ZeroPtrPolicy = DontZeroPtr> struct free_ptr : std::unary_function <T*, void> { void operator()(T*& ptr) { CleanUpPolicy::Destroy(ptr); ZeroPtrPolicy::Zero(ptr); } };
得益于基于策略的设计,free_ptr
非常易于使用,同时又高度可定制。对于最常见的情况(使用 new 创建的单个对象,无需清零指针),可以这样使用:
for_each(dataArray.begin(), dataArray.end(), free_ptr<C>());
如果对象是使用 new[]
创建的,并且我们需要在删除后清零指针,我们可以这样写:
for_each(dataArray.begin(), dataArray.end(), free_ptr<C, DeleteArray, ZeroPtr>());
对于 CleanUpPolicy
,我们有以下选择:
DeleteSingleObject
(默认)– 对每个指针调用delete
。DeleteArray
– 对每个指针调用delete[]
。Free
– 对每个指针调用free()
。
对于 ZeroPtrPolicy
,我们有两个选择:
DontZeroPtr
(默认)– 不执行任何操作。ZeroPtr
- 将每个指针设置为零。
在理想情况下,我们甚至不需要 CleanUpPolicy
– 如果指向单个对象的指针与指向对象数组的指针是不同的类型,我们可以使用一些模板元编程在编译时识别正确的场景。然而,显然情况并非如此,我尝试找出一些运行时机制来做到这一点。我惨败而归:如果我们只有一个指向内存的指针,似乎没有好的方法来区分内存是使用 new
、new[]
还是 malloc
分配的。我甚至尝试了一些非便携的技巧,例如使用 _memsize
来确定分配内存块的大小,但是,即使我们知道分配的内存量,它仍然不一定能显示指针指向的对象数量。因此,我能想到的最好的方法是 CleanUpPolicy
。这种解决方案的一个好处是,在某些自定义内存管理方案的情况下,编写另一个 CleanUpPolicy
类并像使用我提供的类一样容易地使用它会非常简单。
我可能应该补充一点,使用 free_ptr
无法保护您免受某些糟糕的编程实践。例如,如果您这样做:
vector <C*> dataArray; dataArray.push_back(new C()); //single object dataArray.push_back(new C[10]); //array of objects dataArray.push_back((C*)malloc(sizeof(C))); // malloc-ed object
那么 free_ptr 将无法帮助您。正如我所说,不幸的是,前面的示例会编译,但这只是 C 的另一个遗留问题,使得 C++ 不够完美。
结论
我希望这篇小文章能以两种方式为您带来帮助:它展示了策略设计,这是一种用于开发 C++ 库的强大方法,并且它还为您提供了一个很酷的小代码片段,您可以在日常编程任务中找到它的用途。
参考文献
- [1] Boost 智能指针,https://boost.ac.cn/libs/smart_ptr/smart_ptr.htm
- [2] Andrei Alexandrescu:《Modern C++ Design – Generic Programming and Design Patterns Applied》,Addison-Wesley 2001。
- [3] 用于 STL 容器的 del_fun 函数适配器,http://www.geocities.com/w_steiner/Professional/del_fun.html