weak_ptr弱类型指针详解:weak_ptr弱类型指针详解
unique_ptr指针详解:unique_ptr指针详解
“C++中智能指针“解析
四大智能指针的特点
① unique_ptr指针:对于一个指向开辟在堆区内存的指针,在整个指针作用域中指向这片内存区域的指针只能是他自己,谁要跟它抢夺控制权,编译器就会直接报错;
② shared_ptr指针:对于开辟在堆区的内存,我可以使用多个指针指向它,就相当于我先在堆区开辟一块内存使用一个指针指向这片内存区域,然后给这个指针取很多个别名;
③ auto_ptr指针:这个指针的智能化程度比较低,auto_ptr只能管理一个指向堆区内存的指针且保证有且只有一个指针指向该区域否则会导致像“浅拷贝”那样重复释放的异常错误;
④ weak_ptr指针:用于解决使用“循环引用”的问题时,使用weak_ptr而非shared_ptr,与shared_ptr不同,weak_ptr是弱共享指针,当一个shared_ptr指针赋值给weak_ptr指针时,引用计数不会改变。
注意:
① 这些指针维护的内存区域全都开辟在堆区当中,底层均是使用new expression和delete expression来实现的;
② 使用这些指针维护使用new[]返回的指针时,操作与new返回的指针的操作方式稍有不同:
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int[]> ptr1(new int[2]), ptr2;
// shared_ptr<int*> ptr3;
ptr1[0] = 10;
ptr1[1] = 20;
ptr2 = ptr1;
cout << ptr2[1] << endl;
//cout << *(ptr2) << endl; // 错误
//cout << *(ptr2 + 1) << endl; // 错误
}
注意:
⑴ 首先,这些智能指针不是真正的指针,而是模板类,由于模板类中不存在像普通指针一样的“+int整数”的操作,因此无法通过“+int整数”实现地址的偏移;
⑵ 其次,我们应该注意到:构建一个指向“new[]返回指针“的智能模板类指针对象时,模板参数必须是”有数组性质的数据类型指针“,例如double[],int[]……等,但是如果我们使用”不具有数组性质的数据类型指针”,那就会报错:
⑶ 针对于《C++ Primer》中提及的注意事项“除了unique_ptr指针可以维护new[]以及new返回的指针,其他三大智能指针weak_ptr,shared_ptr,auto_ptr只能维护new返回的指针“,我尝试之后发现并不存在这些注意事项。
③ 注意一点:不同模板参数实例化的同一属性的智能模板指针对象不可以相互赋值:
引用计数指针shared_ptr
① shared_ptr的建立以及初始化
1. 初始化方式一:使用同类型的指针或new/new[]返回的常规指针初始化
template<class Other>
explicit shared_ptr(Other * ptr);
template<class Other>
shared_ptr(const shared_ptr<Other>& sp);
template<class Other>
shared_ptr(const shared_ptr<Other>&& sp);
代码示例:
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int[]> ptr1(new int[2]{ 1,2 }); // 使用new[]返回值初始化
shared_ptr<int> ptr2(new int(10)); // 使用new返回值初始化
shared_ptr<int> ptr3(ptr2); // 使用相同类型的模板对象进行初始化
int* ptr4 = new int(1);
shared_ptr<int> ptr5(ptr4); // 使用建立在堆区的指针去初始化模板指针对象
shared_ptr<int> ptr8 = make_shared<int>(19); // 使用make_shared在堆区开辟的内存空间
allocator<int> alloc1;
shared_ptr<int> ptr9 = allocate_shared<int>(alloc1, 10); // 使用迭代器在堆区开辟的内存空间
}
看似很多种,其实就可以分为两大类:
我们常用于构建shared_ptr对象的两个函数:
⑴ make_shared函数:
函数原型:shared_ptr<type_name> make_shared<type_name>(value)
代码示例:
// make_shared example
#include <iostream>
#include <memory>
int main () {
std::shared_ptr<int> foo = std::make_shared<int> (10);
// same as:
std::shared_ptr<int> foo2 (new int(10));
shared_ptr<int> bar = std::make_shared<int> (20);
shared_ptr< std::pair<int,int>> baz = std::make_shared<std::pair<int,int>> (30,40);
std::cout << "*foo: " << *foo << '\n';
std::cout << "*bar: " << *bar << '\n';
std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';
return 0;
}
运行结果:
⑵ allocate_shared函数:
函数原型: shared_ptr<type_name> allocate_shared<type_name>(std::allocate<type_name> obj,value)
代码示例:
// allocate_shared example
#include <iostream>
#include <memory>
int main () {
std::allocator<int> alloc; // the default allocator for int
std::default_delete<int> del; // the default deleter for int
std::shared_ptr<int> foo = std::allocate_shared<int> (alloc,10);
auto bar = std::allocate_shared<int> (alloc,20);
auto baz = std::allocate_shared<std::pair<int,int>> (alloc,30,40);
std::cout << "*foo: " << *foo << '\n';
std::cout << "*bar: " << *bar << '\n';
std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';
return 0;
}
运行结果:
⑶ make_shared函数与allocate_shared函数的区别:
我们看到make_shared定义源代码如下所示:
// FUNCTION TEMPLATE make_shared
emplate<class _Ty,
class... _Types>
_NODISCARD inline shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
const auto _Rx = new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Set_ptr_rep_and_enable_shared(_Rx->_Getptr(), _Rx);
return (_Ret);
}
make_shared函数使用new直接在堆区开辟出一块内存,而allocate_shared函数从定义就可以看出来使用allocator内存分配器进行维护,开辟内存的途径不同是两个函数本质的区别,正如“cplusplus.com“网站所示的那样:
make_shared function uses ::new to allocate storage for the object. A similar function, allocate_shared, accepts an allocator as argument and uses it to allocate the storage.
译文如下:
make_shared函数使用::new为对象分配存储空间。类似的函数allocate_shared接受分配器作为参数,并使用它来分配存储空间。
⒉ 使用弱类型weak_ptr指针初始化shared_ptr对象
弱类型指针weak_ptr解析:weak_ptr弱类型指针详解
template<class Other>
explicit shared_ptr(const weak_ptr<Other>& wp); // 不可以进行强制类型转换,比如:weak_ptr<double>类型的指针是不可以初始化shared_ptr<int>类型的指针
注意:explicit关键字保证了shared_ptr和weak_ptr两个操作参数模板参数的相同。
代码示例:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> ptr(new int(10));
weak_ptr<int> ptr1(ptr); // 只能用shared_ptr初始化shared_ptr
cout << "指向内存的指针数量:" << ptr.use_count() << endl;
shared_ptr<int> ptr2(ptr1); // 可以
cout << "新增ptr2指针后,指向内存的指针数量:" << ptr.use_count() << endl;
}
运行结果:
我们看到弱类型指针只能被shared_ptr引用计数指针初始化,并且当我们使用weak_ptr类型的指针ptr1初始化另一个shared_ptr类型的指针ptr2时,就相当于有多增加了一个指向这片堆区内存区域的指针,此时指向这片内存区域的指针共有三个:1个weak_ptr类型的ptr1指针,两个shared_ptr类型的指针ptr和ptr2。
⒊ 使用unique_ptr指针初始化shared_ptr指针
unique_ptr的讲解:unique_ptr智能指针详解
- template<class Other, class D>
- shared_ptr(unique_ptr<Other, D>&& up);
注意:这里的参数是unique_ptr类型的左值引用
右值与左值引用解析:“右值与左值引用”详解
代码示例:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> ptr(new int(10));
//shared_ptr<int> ptr3(ptr); // 禁止这样操作
shared_ptr<int> ptr1 = make_unique<int>(10), ptr2(nullptr); // make_unique返回的是unique_ptr临时指针
ptr2 = forward<unique_ptr<int> >(ptr); // 使用forward函数将右值转化为左值
if (ptr == nullptr)
{
cout << "使用移动语义之后,ptr变为了nullptr" << endl;
}
if (ptr2 != nullptr)
{
cout << "ptr2被成功赋予初值,指向空间解引用后为" << *ptr2 << endl;
}
}
运行结果:
我们注意到生成unique_ptr左值引用有两种方法:
A. 调用make_unique函数去直接生成unique_ptr类型的左值;
B. 使用移动语义的概念,调用forward函数将unique_ptr类型的右值转为左值。
其实我们在使用过程中经常使用B而不使用A,为什么呢?
因为我们使用unique_ptr类型的指针初始化shared_ptr类型的指针主要是为了“实现两种属性的指针之间的转换”,A的操作完全可以由make_shared函数代替,一点用处也没有。
为什么要使用这种初始化方式?
首先,我们在编程时有这种需求:在一段代码中,我必须保证这个指针指向的区域由且仅由该指针指向不允许其他指针分享所有权,那我们此时就可以把指向该堆区内存区域的指针声明为unique_ptr类型的指针对象;在另一段代码中,这个堆区内存区域需要由多个指针共同指向,此时指针的类型必须发生改变,我们的引用计数指针shared_ptr正好可以满足“一片堆区内存区域需要由多个指针共同指向”的要求,因此,此时我们需要在创建一个指针将unique_ptr的指针地址赋值给shared_ptr类型的指针并且置空原来的指针以防止堆区内存区域被意外释放两次。
因此,这种初始化方式也被称之为“你(unique_ptr)死我(shared_ptr)活初始化方式”。
⒋ 使用auto_ptr类型的指针初始化方式
template<class Other>
shared_ptr(auto_ptr<Other>&& ap);
代码示例:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
auto_ptr<int> ptr(new int(10));
shared_ptr<int> ptr1 = forward<auto_ptr<int> >(ptr);
}
显然,这里也要使用auto_ptr类型的左值对象初始化shared_ptr类型的指针对象。
我们应该注意到:只有使用weak_ptr和shared_ptr类型的指针初始化shared_ptr类型的指针时用的是右值对象,unique_ptr和auto_ptr均用的是“左值对象”,这是为什么呢?
当你仔细看weak_ptr与unique_ptr,auto_ptr类型的指针区别时,你会发现weak_ptr指针不会影响指向内存空间的生命周期,也就是说当weak_ptr类型的指针对象被析构(生命结束)时,指向内存区域并不会被释放掉。此外,只有当指向这片内存区域的最后一个shared_ptr类型的指针生命结束时,这片被shared_ptr指向的内存区域才会被释放掉。
但是,当unique_ptr和auto_ptr两个类型的指针指向“已经有指针指向的内存空间”时,会发生很糟糕的事情:内存在每个指针生命结束后被释放一次,也就意味着“该片内存区域被释放了多次”。如果要避免这种事情发生,可以采用如下方式:
A. 将指针指向的地址移至新类型指针当中;
B. 置空原指针,即赋给原指针nullptr。
注意:置空原指针的目的在于“使得原始指针指向空区域,这样,当指针生命结束之时,他将什么也干不了”。
5. shared_ptr类型的指针指向自定义数据类型的成员数据
template<class Other>
shared_ptr(const shared_ptr<Other>& sp, Ty *ptr);
代码示例:
#include <iostream>
using namespace std;
#include <memory>
struct A
{
int age;
double mark;
};
int main()
{
shared_ptr<A> ptr = make_shared<A>();
// 参数一:堆区对象所在的地址;
// 参数二:对象中数据成员的地址
shared_ptr<int> ptr1(ptr, &ptr->age);
// ptr1为指向*ptr对象中的age数据成员
}
6. 在shared_ptr的构造函数中,添加删除器(deleter)和内存配置器(allocator)
template<class Other, class D>
shared_ptr(Other * ptr, D dtor);
template<class D>
shared_ptr(nullptr_t, D dtor);
template<class Other, class D, class A>
shared_ptr(Other *ptr, D dtor, A alloc);
template<class D, class A>
shared_ptr(nullptr_t, D dtor, A alloc);
代码示例:
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
template <typename T>
class deleter
{
private:
int count;
public:
deleter() :count(0) {};
void ShowInf()
{
cout << "调用此删除器次数为" << this->count << endl;
}
void operator()(T* ptr)
{
this->count++;
cout << "到目前为止,调用删除器次数为" << this->count << endl;
delete ptr;
}
};
int main()
{
int count = 0;
// 如下所示:建立lambda表达式
function<void(int*)> Deleter = [count](int* ptr)mutable {count++; delete ptr; cout << "系统通过该指针是释放的内存区域次数为" << count; };
shared_ptr<int> ptr(new int(10), default_delete<int>(), allocator<int>()); // 使用STL给的默认值
shared_ptr<int> ptr1(new int(11), Deleter, allocator<int>()); // 自定义删除器,但是空间配置器必须使用系统给的
shared_ptr<int> ptr2(new int(12), deleter<int>(), allocator<int>()); // 自定义删除器,但是空间配置器必须使用系统给的
}
运行结果:
我们从shared_ptr初始化列表中得知,deleter删除器必须是可以执行仿函数的类对象/lambda表达式,我们如上的代码给我们演示了使用“自定义删除器类类型的对象”和“自定义的lambda函数句柄”来充当shared_ptr指针对象的删除器。
② shared_ptr类型指针的赋值操作
⑴ 同类型指针赋值
shared_ptr& operator=(const shared_ptr& sp);
template<class Other>
shared_ptr& operator=(const shared_ptr<Other>& sp);
shared_ptr& operator=(shared_ptr&& sp);
template<class Other>
shared_ptr& operator=(shared_ptr<Other>&& sp);
代码示例:
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
int main()
{
// 对于定义时未赋初值的指针一定要进行“零初始化”——"零初始化"就是将指针置空
shared_ptr<int> ptr(new int(10)), ptr1(nullptr);
ptr1 = ptr;
ptr1 = make_shared<int>(11);
ptr1 = forward<shared_ptr<int>&& >(ptr); // 强转为左值
ptr = forward<shared_ptr<int>& >(ptr1); // 强转为右值
}
⑵ 使用weak_ptr赋值给shared_ptr类型的指针(不可以直接赋值)
代码示例:
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
int main()
{
// 对于定义时未赋初值的指针一定要进行“零初始化”——"零初始化"就是将指针置空
shared_ptr<int> ptr(new int(10)), ptr1(nullptr);
weak_ptr<int> ptr2(ptr);
//ptr1 = ptr2; // 直接赋值是不可以的
ptr1 = ptr2.lock(); // 调用weak_ptr指针的成员函数返回shared_ptr类型的指针
}
⑶ 使用unique_ptr和auto_ptr的左值引用进行赋值操作
代码示例:
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
int main()
{
unique_ptr<int> ptr(new int(10));
shared_ptr<int> ptr1(nullptr);
ptr1 = forward<unique_ptr<int>&&>(ptr);
cout << "ptr是否被置空:" << (ptr == nullptr) << endl;
cout << "ptr1指向内存空间解引用为" << *ptr1 << endl;
}
auto_ptr类型的指针操作同上。
运行结果:
使用左值引用作为赋值操作的对象是有考量的:
这样是为了防止有多类(不包括weak_ptr弱类型指针)智能指针同时指向同一片在堆区开辟的内存区域,最终导致堆区内存被释放多次,从而导致内存崩溃。
③ 访问指针指向的对象
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int[]> ptr(new int[2]{ 1,2 });
cout << ptr[0] << endl; // 用[]元素下标来访问
cout << ptr[1] << endl;
shared_ptr<pair<int, int>[]> ptr1(new pair<int, int>[2]);
ptr1[0] = pair<int, int>(3, 4);
ptr1[1] = pair<int, int>(5, 6);
cout << ptr1[0].first << " " << ptr1[0].second << endl;
cout << ptr1[1].first << " " << ptr1[1].second << endl;
// ptr1->first; // 这种访问pair元素的方式不对,应该使用ptr1[0].first这种方式去访问pair中的元素
}
shared_ptr智能指针模板类中成员函数的用法
① get函数:返回智能指针的存储地址
⑴ 函数功能:
返回shared_ptr指针指向的地址
⑵ 代码示例:
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int> ptr(new int(10));
cout << *ptr.get() << endl; // 返回地址解引用后的值
}
② owner_before成员函数:
请详见“owner_before详解”。
③ reset复位成员函数:
⑴ reset成员函数的作用:
reset汉语意思就是“复位”,在shared_ptr中的作用为:使该指针指向一块新的内存区域。
⑵ 代码示例:
#include<iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int> ptr = make_shared<int>(10), ptr1(ptr);
cout << "指向10所在地址的指针个数为" << ptr.use_count() << endl;
cout << "*ptr1 = " << *ptr1 << endl;
cout << "*ptr = " << *ptr << endl;
ptr.reset(new int(90));
cout << "指向90所在地址的指针个数为" << ptr.use_count() << endl;
cout << "*ptr1 = " << *ptr1 << endl;
cout << "*ptr = " << *ptr << endl;
}
运行结果:
我们看到,当调用了ptr.reset(new int(90))之后,ptr不再指向10所在的内存区域,而是转而指向90所在的内存区域,也就是说reset成员函数可以改变指针指向的区域。
reset执行之前:
reset执行之后:
⑶ 扩展reset函数:
void reset() noexcept;
template <class U> void reset (U* p);
template <class U, class D> void reset (U* p, D del);
template <class U, class D, class Alloc> void reset (U* p, D del, Alloc alloc);
以上是shared_ptr中reset的三种形式。以下是reset成员函数的源码:
void reset() noexcept
{ // release resource and convert to empty shared_ptr object
shared_ptr().swap(*this);
}
template<class _Ux>
void reset(_Ux * _Px)
{ // release, take ownership of _Px
shared_ptr(_Px).swap(*this);
}
template<class _Ux,
class _Dx>
void reset(_Ux * _Px, _Dx _Dt)
{ // release, take ownership of _Px, with deleter _Dt
shared_ptr(_Px, _Dt).swap(*this);
}
template<class _Ux,
class _Dx,
class _Alloc>
void reset(_Ux * _Px, _Dx _Dt, _Alloc _Ax)
{ // release, take ownership of _Px, with deleter _Dt, allocator _Ax
shared_ptr(_Px, _Dt, _Ax).swap(*this);
}
我们从源码中就可以看到,reset其实是由swap成员函数来实现的,就是交换了两个指针指向的内存空间。
其中参数p是与shared_ptr模板参数相同的指针,del可以是系统定义的删除器deleter也可以是我们自定义的deleter删除器对象,alloc必须是系统提供的allocator模板类对象。
#include <iostream>
#include <memory>
using namespace std;
template <typename T>
class deleter
{
private:
int count;
public:
deleter() :count(0) {};
void ShowInf()
{
cout << "调用此删除器次数为" << this->count << endl;
}
void operator()(T* ptr)
{
this->count++;
cout << "到目前为止,调用删除器次数为" << this->count << endl;
delete ptr;
}
};
int main()
{
shared_ptr<int> ptr(new int(10));
ptr.reset(new int(11), deleter<int>(), allocator<int>()); // 我这里使用的都是临时变量
}
运行结果:
带有删除器deleter和空间配置器allocator的reset函数的使用:
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
template <typename T>
class deleter
{
private:
int count;
public:
deleter() :count(0) {};
void ShowInf()
{
cout << "调用此删除器次数为" << this->count << endl;
}
void operator()(T* ptr)
{
this->count++;
cout << "到目前为止,调用删除器次数为" << this->count << endl;
delete ptr;
}
};
int main()
{
deleter<int> del;
allocator<int> alloc;
shared_ptr<int> ptr(new int(10), del, alloc);
// deleter<int>()中的()是指“调用了deleter类的默认构造函数”
ptr.reset(new int(11), del, alloc);
// 这样做是保证交换内存空间后deleter删除器和allocator配置器的类型一致
}
注意:reset操作的指针必须是新new出来的,不可以是shared_ptr类型的指针。
④ swap交换元素的成员函数
⑴ 函数作用:
将两个内存空间的地址进行交换,即交换一下两个指针指向的地址。
⑵ 代码示例:
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int> ptr = make_shared<int>(10), ptr1(ptr);
shared_ptr<int> ptr2 = make_shared<int>(90);
cout << "ptr指向的地址:" << ptr.get() << endl;
cout << "ptr2指向的地址:" << ptr2.get() << endl;
cout << "指向10所在内存区域的指针个数:" << ptr.use_count() << endl;
ptr.swap(ptr2);
cout << "ptr指向的地址:" << ptr.get() << endl;
cout << "ptr2指向的地址:" << ptr2.get() << endl;
cout << "指向90所在内存区域的指针个数:" << ptr.use_count() << endl;
}
运行结果:
swap成员函数实质如下:
两个指针指向的地址进行了交换
从代码和输出结果综合来看,ptr和ptr2的属性完全交换。
⑤ unique成员函数:判断指针是否唯一指向一片内存区域
代码示例:
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int> ptr = make_shared<int>(10), ptr1(ptr);
cout << "ptr是否是指向10所在内存区域的唯一指针:" << ptr.unique() << endl;
}
运行结果:
⑥ use_count成员函数:指出有几个指针指向一片相同的内存区域
代码示例:
#include <iostream>
using namespace std;
#include <memory>
int main()
{
shared_ptr<int> ptr = make_shared<int>(10), ptr1(ptr);
cout << "ptr是否是指向10所在内存区域的唯一指针:" << ptr.unique() << endl;
cout << "指向10所在内存区域的指针的数量:" << ptr.use_count() << endl;
}
运行结果:
shared_ptr模板类如何维护类类型内的数据成员?
#include <iostream>
using namespace std;
#include <memory>
struct A
{
int age;
double mark;
};
int main()
{
shared_ptr<A> ptr = make_shared<A>();
// 参数一:堆区对象所在的地址;
// 参数二:对象中数据成员的地址
shared_ptr<int> ptr1(ptr, &ptr->age);
// ptr1为指向*ptr对象中的age数据成员
}
有人说,我们不用写第一个参数ptr不行吗?
如果运行如下代码会出现错误,代码如下:
#include <iostream>
using namespace std;
#include <memory>
struct A
{
int age;
double mark;
};
int main()
{
shared_ptr<A> ptr = make_shared<A>();
shared_ptr<int> ptr1(&ptr->age); // 去掉了原来的第一个参数ptr
}
错误如下:
为什么会报错呢?
因为只有整个类才可以拥有析构函数,当指向数据成员的智能指针生命结束时,会先调用类的析构函数删除数据并且释放内存,但是数据成员的数据类型如果为基本数据类型的话,基本数据类型是没有析构函数的,因此编译器会报错。