C++ 智能指针
在之前的一篇博客《c++智能指针用法》中简单介绍过智能指针
与Java等具有垃圾回收机制的语言相比,C++语言没有垃圾回收机制,必须自己去释放分配的内存,否则就会存在内存泄露的问题。而解决查找内存泄漏需要花费大量的时间和精力,最有效的解决办法就是使用智能指针(Smart Pointer)。智能指针能够自动删除分配的内存,它与普通指针的是使用方法类似,但是不需要手动释放。它会自己管理内存的释放,这样就不必担心因为忘记释放内存而导致内存泄漏。
智能指针是指向动态分配(heap)对象的指针,用于生存周期控制,它能够确保离开指针所在作用域时,正确自动的销毁动态分配的对象,它的一种通用实现技术实采用引用计数,每使用它一次,内部的引用计数加一,每析构一次,内部的引用计数减一,当引用计数为0时,删除所指向的堆内存。
C++11提供了三种智能指针:位于头文件<memory>中
1. shared_ptr
共享的智能指针,使用引用计数,每一个拷贝的shared_ptr都指向相同的内存。当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。
基本使用方法:
对于一个未初始化的智能指针,可以通过reset()方法进行初始化。当智能指针有值的时候,reset()方法实际上相当于使指针与关联的原始指针分离,同时会使得引用计数减一。
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
int main()
{
// 智能指针的初始化
std::shared_ptr<int> p(new int(1)); // 智能指针必须用构造函数进行初始化
std::shared_ptr<int> p1 = p;
std::shared_ptr<int> ptr;
ptr.reset(new int(8));
std::cout << *p << *p1 << std::endl;
std::cout << p.use_count() << std::endl;
std::cout << ptr.use_count() << std::endl; // 获取指针的引用计数
// 智能指针通过重载的bool运算符来判断真假
if (ptr)
{
std::cout << "ptr is not null" << std::endl;
}
ptr.reset(); // 分离ptr指针与new int(8)的关联 reset使得ptr的引用计数减一,此时ptr被释放
if (!ptr)
{
std::cout << "ptr is null" << std::endl;
}
return 0;
}
运行结果:
创建新的shared_ptr对象的最佳方法是使用std :: make_shared:,std::make_shared 一次性为int对象和用于引用计数的数据都分配了内存,而new操作符只是为int分配了内存,所以make_shared的效率更高:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(20); // 创建并初始化
std::shared_ptr<int> p2 = std::make_shared<int>(); // 创建一个指针
*p2 = 12; // 初始化
std::cout << *p1 << " | " << *p2 << std::endl;
p2.reset(new int(14)); // p2指向新的对象
std::cout << *p2 << std::endl;
std::shared_ptr<int> p3(p2); // 利用智能指针初始化一个智能指针
if (p2 == p3) // 智能指针重载了operator==()
{
std::cout << "P2 and p3 point to the same element" << std::endl;
}
p3 = nullptr; // 重置指针
if (!p3)
{
std::cout << "p3 is null" << std::endl;
}
return 0;
}
运行结果:
获取原始指针:
可以通过get方法来获取原始指针:
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(20); // 创建并初始化
int* pr = p1.get();
std::cout << *pr << std::endl;
return 0;
}
自定义删除器:
智能指针在初始化的时候可以指定删除器,当智能指针的引用计数为0的时候,会自动调用定义的删除器释放对象的内存:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
void DeleteIntPointer(int* p)
{
delete p; // 删除
}
int main()
{
std::shared_ptr<int> p1(new int(34), DeleteIntPointer); // 指定删除器
// 也可以用lambda表达式定义删除器
std::shared_ptr<int> p2(new int(2), [](int* p) {delete p; });
return 0;
}
当用shared_ptr管理数组的时候,需要指定删除器,因为shared_ptr默认的删除器不支持数组对象:
1. 自定义删除器
2. 使用default_delete作为删除器,它内部是通过调用delete实现的
3. 可以封装一个make_shared_array
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
template<typename T>
std::shared_ptr<T> make_shared_array(size_t size)
{
return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
int main()
{
// 自定义删除器
std::shared_ptr<int> p1(new int[10], [](int* p) {delete[] p; });
// 使用delete_default
std::shared_ptr<int> p2(new int[10], std::default_delete<int[]>());
// 自定义函数
std::shared_ptr<int> p3 = make_shared_array<int>(10);
for (int i = 0; i < 10; i++)
{
p3.get()[i] = 9;
}
for (int i = 0; i < 10; i++)
{
std::cout << p3.get()[i] << std::endl;
}
return 0;
}
1, 使用shared_ptr需要注意的问题:
a. 不能用一个原始指针初始化多个shared_ptr
int* p = new int;
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p); // invalid
b. 不要在函数参数中创建shared_ptr,应该先创建std::shared_ptr,再传入函数
c. 避免循环引用,造成死锁
具体例子如下所示:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
struct A;
struct B;
struct A
{
std::shared_ptr<B> bptr;
~A()
{
std::cout << "A is deleted" << std::endl;
}
};
struct B
{
std::shared_ptr<A> aptr;
~B()
{
std::cout << "B is deleted" << std::endl;
}
};
int main()
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp; // 导致bp的引用计数为2
bp->aptr = ap; // 导致ap的引用计数为2
return 0;
}
循环引用会导致ap 和bp 的引用计数都为2,在离开作用域之后,ap,bp的引用计数都会减为1,但不会是0,导致两个指针都不会被析构,产生了内存泄漏。
后面会介绍关于这一问题的解决方法。
2. unique_ptr
unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许将一个unique_ptr赋值给另一个unique_ptr。但是,可以通过std::move来转移unique_ptr对内存的所有权,例如:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p1(new int(4));
std::unique_ptr<int> p2 = std::move(p1); // 转移unique对内存的所有权
std::unique_ptr<int> p2 = p1; // 这样的赋值不合法
return 0;
}
unique_ptr指向一个数组的时候和shared_ptr有所差别:
std::shared_ptr<int []> ptr(new int[10]);
当unique_ptr自定义删除器的时候,需要制定删除器的类型,而shared_ptr则不需要:
std::unique_ptr<int, void(*) (int*)> p(new int(1), [](int* p) {delete p; });
3. weak_ptr
弱引用指针weak_ptr是用来监视shared_ptr的,不会使其引用计数加一,weak_ptr不管理shared_ptr内部的指针,它的作用主要是为了监测shared_ptr的生命周期,weak_ptr没有重载* ->操作符,所以他不能操作资源,主要是通过它获取shared_ptr的监测权。weak_ptr的构造i函数不会增加引用计数,析构函数也不会减少引用计数。可以用来解决shared_ptr遇到的循环引用问题。
例如:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> shared_ptr = std::make_shared<int>(10);
std::weak_ptr<int> wp(shared_ptr); // 监测shared_ptr
std::cout << wp.use_count() << std::endl;
// expire可以用来判断被观测的资源是否已经被释放
if (wp.expired())
{
std::cout << "资源已经被释放" << std::endl;
}
else
{
std::cout << "资源有效" << std::endl;
}
shared_ptr.reset(); // 释放资源
if (wp.expired())
{
std::cout << "资源已经被释放" << std::endl;
}
else
{
std::cout << "资源有效" << std::endl;
}
// 通过lock方法获取所监视的shared_ptr
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::weak_ptr<int> wp1(p1); // 创建一个weak_ptr监测p1
if (wp1.expired())
{
std::cout << "资源已经被释放" << std::endl;
}
else
{
std::cout << "资源有效" << std::endl;
auto p = wp1.lock();
std::cout << *p << std::endl;
}
return 0;
}
weak_ptr:
expire方法可以判断所监测的资源是否被释放
lock方法可以获取weak_ptr所监测的shared_ptr
使用weak_ptr解决循环引用中出现的死锁问题
在前面介绍shared_ptr的时候,提到循环引用会导致能存泄露的问题,例如:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
struct A;
struct B;
struct A
{
std::shared_ptr<B> bptr;
~A()
{
std::cout << "A is deleted" << std::endl;
}
};
struct B
{
std::shared_ptr<A> aptr;
~B()
{
std::cout << "B is deleted" << std::endl;
}
};
int main()
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp; // 导致bp的引用计数为2
bp->aptr = ap; // 导致ap的引用计数为2
return 0;
}
通过weak_ptr可以解决这个问题:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>
struct A;
struct B;
struct A
{
std::weak_ptr<B> bptr;
~A()
{
std::cout << "A is deleted" << std::endl;
}
};
struct B
{
std::shared_ptr<A> aptr;
~B()
{
std::cout << "B is deleted" << std::endl;
}
};
int main()
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp; // 导致bp的引用计数为2
bp->aptr = ap; // 导致ap的引用计数为2
return 0;
}
如何通过智能指针管理第三方库分配的内存:
智能指针可以管理当前程序动态分配的内存,还可以用来管理第三方库分配的内存。一般第三方库分配的内存需要通过第三方提供的释放接口进行释放,第三方库返回的指针一般是原始指针,如果在使用完之后没有调用释放接口,会导致内存泄漏。
例如:
void* p = GetHandle()->create();
// do something
...
...
GetHandle()->release(); // 释放内存
上面的代码是不安全的,因为可能会发生下面的问题:
1.在使用第三方库分配内存后,忘记调用release接口
2.程序在执行的过程中返回了,导致无法调用release接口
3. 程序在执行过程中发生了异常,导致无法调用release接口
如果使用智能指针来管理内存,就可以避免上述的问题,只要离开作用域就会自动释放内存。
按照如下的写法就可以保证任何时候都可以正确释放分配的内存:
void* p = GetHandle()->create();
std::shared_ptr<void> sp(p, [this](void* p) {GetHandle()->release(p); });
// do something
...
...
在自定义的删除器中调用第三方库的内存释放接口即可
4. auto_ptr
auto_ptr的详细介绍可以参考之前的博客:https://blog.csdn.net/zj1131190425/article/details/90446954