使用智能指针的原因
会存在这样一种情况:申请的空间在函数结束后忘记释放,造成内存泄漏。智能指针的作用是管理指针,智能指针是一个类,当超出了类的作用域时,类的析构函数会被自动调用,无需手动释放内存。因此,智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。
C++智能指针
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr ,其中后三个是c++11支持,并且第一个已经被11弃用。因此,c++11中智能指针包括以下3种:共享指针(shared_ptr),独占指针(unique_ptr)及弱指针(weak_ptr)。
1. auto_ptr(c++98的方案,c++11已弃用)
auto_ptr对象通过初始化指向由new创建的动态内存,它是这块内存的拥有者,==一块内存不能同时被分给两个拥有者。==当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有的动态内存自动释放。
//利用已经存在的智能指针来构造新的智能指针
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,因为一块动态内存智能由一个智能指针独享,所以在拷贝构造或赋值时都会发生拥有权转移的过程。在此拷贝构造过程中,p1将失去对字符串内存的所有权,而p2将其获得。对象销毁时,p2负责内存的自动销毁。但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
2. unique_ptr(替换auto_ptr)
独享所有权的智能指针,保证同一时间内只有一个智能指针可以指向该对象。
//利用已经存在的智能指针来构造新的智能指针
unique_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
unique_ptr<string> p2;
p2 = p1; //unique_ptr会报错!!!
编译器认为p2 = p1非法,避免了p1不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
如果确实想执行类似与#1的操作,可以调用C++的标准库函数std::move(),即将一个unique_ptr对象赋给另一个unique_ptr对象。
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
链接: unique_ptr的使用场景.
3. shared_ptr
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的)。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()
来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared
函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。调用release()
时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放,从而避免了内存泄漏。
shares_ptr的: 使用场景.
智能指针的实现原理: 计数原理。
#include <iostream>
#include <memory>
template <typename T>
class SmartPtr
{
private :
T *_ptr;
size_t *_count;
public:
SmartPtr(T *ptr = nullptr) : _ptr(ptr)
{
if (_ptr)
{
_count = new size_t(1);
}
else
{
_count = new size_t(0);
}
}
~SmartPtr()
{
(*this->_count)--;
if (*this->_count == 0)
{
delete this->_ptr;
delete this->_count;
}
}
SmartPtr(const SmartPtr &ptr) // 拷贝构造:计数 +1
{
if (this != &ptr)
{
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}
SmartPtr &operator=(const SmartPtr &ptr) // 赋值运算符重载
{
if (this->_ptr == ptr._ptr)
{
return *this;
}
if (this->_ptr) // 将当前的 ptr 指向的原来的空间的计数 -1
{
(*this->_count)--;
if (this->_count == 0)
{
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++; // 此时 ptr 指向了新赋值的空间,该空间的计数 +1
return *this;
}
T &operator*()
{
assert(this->_ptr == nullptr);
return *(this->_ptr);
}
T *operator->()
{
assert(this->_ptr == nullptr);
return this->_ptr;
}
size_t use_count()
{
return *this->count;
}
};
4. weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象,weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr设计的目的是用来解决shared_ptr相互引用时的死锁问题。如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放,从而导致内存泄漏。weak_ptr是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock
函数来获得shared_ptr。
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了。
//把类A里面的shared_ptr pb_; 改为weak_ptr pb_
class A
{
public:
weak_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:
shared_ptr p = pa->pb_.lock();
p->print();
不要使用相同的原始指针来创建多个shared_ptr对象
1、尽量不要使用相同的原始指针来创建多个shared_ptr对象,因为在这种情况下,不同的shared_ptr对象不会知道它们与其他shared_ptr对象共享指针。
这样会造成什么样的问题?
设想两个shared指针由相同的原始指针创建:
int *rawPtr = new int();
std::shared_ptr ptr_1(rawPtr);
std::shared_ptr ptr_2(rawPtr);
假设ptr_2超出范围,那么它将删除关联的原始指针,这样ptr1就会指向一个悬挂指针。
所以,当ptr_1超出范围,那么它将再次尝试删除相关的内存,这实际上是一个悬挂的指针。因此程序会崩溃。
实例:
#include
#include
typedef struct Sample {
Sample() {
internalValue = 0;
std::cout << “Constructor” << std::endl;
}
~Sample() {
std::cout << “Destructor” << std::endl;
}
}Sample;
int main() {
{
Sample* rawPtr = new Sample();
std::shared_ptr ptr_1(rawPtr);
{
std::shared_ptr ptr_2(rawPtr);
}
//由于ptr_2不知道另一个shared_ptr(即ptr_1)使用了相同的原始指针,
//因此这里当ptr_2超出了作用域,并删除了与之关联的原始指针。
//现在ptr_1在内部包含一个悬挂指针。 因此,当我们将超出范围,
//它将再次尝试删除已经删除的原始指针,应用程序将崩溃。
}
return 0;
}
2、不要从栈而不是堆的内存中创建shared_ptr对象
检查下面的例子
#include
#include
int main() {
int x = 12;
std::shared_ptr ptr(&x);
return 0;
}
shared_ptr期望与它关联的内存来自堆,因此当它超出范围时,如果引用计数为0,则删除关联的内存。但是在上面的例子中,我们将shared_ptr对象与栈上的内存关联,在其析构函数中,这个shared_ptr对象将调用这个栈内存上的delete操作。
由于这块内存是栈而不是堆,因此程序会崩溃。
因此,建议不要直接从原始指针创建shared_ptr,而是应该使用make_shared<>
std::shared_ptr ptr_1 = make_shared();
std::shared_ptr ptr_2(ptr_1);