c++ 11 智能指针 解决内存泄露问题

文章目录

  • 一、shared_ptr
    • 1.1 初始化
    • 1.2 获取原始指针
    • 1.3 指定删除器
    • 1.4 下面是使用shared_ptr 的一些注意事项:
  • 二、unique_ptr 独占的智能指针
    • 2.1、unique_ptr 独享所有权
    • 2.2 、创建一个空的 unique_ptr 对象
    • 2.3 、检查 unique_ptr 对象是否为空
    • 2.4、 使用原始指针创建 unique_ptr 对象
    • 2.5、使用 std::make_unique 创建 unique_ptr 对象 / C++14
    • 2.6、获取被管理对象的指针
    • 2.7、重置 unique_ptr 对象
    • 2.8、unique_ptr 对象不可复制
    • 2.9、转移 unique_ptr 对象的所有权
    • 2.10、释放关联的原始指针
    • 2.11、完整示例程序
    • 2.12、总结
      • 成员函数作用
  • 三、C++11 weak_ptr智能指针
    • 3.1、weak_ptr指针的创建
    • 3.2、weak_ptr模板类提供的成员方法
    • 3.3、weak_ptr指针可调用的成员方法

需要包含下面两句代码:

#include 
using namespace std;

一、shared_ptr

shared_ptr是一种 智能指针(smart pointer),作用有如同 指针,但会记录有多少个shared_ptrs共同指向一个对象。这便是所谓的 引用计数(reference counting)。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。这在非环形数据结构中防止资源泄露很有帮助。使得指针可以共享对象,并且不用考虑内存泄漏问题
shared_ptr 可以支持普通指针的所有操作,完全可以像操作普通指针一样操作智能指针。
shared_ptr 可以通过三种方式得到(拷贝初始化,定义delete操作的方式不在罗列,只讨论初始化指针所指对象来源):

1.1 初始化

  1. 通过一个指向堆上申请的空间的指针初始化(切记不要用栈上的指针,否则,当智能指针全部释放控制权(栈中的对象离开作用域本身就会析构一次),将会析构对象,导致出错)
  2. 通过make_shared函数得到
  3. 通过另外一个智能指针初始化
#include 
#include 
int main()
{
    int *p = new int(30);
    std::shared_ptr<int> bptr(p);//方式1
    std::shared_ptr<int> aptr = std::make_shared<int>(20);//方式2
    std::shared_ptr<int> cptr(aptr); //方式3
    std::cout << "aptr.use_count() = " << aptr.use_count() <<"  value = "<<*aptr<<std::endl;//use_count 是引用计数器
    std::cout << "bptr.use_count() = " << bptr.use_count() <<"  value = "<<*bptr<<std::endl;
    std::cout << "cptr.use_count() = " << cptr.use_count() <<"  value = "<<*cptr<<std::endl;
    //输出是:2,20
    //       1,30
    //       2,20
} 

另外还有这种初始化方式:

std::shared_ptr<int> ptr;
ptr.reset(new int(1));

对于一个未初始化的智能指针,可以用过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1.

1.2 获取原始指针

当需要获取原始指针时,可以通过get方法,代码如下:

std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get();

1.3 指定删除器

void DeleteIntPtr(int *p)
{
	delete p;
}
shared_ptr<int> ppp(new int(100), DeleteIntPtr);

当ppp的引用计数为0时,自动调用DeleteIntPtr来释放对象的内存。删除器也可以是一个lambal表达式,如下:

shared_ptr<int> ppp(new int(100), [](int*p){delete p;});

注意:当我们用shared_ptr管理数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象,代码如下:

shared_ptr<int> ppp(new int[10], [](int*p){delete[] p;});  //指定delete[]

也可以将std::default_delete作为删除器 。default_delete的内部是通过调用delete来实现功能的,代码如下:

shared_ptr<int> ppp(new int[10], std::default_delete<int []>); 

另外,我们还可以自己封装一个 make_shared_array 方法来让 shared_ptr 支持数组,代码如下:

#include 
#include `在这里插入代码片`
using namespace std;

template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
    // 返回匿名对象
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

int main()
{
    shared_ptr<int> ptr1 = make_share_array<int>(10);
    cout << ptr1.use_count() << endl;
    shared_ptr<char> ptr2 = make_share_array<char>(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

不过在C++ 14 以后,shared_ptr支持了可以管理数组类型的地址了。

代码如下:

int main()
{
	shared_ptr<Test[]> ptr4(new Test[3]);
	return 0;
}

1.4 下面是使用shared_ptr 的一些注意事项:

  1. 禁止纯指针给智能指针赋值或者拷贝构造。
int* a=new int(2);  
shared_ptr<int>sp=a;//  error  
sp=a;//    error 

2. shared_ptr多次引用同一数据,会导致两次释放同一内存。如下:

 {
 int* pInt = new int[100];
 shared_ptr<int> sp1(pInt);
 // 一些其它代码之后…
 shared_ptr<int> sp2(pInt);
 }
 

3.使用shared_ptr包装this指针带来的问题,如下:

class tester 
 {
 public:
   tester()
   ~tester()
   {
     std::cout << "析构函数被调用!\n"; 
   }
 public:
   shared_ptr<tester> sget()
   {
     return shared_ptr<tester>(this);
   }
 };
 
 int main()
 {
   tester t;
   shared_ptr<tester> sp =  t.sget(); // …
   return 0;
 }
 

也将导致两次释放t对象破坏堆栈,一次是出栈时析构,一次就是shared_ptr析构。若有这种需要,可以使用下面代码。

 class tester : public enable_shared_from_this<tester>
 {
 public:
   tester()
   ~tester()
   {
   std::cout << "析构函数被调用!\n"; 
   }
 public:
  shared_ptr<tester> sget()
   {
   return shared_from_this();
   }
 };
 
 int main()
 {
   shared_ptr<tester> sp(new tester);
   // 正确使用sp 指针。
   sp->sget();
   return 0;
 }

4. shared_ptr循环引用导致内存泄露,代码如下:

class parent;
class child; 

typedef shared_ptr<parent> parent_ptr;
typedef shared_ptr<child> child_ptr; 

class parent
{
public:
       ~parent() { 
              std::cout <<"父类析构函数被调用.\n"; 
       }
public:
       child_ptr children;
};

class child
{
public:
       ~child() { 
              std::cout <<"子类析构函数被调用.\n"; 
       }
public:
       parent_ptr parent;
};

int main()
{
  parent_ptr father(new parent());
  child_ptr son(new child);
  // 父子互相引用。
  father->children = son;
  son->parent = father;
  return 0;
}

如上代码,将在程序退出前,father的引用计数为2,son的计数也为2,退出时,shared_ptr所作操作就是简单的将计数减1,如果为0则释放,显然,这个情况下,引用计数不为0,于是造成father和son所指向的内存得不到释放,导致内存泄露。

5.在多线程程序中使用shared_ptr应注意的问题。代码如下:

class tester 
{
public:
  tester() {}
  ~tester() {}
  // 更多的函数定义…
};

void fun(shared_ptr<tester> sp)
{
  // !!!在这大量使用sp指针.
  shared_ptr<tester> tmp = sp;
}

int main()
{
  shared_ptr<tester> sp1(new tester);
  // 开启两个线程,并将智能指针传入使用。
  thread t1(bind(&fun, sp1));
  thread t2(bind(&fun, sp1));
  t1.join();
  t2.join();
  return 0;
}

这个代码带来的问题很显然,由于多线程同时访问智能指针,并将其赋值到其它同类智能指针时,很可能发生两个线程同时在操作引用计数(但并不一定绝对发生),而导致计数失败或无效等情况,从而导致程序崩溃,如若不知根源,就无法查找这个bug,那就只能向上帝祈祷程序能正常运行。

引入weak_ptr可以解决这个问题,将fun函数修改如下:

void fun(weak_ptr<tester> wp)
{

//这个方案只解决了多线程对引用计数同时访问的读写问题,并没有解决对share_ptr指向的数据的多线程安全问题,因此weak_ptr只是安全的获得share_ptr的一种方式,因为可以确保在获得share_ptr的时候的多线程安全
  shared_ptr<tester> sp = wp.lock;
  if (sp)
  {
    // 在这里可以安全的使用sp指针.
  }
  else
  {
    std::cout << “指针已被释放!<< std::endl;
  }
} 

6.weak_ptr不仅可以解决多线程访问带来的安全问题,而且还可以解决上面第三个问题循环引用。Children类代码修改如下,即可打破循环引用:

class child
{
public:
  ~child() { 
   std::cout <<"子类析构函数被调用.\n"; 
  }
public:
  weak_ptr<parent> parent;
};

因为weak_ptr不增加引用计数,所以可以在退出函数域时,正确的析构。

二、unique_ptr 独占的智能指针

这段原文链接:https://blog.csdn.net/shaosunrise/article/details/85158249

unique_ptr 是 C++ 11 提供的用于防止内存泄漏的智能指针中的一种实现,独享被管理对象指针所有权的智能指针。unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。
unique_ptr具有->和*运算符重载符,因此它可以像普通指针一样使用。
查看下面的示例:

#include 
#include 

struct Task {
    int mId;
    Task(int id ) :mId(id) {
        std::cout << "Task::Constructor" << std::endl;
    }
    ~Task() {
        std::cout << "Task::Destructor" << std::endl;
    }
};

int main()
{
    // 通过原始指针创建 unique_ptr 实例
    std::unique_ptr<Task> taskPtr(new Task(23));

    //通过 unique_ptr 访问其成员
    int id = taskPtr->mId;
    std::cout << id << std::endl;

    return 0;
}

输出:

Task::Constructor
23
Task::Destructor

unique_ptr 对象 taskPtr 接受原始指针作为参数。现在当main函数退出时,该对象超出作用范围就会调用其析构函数,在unique_ptr对象taskPtr 的析构函数中,会删除关联的原始指针,这样就不用专门delete Task对象了。
这样不管函数正常退出还是异常退出(由于某些异常),也会始终调用taskPtr的析构函数。因此,原始指针将始终被删除并防止内存泄漏。

2.1、unique_ptr 独享所有权

unique_ptr对象始终是关联的原始指针的唯一所有者。我们无法复制unique_ptr对象,它只能移动。
由于每个unique_ptr对象都是原始指针的唯一所有者,因此在其析构函数中它直接删除关联的指针,不需要任何参考计数。

2.2 、创建一个空的 unique_ptr 对象

创建一个空的unique_ptr对象,因为没有与之关联的原始指针,所以它是空的。

std::unique_ptr<int> ptr1;

2.3 、检查 unique_ptr 对象是否为空

有两种方法可以检查 unique_ptr 对象是否为空或者是否有与之关联的原始指针。

// 方法1

if(!ptr1)
	std::cout<<"ptr1 is empty"<<std::endl;
// 方法2
if(ptr1 == nullptr)
	std::cout<<"ptr1 is empty"<<std::endl;

2.4、 使用原始指针创建 unique_ptr 对象

要创建非空的 unique_ptr 对象,需要在创建对象时在其构造函数中传递原始指针,即:

std::unique_ptr<Task> taskPtr(new Task(22));

或者(这种新学到的)

std::unique_ptr<Task> taskPtr(new std::unique_ptr<Task>::element_type(23));

不能通过赋值的方法创建对象,下面的这句是错误的

// std::unique_ptr taskPtr2 = new Task(); // 编译错误

2.5、使用 std::make_unique 创建 unique_ptr 对象 / C++14

std::make_unique<>() 是C++ 14 引入的新函数

std::unique_ptr<Task> taskPtr = std::make_unique<Task>(34);

2.6、获取被管理对象的指针

使用get()·函数获取管理对象的指针。

Task *p1 = taskPtr.get();

2.7、重置 unique_ptr 对象

在 unique_ptr 对象上调用reset()函数将重置它,即它将释放delete关联的原始指针并使unique_ptr 对象为空。

taskPtr.reset();

2.8、unique_ptr 对象不可复制

由于 unique_ptr 不可复制,只能移动。因此,我们无法通过复制构造函数或赋值运算符创建unique_ptr对象的副本。

// 编译错误 : unique_ptr 不能复制
std::unique_ptr<Task> taskPtr3 = taskPtr2; // Compile error

// 编译错误 : unique_ptr 不能复制
taskPtr = taskPtr2; //compile error

2.9、转移 unique_ptr 对象的所有权

我们无法复制 unique_ptr 对象,但我们可以转移它们。这意味着 unique_ptr 对象可以将关联的原始指针的所有权转移到另一个 unique_ptr 对象。让我们通过一个例子来理解:

// 通过原始指针创建 taskPtr2
std::unique_ptr<Task> taskPtr2(new Task(55));
// 把taskPtr2中关联指针的所有权转移给taskPtr4
std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
// 现在taskPtr2关联的指针为空
if(taskPtr2 == nullptr)
	std::cout<<"taskPtr2 is  empty"<<std::endl;

// taskPtr2关联指针的所有权现在转移到了taskPtr4中
if(taskPtr4 != nullptr)
	std::cout<<"taskPtr4 is not empty"<<std::endl;

// 会输出55
std::cout<< taskPtr4->mId << std::endl;

std::move() 将把 taskPtr2 转换为一个右值引用。因此,调用 unique_ptr 的移动构造函数,并将关联的原始指针传输到 taskPtr4。在转移完原始指针的所有权后, taskPtr2将变为空。

2.10、释放关联的原始指针

在 unique_ptr 对象上调用 release()将释放其关联的原始指针的所有权,并返回原始指针。这里是释放所有权,并没有delete原始指针,reset()会delete原始指针。

std::unique_ptr<Task> taskPtr5(new Task(55));
// 不为空
if(taskPtr5 != nullptr)
	std::cout<<"taskPtr5 is not empty"<<std::endl;
// 释放关联指针的所有权
Task * ptr = taskPtr5.release();
// 现在为空
if(taskPtr5 == nullptr)
	std::cout<<"taskPtr5 is empty"<<std::endl;

2.11、完整示例程序

#include 
#include 

struct Task {
    int mId;
    Task(int id ) :mId(id) {
        std::cout<<"Task::Constructor"<<std::endl;
    }
    ~Task() {
        std::cout<<"Task::Destructor"<<std::endl;
    }
};

int main()
{
    // 空对象 unique_ptr
    std::unique_ptr<int> ptr1;

    // 检查 ptr1 是否为空
    if(!ptr1)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 检查 ptr1 是否为空
    if(ptr1 == nullptr)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 不能通过赋值初始化unique_ptr
    // std::unique_ptr taskPtr2 = new Task(); // Compile Error

    // 通过原始指针创建 unique_ptr
    std::unique_ptr<Task> taskPtr(new Task(23));

    // 检查 taskPtr 是否为空
    if(taskPtr != nullptr)
        std::cout<<"taskPtr is  not empty"<<std::endl;

    // 访问 unique_ptr关联指针的成员
    std::cout<<taskPtr->mId<<std::endl;

    std::cout<<"Reset the taskPtr"<<std::endl;
    // 重置 unique_ptr 为空,将删除关联的原始指针
    taskPtr.reset();

    // 检查是否为空 / 检查有没有关联的原始指针
    if(taskPtr == nullptr)
        std::cout<<"taskPtr is  empty"<<std::endl;

    // 通过原始指针创建 unique_ptr
    std::unique_ptr<Task> taskPtr2(new Task(55));

    if(taskPtr2 != nullptr)
        std::cout<<"taskPtr2 is  not empty"<<std::endl;

    // unique_ptr 对象不能复制
    //taskPtr = taskPtr2; //compile error
    //std::unique_ptr taskPtr3 = taskPtr2;

    {
        // 转移所有权(把unique_ptr中的指针转移到另一个unique_ptr中)
        std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
        // 转移后为空
        if(taskPtr2 == nullptr)
            std::cout << "taskPtr2 is  empty" << std::endl;
        // 转进来后非空
        if(taskPtr4 != nullptr)
            std::cout<<"taskPtr4 is not empty"<<std::endl;

        std::cout << taskPtr4->mId << std::endl;

        //taskPtr4 超出下面这个括号的作用于将delete其关联的指针
    }

    std::unique_ptr<Task> taskPtr5(new Task(66));

    if(taskPtr5 != nullptr)
        std::cout << "taskPtr5 is not empty" << std::endl;

    // 释放所有权
    Task * ptr = taskPtr5.release();

    if(taskPtr5 == nullptr)
        std::cout << "taskPtr5 is empty" << std::endl;

    std::cout << ptr->mId << std::endl;

    delete ptr;

    return 0;
}

输出:

ptr1 is empty
ptr1 is empty
Task::Constructor
taskPtr is  not empty
23
Reset the taskPtr
Task::Destructor
taskPtr is  empty
Task::Constructor
taskPtr2 is  not empty
taskPtr2 is  empty
taskPtr4 is not empty
55
Task::Destructor
Task::Constructor
taskPtr5 is not empty
taskPtr5 is empty
66
Task::Destructor

2.12、总结

new出来的对象是位于堆内存上的,必须调用delete才能释放其内存。
unique_ptr 是一个装指针的容器,且拥有关联指针的唯一所有权,作为普通变量使用时系统分配对象到栈内存上,超出作用域时会自动析构,unique_ptr对象的析构函数中会delete其关联指针,这样就相当于替我们执行了delete堆内存上的对象。

成员函数作用

reset() 重置unique_ptr为空,delete其关联的指针。
release() 不delete关联指针,并返回关联指针。释放关联指针的所有权,unique_ptr为空。
get() 仅仅返回关联指针
unique_ptr不能直接复制,必须使用std::move()转移其管理的指针,转移后原 unique_ptr 为空。std::unique_ptr taskPtr4 = std::move(taskPtr2);

创建unique_ptr对象有两种方法:

//C++11: 
std::unique_ptr<Task> taskPtr(new Task(23));
//C++14: 
std::unique_ptr<Task> taskPtr = std::make_unique<Task>(34);

三、C++11 weak_ptr智能指针

和 shared_ptr、unique_ptr 类型指针一样,weak_ptr 智能指针也是以模板类的方式实现的。weak_ptr( T 为指针所指数据的类型)定义在头文件,并位于 std 命名空间中。因此,要想使用 weak_ptr 类型指针,程序中应首先包含如下 2 条语句:

#include 
using namespace std;

第 2 句并不是必须的,可以不添加,则后续在使用 unique_ptr 指针时,必须标注std::。

需要注意的是,C++11标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。

需要注意的是,当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。

除此之外,weak_ptr 模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。

3.1、weak_ptr指针的创建

创建一个 weak_ptr 指针,有以下 3 种方式:

  1. 可以创建一个空 weak_ptr 指针,例如:
std::weak_ptr<int> wp1;
  1. 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针,例如:
std::weak_ptr<int> wp2 (wp1);

若 wp1 为空指针,则 wp2 也为空指针;反之,如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,则 wp2 也指向该块存储空间(可以访问,但无所有权)。

  1. weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,因为在构建 weak_ptr 指针对象时,可以利用已有的 shared_ptr 指针为其初始化。例如:
std::shared_ptr<int> sp (new int);
std::weak_ptr<int> wp3 (sp);

由此,wp3 指针和 sp 指针有相同的指针。再次强调,weak_ptr 类型指针不会导致堆内存空间的引用计数增加或减少。

3.2、weak_ptr模板类提供的成员方法

和 shared_ptr、unique_ptr 相比,weak_ptr 模板类提供的成员方法不多,罗列了常用的成员方法及各自的功能。

3.3、weak_ptr指针可调用的成员方法

成员方法 功能
operator=() 重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
swap(x) 其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
reset() 将当前 weak_ptr 指针置为空指针。
use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired() 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
再次强调,weak_ptr 模板类没有重载 * 和 -> 运算符,因此 weak_ptr 类型指针只能访问某一 shared_ptr 指针指向的堆内存空间,无法对其进行修改。

下面的样例演示了 weak_ptr 指针以及表 1 中部分成员方法的基本用法:

#include 
#include 
using namespace std;
int main()
{
    std::shared_ptr<int> sp1(new int(10));
    std::shared_ptr<int> sp2(sp1);
    std::weak_ptr<int> wp(sp2);
    //输出和 wp 同指向的 shared_ptr 类型指针的数量
    cout << wp.use_count() << endl;
    //释放 sp2
    sp2.reset();
    cout << wp.use_count() << endl;
    //借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
    cout << *(wp.lock()) << endl;
    return 0;
}

程序执行结果为:

2
1
10

你可能感兴趣的:(c++,11,14,19,c++11,智能指针)