RAII
RAII(Resource Acquisition Is Initialization
)资源获取即初始化。
它是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。至此我们可以看到实际上就是把一份资源的责任托管给了一个对象。
这种做法有两大好处:
-
不需要显式释放资源。析构时自动调用。
-
采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针是RAII
思想的一种具像
c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用
一、shared_ptr
std::shared_ptr也即智能指针,采用RAII手法,是一个模版对象。std::shared_ptr表示某一个资源的共享所有权。
线程安全
shared_ptr指针类有两个成员变量,一个是指向变量的指针;一个是资源被引用的次数,引用次数加减操作内部自动加锁解锁,是线程安全的。
指针和引用计数是线程安全的,但指针所指对象中的操作就需要自己做控制,并不是线程安全的。因为shared_ptr 有两个数据成员(指向被管理对象的指针,和指向控制块的指针),读写操作不能原子化。
线程安全case1
多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护
shared_ptr<long> global_instance = make_shared<long>(0);
std::mutex g_i_mutex;
void thread_fcn()
{
//std::lock_guard<std::mutex> lock(g_i_mutex);
//shared_ptr<long> local = global_instance;
for(int i = 0; i < 100000000; i++)
{
*global_instance = *global_instance + 1;
//*local = *local + 1;
}
}
int main(int argc, char** argv)
{
thread thread1(thread_fcn);
thread thread2(thread_fcn);
thread1.join();
thread2.join();
cout << "*global_instance is " << *global_instance << endl;
return 0;
}
在线程函数thread_fcn的for循环中,2个线程同时对*global_instance进行加1的操作。这就是典型的非线程安全的场景,最后的结果是未定的,运行结果如下:
*global_instance is 197240539
如果使用的是每个线程的局部shared_ptr对象local,因为这些local指向相同的对象,因此结果也是未定的,运行结果如下:
*global_instance is 160285803
因此,这种情况下必须加锁,将thread_fcn中的第一行代码的注释去掉之后,不管是使用global_instance,还是使用local,得到的结果都是:
*global_instance is 200000000
线程安全case 2
• 一个 shared_ptr 对象实体可被多个线程同时读取(文档例1);
• 两个 shared_ptr 对象实体可以被两个线程同时写入(例2),“析构”算写操作;
• 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁(例3~5)。
请注意,以上是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。
shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
C++ shared_ptr的线程安全问题_c++ shared_ptr 线程安全-CSDN博客
https://www.cnblogs.com/gqtcgq/p/7492772.html
shared_ptr的API使用
函数API | 含义 |
---|---|
p.get() | 返回p中保存的指针。小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了 |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,注意用于调试 |
p.unique() | 若p.use_count()为1,返回true;否则返回false |
p = q | p和q都是shared_ptr,所保存的指针能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放 |
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象 |
shared_ptr<T> p(q) | p是share_ptr q的拷贝;此操作会递增q中的计数器。q中的指针类型必须能转换为T* |
shared_ptr<T> p(u) | p从unique_ptr u那里接管了对象的所有权;将u置为空 |
shared_ptr<T> p(p2,d) | p是share_ptr p2的拷贝;此操作会递增p2中的计数器。p2中的指针类型必须能转换为T*,p将使用可调用的对象d来代替delete |
p.reset() | p置为空。若p是唯一指向其对象的shared_ptr,reset会释放此对象 |
p.reset(q) | 传递了参数内置指针q,会令p指向q |
p.reset(q,d) | 传递了参数d,将会调用d而不是delete来释放q |
p.swap(q)或swap(p,q) | 交换两个智能指针 |
可以通过如下两种方式创建std::shared_ptr对象
auto p = std::shared_ptr<T>(new T);
auto p = std::make_shared<T>(T{});
shared_ptr的拷贝构造函数会增加引用计数,而移动构造函数不会增加引用计数。
别名构造函数
template <class U> shared_ptr (const shared_ptr<U>& x, element_type* p) noexcept; 除了存储的指针是p。该对象不拥有 p,也不会管理其存储。相反,它共同拥有 x 的托管对象并算作 x 的一种额外使用。它还将在发布时删除 x 的指针(而不是 p)。它可以用来指向已经被管理的对象的成员。
https://www.cnblogs.com/chaohacker/p/14802112.html
Make_shared
优点
效率更高
异常安全
缺点
构造函数是保护或私有时,无法使用 make_shared
对象的内存可能无法及时回收
二、unique_ptr
1、如何创建unique_ptr
unique_ptr不像shared_ptr一样拥有标准库函数make_shared来创建一个shared_ptr实例。要想创建一个unique_ptr,我们需要将一个new 操作符返回的指针传递给unique_ptr的构造函数。
2、无法进行复制构造和赋值操作
unique_ptr没有copy构造函数,不支持普通的拷贝和赋值操作。
3、可以进行移动构造和移动赋值操作
unique_ptr虽然没有支持普通的拷贝和赋值操作,但却提供了一种移动机制来将指针的所有权从一个unique_ptr转移给另一个unique_ptr。如果需要转移所有权,可以使用std::move()函数。
4、可以返回unique_ptr
unique_ptr不支持拷贝操作,但却有一个例外:可以从函数中返回一个unique_ptr。
int main()
{
// 创建一个unique_ptr实例
unique_ptr<int> pInt(new int(5));
unique_ptr<int> pInt2(pInt); // 报错
unique_ptr<int> pInt3 = pInt; // 报错
}
int main()
{
unique_ptr<int> pInt(new int(5));
unique_ptr<int> pInt2 = std::move(pInt); // 转移所有权
//cout << *pInt << endl; // 出错,pInt为空
cout << *pInt2 << endl;
unique_ptr<int> pInt3(std::move(pInt2));
}
unique_ptr<int> clone(int p)
{
unique_ptr<int> pInt(new int(p));
return pInt; // 返回unique_ptr
}
int main() {
int p = 5;
unique_ptr<int> ret = clone(p);
cout << *ret << endl;
}