欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Boost 智能指针(一)

程序员文章站 2024-03-14 18:14:04
...

引言:这是学习过程中的智能指针笔记,代码的解释写在了注释之间,为了方便及时查阅,以供大家学习。

内容包含:
1、初步认识 shared_ptr 的引用计数
2、shared_ptr 的缺陷 以及 如何使用 weak_ptr 避免缺陷
3、shared_ptr 的使用技巧


1、主函数
介绍了shared_ptr 的引用计数,shared_ptr 的缺陷 及使用技巧

#define _CRT_SECURE_NO_WARNINGS
#include <boost/shared_ptr.hpp>
#include <iostream>
#include <string>
#include <assert.h>
#include <vector>
#include "FileSharedPtr.h"
#include "IPrinter.h"
using namespace std;

class Foo
{
public:
    Foo(const string & strname):m_strName(strname){}
    ~Foo()
    {
        cout << "Destructing Foo with name = " << m_strName << endl;
    }
private:
    string m_strName;
};

typedef boost::shared_ptr<Foo> FooPtr;

//---------------- shared_ptr 的引用计数----------------------//
void Test_Boost_Shared_Ptr()
{
    /// ptr1 获得 Foo1 指针的所有权
    FooPtr ptr1(new Foo("Foo1"));/// 引用计数为1
    assert(ptr1.use_count() == 1);

    /// ptr2 指向 ptr1
    FooPtr ptr2 = ptr1;/// 调用 shared_ptr 的赋值操作符,引用计数加1
    assert(ptr1.use_count() == ptr2.use_count());/// 两者引用计数相同
    assert(ptr1 == ptr2);/// shared_ptr 重载 == 操作符,等同于ptr1.get() == ptr2.get()
    assert(ptr1.use_count() == 2);/// 现在ptr1 和ptr2 都指向了Foo1,因此计数器都为2

    /// ptr3获得 Foo1 指针的所有权
    FooPtr ptr3 = ptr2;///引用计数加1
    assert(ptr1.use_count() == ptr2.use_count() && ptr1.use_count() == ptr3.use_count());
    assert(ptr1.use_count() == 3 && ptr2.use_count() == 3 && ptr3.use_count() == 3);
    assert(ptr1 == ptr2 && ptr1 == ptr3);

    /// 重置ptr3,测试reset()函数
    ptr3.reset();
    assert(ptr3.use_count() == 0 && ptr3.get() == NULL);
    cout << "ptr3 引用计数为0,get()指针指向NULL,但是不会调用析构函数,因为ptr1和ptr2都指向了原生指针" << endl;
    assert(ptr1.use_count() == ptr2.use_count() && ptr1.use_count() == 2);
    assert(ptr1 == ptr2 && ptr1 != ptr3);

    /// 前面ptr1和ptr2都指向了同一个对象,他们的引用计数都为2,现在创建一个ptr4,让其指向ptr1
    FooPtr ptr4 = ptr1; // 引用计数加一
    assert(ptr1 == ptr2 && ptr4 == ptr1);
    assert(ptr4.use_count() == 3 && ptr1.use_count() == 3 && ptr2.use_count() == 3);

    /// 现在转移ptr2的所有权到另一个Foo指针上去
    cout << "转移ptr2的拥有权到一个新的Foo指针上" << endl;
    ptr2.reset(new Foo("Foo2"));

    /// 在ptr2转移所有权后,ptr2计数器应该是1,并且ptr1和ptr4的计数器是2,并且ptr1 != ptr2
    assert(ptr2.use_count() == 1 && ptr1.use_count() == 2 && ptr4.use_count() == 2);
    assert(ptr1 != ptr2 && ptr1 == ptr4);

    ///此时ptr3因为被重置为0,并且get()为NULL了,我们再使用一个ptr5来指向ptr3也应该引用计数为0
    FooPtr ptr5 = ptr3;
    assert(ptr5.use_count() == 0 && ptr5.get() == NULL);
/*********************** 总结 ********************************
    运行到此时,程序即将结束,在退出作用于前会调用析构函数(引用计数为0,即调用析构函数)
    首先会打印出Destructing a Foo with name = Foo2
    ptr1 和 ptr4也会相继调用析构函数进行引用计数递减
    最终会打印出Destructing a Foo withname = Foo1
    内存释放完毕,这样就没有内存泄漏了
**************************************************************/ 
}

//---------------------shared_ptr 的缺陷------------------------//
void Test_Boost_Shared_ptr_crash()
{
    Foo * pFoo = new Foo("Foo1");
    shared_ptr<Foo> ptr1(pFoo);
    shared_ptr<Foo> ptr2(pFoo);
    cout << ptr1.use_count() << endl;
    cout << ptr2.use_count() << endl;
    /// 函数退出时会崩溃!,因为ptr1、ptr2两次释放同一内存而破坏堆,导致程序崩溃    

/******************** 总结 ******************************
shared_ptr  多次引用同一内存数据将导致程序崩溃
1)直接使用 boost::shared_ptr<int> ptr1(new int(100));
    使用匿名内存分配限制其他shared_ptr多次指向同一内存数据
    多次指向时使用 shared_ptr 的复制操作符或拷贝构造函数,例如 shared_ptr<int> ptr2 = ptr1;
    上面的匿名方式就是资源初始化既分配技术,直接在构造函数中分配内存
2)使用boost的 make_shared 模板函数,例如shared_ptr<int> ptr1 = boost::make_shared<int>(100)
    shared_ptr<int> ptr2 = ptr1;
****************************************************/
}

//----------------------shared_ptr 的使用技巧--------------------------//
/// shared_ptr 的使用技巧一:将shared_ptr用于标准容器库
/// (1)标准容器库作为shared_ptr管理的对象 (2)将 shared_ptr 作为容器的元素
void Test_Vector_Shared_Ptr()
{
    char name[32];
    vector<FooPtr> ptrs;
    for (int i = 0; i < 20; i++)
    {
        sprintf(name, "foo%d", i);
        ptrs.push_back(FooPtr(new Foo(name)));
    }/// 循环完毕:20个FooPtr的引用计数为 1

    for (int i = 0; i < 20; i++)
    {
        FooPtr ptr = ptrs[i];   /// 此时 ptr进行引用后,引用计数加1,此时引用计数为 2
        assert(ptrs[i].use_count() == 2);
    }/// 循环完毕,各自的引用计数为1.为什么不是2?因为 ptr 为局部变量,它的作用域
     /// 在循环内,循环完毕退出作用域的时候,会进行减1操作,所以 2-1=1

    for (int i = 0; i < 20; i++)
    {
        assert(ptrs[i].use_count() == 1);
    }

}/// 函数结束的时候,退出作用域,ptrs里面的FooPtr的引用计数会进行减1操作,最后的引用计数为0
 /// 引用计数为0后,自动调用析构函数释放内存

/// shared_ptr 的使用技巧二:以函数封住现有的c函数
typedef boost::shared_ptr<FILE> FilePtr;
void FileClose(FILE * f)
{
    fclose(f);
    cout << "调用fclose函数释放FILE资源" << endl;
}
FilePtr FileOpen(char const* path, char const* mode)
{
    //第二个参数是指,通过调用 FileClose 来释放第一个参数
    FilePtr fptr(fopen(path, mode), FileClose);
    //FilePtr fptr(fopen(path, mode), fclose);
    return fptr;
}
void FileRead(FilePtr& f, void* data, size_t size)
{
    cout << "use_count() = " << f.use_count() << endl;
    fread(data, 1, size, f.get());
}
void Test_C_File_Ptr()
{
    FilePtr ptr = FileOpen("memory.log", "r");
    FilePtr ptr2 = ptr;
    char data[512] = { 0 };
    FileRead(ptr, data, 512);
    cout << data << endl;
}

/// shared_ptr 使用技巧三:用c++桥接设计模式来封装现有的c函数
void Test_FileSharePtr()
{
    char data[512] = { 0 };
    FileSharedPtr pPile("memory.log", "r");
    pPile.Read(data, 512);
}

/// shared_ptr 使用技巧四:使用面向接口编程方式隐藏实现
/// 注意面向接口编程的思想 
void Test_Printer()
{
    PrinterPtr ptr = CreatePrinter();
    ptr->Print();
    PrinterPtr ptr2 = ptr;
    ptr2->Print();
}

int main() 
{
    //Test_Boost_Shared_Ptr();
    //Test_Boost_Shared_ptr_crash();
    //Test_Vector_Shared_Ptr();
    //Test_C_File_Ptr();
    //Test_FileSharePtr();
    Test_Printer();
    getchar();
    return 0;
}

主函数中使用到的 IPrinter类
IPrinter.h

#pragma once
#include <boost/shared_ptr.hpp>

class IPrinter
{
public:
    virtual void Print() = 0;
protected:
    // 受保护的析构函数,
    // 如果要调用析构函数,必须被继承,也不会被 delete 释放,可以防止用户不小心d elete
    virtual ~IPrinter()
    {
        printf("invoke IPrinter virtual析构函数\n");
    }
};

typedef boost::shared_ptr<IPrinter> PrinterPtr;
PrinterPtr CreatePrinter(); // 工厂方法,创建IPrinter智能指针

IPrinter.cpp

#include "IPrinter.h"

class Printer :public IPrinter
{
private:
    FILE* f;
public:
    Printer(const char* path, const char* mode)
    {
        f = fopen(path, mode);
    }

    ~Printer()
    {
        int result = fclose(f);
        printf("invoke Printer virtual析构函数:%d\n",result);
    }

    virtual void Print() override
    {
        char data[512] = { 0 };
        fread(data, 1, 512, f);
        printf("%s\n", data);
    }

    // 子类释放的时候,会自动调用父类的析构函数
    // 通过这种封装,同时就封装了 delete 的操作,只有调用我们的智能指针才能释放内存
    // 此时就实现了,通过智能指针来管理内存的功能,没有了原始指针的操作,也不会忘记没有释放指针导致的内存泄漏问题
};

PrinterPtr CreatePrinter()
{
    PrinterPtr ptr(new Printer("memory.log", "r"));
    return ptr;
}

主函数中使用到的 FileSharedPtr 类
FileSharedPtr.h

#pragma once
#include <boost/shared_ptr.hpp>

/// shared_ptr 使用技巧三:用c++桥接设计模式来封装现有的c函数
/// 注意桥接模式的用法
class FileSharedPtr
{
public:
    FileSharedPtr();
    ~FileSharedPtr(); 
    FileSharedPtr(char const* name, char const* mode);
    void Read(void* data, size_t size);
private:
    class impl;// 很重要一点,前向声明实现该类,具体实现在.cpp文件中,隐藏实现细节
    boost::shared_ptr<impl> pimpl;// shared_ptr 作为私有成员变量
};

FileSharedPtr.cpp

#include "FileSharedPtr.h"

class FileSharedPtr::impl
{
private:
    impl(impl const &) {}
    impl & operator= (impl const&) {}
    FILE* f;

public:
    impl(char const* name, char const* mode)
    {
        f = fopen(name, mode);
    }
    ~impl()
    {
        int result = fclose(f);
        printf("invoke FileSharedPtr::impl 析构函数 result = %d\n", result);
    }
    void read(void * data, size_t size)
    {
        fread(data, 1, size, f);
    }
};

FileSharedPtr::FileSharedPtr()
{
}

FileSharedPtr::~FileSharedPtr()
{
}

FileSharedPtr::FileSharedPtr(char const* name, char const* mode)
    : pimpl(new FileSharedPtr::impl(name, mode)) {}

void FileSharedPtr::Read(void* data, size_t size)
{
    pimpl->read(data, size);
}

/****************************************
这样的好处是:
上层类(FileSharedPtr)都是调用的实现类(impl)的具体的方法,
这样我们就把我们所有的实现都放在实现类中,上层类相当于一个接口类,
不进行相关操作,只提供相应的接口
*******************************************/

2、使用 weak_ptr 解决 shared_ptr 循环引用导致内存泄露问题

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <iostream>
using namespace std;

class Parent;
class Child;

class Parent
{
public:
    Parent(){}
    ~Parent(){cout << "父类析构函数被调用" << endl;}
public:
    boost::shared_ptr<Child> child_shared_ptr;
    boost::weak_ptr<Child> child_weak_ptr;
};

class Child
{
public:
    Child(){}
    ~Child(){cout << "调用child的析构函数" << endl;}
public:
    boost::shared_ptr<Parent> parent_shared_ptr;
    boost::weak_ptr<Parent> parent_weak_ptr;
};

/// 测试 shared_ptr 循环引用导致内存泄露
void Test_Parent_Child_Ref()
{
    boost::shared_ptr<Parent> father(new Parent());
    boost::shared_ptr<Child> son(new Child());
    father->child_shared_ptr = son;
    son->parent_shared_ptr = father;
/*****************************************************************
测试结果:会发现并没有调用相关的构造函数,导致内存泄露
原因:因为循环引用,导致father、son的引用计数都为2,退出作用域(就是退出此函数)的时候,
    引用计数都减一,此时彼此的引用计数为2-1=1,因为调用析构函数必须的引用计数为0,
    所以此时就无法调用析构函数,于是造成father和son所指向的内存得不到释放,导致内存泄露
*****************************************************************/
}

/// 测试 weak_ptr 解决循环引用和自引用导致的内存泄漏问题
void Test_Weak_ptr()
{
    boost::shared_ptr<Parent> father(new Parent());
    boost::shared_ptr<Child> son(new Child());
    father->child_weak_ptr = son;
    son->parent_weak_ptr = father;
    // 此时我们使用 lock 方法进行操作
    cout << father->child_weak_ptr.lock().use_count() << endl;
    cout << son->parent_weak_ptr.lock().use_count() << endl;
/*********************************************************************
lock()方法作用是转换为 shared_ptr:
函数原型:
shared_ptr<_Ty> lock() const _NOEXCEPT
{ 
    return (shared_ptr<_Ty>(*this, false)); 
}
**********************************************************************/
}

int main()
{
    Test_Parent_Child_Ref();
    getchar();
    return 0;
}

/***************************** 总结 *************************************
1、引用计数是很便利的内存管理机制,但是对于循环引用或自引用对象(例如链表或树节点)无法管理,
    此时用 weak_ptr 来解决这个限制
2、weak_ptr 并不能单独存在,它是与 shared_ptr 同时使用的,它更像 shared_ptr 的助手,而不是
    智能指针,因为它不具备智能指针的行为,没有重载 operator* 和 -> 操作符,这是特意的,这样
    他就不能共享指针,不能操作资源,这是它弱的原因。它最大的作用是协助 shared_ptr 工作,
    像旁观者那样观察资源的使用情况。
3、为什么 weak_ptr 能解决循环引用的问题?
    weak_ptr(弱引用)获得的是资源的观察权,它可以从一个 shared_ptr 或另一个 weak_ptr 构造,
    但 weak_ptr 并没有共享资源,它的构造并不会引起引用计数的增加,同时它的析构也不会引起引用
    计数的减少,它 仅仅! 是观察者。
4、weak_ptr 可以被用于标准容器库中的元素
    weak_ptr实现了拷贝构造函数和重载了复制操作符,因此weak_ptr可以被用于标准容器中的元素,
    例如:在一个树节点中声明子树节点:vector<boost::weak_ptr<Node>> children;
************************************************************************/

本文示例源码(VS2015):http://pan.baidu.com/s/1qYga9LM