C++11智能指针shared_ptr,weak_ptr以及循环引用的问题

1.智能指针

通常内存管理中存在以下问题:

  • 重复析构(释放)同一块内存导致程序运行崩溃

例如,如果类中有指针成员,浅拷贝造成两个对象的指针成员指向同一个内存。当程序运行结束,一块内存被析构了两次。

  • 有些资源的内存被释放,但是其指针并未被修改指向。
void test()
{
	int *dp = new int[10];
	delete dp[];
	//忘记添加dp = nullptr;
}

上面这种情况导致后续万一在该函数使用了dp指针,造成程序崩溃。这是因为虽然delete虽然释放了内存,但是dp并没有改变指向位置,导致dp变成野指针。

  • 没有及时释放一些内存,每次都申请了,但是忘记释放,最后导致内存溢出

为了解决上述问题,C++11中引入了智能指针。C++11标准在废弃的auto_ptr基础上,引入了三种智能指针:

  • shared_ptr
  • unique_ptr
  • weak_ptr

C++ 智能指针底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。

三种指针的都采用类模板实现,需要在使用时包含memory头文件并指定数据的类型

2. shared_ptr

该指针叫做共享指针,和 unique_ptrweak_ptr 不同的是,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr指针(只有引用计数为 0 时,堆内存才会被自动释放)。

常见的使用shared_ptr指针的方式有:

  • 无参构造(默认构造)
std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr);    //传入空指针 nullptr
  • 有参构造
std::shared_ptr<int> p3(new int(10));
  • 拷贝构造
//调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr p4 = p3;

如上所示,p3 和 p4 都是 shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。

  • 移动构造
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr p5 = std::move(p4);

移动构造是为了解决调用函数时返回的局部对象二次拷贝赋值的情况。直接将临时对象的内存作为己用。

  • make_pair模板函数
std::shared_ptr<int> p3 = std::make_shared<int>(10);

注意:同一普通指针不能同时为多个 shared_ptr对象赋值,否则会导致程序发生异常。例如:

int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误

初始化智能指针时,还可以指定释放内存的规则

在某些场景中,自定义释放规则是很有必要的。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

对于申请的动态数组,释放规则可以使用 C++11 标准中提供的 default_delete 模板类,我们也可以自定义释放规则.

//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());

//自定义释放规则
void deleteInt(int*p) {
    delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);

另外还可以用匿名函数(lambda表达式)来指定规则:

std:: shared_ptr<int> p8(new int[10], [](int *p){
	delete []p;
});

2. weak_ptr弱指针

这里我们先介绍弱指针。为什么要引入弱指针呢?这是因为在使用shared_ptr时,有时可能出现循环引用的情况。请看下面的示例:

// intelligentPointer.cpp
#include
#include
using namespace std;
//1.没有借助弱智真weak_ptr会形成环形引用,对象无法销毁
template<typename T>
struct Node
{
    //构造函数
    Node(const T& data):_data(data),_pre(nullptr),_pnext(nullptr){}
    ~Node()
    {
        cout << "~Node()" << endl;
    }
    //成员变量
    T _data;
    shared_ptr<Node<T>>_pre;
    shared_ptr<Node<T>>_pnext;

};

void test()
{
    shared_ptr<Node<int>> sp1(new Node<int>(10));
    shared_ptr<Node<int>> sp2(new Node<int>(20));

    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl; 

    //两个智能指针此时形成了环形引用
    sp1->_pnext = sp2;
    sp2->_pre = sp1;

    cout << sp1.use_count() << endl;
    cout << sp2.use_count() <<endl;
    //按理说,函数调用结束后应该将两个对象应该被销毁,但是这里没有发生销毁,sp1
    //和sp2的引用从1变为2
}
int main()
{
    test();
    system("pause");
    return 0;
}

这里的输出结果是:

1
1
2
2

按理说调用完test()函数后,两个内部的对象应该被销毁,同时调用析构函数。但是这里并没有发生上述情况。这是因为,sp1指向了sp2,而sp2又指向了sp1,双方都在等待对方先销毁,然后自己再销毁。但是这里发生循环引用,导致函数调用结束后不能正常销毁这两个对象。

为了解决上述问题,引入了弱指针,弱指针只具有观测作用,不会使引用计数+1,更不能使用指向的对象。

#include
#include
using namespace std;
//2.借助弱指针,解决环形引用的问题。弱指针仅仅相当于可以指向,但是并不能解引用或者使用指向的对象
template<typename T>
struct Node2{
    //2.弱指针不必初始化
    Node2(const T& data):_data(data){}

    ~Node2()
    {
        cout << "~Node2()" << endl;
    }
    T _data;
    weak_ptr<Node2<T>> _pre;
    weak_ptr<Node2<T>> _pnext;

};

void test2()
{
    shared_ptr<Node2<int>> sp1(new Node2<int>(11));
    shared_ptr<Node2<int>> sp2(new Node2<int>(22));

    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;

    sp1->_pnext = sp2;
    sp2->_pre = sp1;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;

}

int main()
{
	test2();
    system("pause");
    return 0;
}

输出结果:

1
1
1
1
~Node2()
~Node2()

这里明显发现,析构函数被调用了,内存都得到了释放。这就是弱指针的作用。

其实这里的弱指针作用某些方面等同于普通指针Node* _preNode* _pnext

  • 都不会增加引用计数
  • 在函数调用结束后都会被释放掉

下面的例子采用普通指针,虽然析构函数中没有将_pre_pnext置为空,但是在函数调用结束后,栈中的所有数据都不存在了。这就是为什么说在某种程度等价的原因。

#include
#include
using namespace std;
template<typename T>
struct Node3
{
    /* data */
    Node3(const T& data):_data(data),_pre(nullptr),_pnext(nullptr){}
    ~Node3()
    {
        cout << "~Node3" <<endl;
    }
    T _data;
    Node3<T>*_pre;
    Node3<T>*_pnext;
};

void test3()
{
    Node3<int>* sp1(new Node3<int>(11));
    Node3<int>* sp2(new Node3<int>(22));

    sp1->_pnext = sp2;
    sp2->_pre = sp1;
}
int main()
{
    test3();
    system("pause");
    return 0;
}

weak_ptrshared_ptr之间可以相互转化shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr

未完待续……

你可能感兴趣的:(C++,c++,c语言,智能指针,循环引用)