C++之智能指针

为什么会有智能指针

前面我们知道使用异常可能会导致部分资源没有被正常释放, 因为异常抛出之后会直接跳转到捕获异常的地方从而跳过了一些很重要的的代码, 比如说下面的情况:

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{int* p1 = new int;int* p2 = new int;cout << div() << endl;cout << "delete p1" << endl;delete p1;cout << "delete p2" << endl;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

main函数中调用了func函数, func函数里面调用了div函数, func函数中没有捕捉异常, 但是在main函数里面却捕捉了异常, 所以出现异常的话就会导致func函数中的部分代码没有被执行, 进而导致内存泄漏:

无异常抛出正常内存释放: 

有异常抛出, 内存泄漏:  

为了解决这个问题就可以异常重新抛出, 在func函数里面添加捕获异常的代码, 然后在catch里面对资源进行释放最后重新将异常进行抛出, 最后交给main函数中的catch进行处理, 比如说下面的代码: 

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{int* p1 = new int;int* p2 = new int;try {cout << div() << endl;}catch (...){cout << "delete p1" << endl;delete p1;cout << "delete p2" << endl;delete p2;throw;}cout << "delete p1" << endl;delete p1;cout << "delete p2" << endl;delete p2;
}
int main()
{try { Func(); }catch (exception& e){cout << e.what() << endl;}return 0;
}

无论除零是否有异常都会正常释放资源: 

但是这么写就完成正确了吗? 肯定没有, 因为new本身也是会抛异常的, 当内存不足却又使用new申请空间的话就会导致开辟空间失败从而抛出异常, 那new抛异常会导致什么结果呢?

首先p1抛出异常会有什么问题吗?

没有, p1抛出异常会直接跳转到main函数里面进行捕捉并且p2还没有开辟, p1没有开辟成功, 从而不会导致任何的内存泄漏.

那要是p2开辟失败了呢?

这时候也是会直接跳转到main函数里面进行捕捉, 但是p1已经开辟空间了, 如果p2开辟空间失败他会导致p1的申请的资源没有被正常释放, 所以为了安全起见我们给p2也添加一个try上去并且try块里面还得含有后面的代码, 因为一旦内存申请失败后面的调用函数也无需执行了.

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{int* p1 = new int;try{int* p2 = new int;try{cout << div() << endl;}catch (...){cout << "delete p1" << endl;delete p1;cout << "delete p2" << endl;delete p2;throw;}cout << "delete p1" << endl;delete p1;cout << "delete p2" << endl;delete p2;}catch (...){cout << "delete p1" << endl;delete p1;throw;}
}
int main()
{try { Func(); }catch (exception& e){cout << e.what() << endl;}return 0;
}

我们这里只new了两个对象, 那如果有三个有四个甚至更多的话又该如何来嵌套try catch呢? 所以当出现连续抛出异常的情况时, 我们之前学习的try catch语句就很难进行应对, 那么为了解决这个问题就有了智能指针


智能指针的使用及原理

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期控制程序资源(如内
存, 文件句柄, 网络连接, 互斥量等等)的简单技术.
在对象构造时获取资源, 接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构的时候释放资源. 借此, 我们实际上把管理一份资源的责任托管给了一个对象, 这种做法有两大好处:
1. 不需要显式地释放资源. 
2. 采用这种方式, 对象所需的资源在其生命期内始终保持有效.

注意: 智能指针是RAII的一种实现方式.


智能指针实现 

首先智能指针是一个类, 并且这个类要处理各种各样的数据, 所以这个类就要是模板类, 比如:

#pragma once
template<class T>
class SmartPoint
{
public:// RAIISmartPoint(T* ptr = nullptr):_ptr(ptr){}~SmartPoint(){cout << "~SmartPoint" << endl;delete _ptr;}
private:T* _ptr;
};

构造函数拿T类型的指针来初始化内部的_ptr就行, 析构函数在内部使用delete释放指针指向的空间即可.

#include<iostream>
using namespace std;
#include"SmartPoint.h"int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{SmartPoint<int> sp1 = new int;SmartPoint<int> sp2 = new int;cout << div() << endl;//有了智能指针就不需要显示释放资源//cout << "delete p1" << endl;//delete p1;//cout << "delete p2" << endl;//delete p2;
}int main()
{try { Func(); }catch (exception& e){cout << e.what() << endl;}return 0;
}

可以看到出现了除0错误这里也可以将两个申请的空间进行释放, 原理就是智能指针对象的生命周期属于Func函数, 当除0错误抛出异常的时候会从当前函数递归式的匹配catch直到main函数, 在匹配catch的时候虽然函数不会向下执行,但是函数栈帧会随之销毁, 其中类对象的生命周期也就跟着结束, 就会自动调用析构函数来释放空间, 就解决了之前的问题. 而且这样做不需要显式地释放资源, 而且对象所需的资源在其生命期内始终保持有效.


智能指针的原理

上述的SmartPtr还不能将其称为智能指针, 因为它还不具有指针的行为. 指针可以解引用, 也可以通过->去访问所指空间中的内容, 因此模板类中还需要将重载* 、->, 才可让其像指针一样去使用.

template<class T>
class SmartPoint
{
public:// RAIISmartPoint(T* ptr = nullptr):_ptr(ptr){}~SmartPoint(){cout << "~SmartPoint" << endl;delete _ptr;}// 像指针一样T& oprator* (){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
#include<iostream>
using namespace std;
#include"SmartPoint.h"struct Date
{int _year;int _month;int _day;
};int main()
{SmartPoint<int> sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPoint<Date> sparray(new Date);// 需要注意的是这里应该是sparray.operator->()->_year = 2018;// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->sparray->_year = 2023;sparray->_month = 12;sparray->_day = 16;cout << "year:" << sparray->_year<< endl;cout << "month:" << sparray->_month << endl;cout << "day:" << sparray->_day << endl;return 0;
}

总结一下智能指针的原理:

1. RAII特性.
2. 重载operator*和opertaor->, 具有像指针一样的行为.


C++库中的智能指针

在这之前我们首先来看看下面这段代码:

void func2()
{SmartPoint<int>sp1(new int(10));SmartPoint<int>sp2(sp1);
}int main()
{func2();return 0;
}

原因很简单我们自己实现的类里面没有拷贝构造函数, 所以默认的拷贝构造函数以浅拷贝的方式进行构造, 所以对象的生命周期结束时就会调用delete将同一份空间析构两次, 所以就会报错.

那么为了解决这个问题我们就得自己来实现一个拷贝构造函数, 可是这里的拷贝构造并不能是深拷贝, 因为我们这个类的目的是让它和指针一样对资源进行管理, 而并不是一个容器对资源进行存储. 所以这里就不能采用深拷贝的形式来进行拷贝构造, 那么库中是如何来解决这个问题的呢?

先看看最早的auto_ptr如何解决这个问题: 

std::auto_ptr

std::auto_ptr文档

 C++98版本的库中就提供了auto_ptr的智能指针. auto_ptr的实现原理是 管理权转移的思想.

简化模拟实现一份test::auto_ptr来了解它的原理 :

namespace test
{// C++98// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空template<class T>class auto_ptr{public:// RAIIauto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){if (_ptr){cout << "delete->" << _ptr << endl;delete _ptr;_ptr = nullptr;}}// ap2(ap1)auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){//管理权转移ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}

auto_ptr的解决方法就是将管理权进行转移, 把原来的智能指针变为空, 让新的智能指针指向这个空间, 

int main()
{std::auto_ptr<int> sp1(new int);std::auto_ptr<int> sp2(sp1); // 管理权转移// sp1悬空*sp2 = 10;cout << *sp2 << endl;cout << *sp1 << endl;//解引用空指针return 0;
}

结论: auto_ptr是一个失败设计, 很多场景明确要求不能使用auto_ptr, 原因就是auto_ptr使用的方式太不合常理了.

后来为了解决auto_ptr难用的问题, 就有了unique_ptrshare_ptr/weak_ptr .


std::unique_ptr

unique_ptr文档

	// C++11 template<class T>class unique_ptr{public:// RAIIunique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "delete->" << _ptr << endl;delete _ptr;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// C++11unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;private:// C++98// 1、只声明不实现// 2、限定为私有//unique_ptr(const unique_ptr<T>& up);//unique_ptr<T>& operator=(const unique_ptr<T>& up);private:T* _ptr;};

unique_ptr解决拷贝构造问题的思路简单粗暴, 直接禁止拷贝构造(拷贝构造设为私有或者C++11中设为delete).

int main()
{test::unique_ptr<int> sp1(new int);test::unique_ptr<int> sp2(sp1);test::unique_ptr<int> sp3(new int);sp3 = sp1;return 0;
}


std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr 

std::shared_ptr文档

shared_ptr的原理: 通过引用计数的方式来实现多个shared_ptr对象之间共享资源.

1. shared_ptr在其内部, 给每个资源都维护了着一份计数, 用来记录该份资源被几个对象共享
2. 在对象被销毁时(也就是析构函数调用), 就说明自己不使用该资源了, 对象的引用计数减一
3. 如果引用计数是0, 就说明自己是最后一个使用该资源的对象, 必须释放该资源;
4. 如果不是0, 就说明除了自己还有其他对象在使用该份资源, 不能释放该资源, 否则其他对象就成野指针了。 

 一个智能指针指向两块空间, 一块用于存储数据, 另一块记录当前空间被几个智能指针所指向, 当前只有对象sp1指向这个空间所以当前的计数就为1:

当我们再创建一个对象sp2并指向这个空间时图片就变成了:

当sp1对象生命周期结束, 或者sp1指向其他内容时, 就变成了: 

当引用计数为0时, 空间被释放: 

int main()
{std::shared_ptr<string> sp1(new string("xxxxxxxxxx"));std::shared_ptr<string> sp2 = sp1;std::shared_ptr<string> sp3;sp3 = sp1;std::shared_ptr<string> sp4(new string("xxxxxxxxxxxxxxxxx"));sp3 = sp4;cout << *sp1 << endl;cout << *sp2 << endl;cout << *sp3 << endl;cout << *sp4 << endl;return 0;
}

可以看到使用share_ptr既不会出现拷贝完原指针置空(auto_ptr)的情况, 也不会出现禁止拷贝(unique_ptr)的情况, 而且这个智能指针跟普通的指针一样指向的是同一块区域的内容.

模拟实现一下shared_ptr:

首先有个问题: 引用计数的空间如何来分配?

可以是个普通的整型变量放到对象里面吗?

不可以, 因为当计数变量的值发生改变时, 所有指向该空间对象的内部计数变量都得改变, 此时只改变一个对象的引用计数对其它对象内存储的引用计数毫无影响.

那么可以使用静态成员变量来实现吗?

看上去可以, 因为不管类实例化出来了多少个对象, 这个静态变量只有一个, 并且所有对象都会共享这个静态变量, 那么这时只要一个对象对这个静态变量进行修改的话, 其他对象都会跟着一起修改, 这是不是达到了我们的目的呢?

其实没有, 因为静态变量虽然一个对象修改所有对象都会共享, 但是这个对象指的是这个类所有实例化出来的对象, 假如又有一个智能指针指向一块新开辟的资源需要管理, 那么这个智能指针的引用计数需要初始化为1, 这就影响了之前的引用计数, 虽然是同一个类实例化出来的对象, 但是管理的资源可能会不同, 引用计数混在一起就乱套了.

正确的方法是在类里面添加一个整型的指针变量, 让指针指向new开辟的一块空间:

template<class T>
class shared_ptr
{
public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete->" << _ptr << endl;delete _ptr;delete _pcount;}}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;
};

构造函数中将引用计数初始化为1, 调用一次析构函数引用计数就--一次, 当引用计数为0时就释放被管理的资源和为引用计数开辟的那块资源. 

拷贝构造函数:

// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount)
{(*_pcount)++;
}

 拷贝构造函数比较简单, 把_ptr和_pcount拷贝过来然后引用计数++即可.

赋值重载就需要注意了: 

//sp2 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{//要考虑是不是自己给自己赋值if (_ptr != sp._ptr){if (--*(_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;
}

首先要判断要拷贝的被拷贝的对象指向的是不是同一块空间:

1. 如果是同一块空间(也就是自己给自己赋值)就没有必要进行拷贝, 或者说不能进行拷贝, 因为按照这个逻辑会先把引用计数--判断其是否为0决定原来的空间要不要释放, 如果当前引用计数不为1还好, 只是重复拷贝的问题, 但是如果引用计数为1, 先--引用计数然后把空间释放了, 再去拷贝这段已经被释放了的空间, 显然是不对的, 所以要先判断是否是自己给自己赋值.

自己给自己赋值不要用this != &sp来判断了, 因为这里的自己赋值实际指的是底层管理的那段空间是不是一个空间, 所以用_ptr != sp._ptr来判断.

2. 如果不是同一块空间, 先要把原来的引用计数--, 并判断是否为0, 为0就需要把原来的空间释放掉, 处理完原来的空间再去拷贝新的空间.


std::shared_ptr的循环引用

首先我们创建一个名为Listnode的类, 类里面含有两个listnode的指针和一个int的变量用来存储数据, 然后创建一个析构函数用来作为标记, 那么这里的代码就如下:

struct Listnode
{Listnode* next;Listnode* prev;int val;
};void test_shared_ptr()
{test::shared_ptr<Listnode> ptr1 = new Listnode;test::shared_ptr<Listnode> ptr2 = new Listnode;//ptr1->next = ptr2;//ptr2->prev = ptr1;
}

可以看到我希望用shared_ptr去管理创建的Listnode, 但是ptr1->next = ptr2 和ptr2->prev = ptr1的赋值肯定是不能兼容的, 所以改变一下Listnode的成员变量:

struct Listnode
{test::shared_ptr<Listnode> next;test::shared_ptr<Listnode> prev;int val;//顺便添加一个析构, 等会以便于观察~Listnode(){cout << "~Listnode()" << endl;}
};void test_shared_ptr()
{test::shared_ptr<Listnode> n1 = new Listnode;test::shared_ptr<Listnode> n2 = new Listnode;//循环引用n1->next = n2;n2->prev = n1;
}

调用 test_shared_ptr():

int main()
{test_shared_ptr();return 0;
}

 

发现没有调用析构, 也就是发生了内存泄漏. 

现在将下面一行代码注释掉, 看看是否能正常析构:

void test_shared_ptr()
{test::shared_ptr<Listnode> n1 = new Listnode;test::shared_ptr<Listnode> n2 = new Listnode;//循环引用n1->next = n2;//n2->prev = n1;
}int main()
{test_shared_ptr();return 0;
}

可以正常析构, 为什么呢? 

 这是一开始n1和n2都只是默认初始化时的状态:

 n1->next = n2之后, 指针指向变成了这样:

这种情况下n1和n2可以正常, 按照构造函数相反的方向, n2的析构函数先调用, 引用计数--为1, 不发生任何改变, n1的析构函数再调用, 引用计数--为0, 调用delete _ptr也就是调用Listnode的析构函数, 打印~ListNode()后, prev先析构, 正常析构即可, next析构引用计数--为0, 释放原来n2指向的那片Listnode空间, 这部分空间内的两个shared_ptr都未初始化, 正常析构没有问题, 至此析构完成.

再来重新分析这段代码的析构:

void test_shared_ptr()
{test::shared_ptr<Listnode> n1 = new Listnode;test::shared_ptr<Listnode> n2 = new Listnode;//循环引用n1->next = n2;n2->prev = n1;
}

同样的, n2先析构(*_pcount)--为1, n1再析构(*_pcount)--为1, 此时析构已经调用完成了, 引用计数都没有减到0, 不会对资源进行释放, 就发生了内存泄漏, 这也就是shared_ptr发生的循环引用问题.

对于std:: shared_ptr也同样有这个问题, 而且库里的shared_ptr的构造函数加了explicit, 不能隐式类型转换了:

struct Listnode
{int val;std::shared_ptr<Listnode> next;std::shared_ptr<Listnode> prev;~Listnode(){cout << "~Listnode()" << endl;}
};void test_shared_ptr2()
{std::shared_ptr<Listnode> n1(new Listnode);std::shared_ptr<Listnode> n2(new Listnode);//循环引用n1->next = n2;n2->prev = n1;
}int main()
{test_shared_ptr();return 0;
}


std::weak_ptr 

上面的问题最根本在于我们默许prev和next指针参与资源的管理, 因此为了解决循环引用的问题,weak_ptr就出现了. 库里面的解决方式是把Listnode里的next和prev换成weak_ptr :

 可以看到weak_ptr是支持用shared_ptr去构造的.

struct Listnode
{int val;std::weak_ptr<Listnode> next;std::weak_ptr<Listnode> prev;~Listnode(){cout << "~Listnode()" << endl;}
};

weak_ptr是一个有资源指向能力但没有管理权的指针, 所以无论创建多少个weak_ptr, 对应shared_ptr的引用计数也不会增加.

 weak_ptr特点

1. 不是传统的智能指针, 不支持RAII

2. weak_ptr中只有一个普通指针作为成员变量, 用来指向开辟的内存空间, 它不会去管理引用计数, 但是能去查看引用计数.

3. 能像指针一样使用

 虽然weak_ptr不能去管理引用计数, 但是它需要能查看引用计数, 因为假如有一个shared_ptr和weak_ptr指向同一块空间, shared_ptr释放了但是weak_ptr并不知道, 为了解决这个问题, weak_ptr也有use_count接口可以查看引用计数, 其次它还有一个expired接口检查它是否"过期".

void test_shared_ptr2()
{std::shared_ptr<Listnode> n1(new Listnode);std::shared_ptr<Listnode> n2(new Listnode);cout << n1.use_count()<<endl;cout << n2.use_count() << endl;//循环引用n1->next = n2;n2->prev = n1;//weak_ptr接口cout << n1->next.use_count() << endl;cout << n2->prev.use_count() << endl;cout << n1->next.expired() << endl;;cout << n2->prev.expired() << endl;;
}

 简单实现一个weak_ptr:

template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr<T>& wp){_ptr = wp.get();return *this;}//像指针一样使用T* operator->(){return _ptr;}T& operator*(){return *_ptr;}private:T* _ptr;
};

这里用shared_ptr去构造weak_ptr需要调用shared_ptr的get接口去得到管理的那部分空间的地址, 在shared_ptr里添加一个get:


定制删除器 

智能指针不光可以管理一个变量, 也可以管理一个数组。

对于一个变量而言,可以使用delete _ptr,但对于一个数组而言,回收资源就需要用delete[] _ptr,可是我们之前实现的shared_ptr释放用的是delete, 所以去管理一个数组空间时析构会崩溃(std::shared_ptr也会崩溃):

void test_shared_ptr3()
{test::shared_ptr<string> n(new string[10]);//std::shared_ptr<string> n(new string[10]);
}

所以这里我们需要一个定制删除器来实现对不同资源的不同释放方式 :

 在构造shared_ptr时可以传递第二个 对象参数, 也就是定制删除器, 来实现自定义的删除方式.

1. 我们可以选择传递一个仿函数对象: 

template<class T>
struct ListnodeDL
{void operator()(T* ptr){delete[] ptr;}
};
void test_shared_ptr3()
{std::shared_ptr<Listnode> p(new Listnode[10], ListnodeDL<Listnode>());
}int main()
{test_shared_ptr3();return 0;
}

2. 还可以选择传递一个lambda对象, 更加方便:

void test_shared_ptr3()
{std::shared_ptr<Listnode> p(new Listnode[10], [](Listnode* ptr) {delete[] ptr; });
}

 3. 还可以去管理一个文件指针:

void test_shared_ptr3()
{std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
}

 了解完定制删除器后, 现在可以为之前模拟实现的shared_ptr添加上删除器:

 

可以看到新添加了一个包装器成员_del, 因为我们要在释放资源时中去调用删除器自己实现的删除功能, 它是一个返回值类型为void参数为T*类型的函数包装器.

注意: 这里_del要给一个缺省值, 或者在单参数的构造里初始化列表初始化, 因为之前的delete注释掉了, 对于普通场景的资源释放也需要处理.

完整代码: 

	template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}~shared_ptr(){if (--(*_pcount) == 0){//cout << "delete->" << _ptr << endl;//delete _ptr; 有了定制删除器, 就不能直接用delete了_del(_ptr);delete _pcount;}}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}//sp2 = sp1shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){(*_pcount)--;if (*_pcount == 0){//delete _ptr;_del(_ptr);delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}T* get() const{return _ptr;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;function<void(T*)>_del = [](T* ptr) {delete ptr; };};

测试一下: 

void test_shared_ptr3()
{test::shared_ptr<Listnode> p1(new Listnode[10], ListnodeDL<Listnode>());test::shared_ptr<Listnode> p2(new Listnode[10], [](Listnode* ptr) {delete[] ptr; });test::shared_ptr<FILE> p3(fopen("test.txt","r"), [](FILE* ptr) {fclose(ptr); });test::shared_ptr<Listnode> p4(new Listnode);
}int main()
{test_shared_ptr3();return 0;
}

传alloc参数 

此外, 库中还有一种构造可以传Allocator, 这是为了防止内存碎片化, 因为每一个智能指针都维护一个引用计数, 大量使用智能指针就会有大量的内存碎片


 C++智能指针发展历史

C++ 98的auto_ptr: 管理权的转移->不好的设计, 对象悬空. (不建议使用)

boost的scoped_ptr: 防止拷贝->简单粗暴, 对于不需要拷贝的场景很好.

boost的shared_ptr: 引用计数, 最后一个释放的对象释放资源-> 复杂一些, 但支持拷贝, 很好->问题, 循环引用.

C++11的unique_ptr: 防止拷贝->简单粗暴, 对于不需要拷贝的场景很好.

C++11的shared_ptr: 引用计数, 最后一个释放的对象释放资源-> 复杂一些, 但支持拷贝, 很好->问题, 循环引用.

而C++11的unique_ptr和shared_ptr, 分别对应boost库中的scoped_ptr和shared_ptr. 

什么是boost?

一项改变要正式进入标准是很严谨的事情, 所以标准委员会库工作组织成立了一个boost的第三方库作为标准库的后备:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/724652.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第三天 Kubernetes进阶实践

第三天 Kubernetes进阶实践 本章介绍Kubernetes的进阶内容&#xff0c;包含Kubernetes集群调度、CNI插件、认证授权安全体系、分布式存储的对接、Helm的使用等&#xff0c;让学员可以更加深入的学习Kubernetes的核心内容。 ETCD数据的访问 kube-scheduler调度策略实践 预选与…

centos7安装maven离线安装

1、从官方网站下载maven文件包 官方下载网站&#xff1a;https://maven.apache.org/download.cgi 2、创建文件夹解压文件 将下载好的安装包&#xff0c;放到创建的目录下&#xff0c;并解压 a、创建/app/maven文件 mkdir /app/mavenb、解压文件 tar -zxvf apache-maven-…

重磅:2024广州国际酒店工程照明展览会

2024广州国际酒店工程照明展览会 Guangzhou international hotel engineering lighting exhibition 2024 时间&#xff1a;2024年12月19-21日 地点&#xff1a;广州.中国进出口商品交易会展馆 承办单位&#xff1a;广州佛兴英耀展览服务有限公司 上海昶文展览服务有限公司…

【Java面试/24春招】技术面试题的准备

Spring MVC的原理 Mybatis的多级缓存机制 线程池的大小和工作原理 上述问题&#xff0c;我们称为静态的问题&#xff0c;具有标准的答案&#xff0c;而且这个答案不会变化&#xff01; 如果没有Spring&#xff0c;会怎么样&#xff1f;IOC这个思想是解决什么问题&#xff1f…

【牛客】VL65 状态机与时钟分频

描述 题目描述&#xff1a; 使用状态机实现时钟分频&#xff0c;要求对时钟进行四分频&#xff0c;占空比为0.25 信号示意图&#xff1a; clk为时钟 rst为低电平复位 clk_out 信号输出 Ps 本题题解是按照1000的状态转移进行的&#xff0c;不按照此状态进行&#xff0c;编译器…

蓝桥杯练习系统(算法训练)ALGO-985 幸运的店家

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 炫炫开了一家商店&#xff0c;卖的货只有一个&#xff0c;XXX&#xff0c;XXX卖N元钱。有趣的是&#xff0c;世界上只有面值…

剑指offer 二维数组中的查找 C++

目录 前言 一、题目 二、解题思路 1.直接查找 2.二分法 三、输出结果 前言 最近在牛客网刷题&#xff0c;刷到二维数组的查找&#xff0c;在这里记录一下做题过程 一、题目 描述 在一个二维数组中&#xff08;每个一维数组的长度相同&#xff09;&#xff0c;每一行都按照…

微信小程序开发:记一次提审失败的反馈重审

我在第一次提审小程序的时候很明确说了我这个是接入的阿里云的人像动漫化接口&#xff0c;但是还是给我不通过&#xff1a; 说我涉及AI合成&#xff0c;个人是做不了一点AI相关的东西&#xff0c;一点都不行&#xff1a; 我肯定不接受了&#xff0c;反馈说&#xff1a; 还把…

2024.3.6

作业1&#xff1a;使用C语言完成数据库的增删改 #include <myhead.h>//定义添加员工信息函数 int Add_worker(sqlite3 *ppDb) {//准备sql语句printf("请输入要添加的员工信息:\n");//从终端获取员工信息char rbuf[128]"";fgets(rbuf,sizeof(rbuf),s…

ArrayList的扩容机制

ArrayList 的底层操作机制源码分析 ArrayList中维护一个Object类型的数组elementData transient Object[] elementData; //transient表示瞬间 短暂的&#xff0c;表示该属性不会被序列化当创建ArrayList对象时&#xff0c;如果使用的是无参构造器&#xff0c;则初始elementDa…

C#知识点-22(ADO.NET五个对象,SQL漏洞注入攻击)

ADO.NET 概念&#xff1a;ADO.NET就是一组类库&#xff0c;这组类库可以让我们通过程序的方式访问数据库&#xff0c;就像System.IO的类用类操作文件一样&#xff0c;System.Data这组类是用来操作数据库的&#xff08;不光是MSSql Server&#xff09;&#xff0c;它提供了统一…

【Windows 常用工具系列 14 -- windows 网络驱动映射】

文章目录 windows 网络驱动映射 windows 网络驱动映射 映射网络驱动器的意思是将局域网中的某个目录映射成本地驱动器号。 在windows上将服务器目录映射到本地盘&#xff1a; 进入到服务器执行下面命令既可以看到对应的 IP地址&#xff1a; 将对应的IP地址填入上图中。 映…

Synchronized(三:JVM中锁的优化)

简单来说在JVM中的monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的&#xff0c;但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行&#xff0c;这种切换的代价是非常昂贵的&#xff1b;然而在现实中大部分情况下&#xff0c;同…

C++ Floyd求最短路 Floyd算法(多源汇最短路)

给定一个 n 个点 m 条边的有向图&#xff0c;图中可能存在重边和自环&#xff0c;边权可能为负数。 再给定 k 个询问&#xff0c;每个询问包含两个整数 x 和 y &#xff0c;表示查询从点 x 到点 y 的最短距离&#xff0c;如果路径不存在&#xff0c;则输出 impossible。 数据…

docker部署前后端分离项目

docker部署前后端分离项目 前提&#xff0c;服务器环境是docker环境&#xff0c;如果服务器没有安装docker&#xff0c;可以先安装docker环境。 各个环境安装docker&#xff1a; Ubuntu上安装Docker&#xff1a; ubuntu离线安装docker: CentOS7离线安装Docker&#xff1a; Cen…

类与对象-对象特性

师从黑马程序员 对象的初始化和清理 构造函数和析构函数 用于完成对象的初始化和清理工作 如果我们不提供构造和析构&#xff0c;编译器会提供编译器提供的构造函数和析构函数是空实现 构造函数&#xff1a;主要用于创建对象时为对象的成员属性赋值&#xff0c;构造函数由编…

什么是机器视觉?

什么是机器视觉 近年来&#xff0c;人工智能渐渐成为一个热点话题。作为人工智能领域的一个分支&#xff0c;图像处理技术也随之发展到了一个新的高度&#xff0c;各种新的软件工具、算法库、开源资料不断涌现&#xff0c;各行各业也渐渐开始进行技术变革。比较典型的例子是&a…

【论文笔记】Gemma: Open Models Based on Gemini Research and Technology

Gemma 日期: March 5, 2024 平台: CSDN, 知乎 状态: Writing Gemma: Open Models Based on Gemini Research and Technology 谷歌最近放出的Gemma模型【模型名字来源于拉丁文gemma&#xff0c;意为宝石】采用的是与先前Gemini相同的架构。这次谷歌开源了两个规模的模型&…

【2024】vue-router和pinia的配置使用

目录 vue-routerpiniavue-routerpinia进阶用法---动态路由 有同学在项目初始化后没有下载vue-router和pinia&#xff0c;下面开始&#xff1a; vue-router npm install vue-router然后在src目录下创建文件夹router&#xff0c;以及下面的index.ts文件&#xff1a; 写进下面的…

短剧小程序:掌中剧院,随时上演

在繁忙的生活节奏中&#xff0c;我们总渴望找到一片属于自己的休闲空间。短剧小程序&#xff0c;就是这样一处随时随地都能让你沉浸其中的掌上剧院。无论你在何处&#xff0c;无论何时&#xff0c;只要轻轻一点&#xff0c;精彩纷呈的短剧即刻上演。 一、掌中剧院&#xff0c;…