[c++] Why should I use Smart Pointers
深入理解智能指针
专有指针
Ref: unique_ptr的使用和陷阱
一、初始化
只可以使用new来分配内存,不可 拷贝和赋值。
unique_ptr<int> up1(new int()); // okay,直接初始化 unique_ptr<int> up2 = new int(); // error! 构造函数是 explicit unique_ptr<int> up3(up1); // error! 不允许拷贝
智能指针的创建。
// 智能指针的创建 unique_ptr<int> u_i; // 创建空智能指针 u_i.reset(new int(3)); // 绑定动态对象 unique_ptr<int> u_i2(new int(4)); // 创建时指定动态对象 unique_ptr<T,D> u(d); // 创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete
二、基本操作

unique_ptr<T> up 空的unique_ptr,可以指向类型为T的对象,默认使用delete来释放内存 unique_ptr<T,D> up(d) 空的unique_ptr同上,接受一个D类型的删除器d,使用删除器d来释放内存 up = nullptr 释放up指向的对象,将up置为空 up.release() up放弃对它所指对象的控制权,并返回保存的指针,将up置为空,不会释放内存 up.reset(…) 参数可以为 空、内置指针,先将up所指对象释放,然后重置up的值.
有意思的地方,指针设置为空,则控制的那块“空间”也会自动被执行释放操作。
// 所有权的变化 int *p_i = u_i2.release(); // 释放:释放所有权 unique_ptr<string> u_s(new string("abc")); unique_ptr<string> u_s2 = std::move(u_s); // 转移:所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” u_s2.reset(u_s.release()); // 所有权转移:释放+赋值 == 转移 u_s2=nullptr; // 显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价
三、参数、返回值
unique_ptr不可拷贝和赋值,那要怎样传递unique_ptr参数和返回unique_ptr呢?
事实上不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr (C++ Primer 5th p418)
-
返回值
//从函数返回一个unique_ptr unique_ptr func1(int a) { return unique_ptr<int> (new int(a)); } //返回一个局部对象的拷贝 unique_ptr func2(int a) { unique_ptr<int> up(new int(a)); return up; }
-
参数
使用 “引用”,避免所有权的转移,或者暂时的移交所有权。
// 传引用
void func1(unique_ptr<int> &up){ cout<< *up <<endl; }
// 传值 unique_ptr<int> func2(unique_ptr<int> up){ cout<< *up <<endl; return up; } unique_ptr<int> up(new int(10)); // 传引用,不拷贝,不涉及所有权的转移 func1(up);
// 暂时转移所有权,函数结束时返回拷贝,重新收回所有权 up = func2(unique_ptr<int> (up.release()));
四、删除器
类似shared_ptr,用unique_ptr管理非new对象、没有析构函数的类时,需要向unique_ptr传递一个删除器。
不同的是,unique_ptr管理删除器的方式,我们必须在尖括号中unique_ptr指向类型后面提供删除器的类型,在创建或reset一个这种unique_ptr对象时,必须提供一个相同类型的可调用对象(删除器),这个删除器接受一个T*参数。
五、常见问题
Ref: 将unique_ptr传递给函数
用作参数,加上 const,至少不准轻松地被修改.
但const unique_ptr<Device>&取代每一次Device*都是一个好的开始。 您显然无法复制unique_ptr并且您不想移动它。通过引用unique_ptr来替换现有函数体的主体可以继续工作。
现在有一个陷阱,您必须通过const &来阻止被调用者执行unique_ptr.reset()或unique_ptr().release()。
请注意,这仍然会将可修改的指针传递给设备。使用此解决方案,您无法轻松地将指针或引用传递给const Device。
Ref: 如何评价 C++11 的右值引用(Rvalue reference)特性?
unique_ptr<SomeObj> create_obj(/*...*/) { unique_ptr<SomeObj> ptr(new SomeObj(/*...*/)); ptr->foo(); // 一些可能的初始化 return ptr; // 左值 }
unique_ptr<SomeObj> create_obj(/*...*/) { return unique_ptr<SomeObj>(new SomeObj(/*...*/)); // 右值 }
在工厂类中,这样的语义是非常常见的。返回unique_ptr能够明确对所构造对象的所有权转移,特别的,这样的工厂类返回值可以被忽略而不会造成内存泄露。上面两种形式分别返回 栈上的左值和右值,但都适用移动语义(unique_ptr不支持拷贝)。
MyObj::MyObj() { for (...) { vec.push_back(new T()); } // ... } MyObj::~MyObj() { for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) { if (*iter) delete *iter; // 其实这里没什么用 } // ... }
共享指针
一、学习资源
C++中的智能指针
Ref: https://zhuanlan.zhihu.com/p/71649913
二、销毁
[ 这部分仅关于:shared_ptr ]
vector销毁,vector中的指针们所指向的各自“空间“也需要销毁。
vector的某个指针改变,相联系的指针内容全部改变。

// util/sharedptr1.cpp #include <iostream> #include <string> #include <vector> #include <memory> using namespace std; int main() { // two shared pointers representing two persons by their name shared_ptr<string> pNico(new string("nico")); shared_ptr<string> pJutta(new string("jutta")); // capitalize person names (*pNico)[0] = ’N’; pJutta->replace(0,1,"J"); // put them multiple times in a container vector<shared_ptr<string>> whoMadeCoffee; whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pNico); whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pNico); // print all elements for (auto ptr : whoMadeCoffee) { cout << *ptr << " "; } cout << endl; // overwrite a name again *pNico = "Nicolai"; // print all elements again for (auto ptr : whoMadeCoffee) { cout << *ptr << " "; } cout << endl; // print some internal data cout << "use_count: " << whoMadeCoffee[0].use_count() << endl; }
定义销毁行为
如果某个指针要放弃某一块内存时,使用 lambda 作为构造函数的第二个参数,定义销毁操作。
当对象的最后一个指针被调用时,lambda被调用。
shared_ptr<string> pNico(new string("nico"), [](string* p) { cout << "delete " << *p << endl; delete p; } ); ... pNico = nullptr; // pNico does not refer to the string any longer whoMadeCoffee.resize(2); // all copies of the string in pNico are destroyed
处理数组
默认的销毁行为不会执行delete[],所以要自己定义销毁行为,例如:
std::shared_ptr<int> p(new int[10], [](int* p) { delete[] p; // <---- lambda } );
另一种方式也可以:
std::shared_ptr<int> p(new int[10], std::default_delete<int[]>());
清理临时文件
处理清理内存以外,还有其他的资源需要处理,以下是一个清理临时文件的示例:
共享指针,指向 ”输出流“ 这个 ”对象“。
#include <string> #include <fstream> // for ofstream #include <memory> // for shared_ptr #include <cstdio> // for remove()
class FileDeleter { private: std::string filename;
public: FileDeleter (const std::string& fn): filename(fn) {
}
void operator () (std::ofstream* fp) { fp->close(); // close.file std::remove(filename.c_str()); // delete file } };
int main() { // create and open temporary file: std::shared_ptr<std::ofstream> fp( new std::ofstream("tmpfile.txt"), FileDeleter("tmpfile.txt") ); ... }
清理共享内存
shm_unlink主要用于linux Posix模式共享内存中的删除共享内存。
// util/sharedptr3.cpp #include <memory> // for shared_ptr #include <sys/mman.h> // for shared memory #include <fcntl.h> #include <unistd.h> #include <cstring> // for strerror() #include <cerrno> // for errno #include <string> #include <iostream>
class SharedMemoryDetacher { public: void operator () (int* p) { std::cout << "unlink /tmp1234" << std::endl; if (shm_unlink("/tmp1234") != 0) { std::cerr << "OOPS: shm_unlink() failed" << std::endl; } } };
std::shared_ptr<int> getSharedIntMemory(int num) { void* mem;
int shmfd = shm_open("/tmp1234", O_CREAT|O_RDWR, S_IRWXU|S_IRWXG); if (shmfd < 0) { throw std::string(strerror(errno)); } if (ftruncate(shmfd, num*sizeof(int)) == -1) { throw std::string(strerror(errno)); }
mem = mmap(nullptr, num*sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, shmfd, 0); if (mem == MAP_FAILED) { throw std::string(strerror(errno)); } return std::shared_ptr<int>(static_cast<int*>(mem), SharedMemoryDetacher()); }
int main() { // get and attach shared memory for 100 ints: std::shared_ptr<int> smp(getSharedIntMemory(100));
// init the shared memory for (int i = 0; i < 100; ++i) { smp.get()[i] = i*42; }
// deal with shared memory somewhere else: ... std::cout << "<return>" << std::endl; std::cin.get();
// release shared memory here: smp.reset(); ... }
析构函数清理
当然还有一种更加清晰的实现方法:构造函数实现初始化,析构函数实现清理。这样就可以简单地使用shared_ptr管理对象。
三、初始化
(1) 为了避免隐式转换,智能指针不能使用赋值的方式初始化,使用括号初始化或者列表初始化是没有问题的。
(2) 另一种初始化的方法是使用make_shared<>,它是一种 更好且更安全 的方法:因为使用new时会创建一个对象,计算它的引用计数时会创建一个对象,而make_shared只会创建一个对象,并且不会出现控制模块失效的情况。
shared_ptr<string> p1 = make_shared<string>(10, '9'); shared_ptr<string> p2 = make_shared<string>("hello"); shared_ptr<string> p3 = make_shared<string>();
(3) 先定义一个智能指针再进行赋值。但是不能使用赋值运算符(=),必须使用reset函数。
shared_ptr<string> pNico4; pNico4 = new string("nico"); // ERROR: no assignment for ordinary pointers pNico4.reset(new string("nico")); // OK
弱指针
一、有什么特点
Ref: C++11中weak_ptr的使用
Ref: C++ STL 四种智能指针 [写得用心]
C++11标准库还定义了一个名为weak_ptr的辅助类,它是一种弱引用,指向shared_ptr所管理的对象。
weak_ptr 被设计为与 shared_ptr 共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者。
weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。
weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。
weak_ptr提供了 expired() 与 lock() 成员函数:
a) 前者用于判断weak_ptr指向的对象是否已被销毁,
b) 后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空”shared_ptr)。
二、用法
基本的方法。
weak_ptr<T> w; // 创建空 weak_ptr,可以指向类型为 T 的对象
// 共享另一个对象 weak_ptr<T> w(sp); // 与 shared_ptr 指向相同的对象,shared_ptr 引用计数不变。T必须能转换为 sp 指向的类型 w=p; // p 可以是 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象
// 基本方法 w.reset(); // 将 w 置空 w.use_count(); // 返回与 w 共享对象的 shared_ptr 的数量 w.expired(); // 若 w.use_count() 为 0,返回 true,否则返回 false w.lock(); // 如果 expired() 为 true,返回一个空 shared_ptr,否则返回非空 shared_ptr
典型的 weak_ptr 使用方法。
#include <assert.h> #include <iostream> #include <memory> #include <string> using namespace std; int main() { shared_ptr<int> sp(new int(10)); assert(sp.use_count() == 1);
// 创建 weak_ptr 通过 shared_ptr weak_ptr<int> wp(sp);
assert(wp.use_count() == 1); if (!wp.expired()) {
// 通过 weak_ptr 得到 shared_ptr,然后修改一下值 shared_ptr<int> sp2 = wp.lock(); *sp2 = 100; assert(wp.use_count() == 2); } assert(wp.use_count() == 1); cout << "int:" << *sp << endl; return 0; }
三、解决什么问题
weak_ptr 到底有什么作用呢?从上面那个例子看来,似乎没有任何作用。
(1) weak_ptr 用于解决”引用计数”模型 循环依赖 问题,weak_ptr指向一个对象,并不增减该对象的引用计数器。
(2) weak_ptr 用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。
不能管理循环引用的对象。一个简单的例子如下:
(1) 互为 “夫妻” 的 “循环引用”。
int main(int argc, char** argv) { std::shared_ptr<Man> m(new Man()); std::shared_ptr<Woman> w(new Woman()); if(m && w) { m->setWife(w); w->setHusband(m); } return 0; }
(2) 类的定义
做法就是代码注释的地方取消注释,取消 Woman 类或者 Man 类的任意一个即可,也可同时取消注释,全部换成弱引用 weak_ptr。
class Man { private: std::weak_ptr<Woman> _wife; // 管理wife的生命周期,不方便使用shared_ptr // std::shared_ptr<Woman> _wife;
public: void setWife(std::shared_ptr<Woman> woman) { _wife = woman; } void doSomthing() { if(_wife.lock()) {} } ~Man() { std::cout << "kill man\n"; } };
class Woman { private: std::weak_ptr<Man> _husband; // std::shared_ptr<Man> _husband;
public: void setHusband(std::shared_ptr<Man> man) { _husband = man; }
~Woman() { std::cout <<"kill woman\n"; } };
破解相互引用
在 Man 类内部会引用一个 Woman,Woman 类内部也引用一个 Man。
当一个 man 和一个 woman 是夫妻的时候,他们直接就存在了相互引用问题。
man 内部有个 用于管理wife生命期 的 shared_ptr 变量,也就是说 wife 必定是在 husband 去世之后才能去世。同样的,woman 内部也有一个管理 husband 生命期的 shared_ptr 变量,也就是说 husband 必须在 wife 去世之后才能去世。这就是循环引用存在的问题。
husband 的生命期由 wife 的生命期决定,wife 的生命期由 husband 的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。
lock()的价值
另外很自然地一个问题是:既然 weak_ptr 不增加资源的引用计数,那么在使用 weak_ptr 对象的时候,资源被突然释放了怎么办呢?
不用担心,因为不能直接通过 weak_ptr 来访问资源。需要访问资源的时候,weak_ptr 为你生成一个shared_ptr,shared_ptr 能够保证在 shared_ptr 没有被释放之前,其所管理的资源是不会被释放的。创建 shared_ptr 的方法就是 lock() 成员函数。
如何选择智能指针
一、下面给出几个使用指南
(1) 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:
a)将指针作为参数或者函数的返回值进行传递的话,应该使用 shared_ptr;
b)两个对象都包含指向第三个对象的指针,此时应该使用 shared_ptr 来管理第三个对象;
c)STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出warning)和 auto_ptr(行为不确定)。如果你的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。
(2) 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。
如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。
可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋值给另一个的算法(如 sort())。例如,可在程序中使用类似于下面的代码段。
unique_ptr<int> make_int(int n) { return unique_ptr<int>(new int(n)); // 返回的是一个临时对象 } void show(unique_ptr<int>& p1) // 需要按照引用去show { cout << *p1 << ' '; } int main() { //... vector<unique_ptr<int>> vp(size); for(int i = 0; i < vp.size(); i++) { vp[i] = make_int(rand() % 1000); //copy temporary unique_ptr }
vp.push_back(make_int(rand() % 1000)); //ok because arg is temporary for_each(vp.begin(), vp.end(), show); //use for_each() //... }
其中 push_back 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr。另外,如果按值而不是按引用给 show() 传递对象,for_each() 将非法,因为这将导致使用一个来自 vp 的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。
在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给另一个 unique_ptr 需要满足的条件相同,即 unique_ptr 必须是一个临时对象。
(3) unique_ptr --> shared_ptr
与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr<int>:
unique_ptr<int> pup(make_int(rand() % 1000)); // ok shared_ptr<int> spp(pup); // not allowed, pup as lvalue shared_ptr<int> spr(make_int(rand() % 1000)); // ok
模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptr。shared_ptr 将接管原来归 unique_ptr 所有的对象。简而言之,您可以轻松有效地将std :: unique_ptr转换为std :: shared_ptr,但不能将std :: shared_ptr转换为std :: unique_ptr
Ref: C 11 unique_ptr和shared_ptr是否能够转换为彼此...
第一策略:
std::unique_ptr<std::string> unique = std::make_unique<std::string>("test"); std::shared_ptr<std::string> shared = std::move(unique);
第二策略:
std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");
二、 更多的智能指针
上文简单地介绍了 C++ STL 的四种智能指针。当然,除了 STL 的智能指针,C++ 准标准库 Boost 的智能指针,比如 boost::scoped_ptr、boost::shared_array、boost::intrusive_ptr 也可以在编程实践中拿来使用,但这里不做进一步的介绍,有兴趣的读者可以参考:C++ 智能指针详解。
End.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律