深入学习C++——44智能指针
我们都知道使用new在堆上分配内存,使用delete释放内存,因为它不会自动释放内存。智能指针本质上是一个原始指针的包装类,当创建一个智能指针,它会调用new并为其分配内存,基于这个智能指针,这些内存会在某一时刻自动释放。下面介绍三种智能指针
使用智能指针的时候要包含memory头文件
作用域指针unique_ptr
unique_ptr
是作用域指针,超出作用域时会被销毁,然后调用delete。
我们不能复制一个unique_ptr,因为如果复制一个unique_ptr,那么他们会指向同一个内存块。如果其中一个死了,它会释放那段内存,而指向同一块内存的另一个指针就会指向已经被释放的内存。
我们首先创建一个类来理解智能指针,类中只包含构造和析构函数
class Entity
{
public:
Entity()
{
std::cout << "Created Entity" << std::endl;
}
~Entity()
{
std::cout << "Destroyed Entity" << std::endl;
}
void Print() {}
};
在特定的作用域下创建一个unique_ptr:在main中创建一对大括号,括号内即为空作用域。在里面使用unique_ptr来分配Entity:
int main()
{
{
std::unique_ptr<Entity> entity = new Entity(); //错误
}
}
这样构造会报错,因为unique_ptr的构造函数是explicit的,需要显式调用构造函数,没有构造函数的隐式转换,所以应该:
int main()
{
{
std::unique_ptr<Entity> entity(new Entity()); //正确
}
}
不过更推荐使用下面这种方法!原因是出于异常安全。如果构造函数碰巧抛出异常,使用make_unique会保证你最终得到的不是没有引用的悬空指针,从而造成内存泄漏。
int main()
{
{
std::unique_ptr<Entity> entity = std::make_unique<Entity>();
}
}
然后就可以跟原始指针一样地使用智能指针,比如使用箭头操作符调用函数:
int main()
{
{
std::unique_ptr<Entity> entity = std::make_unique<Entity>();
entity->Print();
}
}
单步调试,可以看到程序在进入作用域{时输出Created Entity
,出作用域}时输出Destroyed Entity
,即为智能指针的自动创建与销毁。
如果需要拷贝或共享这个指针,使得这个指针可以被传递到一个函数中或一个类中,unique_ptr将不可用。如果试图复制一个unique_ptr:
int main()
{
{
std::unique_ptr<Entity> entity = std::make_unique<Entity>();
std::unique_ptr<Entity> e0 = entity;
}
}
这样会报错,因为在unique_ptr的定义中,删除了拷贝构造函数和拷贝构造操作符,因为这是不被允许的。这是为了防止你跳到大坑里,因为其中一个unique_ptr死了,这个堆分配对象的底层内存会被释放,另一个unique_ptr会指向这个不存在的内存。所以出现了共享指针shared_ptr:
共享指针shared_ptr
共享指针shared_ptr更牛逼一点,shared_ptr实现的方式实际上取决于编译器和你在编译器中使用的标准库,在大多数情况下,它使用的是引用计数。引用计数基本上是一种方法,可以跟踪你的指针有多少个引用,一旦引用计数达到0,它就会被删除。如果我创建了一个shared_ptr,又创建了另外一个shared_ptr来复制他,此时的引用计数就是2,当第一个shared_ptr死了,引用计数减1变成1,当最后一个shared_ptr也死了,引用计数变为0,这个指针也就会被销毁内存被释放。
但是不要这样写!!!
int main()
{
{
std::shared_ptr<Entity> entity(new Entity());
}
}
在unique_ptr中不直接调用new的原因是因为异常安全,而在shared_ptr中有所不同。因为shared_ptr需要分配另一块内存,叫做控制块,用来存储引用计数。如果使用new创建一个Entity然后传递给shared_ptr构造函数,那么它必须做两次内存分配:先做一次new Entity的分配,然后是shared_ptr的控制内存块的分配。使用make_shared就可以将两个步骤合起来:
int main()
{
{
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
}
}
shared_ptr可以被复制。
int main()
{
{
std::shared_ptr<Entity> entity = std::make_shared<Entity>();
std::shared_ptr<Entity> e0 = sharedEntity;
}
}
我们更改一下main函数,创建两个作用域来演示:
int main()
{
{//作用域1
std::shared_ptr<Entity> e0;
{//作用域2
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
e0 = sharedEntity;
}
// 此时SharedEntity已经死亡,但是e0还存活(引用计数为1),所以这里没有调用析构函数
}
//这里e0也死亡了(引用计数为0),此时调用析构函数。
}
单步调试,进入作用域1时输出Created Entity
,此时出作用域2,并没有析构Entity,因为e0还存活,并且持有对该Entity的引用。出作用域1,输出Destroyed Entity
,所有的引用此时都消失。
弱指针weak_ptr
将一个shared_ptr赋值给另一个shared_ptr时会增加引用计数,但是将一个shared_ptr赋值给一个weak_ptr时不会增加引用计数。这常用于:如果你不想要Entity的所有权,例如你在排序一个Entity列表,你不关心他们是否有效,只需要存储一个他们的引用,这时就可以使用weak_ptr。可以询问weak_ptr底层对象是否还存活,但它不会保持底层对象存活,因为它不会增加引用计数。
int main()
{
{//作用域1
std::weak_ptr<Entity> e0;
{//作用域2
std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>();
e0 = sharedEntity; // 弱指针不会引用计数
}//出作用域2的时候就调用了析构函数
//此时weak_ptr为一个无效指针
}
}
综上,使用智能指针可以使内存管理自动化,防止忘记调用delete而造成内存泄露。优先选用unique_ptr,如需在作用域之间复制共享就使用shared_ptr。