C++ 11中的智能指针


引言

普通指针使用时存在挂起引用以及内存泄漏的问题,C++ 11中引入了智能指针来解决它

std::unique_ptr

std::auto_ptr,时代的眼泪

std::unique_ptrstd::auto_ptr的替代品,解决了C++ 11之前std::auto_ptr的很多缺漏

简单的看一下std::auto_ptr的复制构造函数

template<typename T>
class auto_ptr {
public:
    //Codes..
    auto_ptr(auto_ptr& atp) {
    	m_ptr = atp.m_ptr;
    	atp.m_ptr = nullptr; 
    }
private:
    T* m_ptr;
};

可以很容易的看出,该函数将指针所有权从一个对象转移到另外一个对象,且将原对象置空。该函数中std::auto_ptr实际上在运用引用去实现移动语义。但若是在转移所有权后仍去访问前一个对象(现在已经被置为空指针),程序会崩溃。

std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
// auto _data = atp.data;		// undefined behavior
// 此时的atp已经为nullptr,因为“移动函数”将所有权转接给了另外一个对象

u_ptr的移动构造

在C++ 11中引入了右值引用,可在此基础上实现移动构造函数。如今std::auto_ptr在C++17中被移除,正式被std::unique_ptr替代

// unique_ptr的移动构造函数
unique_ptr(unique_ptr&& unip) : m_ptr(unip.m_ptr)
{
	unip.m_ptr = nullptr;
}

虽然说这个函数与std::auto_ptr中的做法一样,但它只能接受右值作为参数。传递右值,即为转换指针所有权

std::unique_ptr<int> a(new int(10));
// std::unique_ptr<int> b(a);	// error
std::unique_ptr<int> b(std::move(a));	// ok

u_ptr的构造方式

std::shared_ptr不同,该智能指针用于不能被多个实例共享的内存管理,也就是说,仅有一个实例拥有内存所有权

对于智能指针而言,声明后会默认置为空

建议使用std::make_unique来替代直接使用构造函数传入new指针的方式来创建实例。若在new时期智能指针指向的类(也就是模板接受到的参数)抛出了异常,或是类的构造函数执行失败,这样会导致智能指针无法管理到新分配的内存,导致内存泄露

std::unique_ptr<int> unip1;	// 默认置nullptr
std::unique_ptr<int> unip2(new int(5));	// 旧方法
std::unique_ptr<int> unip3 = std::make_unique<int>(10);	// 新规范
// std::unique_ptr<int> unip3 = std::make_unique<int>(new int(10));	// 迷惑操作

std::unique_ptr<std::string> unip4 = std::make_unique<std::string>("Pointer");

std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> unip5 = std::make_unique<std::array<int, 10>>(arr);

需要注意的是,接受指针参数的ctorexplicit的,因此我们不能将一个内置指针隐式转换为一个智能指针

template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}

// std::unique_ptr<int> unip = new int(10); 	//错误

std::shared_ptrstd::unique_ptr都建议采用std::make_方法创建智能指针对象

u_ptr的unique性

std::unique_ptr通过删除拷贝构造函数和拷贝赋值函数来确保unique性,它是move-only的,独占资源

unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

可以通过移动语义转移所有权

std::unique_ptr<int> unip = std::make_unique<int>(10);
// std::unique_ptr<int> copy_unip = unip;
// std::unique_ptr<int> copy_unip(unip);

// 移动构造 unip被置空,所有权转到move_unip上
std::unique_ptr<int> move_unip = std::move(unip);

但我们可以“拷贝或是赋值”一份即将销毁的std::unique_ptr,最常见的例子是从函数返回一个std::unique_ptr

std::unique_ptr<int> clone() 
{
    std::unique_ptr<int> unip = std::make_unique<int>(10);
    // 返回值实际上调用到移动构造
    return unip;
}
// 编译器优化为一次移动构造
std::unique_ptr<int> p1 = clone();

std::unique_ptr<int> p2;
// p2调用一次移动赋值 clone调用一次移动构造
p2 = clone();

std::unique_ptr对象可以传给左值常量引用常数,因为这样并不会改变内存所有权,也可以使用移动语义进行右值传值(注意区分转移所有权和std::move的区别)

class Test {};
void right_param(std::unique_ptr<Test>&& t) {}
void cref_param(const std::unique_ptr<Test>& t) {}

int main()
{
    auto temp = std::make_unique<Test>();
    
    // 错误 无法将右值引用绑定到左值
    // right_param(temp);

    // 利用std::move强转为右值类型
    right_param(std::move(temp));
    cref_param(std::move(temp));
    
    cref_param(temp);
}

u_ptr构造的坑点

对于该智能指针来说,应避免犯以下两个错误(std::unique_ptrstd::shared_ptr同理)

  • 用同一份指针初始化多个智能指针

    int *p = new int(10);
    std::unique_ptr<int> unip(p);
    std::unique_ptr<int> unip2(p);
    

    拯救方法,释放其中一个智能指针的管理权

    unip.release();
    
  • 混用普通指针与智能指针

    int *p = new int(10);
    std::unique_ptr<int> unip(p);
    delete p;
    // 离开作用域后unip将会释放一块已经释放的内存
    

u_ptr的主动弃权与释放

std::unique_ptr中有get()release()reset()方法

  • get()可以获得智能指针中管理的资源,返回的是指针
  • release()会返回指向所管理的资源的指针,并释放其管理权(并不会析构
  • reset()会调用构造函数并回收内存,同时reset()还可以接受一个内置指针
std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);

// unip.reset(unip2.get())		// 绝对禁止的操作 会导致挂起引用
unip.reset(unip2.release());	// unip析构其管理的内存后 接收unip2转接过来的所有权

// unip = std::move(unip2);		// 与reset-release相同

std::make_unique的实现

运用变参模板,简单实现my_make_unique

//不支持处理数组 
template<typename T, typename ... Ts>
std::unique_ptr<T> my_make_unique(Ts ... args)
{
    return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }};	//泛型工厂函数
}

完整的make_unique实现

// 普通构造函数
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
unique_ptr<_Ty> make_unique(_Types&&... _Args) {
    return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}

// std::unique_ptr<T[]>
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
unique_ptr<_Ty> make_unique(const size_t _Size) {
    using _Elem = remove_extent_t<_Ty>;
    return unique_ptr<_Ty>(new _Elem[_Size]());
}

// 过滤定长数组
template <class _Ty, class... _Types, enable_if_t<extent_v<_Ty> != 0, int> = 0>
void make_unique(_Types&&...) = delete;

std::make_的缺陷

  • std::make_函数都不允许使用定制删除器,deleter只能在std::unique_ptrstd::shared_ptr的构造函数中传入

  • std::make_函数不能完美的传递std::initializer_list

    // auto unip = std::make_unique<std::vector<int>>({ 1,2,3 });	// 错误
    
    std::initializer_list<int> il = { 1,2,3 };
    auto unip = std::make_unique<std::vector<int>>(il);			// 正确
    
  • 当构造函数是私有或者保护时,无法std::make_std::make_实际上是类似调用到了构造函数。解决方法是将std::make_声明为友元函数

    class PrivateCtorClass
    {
        friend std::unique_ptr<PrivateCtorClass> std::make_unique<PrivateCtorClass>();
        PrivateCtorClass() : data(100) {}
    public:
        int data;
    };
    
    int main()
    {
        std::unique_ptr<PrivateCtorClass> up = std::make_unique<PrivateCtorClass>();
        std::cout << up->data << std::endl;
    }
    
  • 对于std::shared_ptr来说,对象的内存可能无法及时回收

    要理解这一点首先得知道std::make_shared和直接调用构造函数两者的内存分配原理

    对于std::shared_ptr而言,内部维护了两个指针指向资源块和控制块,前者是智能指针管理的对象,后者用于记录引用次数。如果使用构造函数初始化智能指针,那么控制块是单独分配的,即资源块与控制块处于两个内存中

    内存

    而若采用std::make_shared方法,资源块和控制块将分配到同块内存中,而在程序运行中,内存分配是代价高昂的操作,因此std::make_shared效率更高

    虽然std::make_shared减少了内存分配次数,提高了性能,但是在回收内存的时候存在一点问题:

    我们知道,当对象的引用计数降为0时(强引用次数降为0,与弱引用次数无关,std::weak_ptr只是个旁观者),对象被销毁(调用析构函数并释放内存)。但是对于资源快与控制块处于同一块内存上的情况而言,由于内存必须完整地申请释放,因此只有当控制块也可以被回收时,这块内存才会被释放。而控制块中包含了两个计数,一个为强引用(std::shared_ptr)计数,一个为弱引用(std::weak_ptr)计数,当两个引用计数都归为0时,控制块才能释放。那么也就是说只要还有一个std::weak_ptr指向控制块(弱引用计数 > 0),控制块就不能被释放,也就代表这块内存也不能被释放(即使std::shared_ptr已经离开作用域了)

    总的来说,通过std::make_shared分配出来的这块内存,只有当最后一个std::shared_ptrstd::weak_ptr都被销毁时,才会被释放。下面为验证代码

    int main()
    {
        // 64位的程序才能申请2G大小的内存
        std::shared_ptr<char[]> shrp(new char[INT32_MAX]);
        std::weak_ptr<char[]> wp(shrp);
        shrp.reset();
        // 在等待任意键的时候 shrp管理的资源已经得到释放 程序内存占用很低
        system("pause");
    }
    
    int main()
    {
        // 64位的程序才能申请2G大小的内存
        std::shared_ptr<char[]> shrp = std::make_shared<char[]>(INT32_MAX);
        std::weak_ptr<char[]> wp(shrp);
        shrp.reset();
        // 在等待任意键的时候 shrp管理的资源没有得到释放 程序内存占用高达2G
        system("pause");
    }
    

std::shared_ptr

std::shared_ptrstd::unique_ptr的主要区别在于前者是使用引用计数的智能指针,后者是独占资源的智能指针。强引用计数维护了“引用同一个真实指针对象的智能指针实例”的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存(详情请见上文:std::make_的缺陷)

std::unique_ptr不同,std::shared_ptr支持拷贝构造,也允许拷贝赋值

std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::shared_ptr<int> shrp2 = shrp;
std::shared_ptr<int> shrp3(shrp2);
std::cout << shrp.use_count() << std::endl;			// 3

使用std::shared_ptr管理第三方库

假设第三方库有这么一些代码

struct IntData {
    int data = 100;
    ~IntData() { std::cout << "int data destroy" << std::endl; }
};

class OtherLibHandler {
public:
    template<typename T>
    void* Create() { return new T(); }

    template<typename T>
    void Release(void *p) { delete reinterpret_cast<T*>(p); }
};

static OtherLibHandler& GetHandle() {
    static OtherLibHandler p;
    return p;
}

第三方库通常会通过接口提供原始指针,用来管理其中的内存,既然是原始指针,就总会出现忘记使用库中的释放函数,导致内存泄露的情况

void* p = GetHandle().Create<IntData>();
// Codes..
// GetHandle().Release<IntData>(p);			//程序员由于不可抗力导致漏写这行代码了

那么使用智能指针去维护就会显得非常方便,以下仅提供代码思路

auto deleter = [](void* p) { GetHandle().Release<IntData>(p); };
std::shared_ptr<void> shrp(GetHandle().Create<IntData>(), deleter);

包装成一个函数

template<typename T>
std::shared_ptr<T> GetShared(void* p) {
    std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
    return shrp;
}

int main() {
    std::shared_ptr<Intdata> shrp = GetShared<IntData>(GetHandle().Create<IntData>());
}

但是包装成函数后会有这么一个问题,GetShared返回出来的智能指针必须得被接下来(不管是左值或是右值引用,或是常量左值引用),否则该对象将会被释放,导致后续代码去访问野指针的内容

void* p = GetHandle().Create<IntData>();
GetShared<IntData>(p);
// 出现不确定的结果
std::cout << reinterpret_cast<IntData*>(p)->data << std::endl;

那我们可以使用宏来确保一定创建一个对象来接住返回值

#define CreateShared_IntData(p) auto shared_##p = GetShared<IntData>(p);

template<typename T>
std::shared_ptr<T> GetShared(void* p) {
    std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
    return shrp;
}

int main() {
    auto origin_p = GetHandle().Create<IntData>();
    CreateShared_IntData(origin_p);
    // 100
    std::cout << shared_origin_p->data << std::endl;
}

常用API

  • 使用use_count()来查看std::shared_ptr的强引用次数
  • reset()则可以释放关联内存块的所有权(相当于std::unique_ptrrelease(),但它没有返回值 ),如果是最后一个指向该资源的std::shared_ptr,就释放这块内存。同时reset()还可以传入一个新的原始指针,来让智能指针管理新的内存
  • 三种智能指针都有swap(),用于交换两个同种类型的指针对象

s_ptr的坑点

上文中提到过我们应该避免用同一份指针初始化多个智能指针

int *p = new int(10);
std::shared_ptr<int> shrp(p);
std::shared_ptr<int> shrp2(p);
//导致同一片地址的资源被释放两次

因为这两个智能指针是独立初始化的,所以它们之间并没有通讯共享引用计数。std::shared_ptr的内部实际上维护两个指针,一个用于管理实际的资源,另外一个则指向一个控制块,其中记录了有哪些std::shared_ptr共同管理同一片内存。

// std::shared_ptr的基类_Ptr_base维护了两个指针
private:
    element_type* _Ptr{nullptr};
    _Ref_count_base* _Rep{nullptr};

这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。但是如果是使用复制构造函数还有赋值运算时,控制块将会同步更新,这样就达到了引用计数的目的。

智能指针的deleter

智能指针与动态数组

在C++17之前,std::shared_ptr的构造与std::unique_ptr存在一点差异:std::unique_ptr支持动态数组,而std::share_ptr不支持动态数组

std::unique_ptr<int[]> unip(new int[10]);	// 合法
std::shared_ptr<int[]> shrp(new int [10]);	// 在C++ 17之前时非法操作 不能传入数组类型作为模板参数
std::shared_ptr<int> shrp2(new int[10]);	// 可编译,但存在未定义行为

因此,对于shrp2而言,在它析构时会发生这种情况

int* p = new int[10];
delete p;

这对于new int[10] 而言肯定是非法的,对它应使用delete[]

自定义deleter-s_ptr

对此我们有两种解决方法,一种是传入std::default_delete,另外一种是自行构造删除器

std::shared_ptr<int> shrp(new int[10], std::default_delete<int[]>());
// lambda表达式转换为函数指针
std::shared_ptr<int> shrp1(new int[10], [](int* p) { delete[] p; });

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

// 还可以封装一个my_make_shared_dynamic_arr的方法
template<typename T>
decltype(auto) my_make_shared_dynamic_arr(std::size_t size)
{
    return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
std::shared_ptr<int> shrp3 = my_make_shared_dynamic_arr(10);

虽然说能用了,但存在着几个缺点

  • 我们想管理的是int[]类型,但std::shared_ptr的模板类型却为int
  • 需要显示提供删除器
  • 无法使用std::make_shared,无法保证异常安全
  • 由于std::shared_ptr<int>没有重载operator[],故需要使用.get()[Index]来获取数组中的元素

有人可能会说了,那使用std::shared_ptr<int*>不就行了吗,数组退化成指针

其实事情并没有这么简单

int* p = new int[10]{};
std::shared_ptr<int*> shrp(&p, [](int** p) { delete[] *p; });
std::cout << (*(shrp.get()))[5] << std::endl;

自定义deleter-u_ptr

对于std::shared_ptr来说,自定义一个删除器是比较简单的,而对于std::unique_ptr来说,情况有点不同

std::shared_ptr<int> shrp(new int(10), [](int* p) { delete p; });	// 正确
// std::unique_ptr<int> unip(new int(10), [](int* p) { delete p; });	// 错误

std::unique_ptr在指定删除器时需要在模板参数中传入删除器的类型

// 一个接受int*且无返回值的函数指针
std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) { delete p; });

如果在lambda表达式中捕获了变量,那么需要使用std::function来进行包装,因为捕获了变量的lambda表达式无法转换为函数指针

// std::unique_ptr<int, void(*)(int*)> unip(new int(10), [&](int* p) {delete p; });
std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) { delete p; });

除了用lambda表达式,我们还可以这么干

template<typename T>
void deleter(T* p) { delete[] p; }

//使用decltype类型推断
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取别名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//联合typedef与decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);

利用using+decltype,间接直观

template<typename T>
void deleter(T* p) { delete[] p; }

template<typename T>
using deleter_t = decltype(deleter<T>)*;

int main() {
    std::unique_ptr<int, deleter_t<int>> unip(new int(10), deleter<int>);
}

C++17后的做法

在C++ 17中,std::shared_ptr支持传入T[]类型作为模板参数

std::shared_ptr<int[]> shrp(new int[10]{});
std::cout << shrp[3] << std::endl;	// 0

在C++20中,可以使用std::make_shared来创建智能指针

// 分配一个管理有10个int元素的动态数组的std::shared_ptr
std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);
// 同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);

使用智能指针管理动态二维数组

正常我们申请动态二维数组是这么做的

int** p = new int*[3];
for (int i = 0; i < 3; i++)
    p[i] = new int[5]{};

for (int i = 0; i < 3; i++)
    delete[] p[i];
delete[] p;

有了智能指针后,我们可以做出这么一件明明可以用std::vector但就是偏偏要折磨自己的事情

// C++20下编译通过 创建一个3*5的元素都为10的二维数组
std::shared_ptr<std::shared_ptr<int[]>[]> sp = std::make_shared<std::shared_ptr<int[]>[]>(3);
for (int i = 0; i < 3; i++)
    sp[i] = std::make_shared<int[]>(5, 10);

死锁与自锁

如果两个强引用类型的智能指针互相引用会如何

//照搬知乎上的代码
class Person
{
public:
	Person(std::string name) :
		m_name(std::move(name)), m_partner(nullptr)
	{
		std::cout << m_name << " created" << std::endl;
	}

    ~Person()
    {
        std::cout << m_name << " destoryed" << std::endl;
    }

    friend void partnerUp(std::shared_ptr<Person>& shrp1, std::shared_ptr<Person>& shrp2)
    {
        if (!shrp1 || !shrp2)
            return;
        shrp1->m_partner = shrp2;
        shrp2->m_partner = shrp1;
    }

private:
    string m_name;
    std::shared_ptr<Person> m_partner;
};

int main()
{
    {
        auto shrp1 = std::make_shared<Person>("Lucy");
        auto shrp2 = std::make_shared<Person>("Ricky");
        partnerUp(shrp1, shrp2);  // 互相设为伙伴
    }
    return 0;
}

运行之后可以发现控制台只输出了两行created字符,也就是说对象并没有被析构,导致了内存的泄漏。当作用域结束时,理应执行析构函数,但是当析构shrp1时,却发现shrp2内部引用了shrp1,那么就得先析构shrp2,但是发现shrp1中内部又引用了shrp1,互相引用导致了 “ 死锁 ” ,最终导致内存泄漏

“自锁”同理。

auto shrp = std::make_shared<Person>("Lucy");
partnerUp(shrp, shrp);

那么这种情况下就需要使用到std::weak_ptr了

std::weak_ptr

std::weak_ptr能够引用std::shared_ptr中的内存,但是它只作为旁观者的存在,并不享有内存的所有权,也就是说使用std::weak_ptr去接受一个std::shared_ptr,并不会增加强引用计数,当然也不会影响到std::shared_ptr的析构,有效的阻止了 “ 死锁 ”, “ 自锁 ” 的问题

class Person
{
	//Codes..
private:
    string m_name;
    std::weak_ptr<Person> m_partner;	//此时程序能够正常析构了
};

w_ptr的API

  • std::weak_ptr一般与std::shared_ptr搭配使用,使用lock()可以返回一个他所监视的std::shared_ptr

  • 使用use_count()可以获取他所监视的std::shared_ptr的强引用奇数

    std::shared_ptr<int> shrp = std::make_shared<int>(10);
    std::weak_ptr<int> wkp = shrp;
    auto shrp2 = wkp.lock();
    auto shrp3 = wkp.lock();
    std::cout<< shrp2.use_count() << std::endl;		// 3
    
  • 使用expired()可以返回一个bool值,若管理对象已删除则返回false,否则返回true

    bool expired() const noexcept {
        return this->use_count() == 0;
    }
    

    注意,std::weak_ptrstd::shared_ptr一样,都是派生自_Ptr_base的,use_count()是基类中的方法

题外话:std::initializer_list

C++ 11中包含了花括号初始的方法,叫做std::initializer_list

小括号调用到了类型的构造函数,包含了隐式转换的规则,而花括号并没有调用到构造函数,而是用到了列表初始化,这种方法当碰到精度降低,范围变窄等情况(narrowing conversion)时,编译器都会给出报错

double num = 3.1415926;
int test1(num);		//编译器给出警告,该转换可能会丢失数据
int test2{num};		//编译器给出报错,从double转换到int需要收缩转换

同时,C++ 11也支持使用者在自定义类的初始化时使用列表初始化

class Test
{
public:
    Test(const std::initializer_list<int>& i_list)
    {
        vec.reverse(i_list.size());
        for (auto i : i_list)
            vec.push_back(i);
    }
private:
    std::vector<int> vec;
};

int main()
{
    Test t1{ 1, 2, 3 };
    Test t2 = { 1, 2, 3, 4 };
}


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM