RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原
理
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
auto_ptr的主要特点是实现了独占式所有权,即一个auto_ptr对象拥有对动态分配对象的唯一所有权。当auto_ptr被销毁时,它会自动释放所拥有的对象。
然而,auto_ptr存在一些严重的问题,限制了它的使用:
所有权转移:当auto_ptr被拷贝或赋值时,所有权会从源对象转移到目标对象,源对象会变成空指针。这种特性在函数参数传递和返回值时尤其需要注意,因为它可能导致意外的资源释放或悬空指针。
在拷贝构造时,原指针被置空,如果后续代码仍然尝试访问原 auto_ptr 对象所指向的内存,这将导致悬空指针问题,进而可能引发程序崩溃或不可预测的行为。
不支持数组:auto_ptr不能用于管理动态分配的数组,因为它使用delete而不是delete[]来释放资源,这会导致未定义行为。
不支持STL容器:由于auto_ptr的拷贝行为(所有权转移),它不适合作为STL容器的元素,因为容器在内部可能会进行元素的拷贝或赋值操作。
unique_ptr
为了解决auto_ptr的问题,C++11中开始提供更靠谱的unique_ptr
std::unique_ptr也是独占所有权的智能指针,同时禁止拷贝操作,提供了移动操作,允许在需要时转移所有权,这种设计避免了意外的所有权转移和相关的风险。
unique_ptr还提供了对数组的支持。通过指定模板参数为 T[],unique_ptr 可以使用 delete[] 来正确释放数组资源。
支持STL容器
下面简化模拟实现了一份UniquePtr来了解它的原理
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
通过将拷贝构造函数和拷贝赋值运算符声明为 delete,禁止了 unique_ptr 的拷贝操作。这是 unique_ptr 设计中的一个关键部分,因为它确保了资源的独占所有权,并防止了由于拷贝操作而导致的所有权转移问题。
share_ptr
C11还提供了share_ptr和weak_ptr
std::shared_ptr允许多个智能指针共同拥有同一个资源。它们通过内部的计数器来跟踪有多少个std::shared_ptr实例指向该资源。当最后一个std::shared_ptr被销毁或重置时,资源才会被释放。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
可以通过use.count()函数来查看当前资源的引用计数
struct Node
{
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
int _val;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<Node>p1(new Node);
shared_ptr<Node>p2(new Node);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1 = p2;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
return 0;
}
shared_ptr的线程安全问题
智能指针(如C++中的std::shared_ptr、std::unique_ptr等)主要用于自动管理堆上分配的内存,确保资源在不再需要时能够被正确释放,从而避免内存泄漏。然而,智能指针本身并不解决多线程环境下的线程安全问题。
当两个或多个线程同时访问由智能指针管理的堆上对象时,如果这些访问涉及到对对象状态的修改(即非只读访问),就可能发生数据竞争(data race),这是一种未定义行为,可能导致程序崩溃、数据损坏或不可预测的行为。
为了在多线程环境中安全地共享由智能指针管理的对象,需要采取额外的同步措施,例如使用互斥锁(mutexes)、读写锁(reader-writer locks)、原子操作(atomic operations)或其他同步机制来确保对共享资源的访问是互斥的。
可以参考C++线程中的原子操作。
shared_ptr循环引用
struct Node
{
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
int _val;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<Node>node1(new Node);
shared_ptr<Node>node2(new Node);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
分析:
看运行结果发现没有调用析构函数。
node1和node2两个智能指针对象指向两个节点,引用计数变成1。
node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
由于相互指向的关系,_next析构了,node2就释放了,_prev析构了,node1就释放了。
但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
为了解决循环引用的问题,我们引用了weak_ptr。
weak_ptr
std::weak_ptr是一种不拥有其所指向对象的智能指针,它主要用来解决std::shared_ptr可能引起的循环引用问题。它必须和一个std::shared_ptr一起使用,但不能单独用来管理资源。
weak_ptr的_next和_prev不会增加node1和node2的引用计数。
struct Node
{
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
int _val;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<Node>node1(new Node);
shared_ptr<Node>node2(new Node);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}