4.智能指针的原理、常用的智能指针及实现
1.auto_ptr
当你在读这篇文章的时候,应该都有这样一个疑问?那就是为什么要使用智能指针。
我们先看这样一个示例:
#include <iostream>
#include <string>
#include <exception>
using namespace std;
void memory_demo1()
{
string* str = new string("今天又找了一天的bug,太累了!!!");
std::cout << *str << std::endl;
//并没有调用delete str,进行释放
return;
}
int memory_demo2()
{
string* str = new string("这个世界到处是坑啊,异常处理都要坑我!!!");
/*
* 程序这里省略执行了一段复杂的逻辑并会执行到throw这个函数
*/
{
throw exception("文件不存在");
}
std::cout << *str << std::endl;
delete str; //虽然此处调用了,但执行到throw时就结束了,并不会执行到这一布
return 0;
}
int main()
{
memory_demo1();
try
{
memory_demo2();
}
catch (exception e)
{
std::cout << "catch exception: " << e.what() << std::endl;
}
system("pause");
return 0;
}
这段程序如果乍一看是不是没有什么问题,也可以正常编译和运行。但这只是少量代码,体现不出来问题在哪。如果一涉及到工程级代码,那就很容易造成内存泄漏问题(真的是程序员最头疼的问题之一)。
对于上面问题,最简单的一个办法就是把string定义为auto变量。那在这个函数周期结束时就会自动的进行释放(局部变量)。
void memory_demo1()
{
//string* str = new string("今天又找了一天的bug,太累了!!!")
string str("今天又找了一天的bug,太累了!!!");
std::cout << str << std::endl;
return;
}
int memory_demo2()
{
//string* str = new string("这个世界到处是坑啊,异常处理都要坑我!!!");
string str("这个世界到处是坑啊,异常处理都要坑我!!!");
...
return 0;
}
现在我们定义的是一个string(局部)对象,那在函数结束的时候,string这个函数内部会自己调用析构函数就行释放。
看到这,那是不是可以这样做:如果我们分配的动态内存都交给有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个非常好的方案?
或许正是由于这样的想法,智能指针一词被那些C++的大牛们所实践出来。
智能指针也是通过这个原理来解决指针智能释放问题。
智能指针的原理:
直接点说就是资源分配即初始化RAII(Resource Acquisition Is Initialization):
定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
实现机制:利用类的构造和析构函数(释放资源)是由编译器自动调用的。
在C++标准库中主要有下面4个智能指针:
(1)C++98提供了auto_ptr的模板
(2)C++11 增加了unique_ptr、shared_ptr 和weak_ptr三种智能指针
其中:shared_ptr是引用计数的智能指针,被奉为裸指针的完美替身,因此被广泛使用。也可能正是这个原因,scope_ptr 和 weak_ptr似乎被大家遗忘了。
简单总结下这几个只智能指针
1.auto_ptr
管理权转移
带有缺陷的设计 ----->C++98
在任何情况下都不要使用;
2.scoped_ptr(boost)
unique_ptr(c++11)
防拷贝—>简单粗暴设计—>功能不全。
3、shared_ptr(boost/c++11)
引用计数—>功能强大(支持拷贝、支持定制删除器)
缺陷---->循环引用(使用weak_ptr配合解决)。
下面来介绍这篇文章的主角:auto_ptr,剩下的三个会在后面的篇章中进行一一介绍。
auto_ptr
看到这,也许有人会问,既然上面都表明了任何情况下都不要使用,那为什么还要介绍,还要放到C++标准库里面呢?个人感觉或许是因为C++标准库里面的内容一经规定后就不允许修改了吧。这里就不对这个问题就行纠结了,对于auto_ptr,这里权当学习了解一下就好了。
定义:
auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!
用法:
auto_ptr<类型> 变量名(new 类型)
注意:
(1)std::auto_ptr 属于 STL,也在namespace std中,使用的时候要加上#include 头文件。
(2)由于源代码中构造函数声明为explicit的,因此不能通过隐式转换来构造,只能显式调用构造函数。
auto_ptr<string> pstr(new string("abcd")); // success
auto_ptr<string> pstr = new string("abcd"); // error
例如:
auto_ptr str(new string(“今天又找了一天的bug,太累了!!!”));
//对比刚开始的示例使用string* str = new string(“今天又找了一天的bug,太累了!!!”);会造成内存泄漏问题。
讲解示例1.1如下:
#include <iostream>
#include <string>
#include <exception>
#include <memory>
using std::string;
class Test
{
public:
Test() { std::cout << "test is construct" << std::endl; num = 1; }
~Test() { std::cout << "test is destruct" << std::endl; }
int getNum() { return num; }
private:
int num;
};
//用 法: auto_ptr<类型> 变量名(new 类型)
void memory_demo1()
{
Test *test = new Test; //代码1
//std::auto_ptr<Test> test(new Test());//代码2 //创建对象
std::cout << test->getNum() << std::endl; //代码3
//(*test).getNum(); //代码4
//Test *temp = test.get(); //代码5
//temp->getNum();
//代码6
//Test *tmp1 = test.release(); //取消智能指针对动态内存的托管,之前分配的内存必须手动释放
//tmp1->getNum();
//delete tmp1;
//代码7
//test.reset(); //括号内可以传递一个参数,默认参数是一个空指针
//test.reset(new Test());
return;
}
int memory_demo2()
{
Test* t = new Test;
{
throw std::exception("文件不存在");
}
delete t;
return 0;
}
int main()
{
memory_demo1();
try{
memory_demo2();
}
catch (std::exception e)
{
std::cout << "catch exception: " << e.what() << std::endl;
}
system("pause");
return 0;
}
(1)如果将代码1放出来,代码2注释(没有使用智能指针的情况下),运行结果如下:
test is construct
1
test is construct
catch exception: 文件不存在
请按任意键继续. . .
结果显示只执行了构造函数,并没有自动执行析构函数
(2)如果将代码1注释,而放出来代码2,使用代码2替代代码1,结果如下:
test is construct
1
test is destruct
test is construct
catch exception: 文件不存在
请按任意键继续. . .
结果显示既执行了构造,又自动执行了析构函数
这是怎么一回事呢,使用vs2015(其他的也可以),代码2处auto_ptr按F12进入源码。如下:
是实话这里的很多代码我自己也看不懂,但并不影响我们了解实现原理。看红色箭头的地方,此处是构造函数的地方。注意_Myptr(_Ptr),这里应该还挺好理解的,将我们外面的auto_ptr test(new Test());这个代码定义的对象,赋值到它内部。当看到源码实现最底下这里时:
是不是突然间感觉容易理解了很多。这不就是将它的类对象替换成前面我们在外面定义的对象吗?当它内部执行完成后,就会调用析构函数释放,这样不就能实现对象的自动释放了吗。
不知道在看auto_ptr源代码的时候有没有注意到里面这几个类方法
(3)当上面示例1.1在调用代码3的时候,其实auto_ptr源码里面重构了->,返回了一个get()方法,而内部get()的实现则返回了内部类对象,这样当外部示例1.1调用test->getNum()的时候,其他变相的还是使用传进去的test对象再调用这个函数。至于示例1.1中的代码4实现原理也是一样的。
注:以后可以使用对象调用.get()来判断智能指针是否为空,是否创建成功。
在示例1.1代码2之后可以调用test.get()来判断是否生成成功了。
(4)既然(2)中说了get()返回的是内部类对象,那示例1.1中调用代码5肯定是可以行得通的。将代码5放出来后运行发现也是可以正常运行的。但这样做的意义并不大,不建议去使用。
(5)当示例1.1代码中放出来代码6,调用release时,通过源码可以看到:
原理就是定义一个此类型的临时指针变量,指向之前保存的指针的地址,然后将它(之前)的属性置为空指针,并将临时的返回了。但是它并没有帮我们去释放之前的地址。所以在代码6调用之后,我还加上了一个delete tmp1操作。即调用release时就相当于取消了智能指针对动态内存的托管,而且之前分配的内存必须手动释放。(个人感觉有点多此一举,不推荐)
realease实现的功能就是将原智能指针的控制权取消。
(6)当调用reset时(释放代码7).通过源码,可以看到还可以传递一个参数。当不传参数或者传递NULL时默认参数是一个空指针。此函数功能就是重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉。也就是说当传递一个空值时,会将原来的析构掉,将空值赋值给原来的值。通俗点就是:无参时智能指针会释放当前管理的内存取消对原对象的控制权。如果传递一个对象,则智能指针会释放当前原对象,来管理新传入的对象。
当调用代码7中test.reset(new Test())时,此时新对象会析构并替换掉原来的对象。(此时结果会调用两次构造和析构)
说到这里,对于auto_ptr的个人认识也差不多结束了。虽然auto_ptr叫智能智能,如果说它智能的话,那它真智能吗。在我看来无非就是利用对象析构的时候要调用析构函数,然后再利用析构函数帮我们做这个事情(释放内存)。
哦。对了。这里做一下用法的总结:(虽然没有什么用,因为它几乎被禁用了,而且shared_ptr也完美的替代了它。这里就当做一个知识的了解吧)
总结:
(1)尽可能不要将auto_ptr 变量定义为全局变量或指针
当设定为全局变量的时候,比如全局定义这样的一个对象:auto_ptr t(new Test()),那t这个对象只有在程序执行结束的时候才会被释放,就达不到我们使用它的目的和初衷。使用智能指针就没有意义了
(2)除非自己知道后果,否则不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
auto_ptr< Test> t(new Test());
auto_ptr< Test> t1;
t1 = t;
(3)不要定义指向智能指针对象的指针变量
auto_ptr<Test>* tp = new auto_ptr<Test>(new Test());
当这样使用的时候,运行结束时,C++根本不会去析构tp这个对象。因为后面new auto_ptr(new Test())也是动态内存分配的。那它也就成为了一个普通对象,也就自然不会去自动再去析构它。那它跟文章最开始写的new string也就没有区别了。
(4)想使用 std::auto_ptr 的限制感觉还真多,还不能用来管理堆内存数组,就算使用了还怕哪天一个不小心,就导致问题了。或许由于 std::auto_ptr 引发了诸多问题,而且一些设计并不是非常符合 C++ 编程思想。所以C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代
话说这点才是最重要的,说了这么多是不是等于白说了。话虽如此,但看到这里说明前面知识点已经看完了,就当学习知识了,对自己又没有坏处。
auto_ptr的补充
这里忘记说明了一个重要的知识点,或许这才是auto_ptr被unique_ptr取代的主要原因:
为了更直观的说明,以示例代码2.1讲解:
#include <stdio.h>
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using std::string;
using std::vector;
using std::auto_ptr;
int main()
{
//弊端1:复制或赋值都会改变资源的所有权
auto_ptr<string> p1(new string("I 'm string1."));
auto_ptr<string> p2(new string("I 'm string2."));
printf("p1: %p\n", p1.get());
printf("p2: %p\n", p2.get());
p1 = p2; //代码1
printf("after p1 = p2\n");
printf("p1: %p\n", p1.get());
printf("p2: %p\n", p2.get());
//弊端2
vector<auto_ptr<string>> va;
auto_ptr<string> p3(new string("I 'm p3."));
auto_ptr<string> p4(new string("I 'm p4."));
va.push_back(p3); //代码2
va.push_back(p4); //代码3
//va.push_back(std::move(p3)); //代码4 //右值化后才能转换赋值
//va.push_back(std::move(p4)); //代码5
std::cout << "va[0]: " << *va[0] << std::endl;
std::cout << "va[1]: " << *va[1] << std::endl;
//弊端3
{
auto_ptr<string> p5;
string* str = new string("智能指针的内存管理陷阱");
p5.reset(str); //代码9
{
auto_ptr<string> p6;
p6.reset(str); //代码10
}
std::cout << "str: " << *p5 << std::endl;//代码11
}
system("pause");
return 0;
}
运行结果如下:
这里可以清晰的看到在执行完代码1:p1 = p2后,p1的地址变为p2了,而p2的地址为空了。而这个时候如果再去使用p2进行操作时肯定就会报错的,至于报什么类型的错应该都能猜到。
(1)总结弊端1:当进行 p1= p2,复制或赋值都会改变资源的所有权,这估计是auto_ptr在C++11之后被抛弃的主要原因之一。
当将弊端1和3的代码全部注释,运行出来弊端2的代码。
在示例2.1上,代码2跟代码3按平时的写法貌似没有问题,但编译的时候却会报错。原因在于不支持左值操作,只有进行右值化后才能转换赋值。也就是上面会有代码4跟代码5的原因,它们是为了替代代码2和3而写上的。当使用代码4和5时,运行结果如下:
很正常的数据。还记得刚刚提到的弊端1吗?当添加上如下代码时,再去编译运行查看一下结果:
va[0] = va[1]; //代码6
std::cout << "va[0]: " << *va[0] << std::endl; //代码7
std::cout << "va[1]: " << *va[1] << std::endl; //代码8
再去源代码764查看:
归根结底,还是在于执行代码6后,再去访问代码8时,它已经是一个空值了,它的地址被赋值到了va[0]了,而空值是不允许被访问使用的。
(2)总结弊端2:在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable),STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权,而弊端1中明确的说到了赋值和复制都会改变资源的所有权。所以一般情况下尽量不要在STL容器中使用auto_ptr。
当将弊端1和2的代码注释,运行弊端3下面的代码时,看起来并没有错误才是,但运行的结果却是失败的。如下
那造成这样的原因是什么呢?我们注意一下弊端3下面的代码9,代码10,代码11。当执行完代码9时,相当于初始化p5,将str这个对象返回给p5这个对象,相当于替换吧。(reset内部实现就是将原来的对象delete,并将新传入的对象交给原来的对象)。但当执行完代码10是,就是将str这个对象又交给了p6,由p6去管理。再执行代码11之前,由于加了括号是局部对象,当出了括号p6就会自动执行析构,即同时会delete掉str这个对象。所以当执行代码11时,由于p5内部的地址被替换为了str(它已经被p6的析构释放掉了),所以会报错。
(3)弊端总结3:auto_ptr不能共享所有权,不能把同一段内存交给多个auto_ptr 变量去管理。
(4)auto_ptr不能指向数组,不支持对象数组的内存管理,因为auto_ptr在析构的时候只是调用delete,而数组应该要调用delete[]。
auto_ptr<int[]> ai(new int[5]);
当添加这种类似的代码也是会报错的。不能这样用。
所以,C++11用更严谨的unique_ptr 取代了auto_ptr!
好了,对于auto_ptr的用法就到这里了,其他的我自己也不知道了。
参考:C++之智能指针auto_ptr
2.std::unique_ptr
作为对 std::auto_ptr 的改进,std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是 std::unique_ptr 不可以拷贝或赋值给其他对象,其拥有的堆内存仅自己独占,std::unique_ptr 对象销毁时会释放其持有的堆内存。
可以使用以下方式初始化一个 std::unique_ptr 对象:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
int main()
{
//初始化方式1
std::unique_ptr<int> up1(new int(123));
//初始化方式2
std::unique_ptr<int> up2;
up2.reset(new int(123));
//初始化方式3 (-std=c++14)
std::unique_ptr<int> up3 = std::make_unique<int>(123);
system("pause");
return EXIT_SUCCESS;
}
应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已经解释过了,有兴趣的可以阅读此书相关章节。
令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:
template <typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts &&...params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete。
template <class T>
class unique_ptr
{
//省略其他代码...
//拷贝构造函数和赋值运算符被标记为delete
unique_ptr(const unique_ptr &) = delete;
unique_ptr &operator=(const unique_ptr &) = delete;
};
因此,下列代码是无法通过编译的:
std::unique_ptr<int> up1(std::make_unique<int>(123));;
//以下代码无法通过编译
//std::unique_ptr<int> up2(up1);
std::unique_ptr<int> up3;
//以下代码无法通过编译
//up3 = up1;
禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:
#include <memory>
std::unique_ptr<int> func(int val)
{
std::unique_ptr<int> up(new int(val));
return up;
}
int main()
{
std::unique_ptr<int> up1 = func(123);
return 0;
}
上述代码从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 up1。
既然 std::unique_ptr 不能复制,那么如何将一个 std::unique_ptr 对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:
int main()
{
std::unique_ptr<int> up1(std::make_unique<int>(123));
std::unique_ptr<int> up2(std::move(up1));
std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;
std::unique_ptr<int> up3;
up3 = std::move(up2);
std::cout << ((up2.get() == nullptr) ? "up2 is NULL" : "up2 is not NULL") << std::endl;
return 0;
}
运行结果:
up1 is NULL
up2 is NULL
以上代码利用 std::move 将 up1 持有的堆内存(值为 123)转移给 up2,再把 up2 转移给 up3。最后,up1 和 up2 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数或移动赋值运算符的类才行,而 std::unique_ptr 正好实现了这二者,以下是实现伪码:
template <typename T, typename Deletor>
class unique_ptr
{
//其他函数省略...
public:
unique_ptr(unique_ptr &&rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
}
unique_ptr &operator=(unique_ptr &&rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
return *this;
}
private:
T *m_pT;
};
这是 std::unique_ptr 具有移动语义的原因。关于移动构造和 std::move 也是 C++11 的新特性,这里不详细说明。
std::unique_ptr 不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:
int main()
{
//创建10个int类型的堆对象
//形式1
std::unique_ptr<int[]> up1(new int[10]);
//形式2
std::unique_ptr<int[]> up2;
up2.reset(new int[10]);
//形式3
std::unique_ptr<int[]> up3(std::make_unique<int[]>(10));
for (int i = 0; i < 10; ++i)
{
up1[i] = i;
up2[i] = i;
up3[i] = i;
}
for (int i = 0; i < 10; ++i)
{
std::cout << up1[i] << ", " << up2[i] << ", " << up3[i] << std::endl;
}
return 0;
}
程序执行结果如下:
0, 0, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 9, 9
std::shared_ptr 和 std::weak_ptr 也可以持有一组堆对象,用法与 std::unique_ptr 相同,下文不再赘述。
另外,std::unique_ptr 有几个常用函数如下:
void reset(pointer p = pointer())
释放当前由 unique_ptr(如果有)管理的指针并获得参数 p(参数 p 默认为 NULL)的所有权。如果 p 是空指针(例如默认初始化的指针),则 unique_ptr 变为空,调用后不管理任何对象。
pointer release()
返回管理的指针并将其替换为空指针, 释放其管理指针的所有权。这个调用并不会销毁托管对象,但是将 unique_ptr 对象管理的指针解脱出来。如果要强制销毁所指向的对象,请调用 reset 函数或对其执行赋值操作。
element_type* get()
返回存储的指针,不会使 unique_ptr 释放指针的所有权。因此,该函数返回的值不能于构造新的托管指针,如果为了获得存储的指针并释放其所有权,请调用 release。
void swap (unique_ptr& x)
将 unique_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏二者。
自定义智能指针对象持有的资源的释放函数
默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:
class Socket
{
public:
Socket()
{
}
~Socket()
{
}
//关闭资源句柄
void close()
{
}
};
int main()
{
auto deletor = [](Socket *pSocket)
{
//关闭句柄
pSocket->close();
//TODO: 你甚至可以在这里打印一行日志...
delete pSocket;
};
std::unique_ptr<Socket, void (*)(Socket * pSocket)> upSocket(new Socket(), deletor);
return 0;
}
自定义 std::unique_ptr 的资源释放函数其规则是:
std::unique_ptr<T, DeletorFuncPtr>
其中 T 是你要释放的对象类型,DeletorPtr 是一个自定义函数指针。上述代码 28 行表示 DeletorPtr 有点复杂(是 C++11 中的 Lambda 函数),我们可以使用 decltype(deletor) 让编译器自己推导 deletor 的类型,因此可以将 28 行代码修改为:
std::unique_ptr<Socket, decltype(deletor)> upSocket(new Socket(), deletor);
3.std::shared_ptr
std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)。std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。
下面是一个初始化 std::shared_ptr 的示例:
int main()
{
//初始化方式1
std::shared_ptr<int> sp1(new int(123));
//初始化方式2
std::shared_ptr<int> sp2;
sp2.reset(new int(123));
//初始化方式3
std::shared_ptr<int> sp3;
sp3 = std::make_shared<int>(123);
return 0;
}
和 std::unique_ptr 一样,你应该优先使用 std::make_shared 去初始化一个 std::shared_ptr 对象。
再来看另外一段代码:
#include <iostream>
#include <memory>
class A
{
public:
A()
{
std::cout << "A constructor" << std::endl;
}
~A()
{
std::cout << "A destructor" << std::endl;
}
};
int main()
{
{
//初始化方式1
std::shared_ptr<A> sp1(new A());
std::cout << "use count: " << sp1.use_count() << std::endl;
//初始化方式2
std::shared_ptr<A> sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
sp2.reset();
std::cout << "use count: " << sp1.use_count() << std::endl;
{
std::shared_ptr<A> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
}
std::cout << "use count: " << sp1.use_count() << std::endl;
}
return 0;
}
输出:
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor
上述代码 22 行 sp1 构造时,同时触发对象 A 的构造,因此 A 的构造函数会执行。
此时只有一个 sp1 对象引用 22 行 new 出来的 A 对象(为了叙述方便,下文统一称之为资源对象 A),因此代码 24 行打印出来的引用计数值为 1。
代码 27 行,利用 sp1 拷贝一份 sp2,导致代码 28 行打印出来的引用计数为 2。
代码 30 行调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用,因此代码 31 行打印的引用计数值再次变为 1。
代码 34 行 利用 sp1 再次 创建 sp3,因此代码 35 行打印的引用计数变为 2。
程序执行到 36 行以后,sp3 出了其作用域被析构,资源 A 的引用计数递减 1,因此 代码 38 行打印的引用计数为 1。
程序执行到 39 行以后,sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。
另外,std::shared_ptr 有几个常用函数如下:
void swap (unique_ptr& x)
将 shared_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏或改变二者的引用计数。
void reset()
void reset (ponit p)
没有参数时,先将管理的计数器引用计数减一并将管理的指针和计数器置清零。有参数 p 时,先做面前没有参数的操作,再管理 p 的所有权和设置计数器。
element_type* get()
得到其管理的指针。
long int use_count()
返回与当前智能指针对象在同一指针上共享所有权的 shared_ptr 对象的数量,如果这是一个空的 shared_ptr,则该函数返回 0。如果要用来检查 use_count 是否为 1,可以改用成员函数 unique 会更快。
bool unique()
返回当前 shared_ptr 对象是否不和其他智能指针对象共享指针的所有权,如果这是一个空的 shared_ptr,则该函数返回 false。
element_type& operator*()
重载指针的 * 运算符,返回管理的指针指向的地址的引用。
element_type* operator->()
重载指针的 -> 运算符,返回管理的指针,可以访问其成员。
explicit operator bool()
返回存储的指针是否已经是空指针,返回的结果与 get() != 0 相同。
std::enable_shared_from_this
实际开发中,有时候需要在类中返回包裹当前对象(this)的一个 std::shared_ptr 对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自 std::enable_shared_from_this 模板对象即可。用法如下:
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A>
{
public:
A()
{
std::cout << "A constructor" << std::endl;
}
~A()
{
std::cout << "A destructor" << std::endl;
}
std::shared_ptr<A> getSelf()
{
return shared_from_this();
}
};
int main()
{
std::shared_ptr<A> sp1(new A());
std::shared_ptr<A> sp2 = sp1->getSelf();
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
上述代码中,类 A 的继承 std::enable_shared_from_this 并提供一个 getSelf() 方法返回自身的 std::shared_ptr 对象,在 getSelf() 中调用 shared_from_this() 即可。
std::enable_shared_from_this 用起来比较方便,但是也存在很多不易察觉的陷阱。
陷阱一:不应该共享栈对象的 this 给智能指针对象
假设我们将上面代码 main 函数 25 行生成 A 对象的方式改成一个栈变量,即:
//其他相同代码省略...
int main()
{
A a;
std::shared_ptr<A> sp2 = a.getSelf();
std::cout << "use count: " << sp2.use_count() << std::endl;
return 0;
}
运行修改后的代码会发现程序在 std::shared_ptr sp2 = a.getSelf() 产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过 shared_from_this() 将该对象交由智能指针对象管理。
切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)。
陷阱二:避免 std::enable_shared_from_this 的循环引用问题
再来看另外一段代码:
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A>
{
public:
A()
{
m_i = 9;
//注意:
//比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
//但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
std::cout << "A constructor" << std::endl;
}
~A()
{
m_i = 0;
std::cout << "A destructor" << std::endl;
}
void func()
{
m_SelfPtr = shared_from_this();
}
public:
int m_i;
std::shared_ptr<A> m_SelfPtr;
};
int main()
{
{
std::shared_ptr<A> spa(new A());
spa->func();
}//39行
return 0;
}
我们来分析一下为什么 new 出来的 A 对象得不到释放。当程序执行到 39 行后,spa 出了其作用域准备析构,在析构时其发现仍然有另外的一个 std::shared_ptr 对象即 A::m_SelfPtr 引用了 A,因此 spa 只会将 A 的引用计数递减为 1,然后就销毁自身了。现在留下一个矛盾的处境:必须销毁 A 才能销毁其成员变量 m_SelfPtr,而销毁 m_SelfPtr 必须先销毁 A。这就是所谓的 std::enable_shared_from_this 的循环引用问题。我们在实际开发中应该避免做出这样的逻辑设计,这种情形下即使使用了智能指针也会造成内存泄漏。也就是说一个资源的生命周期可以交给一个智能指针对象,但是该智能指针的生命周期不可以再交给整个资源来管理。
4.std::weak_ptr
std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。
std::weak_ptr 可以从一个 std::shared_ptr 或另一个 std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。
示例代码如下:
#include <iostream>
#include <memory>
int main()
{
//创建一个std::shared_ptr对象
std::shared_ptr<int> sp1(new int(123));
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过构造函数得到一个std::weak_ptr对象
std::weak_ptr<int> sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过赋值运算符得到一个std::weak_ptr对象
std::weak_ptr<int> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
std::weak_ptr<int> sp4 = sp2;
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
运行结果:
use count: 1
use count: 1
use count: 1
use count: 1
无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。
既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptr 的 lock() 方法得到一个 std::shared_ptr 对象然后继续操作资源,以下代码演示了该用法:
// tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
// tmpConn_ 引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
return;
std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn)
{
//对conn进行操作,省略...
}
之所以 std::weak_ptr 不增加引用资源的引用计数不管理资源的生命周期,是因为,即使它实现了以上说的几个方法,调用它们也是不安全的,因为在调用期间,引用的资源可能恰好被销毁了,这会造成棘手的错误和麻烦。
因此,std::weak_ptr 的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。
std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。
class Subscriber
{
};
class SubscribeManager
{
public:
void publish()
{
for (const auto &iter : m_subscribers)
{
if (!iter.expired())
{
//TODO:给订阅者发送消息
}
}
}
private:
std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};
另外,std::weak_ptr 有几个常用函数如下:
void swap (weak_ptr& x)
将当前 weak_ptr 对象的内容与 x 的内容交换。
void reset()
将当前 weak_ptr 对象管理的指针和计数器变成空的,就像默认构造的一样。
long int use_count()
返回与当前 weak_ptr 对象在同一指针上共享所有权的 shared_ptr 对象的数量。
bool expired()
检查是否过期,返回 weak_ptr 对象管理的指针为空,或者和他所属共享的没有更多 shared_ptr。lock 函数一般需要先调用 expired 判断,如果已经过期,就不能通过 weak_ptr 恢复拥有的 shared_ptr。此函数应返回与(use_count() == 0)相同的值,但是它可能以更有效的方式执行此操作。
shared_ptr<element_type> lock()
如果它没有过期,则返回一个 shared_ptr,其中包含由 weak_ptr 对象保留的信息。如果 weak_ptr 对象已经过期,则该函数返回一个空的 shared_ptr(默认构造一样)。因为返回的 shared_ptr 对象也算作一个所有者,所以这个函数锁定了拥有的指针,防止它被释放(至少在返回的对象没有释放它的情况下)。 此操作以原子方式执行。
智能指针的大小
一个 std::unique_ptr 对象大小与裸指针大小相同(即 sizeof(std::unique_ptr) == sizeof(void)),而 std::shared_ptr 的大小是 std::unique_ptr 的一倍。以下是我分别在 Visual Studio 2019 和 gcc/g++ 4.8 上(二者都编译成 x64 程序)的测试结果:
#include <iostream>
#include <memory>
#include <string>
int main()
{
std::shared_ptr<int> sp0;
std::shared_ptr<std::string> sp1;
sp1.reset(new std::string());
std::unique_ptr<int> sp2;
std::weak_ptr<int> sp3;
std::cout << "sp0 size: " << sizeof(sp0) << std::endl;
std::cout << "sp1 size: " << sizeof(sp1) << std::endl;
std::cout << "sp2 size: " << sizeof(sp2) << std::endl;
std::cout << "sp3 size: " << sizeof(sp3) << std::endl;
return 0;
}
Visual Studio 2022(32bit) 运行结果:
sp0 size:8
sp1 size:8
sp2 size:4
sp3 size:8
Visual Studio 2022(64bit) 运行结果:
sp0 size:16
sp1 size:16
sp2 size:8
sp3 size:16
在 32 位机器上,std_unique_ptr 占 4 字节,std::shared_ptr 和 std::weak_ptr 占 8 字节。
在 64 位机器上,std_unique_ptr 占 8 字节,std::shared_ptr 和 std::weak_ptr 占 16 字节。
也就是说,std_unique_ptr 的大小总是和原始指针大小一样,std::shared_ptr 和 std::weak_ptr 大小是原始指针的一倍
智能指针使用注意事项
C++ 新标准提倡的理念之一是不应该再手动调用 delete 或者 free 函数去释放内存了,而应该把它们交给新标准提供的各种智能指针对象。C++ 新标准中的各种智能指针是如此的实用与强大,在现代 C++ 项目开发中,我们应该尽量去使用它们。智能指针虽然好用,但稍不注意,也可能存在许多难以发现的 bug,这里我根据经验总结了几条:
●一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作
看一段代码:
#include <memory>
class Subscriber
{
};
int main()
{
Subscriber *pSubscriber = new Subscriber();
std::unique_ptr<Subscriber> spSubscriber(pSubscriber);
delete pSubscriber;
return 0;
}
这段代码利用创建了一个堆对象 Subscriber,然后利用智能指针 spSubscriber 去管理之,可以却私下利用原始指针销毁了该对象,这让智能指针对象 spSubscriber 情何以堪啊?
记住,一旦智能指针对象接管了你的资源,所有对资源的操作都应该通过智能指针对象进行,不建议再通过原始指针进行操作了。
当然,除了 std::weak_ptr 之外,std::unique_ptr 和 std::shared_ptr 都提供了获取原始指针的方法——get() 函数。
int main()
{
Subscriber *pSubscriber = new Subscriber();
std::unique_ptr<Subscriber> spSubscriber(pSubscriber);
//pTheSameSubscriber和pSubscriber指向同一个对象
Subscriber *pTheSameSubscriber = spSubscriber.get();
return 0;
}
●分清楚场合应该使用哪种类型的智能指针
通常情况下,如果你的资源不需要在其他地方共享,那么应该优先使用 std::unique_ptr,反之使用 std::shared_ptr,当然这是在该智能指针需要管理资源的生命周期的情况下;如果不需要管理对象的生命周期,请使用 std::weak_ptr。
●认真考虑,避免操作某个引用资源已经释放的智能指针
前面的例子,一定让你觉得非常容易知道一个智能指针的持有的资源是否还有效,但是还是建议在不同场景谨慎一点,有些场景是很容易造成误判。例如下面的代码:
#include <iostream>
#include <memory>
class T
{
public:
void doSomething()
{
std::cout << "T do something..." << m_i << std::endl;
}
private:
int m_i;
};
int main()
{
std::shared_ptr<T> sp1(new T());
const auto &sp2 = sp1;
sp1.reset();
//由于sp2已经不再持有对象的引用,程序会在这里出现意外的行为
sp2->doSomething();
return 0;
}
上述代码中,sp2 是 sp1 的引用,sp1 被置空后,sp2 也一同为空。这时候调用 sp2->doSomething(),sp2->(即 operator->)在内部会调用 get() 方法获取原始指针对象,这时会得到一个空指针(地址为 0),继续调用 doSomething() 导致程序崩溃。
你一定仍然觉得这个例子也能很明显地看出问题,ok,让我们把这个例子放到实际开发中再来看一下:
//连接断开
void MonitorServer::OnClose(const std::shared_ptr<TcpConnection> &conn)
{
std::lock_guard<std::mutex> guard(m_sessionMutex);
for (auto iter = m_sessions.begin(); iter != m_sessions.end(); ++iter)
{
//通过比对connection对象找到对应的session
if ((*iter)->GetConnectionPtr() == conn)
{
m_sessions.erase(iter);
//注意这里:程序在此处崩溃
LOGI("monitor client disconnected: %s", conn->peerAddress().toIpPort().c_str());
break;
}
}
}
该段程序会在代码 12 行处崩溃,崩溃原因是调用了 conn->peerAddress() 方法。为什么这个方法的调用可能会引起崩溃?现在可以一目了然地看出了吗?
崩溃原因是传入的 conn 对象和上一个例子中的 sp2 一样都是另外一个 std::shared_ptr 的引用,当连接断开时,对应的 TcpConnection 对象可能早已被销毁,而 conn 引用就会变成空指针(严格来说是不再拥有一个 TcpConnection 对象),此时调用 TcpConnection 的 peerAddress() 方法就会产生和上一个示例一样的错误。
●作为类成员变量时,应该优先使用前置声明(forward declarations)
我们知道,为了减小编译依赖加快编译速度和生成二进制文件的大小,C/C++ 项目中一般在 *.h 文件对于指针类型尽量使用前置声明,而不是直接包含对应类的头文件。例如:
//Test.h
//在这里使用A的前置声明,而不是直接包含A.h文件
class A;
class Test
{
public:
Test();
~Test();
private:
A *m_pA;
};
同样的道理,在头文件中当使用智能指针对象作为类成员变量时,也应该优先使用前置声明去引用智能指针对象的包裹类,而不是直接包含包含类的头文件。
//Test.h
#include <memory>
//智能指针包裹类A,这里优先使用A的前置声明,而不是直接包含A.h
class A;
class Test
{
public:
Test();
~Test();
private:
std::unique_ptr<A> m_spA;
};
Modern C/C++ 已经变为 C/C++ 开发的趋势,希望能善用和熟练这些智能指针对象。
智能指针的简单实现
最后,给出智能指针的简单实现,因为 weak_ptr 作为弱引用指针,其实现依赖于 Counter 计数器类和 shared_ptr 的赋值,所以先进行 Counter 计数器类和 share_ptr 的简单实现。
Counter的简单实现
/*
* 计数器
* Counter对象就是用来申请一块内存存储引用计数
* m_refCount是SharedPtr的引用计数
* m_weakCount是WeakPtr的引用计数
* 当m_weakCount为0时删除Counter对象
*/
template <typename T>
class Counter
{
friend class SharedPtr<T>;
friend class WeakPtr<T>;
public:
Counter() : m_refCount(0), m_weakCount(0) {}
virtual ~Counter() {}
private:
Counter(const Counter &) = delete;
Counter &operator=(const Counter &) = delete;
private:
atomic_uint m_refCount; // #shared,原子操作
atomic_uint m_weakCount; // #weak,原子操作
};
shared_ptr的简单实现
/*
* SharedPtr的简单实现
*/
template <typename T>
class SharedPtr
{
friend class WeakPtr<T>;
public:
/*
* 构造函数,用原生指针构造
*/
SharedPtr(T *ptr) : m_ptr(ptr), m_cnt(new Counter<T>)
{
if (ptr)
{
m_cnt->m_refCount = 1;
}
cout << "Ptr Construct S." << endl;
}
~SharedPtr()
{
release();
}
/*
* 拷贝构造函数,用另一个SharedPtr对象构造
*/
SharedPtr(const SharedPtr &s)
{
m_ptr = s.m_ptr;
s.m_cnt->m_refCount++;
m_cnt = s.m_cnt;
cout << "S Copy Construct S." << endl;
}
/*
* 拷贝构造函数,用另一个WeakPtr对象构造
* 为了WeakPtr对象调用自己的lock()方法将自己传进来构造一个SharedPtr返回
*/
SharedPtr(const WeakPtr<T> &w)
{
m_ptr = w.m_ptr;
w.m_cnt->m_refCount++;
m_cnt = w.m_cnt;
cout << "W Copy Construct S." << endl;
}
/*
* 赋值构造函数,用另一个SharedPtr对象构造
*/
SharedPtr<T> &operator=(const SharedPtr<T> &s)
{
if (this != s)
{
this->release();
m_ptr = s.m_ptr;
s.m_cnt->m_refCount++;
m_cnt = s.m_cnt;
cout << "S Assign Construct S." << endl;
}
return *this;
}
T &operator*()
{
return *m_ptr;
}
T *operator->()
{
return m_ptr;
}
protected:
void release()
{
m_cnt->m_refCount--;
if (m_cnt->m_refCount < 1)
{
delete m_ptr;
m_ptr = nullptr;
cout << "SharedPtr Delete Ptr." << endl;
if (m_cnt->m_weakCount < 1)
{
delete m_cnt;
m_cnt = nullptr;
cout << "SharedPtr Delete Cnt." << endl;
}
cout << "SharedPtr Release." << endl;
}
}
private:
T *m_ptr;
Counter<T> *m_cnt;
};
weak_ptr的简单实现
template <typename T>
class WeakPtr
{
public:
/*
* 构造函数,用SharedPtr对象构造
*/
WeakPtr(SharedPtr<T> &s) : m_ptr(s.m_ptr), m_cnt(s.m_cnt)
{
m_cnt->m_weakCount++;
cout << "S Construct W." << endl;
}
/*
* 构造函数,用WeakPtr对象构造
*/
WeakPtr(WeakPtr<T> &w) : m_ptr(w.m_ptr), m_cnt(w.m_cnt)
{
m_cnt->m_weakCount++;
cout << "W Construct W." << endl;
}
~WeakPtr()
{
release();
}
/*
* 赋值构造函数,用另一个SharedPtr对象构造
*/
WeakPtr<T> &operator=(SharedPtr<T> &s)
{
release();
m_cnt = s.m_cnt;
m_cnt->m_weakCount++;
m_ptr = s.m_ptr;
cout << "S Assign Construct W." << endl;
return *this;
}
/*
* 赋值构造函数,用另一个WeakPtr对象构造
*/
WeakPtr<T> &operator=(WeakPtr<T> &w)
{
if (this != &w)
{
release();
m_cnt = w.m_cnt;
m_cnt->m_weakCount++;
m_ptr = w->m_ptr;
cout << "W Assign Construct W." << endl;
}
return *this;
}
/*
* WeakPtr通过lock函数获得SharedPtr
*/
SharedPtr<T> &lock()
{
return SharedPtr<T>(*this);
}
/*
* 检查SharedPtr是否已过期
*/
bool expired()
{
if (m_cnt)
{
if (m_cnt->m_refCount > 0)
return false;
}
return true;
}
private:
WeakPtr() = delete; WeakPtr禁止默认构造,只能从SharedPtr或者WeakPtr构造
T &operator*() = delete; //WeakPtr禁止*
T *operator->() = delete; //WeakPtr禁止->
private:
void release()
{
if (m_cnt)
{
m_cnt->m_weakCount--;
if (m_cnt->m_weakCount < 1 && m_cnt->m_refCount < 1)
{
delete m_cnt;
m_cnt = nullptr;
cout << "Delete Cnt." << endl;
}
cout << "WeakPtr Release." << endl;
}
}
private:
T *m_ptr;
Counter<T> *m_cnt;
};
面的实现可能不是非常严谨,仅实现了常用的的函数接口而已,但其主要的目的是为了更深刻的了解智能指针的原理,这样才能更有把握的使用智能指针,只有了解它的内部实现,对于使用中的一些坑才能有效避免。
参考:[深入实践C++11智能指针](
标签:std,对象,智能,shared,原理,unique,ptr,指针 From: https://www.cnblogs.com/codemagiciant/p/17601921.html