“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的建立以及初始化
#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;
}
运行结果:
除此之外,使用make_shared进行初始化也是有其独领风骚的作用的:智能指针的辅助函数
⑵ 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接受分配器作为参数,并使用它来分配存储空间。
访问指针指向的对象
#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);
以上是unique_ptr的三种形式。
其中参数p是与unique_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>()); // 我这里使用的都是临时变量
}
运行结果:
④ 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
}
错误如下:
为什么会报错呢?
因为只有整个类才可以拥有析构函数,当指向数据成员的智能指针生命结束时,会先调用类的析构函数删除数据并且释放内存,但是数据成员的数据类型如果为基本数据类型的话,基本数据类型是没有析构函数的,因此编译器会报错。