我发现智能指针比原始指针舒服得多。因此,始终使用智能指针是一个好主意吗?(请注意,我来自Java背景,因此与显式内存管理的概念不太相似。因此,除非智能指针存在一些严重的性能问题,否则我将坚持使用它们。)
注意:尽管我来自Java背景,但我对智能指针的实现和RAII的概念非常了解。因此,发布答案时,您可以从我这方面将这些知识视为理所当然。我几乎在所有地方都使用静态分配,并且仅在必要时使用指针。我的问题仅仅是:我可以一直使用智能指针代替原始指针吗?
我发现智能指针比原始指针舒服得多。因此,始终使用智能指针是一个好主意吗?(请注意,我来自Java背景,因此与显式内存管理的概念不太相似。因此,除非智能指针存在一些严重的性能问题,否则我将坚持使用它们。)
注意:尽管我来自Java背景,但我对智能指针的实现和RAII的概念非常了解。因此,发布答案时,您可以从我这方面将这些知识视为理所当然。我几乎在所有地方都使用静态分配,并且仅在必要时使用指针。我的问题仅仅是:我可以一直使用智能指针代替原始指针吗?
Answers:
鉴于进行了几次编辑,我觉得全面的总结会很有用。
1.什么时候不去
在两种情况下,不应使用智能指针。
首先是完全相同的情况,您不应使用 C++类。IE:DLL边界(如果不向客户端提供源代码)。让我们说轶事。
第二种情况经常发生:聪明的经理意味着所有权。您可以使用指针指向现有资源,而无需管理其寿命,例如:
void notowner(const std::string& name)
{
Class* pointer(0);
if (name == "cat")
pointer = getCat();
else if (name == "dog")
pointer = getDog();
if (pointer) doSomething(*pointer);
}
此示例受约束。但是,指针在语义上与引用不同,因为它可能指向无效的位置(空指针)。在这种情况下,最好不要使用智能指针,因为您不想管理对象的生存期。
2.聪明的经理
除非您正在编写智能管理器类,否则如果使用关键字 delete ,则会做错事情。
这是一个有争议的观点,但是在回顾了很多有缺陷的代码示例之后,我不再冒险了。因此,如果您编写new新的内存,则需要一个智能管理器。您现在就需要它。
这并不意味着您不算程序员!相反,重用已被证明可以工作的代码而不是一遍又一遍地重复开发轮子是一项关键技能。
现在,真正的困难开始了:哪个聪明的经理?
3.智能指针
有各种各样的智能指针,具有各种特征。
std::auto_ptr通常应该避免的跳过(其复制语义不正确)。
scoped_ptr:无开销,无法复制或移动。unique_ptr:无开销,无法复制,可以移动。shared_ptr/ weak_ptr:可以复制一些开销(引用计数)。通常,尝试使用scoped_ptr或unique_ptr。如果需要多个所有者,请尝试更改设计。如果您不能更改设计并确实需要多个所有者,请使用shared_ptr,但要注意应该使用weak_ptr中间的某个地方来破坏引用循环。
4.智能容器
许多智能指针并不是要复制的,因此它们在STL容器中的使用会有所妥协。
与其求助于shared_ptr其开销,不如使用Boost Pointer Container中的智能容器。它们模拟经典STL容器的接口,但存储它们拥有的指针。
5.自己动手
在某些情况下,您可能希望推出自己的智能管理器。请检查您是否不仅仅是错过了正在使用的库中的某些功能。
在出现异常的情况下编写智能管理器非常困难。通常,您不能假定内存可用(new可能会失败)或Copy Constructor具有no throw保证。
在某种程度上,可以忽略该std::bad_alloc异常并强加Copy Constructor许多帮助程序不会失败...毕竟,这就是boost::shared_ptr其删除器D模板参数的作用。
但是我不推荐这样做,特别是对于初学者。这是一个棘手的问题,您现在不太可能注意到这些错误。
6.例子
// For the sake of short code, avoid in real code ;)
using namespace boost;
// Example classes
// Yes, clone returns a raw pointer...
// it puts the burden on the caller as for how to wrap it
// It is to obey the `Cloneable` concept as described in
// the Boost Pointer Container library linked above
struct Cloneable
{
virtual ~Cloneable() {}
virtual Cloneable* clone() const = 0;
};
struct Derived: Cloneable
{
virtual Derived* clone() const { new Derived(*this); }
};
void scoped()
{
scoped_ptr<Cloneable> c(new Derived);
} // memory freed here
// illustration of the moved semantics
unique_ptr<Cloneable> unique()
{
return unique_ptr<Cloneable>(new Derived);
}
void shared()
{
shared_ptr<Cloneable> n1(new Derived);
weak_ptr<Cloneable> w = n1;
{
shared_ptr<Cloneable> n2 = n1; // copy
n1.reset();
assert(n1.get() == 0);
assert(n2.get() != 0);
assert(!w.expired() && w.get() != 0);
} // n2 goes out of scope, the memory is released
assert(w.expired()); // no object any longer
}
void container()
{
ptr_vector<Cloneable> vec;
vec.push_back(new Derived);
vec.push_back(new Derived);
vec.push_back(
vec.front().clone() // Interesting semantic, it is dereferenced!
);
} // when vec goes out of scope, it clears up everything ;)
std::auto_ptr大多数人所期望的不同,但是它是有道理的,可以避免代码中的设计问题,说“通常避免”是没有意义的。
auto_ptr在C ++ 0x中弃用,建议使用unique_ptr代替;-)来犯一个可怕的错误。公平unique_ptr地说auto_ptr,如果要在C ++ 03中获得严格的可转让所有权,则需要依靠移动构造/分配来替代它,您没有太多选择。
智能指针确实执行显式的内存管理,如果您不了解它们的工作方式,那么在使用C ++进行编程时会遇到麻烦。请记住,内存并不是他们管理的唯一资源。
但是要回答您的问题,您应该更喜欢使用智能指针作为解决方案的首选,但可能会在必要时准备抛弃它们。如果可以避免,则永远不要使用指针(或任何排序)或动态分配。例如:
string * s1 = new string( "foo" ); // bad
string s2( "bar" ); // good
编辑:要回答您的补充问题“我可以始终使用智能指针代替原始指针吗?那么,不可以。如果(例如)您需要实现自己的operator new版本,则必须使它返回一个指针,而不是智能指针。
通常,如果不需要指针,则不应使用它们(智能指针或其他指针)。更好地使局部变量,类成员,向量元素和类似项目成为普通对象,而不是指向对象的指针。(由于您来自Java new,因此可能不愿意使用分配所有内容,因此不建议这样做。)
这种方法(“ RAII ”)使您大部分时间不必担心指针。
当您必须使用指针时,这取决于具体情况以及为什么需要指针,但是通常可以使用智能指针。并非总是(以粗体显示)最佳选择,但这取决于具体情况。
boost::scoped_ptr然后仍可以使用,但是也许您不想再依赖boost了吗?)-或者您需要与C API接口,在这种情况下,原始指针更加一致。如果您需要遍历数组,则迭代器也可能是原始指针。
auto_ptr,unique_ptr也shared_ptr可以像将原始指针从范围转让中传递出去一样。scoped_ptr是一组将无法转移/股权的唯一智能指针
好时光不是使用智能指针的位于DLL的接口边界。您不知道是否将使用相同的编译器/库来构建其他可执行文件。系统的DLL调用约定不会指定标准或TR1类的外观,包括智能指针。
在可执行文件或库中,如果要表示指针的所有权,则平均而言,智能指针是最佳的实现方式。因此,最好始终优先使用原始数据而不使用原始数据。您是否真的可以始终使用它们是另一回事。
举一个具体的例子-假设您正在编写一个通用图的表示形式,其顶点由对象表示,边缘由对象之间的指针表示。通常的智能指针将无济于事:图可以是循环的,没有特定的节点可以负责其他节点的内存管理,因此共享和弱指针不足。例如,您可能将所有内容放入向量中并使用索引而不是指针,或者将所有内容放入双端队列并使用原始指针。您可以shared_ptr根据需要使用它,但是除了开销之外,它不会添加任何其他内容。或者,您可以寻找标记扫描GC。
一个更微不足道的情况:我更喜欢看到函数通过指针或引用获取参数,并保证不保留指针或对其的引用,而不是接受ashared_ptr并让您想知道函数返回后是否保留引用,也许您将再次修改引荐,并且会破坏某些内容,等等。不保留引言通常是没有明确记录的,这不用多说。也许不应该,但是可以。智能指针暗示了有关所有权的某些信息,并错误地暗示了这种混淆。因此,如果您的函数采用shared_ptr,请确保记录是否可以保留引用。
在许多情况下,我相信它们绝对是正确的方法(减少混乱的清理代码,降低泄漏的风险等)。但是有一些非常小的额外费用。如果我正在编写一些必须尽可能快的代码(例如必须进行一些分配和释放的紧密循环),则我可能不会使用智能指针来希望提高速度。但是我怀疑这在大多数情况下是否会带来可衡量的变化。
scoped_ptr)
shared_ptr在共享信息块的分配中都有开销,并根据共享信息检查是否要取消分配。但是使用单一所有权的智能指针,析构函数不需要执行任何测试:只需删除内部指针。示例:libstdc ++:,~auto_ptr() { delete _M_ptr; }boost 1.37:~scoped_ptr() { checked_delete(ptr); }其中checked_delete是编译时检查类型完整性和对的单个调用delete,很可能会内联。
如果要处理资源,则应始终使用RAII技术,对于内存而言,则意味着使用某种形式或另一种形式的智能指针(注意:智能,而不是 shared_ptr,请选择最适合您特定用例的智能指针)。这是避免出现异常时泄漏的唯一方法。
在某些情况下,当不通过指针处理资源管理时,则需要原始指针。特别地,它们是拥有可重置参考的唯一方法。考虑将引用保留到无法显式处理其生存期的对象(成员属性,堆栈中的对象)中。但这是一个非常特殊的情况,我在真实代码中只见过一次。在大多数情况下,使用ashared_ptr是共享对象的更好方法。
我对智能指针的看法:很棒,当很难知道何时会发生释放时(例如在try / catch块中,或者在调用函数(甚至是构造函数!)的函数内部,这可能使您退出当前函数) ,或向代码中到处返回的函数添加更好的内存管理。或将指针放在容器中。
但是,智能指针的代价是您可能不想为整个程序付出代价。如果易于手动进行内存管理(“嗯,我知道该功能结束时,我需要删除这三个指针,并且我知道该功能将运行完成”),那么为什么要浪费计算机的时间呢?它?
auto_ptr就是or的意思scoped_ptr。对于他们而言,创建可测量的开销是很少见的,同时,它们使得更容易正确编写代码。例如,如果获取这三个指针中的第二个引发异常,您是否释放第一个?与使用智能指针相比,您需要编写多少代码?你多久真的获得资源,你需要释放,但如果你的收购不能失败?
是的,但是我没有使用智能指针或任何指针就进行了多个项目。使用容器(如双端队列,列表,地图等)是一种很好的做法。在可能的情况下,我也使用引用。我没有传递指针,而是传递了引用或const引用,并且删除/释放引用几乎总是不合逻辑的,因此我在那里从来就没有问题(通常我会通过编写在堆栈上创建它们){ Class class; func(class, ref2, ref3); }