0 前言
涉及的代码可在repo中找到:https://github.com/leoda1/the-notes-of-cuda-programming/tree/main/code/CPU
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
C++里面的四个智能指针: ,auto_ptr
unique_ptr
,shared_ptr
, weak_ptr
其中后三个是C++11支持, 第一个已经被C++11弃用。使用这些智能指针时需要引用头文件<memory>
。
1 shared_ptr共享的智能指针
共享的智能指针 std::shared_ptr
使用引用计数,每一个shared_ptr
的拷贝都指向相同的内存。再最后一个shared_ptr
析构的时候,内存才会被释放。shared_ptr
共享被管理对象,同一时刻可以有多个shared_ptr
拥有对象的所有权,当最后一个 shared_ptr
对象销毁时,被管理对象自动销毁。简单来说,shared_ptr
实现包含了两部分:
- 一个指向堆上创建的对象的裸指针:
raw_ptr
- 一个指向内部隐藏的、共享的管理对象:
share_count_object
-
初始化:
使用new初始化,使用make_shared初始化,使用另一个shared_ptr初始化,使用unique_ptr初始化后move给shared_ptr,使用自定义删除器初始化等等。这里简单说明前三种,最推荐使用第二种(因为他更高效)。
第一种是:
std::shared_ptr<int> ptr(new int(42));
第二种是:
std::shared_ptr<int> ptr = std::make_shared<int>(42);
第三种是:
std::shared_ptr<int> ptr1 = std::make_shared<int>(42); std::shared_ptr<int> ptr2 = ptr1; // 拷贝初始化
初始化的时候会涉及两个动态内存分配:
- 对象的内存分配:这是为了分配存储对象本身的内存,这一步是
new
表达式完成的。它会分配足够内存给这个int(42),并调用int型的构造函数来初始化这块内存。 - 控制块的内存分配:
std::shared_ptr
需要一个额外的控制块来存储与对象管理相关的信息,比如引用计数和可自定义删除器。
在
make_shared
中只进行了一次就将这两个动态内存就分配完毕了,并且此时的对象和控制块的内存是相邻连续的,会提高缓存的使用效率。而第一种先new分配内存给int(42),再shared_ptr
构造函数为自己控制块分配另一块内存。对于一个未初始化的智能指针,这里会用到
shared_ptr
的第一个成员函数reset()
方法来初始化。当智能指针有值的时候调用reset()
会将当前管理的对象的引用计数减1。这个管理的对象的引用计数为0就会被销毁,没到0的话当前shared_ptr
不再管理它。那么不管它了就结束了吗?不是,reset有三种重载形式:无参,带指针参数,以及带指针和删除器参数。定义如下://释放当前管理的对象,并将 shared_ptr 置为空指针(即不再管理任何对象)。 void reset() noexcept; //释放当前管理的对象,并将 shared_ptr 指向 ptr 所指向的对象。ptr 必须是一个可以转换为 T* //的指针类型(T 是 shared_ptr 管理的对象类型)。 template <class Y> void reset(Y* ptr); //释放当前管理的对象,并将 shared_ptr 指向 ptr 所指向的对象,同时指定一个自定义的删除器 d。 //删除器 d 是一个可调用对象,用于在 shared_ptr 不再管理对象时释放资源。 template <class Y, class Deleter> void reset(Y* ptr, Deleter d);
初始化部分范例代码:
// init_sample.cpp #include <iostream> #include <memory> using namespace std; void test(shared_ptr<int> sp) { cout << "sp3.use_count() =" << sp.use_count() << endl; } int main() { auto sp1 = make_shared<int>(100); //use make_shared shared_ptr<int> sp2(new int(100)); //use new to init cout << "sp1.use_count() =" << sp1.use_count() << endl; cout << "sp2.use_count() =" << sp2.use_count() << endl; shared_ptr<int> sp3(new int(100)); test(sp3); cout << "sp4.use_count() =" << sp3.use_count() << endl; shared_ptr<int> p1; p1.reset(new int(1)); //带参数的reset会将p1指向int(1) shared_ptr<int> p2 = p1; // 现在p1 和 p2 都是指向int(1) cout << "p2.use_count() = " << p2.use_count()<< endl;//输出2 cout << "p1.use_count() = " << p1.use_count()<< endl;//输出2 p1.reset(); // 没有参数就是释放资源 cout << "p2.use_count() = " << p2.use_count() << endl;//输出1 cout << "p1.use_count() = " << p1.use_count()<< endl;//输出0 return 0; } /* sp1.use_count() =1 sp2.use_count() =1 sp3.use_count() =2 sp4.use_count() =1 p2.use_count() = 2 p1.use_count() = 2 p2.use_count() = 1 p1.use_count() = 0 */
补充:这里会用到
shared_ptr
的第二个成员函数use_count()
:它是std::shared_ptr
的一个成员函数,用来返回 当前shared_ptr
所管理的对象 被多少个shared_ptr
实例共享引用。 - 对象的内存分配:这是为了分配存储对象本身的内存,这一步是
-
获取原始指针:
当需要获取原始指针时,可以通过
get
方法来返回原始指针,代码如下所示:std::shared_ptr<int> ptr(new int(1)); int *p = ptr.get(); //万一不小心 delete p;
谨慎使用
p.get()
的返回值,如果你不知道其危险性则永远不要调用get()函数。p.get()
的返回值就相当于一个裸指针的值,上述陷阱的所有错误都有可能发生, 遵守以下几个约定:- 不要保存
p.get()
的返回值 ,无论是保存为裸指针还是shared_ptr
都是错误的。保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr
则产生了独立指针 - 不要
delete
p.get()
的返回值 ,会导致对一块内存delete
两次的错误
- 不要保存
-
指定删除器:
如果用
shared_ptr
管理非new
对象或是没有析构函数的类时,应当为其写一个合适的删除器。#include <iostream> #include <memory> using namespace std; void deleter (int *p) { cout << "call Deleter delete p1" << endl; delete p; } int main() { shared_ptr<int> p1(new int(1), deleter); shared_ptr<int> p2(new int(1), [](int *p) { cout << "call lambda1 delete p2" << endl; delete p; }); std::shared_ptr<int> p3(new int[10], [](int *p) { cout << "call lambda2 delete p3" << endl; delete []p; }); std::shared_ptr<int> p4; p4.reset(new int(1), [](int* ptr) { std::cout << "use reset init and call lambda3 delete p4" << *ptr << std::endl; delete ptr; }); return 0; } /****************************************************************** use reset init and call lambda3 delete p41 call lambda2 delete p3 call lambda1 delete p2 call Deleter delete p1 *****************************************************************/