C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。
使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,等问题,使用智能指针能更好的管理堆内存。
“有时候我们会忘记释放内存,甚至有时候我们根本就不知道什么时候释放内存。特别时在多个线程间共享数据时,更难判断内存该何使释放。这种情况下就机器容易产生引用非法内存的指针。"
智能指针的行为类似于常规指针。重要的区别是它负责自动释放所指向的对象。
1)从较浅的层面看,智能指针是利用了一种叫做RAII(Resource Acquisition Is Initialization,资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
RAII是C++语言的一种管理资源、避免泄漏的惯用法。
2)智能指针的作用是防止忘记调用delete释放内存,和程序异常进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针在C++11版本以及之后提供,包含在头文件
shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。标准库还定义了一个weak_ptr类,它是一种弱引用,指向shared_ptr所管理的对象。
shared_ptr 多个指针指向相同的对象。shared_ptr 使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
“只要将 new 运算符返回的指针 p 交给一个 shared_ptr 对象“托管”,就不必担心在哪里写delete p
语句——实际上根本不需要编写这条语句,托管 p 的 shared_ptr 对象在消亡时会自动执行delete p
。而且,该 shared_ptr 对象能像指针 p —样使用,即假设托管 p 的 shared_ptr 对象叫作 ptr,那么 *ptr 就是 p 指向的对象。”
"多个 shared_ptr 对象可以共同托管一个指针 p,当所有曾经托管 p 的 shared_ptr 对象都解除了对其的托管时,就会执行delete p
。"
智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr
解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
常见的初始化:
int a = 10;
std::shared_ptr ptra = std::make_shared(a);
/*---------空指针------------*/
shared_ptr p1;
if(!p1) //默认初始化的智能指针中保存着一个空指针!并不是""空字符串
cout<<"p1==NULL"< p1(new string);
if(p1&&p1->empty()){ //需要注意 empty是属于string类的成员函数。
*p1="helloworld";
cout<<*p1< pa = new int(1);//error:不允许以暴露裸漏的指针进行赋值操作。
//一般的初始化方式
shared_ptr pint(new string("normal usage!"));
cout<<*pint< pint1 = make_shared("safe uage!");
cout<<*pint1<("auto"); //!更简单,更常用的方式。
cout<<*pau<
不推荐的初始化方式:
/*不推荐*/
int * p = new int(32);
shared_ptr pp(p);
cout<<*pp<
#include
#include
using namespace std;
class A
{
public:
int i;
A(int n):i(n) { };
~A() { cout << i << " " << "destructed" << endl; }
};
int main()
{
shared_ptr sp1(new A(2)); //A(2)由sp1托管,
shared_ptr sp2(sp1); //A(2)同时交由sp2托管
shared_ptr sp3;
sp3 = sp2; //A(2)同时交由sp3托管
cout << sp1->i << "," << sp2->i <<"," << sp3->i << endl; //2,2,2
A * p = sp3.get(); // get返回托管的指针,p 指向 A(2)
cout << p->i << endl; //输出 2
sp1.reset(new A(3)); // reset导致托管新的指针, 此时sp1托管A(3)
sp2.reset(new A(4)); // sp2托管A(4)
cout << sp1->i << endl; //输出 3
sp3.reset(new A(5)); // sp3托管A(5),然后,A(2)无人托管,被delete
cout << "end" << endl;
return 0;
}
上例中执行sp3.reset(new int(5))之后,A(2)无人托管,被delete,但是这时A* p还是指向A(2)原先空间,危险! 所以,用sharedPtr的话就不要用裸指针啦!!
输出:
2,2,2
2
3
2 destructed
end
5 destructed
4 destructed
3 destructed
可以让多个 shared_ptr 对象去托管同一个指针,这些多个 shared_ptr 对象会共享一个对共同托管的指针的“托管计数”。有 n 个 shared_ptr 对象托管同一个指针 p,则 p 的托管计数就是 n。当一个指针的托管计数减为 0 时,该指针会被释放。shared_ptr 对象消亡或托管了新的指针,都会导致其原托管指针的托管计数减 1。
shared_ptr 的 reset 成员函数可以使得对象解除对原托管指针的托管(如果有的话),并托管新的指针。原指针的托管计数会减 1。
main 函数结束时,sp1、sp2、sp3 对象消亡,各自将其托管的指针的托管计数减为 0,并且释放其托管的指针,于是输出5 destructed ...
只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。将指向普通局部变量、全局变量的指针交给 shared_ptr 托管,编译时不会有问题,但程序运行时会出错,因为不能delete一个没有指向动态分配(堆内存)的内存空间的指针。
注意,不能用下面的方式使得两个 shared_ptr 对象托管同一个指针:
A* p = new A(10);
shared_ptr sp1(p), sp2(p);
sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致程序崩溃。
std::shared_ptr sp = std::make_shared(10)
int *pRaw = sp.get();
但是这种方方法不安全,例如取出的指针可能被释放等。
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。
循环引用问题举例:
#include
using namespace std;
#include"boost/shared_ptr.hpp"
struct Node
{
Node(int value)
:_value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
}
shared_ptr _prev;
shared_ptr _next;
int _value;
};
void Test2()
{
shared_ptr sp1(new Node(1));
shared_ptr sp2(new Node(2));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
Test2();
return 0;
}
在这里我们使用了双向链表来说明这个问题,并且双向链表中的指针都是使用shared_ptr来维护的,在这里我使用的是标准库里的shared_ptr智能指针。
从下图可以看出,构造的sp1和sp2在出它们的作用域时(即Test2())都没有被析构,从而造成了内存泄漏。
由于先构造的后释放,后构造的先释放可知,先释放的是sp2,那么因为它的引用计数为2,减去1之后就成为了1,不能释放空间,因为还有其他的对象在管理这块空间。但是sp2这个变量已经被销毁,因为它是栈上的变量,但是sp2管理的堆上的空间并没有释放。
接下来释放sp1,同样,先检查引用计数,由于sp1的引用计数也是2,所以减1后成为1,也不会释放sp1管理的动态空间。
通俗点讲:就是sp2要释放,那么必须等p1释放了,而sp1要释放,必须等sp2释放,所以,最终,它们两个都没有释放空间。
所以,造成了内存泄漏。
使用weak_ptr来解决上述的循环引用问题:
#include
using namespace std;
#include"boost/shared_ptr.hpp"
struct Node
{
Node(int value)
:_value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
}
weak_ptr _prev;
weak_ptr _next;
int _value;
};
void Test2()
{
shared_ptr sp1(new Node(1));
shared_ptr sp2(new Node(2));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main()
{
Test2();
return 0;
}
输 出:
“weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。” ---》 解决了问题啊。
shared_ptr对象每次离开作用域时会自动调用shared_ptr类的析构函数,而析构函数并不像其他类的析构函数一样,而是在释放内存是时先判断引用计数器是否为0。等于0才做delete操作,否则只对引用计数器做减一操作。
一般来说,智能指针的实现需要以下步骤:
1.一个模板指针T* ptr,指向实际的对象。
2.一个引用次数(必须new出来的!!不然会多个shared_ptr里面会有不同的引用次数而导致多次delete)。
3.定义operator*和operator->,使得能像指针一样使用shared_ptr。
4.定义copy constructor,使其引用次数加一。
5.定义operator=,如果原来的shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。
然后将新的对象引用次数加一。
6.定义析构函数,使引用次数减一并判断引用是否为零(是否调用delete)。
#include
#include
template
class SmartPointer {
private:
T* _ptr;
size_t* _count;
public:
SmartPointer(T* ptr = nullptr) : _ptr(ptr)
{
if (_ptr) {
_count = new size_t(1);
} else {
_count = new size_t(0);
}
}
//复制构造函数,用一个已经存在的对象去初始化一个新的对象。注意,两个对象的指针
//成员_count指向同一个内存地址,所以最后两者的对_ptr的计数是相同的。我曾疑惑
SmartPointer(const SmartPointer& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}
SmartPointer& operator=(const SmartPointer& ptr) {
if (this->_ptr == ptr._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_count)--;
if (this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}
T& operator*() {
assert(this->_ptr == nullptr);
return *(this->_ptr);
}
T* operator->() {
assert(this->_ptr == nullptr);
return this->_ptr;
}
~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
size_t use_count(){
return *this->_count;
}
};
int main() {
{
SmartPointer sp(new int(10));
SmartPointer sp2(sp);
SmartPointer sp3(new int(20));
sp2 = sp3;
std::cout << sp.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
}
//delete operator
}
把对象直接存入容器中有时会有些麻烦。以值的方式保存对象意味着使用者将获得容器中的元素的拷贝,对于那些复制是一种昂贵的操作的类型来说可能会有性能的问题。此外,有些容器,特别是 std::vector, 当你加入元素时可能会复制所有元素,这更加重了性能的问题。最后,传值的语义意味着没有多态的行为。如果你需要在容器中存放多态的对象而且你不想切割它们,你必须用指针。如果你用裸指针,维护元素的完整性会非常复杂。从容器中删除元素时,你必须知道容器的使用者是否还在引用那些要删除的元素。
#include "boost/shared_ptr.hpp"
#include
#include
class A {
public:
virtual void sing()=0;
protected:
virtual ~A() {};
};
class B : public A {
public:
virtual void sing() {
std::cout << "Do re mi fa so la";
}
};
boost::shared_ptr createA() {
boost::shared_ptr p(new B());
return p;
}
int main() {
typedef std::vector > container_type;
typedef container_type::iterator iterator;
container_type container;
for (int i=0;i<10;++i) {
container.push_back(createA());
}
std::cout << "The choir is gathered: \n";
iterator end=container.end();
for (iterator it=container.begin();it!=end;++it) {
(*it)->sing();
}
}
这里有两个类, A和 B, 各有一个虚拟成员函数 sing。 B从 A公有继承而来,并且如你所见,工厂函数 createA返回一个动态分配的B的实例,包装在shared_ptr里。
在 main里, 一个包含shared_ptr的 std::vector被放入10个元素,最后对每个元素调用sing。如果我们用裸指针作为元素,那些对象需要被手工删除。而在这个例子里,删除是自动的,因为在vector的生存期中,每个shared_ptr的引用计数都保持为1;当 vector被销毁,所有引用计数器都将变为零,所有对象都被删除。
上面的例子示范了一个强有力的技术,它涉及A里面的protected析构函数。因为函数 createA返回的是 shared_ptr, 因此不可能对shared_ptr::get返回的指针调用 delete。这意味着如果向某个需要裸指针的函数传送从shared_ptr中取出的裸指针,不会由于意外地被删除而导致灾难。这是非常有用的方法,用于给shared_ptr中的对象增加额外的安全性。
https://baike.baidu.com/item/shared_ptr/2708164?fr=aladdin
多线程读写同一个shared_ptr 对象,必须加锁:
https://www.cnblogs.com/gqtcgq/p/7492772.html(看这篇文章的 “三:为什么多线程读写 shared_ptr 要加锁?” 相当容易理解哦。)
我们都会有一种普遍的认知,shared_ptr智能指针只能用来管理动态开辟的空间,实际不然,shared_ptr是用来管理资源的,这里的资源并不仅仅是动态开辟的空间,还有例如:打开文件,其维护一个文件指针,那么在程序快要结束时,需要关闭文件;
所以呢,我们就需要为其定制删除器。
例子,尚未验证:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
//为解决文件指针
struct Fclose
{
void operator()(FILE*& fp)
{
cout<< "Fclose()" << endl;
fclose(fp);
fp = NULL;
}
};
//为解决malloc开辟的空间
template
struct Free
{
void operator()(T*& p)
{
free(p);
p = NULL;
}
};
//一般情况下(使用new动态开辟的空间)
template
class Delete
{
public :
void operator()(T*& p)
{
delete p;
p = NULL;
}
};
template >
class SharedPtr
{
public :
SharedPtr(T* ptr = 0)//构造函数
:_ptr(ptr)
,_pCount(NULL)
{
if (_ptr)
{
_pCount = new int(1);
}
}
SharedPtr(const SharedPtr& sp)//拷贝构造函数
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
if (_ptr)
{
++GetRef();
}
}
//sp1 = sp2
SharedPtr& operator=(const SharedPtr& sp)
{
//可有三种情况:①sp1._ptr = NULL ②sp1的引用计数为1 ③sp1的引用计数大于1
if (this != &sp)
{
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++GetRef();
}
return *this;
}
//辅助函数
void Release()
{
if (_ptr && --GetRef() == 0)
{
Destory()(_ptr);
delete _pCount;
_pCount = NULL;
}
}
//析构函数
~SharedPtr()
{
Release();
}
int& GetRef()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
};
void Test2()
{
FILE* sp1 = fopen("test.txt", "rb");
SharedPtr sp2(sp1);
int* sp3 = (int*)malloc(sizeof(int));
SharedPtr >sp4(sp3);
int* sp5 = new int(1);
SharedPtr sp6(sp5);
}
int main()
{
Test2();
return 0;
}
在以下情况时使用 shared_ptr :
当有多个使用者使用同一个对象,而没有一个明显的拥有者时
当要把指针存入标准库容器时
当要传送对象到库或从库获取对象,而没有明确的所有权时
当管理一些需要特殊清除方式的资源时
通过定制删除器的帮助。
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
#include
#include
int main() {
{
std::unique_ptr uptr(new int(10)); //绑定动态对象
//std::unique_ptr uptr2 = uptr; //不能賦值
//std::unique_ptr uptr2(uptr); //不能拷貝
std::unique_ptr uptr2 = std::move(uptr); //轉換所有權
uptr2.release(); //释放所有权
}
//超過uptr的作用域,內存釋放
}
Ref:
https://www.cnblogs.com/wxquare/p/4759020.html
https://www.cnblogs.com/wangkeqin/p/9351191.html
https://blog.csdn.net/worldwindjp/article/details/18843087#comments
http://c.biancheng.net/view/430.html
https://blog.csdn.net/qq_34992845/article/details/69218843
别人笑我太疯癫,我笑别人看不穿。--唐寅