第五章,技术

28-Smart Pointers(智能指针)

前置申明

std::auto_ptr已经过时,在C++11中标记为弃用,C++17中已经被完全移除;

理解该条款其核心思想:
1、RALL(资源获取即初始化原则)
2、通过对象管理资源,避免内存泄漏
3、控制对象所有权语义(独占vs共享)

需要更新的技术细节:
1、不再使用std::auto_ptr,改用std::unique_ptr(独占所有权)
2、优先使用std::make_unique和std::make_shared而非裸new
3、现代智能指针已标准化,通常不需要自己编写智能指针类

注意

现代C++代码编写应该优先使用智能指针而非裸指针

std::unique_ptr

基本用法

#include <memory>

// 创建方式1:直接构造(C++11)
std::unique_ptr<int> p1(new int(42));

// 创建方式2:make_unique(C++14,推荐)
auto p2 = std::make_unique<int>(42);
auto p3 = std::make_unique<std::string>("hello");

// 自动释放,无需delete
// 离开作用域时自动销毁

关键特性

// 1. 不可拷贝(编译错误)
std::unique_ptr<int> a = std::make_unique<int>(10);
std::unique_ptr<int> b = a; // ❌ 编译错误!
std::unique_ptr<int> c(a); // ❌ 编译错误!

// 2. 可以移动(转移所有权)
std::unique_ptr<int> d = std::move(a); // ✅ a变为nullptr,d拥有对象

// 3. 支持数组(C++11)
std::unique_ptr<int[]> arr(new int[100]); // 自动调用 delete[]

// 4. 自定义删除器
std::unique_ptr<FILE, decltype(&fclose)> file(
fopen("test.txt", "r"),
&fclose
);

作为函数参数与返回值

// 传参:by reference(不转移所有权)
void useButNotOwn(std::unique_ptr<Widget>& widget) {
widget->doSomething(); // 使用对象
// 不转移,不销毁
}

// 传参:by value(转移所有权)
void takeOwnership(std::unique_ptr<Widget> widget) {
// widget在此拥有所有权
} // 自动销毁

auto w = std::make_unique<Widget>();

useButNotOwn(w); // 无需move
// w 仍然拥有对象,可以继续用

takeOwnership(std::move(w)); // 转移所有权
// w 指向null

// 返回:天然支持(移动语义)
std::unique_ptr<Widget> createWidget() {
return std::make_unique<Widget>(); // 无拷贝,直接构造
}

// 接收
auto widget = createWidget(); // 类型推导为 unique_ptr<Widget>

std::shared_ptr

shared_ptr 类描述使用引用计数来管理资源的对象。 shared_ptr 对象有效保留一个指向其拥有的资源的指针或保留一个 null 指针。 资源可由多个 shared_ptr 对象拥有;当拥有特定资源的最后一个 shared_ptr 对象被销毁后,资源将释放。

引用计数在独立控制块内(堆),所有shared_ptr共享,即所有shared_ptr都指向该控制块

#include <memory>

// 创建方式1:直接构造(不推荐,两次分配)
std::shared_ptr<int> p1(new int(42));

// 创建方式2:make_shared(C++11,强烈推荐)
auto p2 = std::make_shared<int>(42);
auto p3 = std::make_shared<std::vector<int>>(10, 100); // 10个100

// 拷贝:引用计数+1
std::shared_ptr<int> p4 = p2; // 所指资源对象 引用计数变为2

// 赋值:引用计数调整
p4 = p3; // p4指向p3所指,p2原指资源对象引用计数-1,p3所指资源对象引用计数+1

//当引用计数为0,自动销毁 ( 即持有资源的最后一个shared_ptr被销毁后,释放资源 )

关键特性

// 1. 引用计数(线程安全)
auto p = std::make_shared<int>(10);
{
auto q = p; // 计数=2
auto r = p; // 计数=3
} // q,r销毁,计数=1

// 2. use_count() 查看计数(调试用)
std::cout << p.use_count(); // 输出1

// 3. 自定义删除器
std::shared_ptr<FILE> file(
fopen("test.txt", "r"),
&fclose // 自定义删除器
);

// 4. 从this创建shared_ptr(需继承enable_shared_from_this)
class Widget : public std::enable_shared_from_this<Widget> {
public:
std::shared_ptr<Widget> getShared() {
return shared_from_this(); // ✅ 安全获取shared_ptr
}
};

auto w = std::make_shared<Widget>();
auto w2 = w->getShared(); // 引用计数+1
// 注意:不能直接在构造函数中用shared_from_this!

常见陷阱

// 错误:重复管理同一对象
int* raw = new int(10); // 这里不该使用裸指针
std::shared_ptr<int> a(raw);
std::shared_ptr<int> b(raw); // ❌ 后续会双重释放!崩溃!

// 正确:始终通过make_shared或拷贝shared_ptr
auto a = std::make_shared<int>(10);
auto b = a; // ✅ 引用计数机制

std::weak_ptr

描述了一个指向由一个或多个 shared_ptr 对象管理的资源的对象。 指向某个资源的 weak_ptr 对象不会影响该资源的引用计数。 当最后一个管理该资源 shared_ptr 的对象被销毁时,则即使存在指向该资源的 weak_ptr 对象,该资源也将被释放。 此行为对于避免数据结构中的循环至关重要。

需要使用该资源的代码可通过一个 shared_ptr 对象来执行该操作,该对象通过调用成员函数 lock 创建并拥有该资源。 weak_ptr 对象在其所指向的资源被释放时已过期,因为所有拥有该资源的 shared_ptr 对象已被销毁。 调用已过期的 weak_ptr 对象上的 lock 将创建一个空 shared_ptr 对象

#include <memory>

// weak_ptr必须由shared_ptr创建
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加引用计数

// 使用前必须lock()检查有效性
if (auto locked = wp.lock()) { // lock()返回shared_ptr
std::cout << *locked; // 使用对象
} else {
std::cout << "对象已销毁";
}

// 检查是否过期
if (wp.expired()) {
std::cout << "对象已不存在";
}

解决循环引用问题

两个对象互相持有对方的shared_ptr,导致引用计数永远不为0,无法销毁。

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
//std::weak_ptr<B> b_ptr; //解决循环引用问题
std::shared_ptr<B> b_ptr; // 强引用B
~A() { std::cout << "A 销毁\n"; }
};

class B {
public:
std::shared_ptr<A> a_ptr; // 强引用A
~B() { std::cout << "B 销毁\n"; }
};

int main() {
auto a = std::make_shared<A>(); // A计数=1
auto b = std::make_shared<B>(); // B计数=1

a->b_ptr = b; // B计数=2(b和a->b_ptr)
b->a_ptr = a; // A计数=2(a和b->a_ptr)

std::cout << "离开作用域...\n";
} // a、b销毁,但A计数=1,B计数=1,对象不销毁!内存泄漏!

// 程序结束,没有输出"A 销毁"和"B 销毁"
//应尽量避免双向持有

实用技法: 内部提供函数self获取指向本身的智能指针shared_ptr

将当前对象作为shared_ptr传递时安全保证引用计数正常,不会重复释放

class Foo : public std::enable_shardered_from_this<Foo>{
public:
Foo(){...}
~Foo(){...}
...
//正确实现:
shared_ptr<Foo> self()
{
return shared_from_this();//会在类中隐藏存储一个weak_ptr,跟踪管理当前外部对象的shared_ptr
}

/*错误实现:
shared_ptr<Foo> self()
{
return shared_ptr<Foo>(this);
}

{
std::shared_ptr<Foo> f1 = std::make_shared<Foo>(); //f1指向一块controlBlock,引用计数为1
std::shared_ptr<Foo> f2 = f1.self();//f2指向另一独立的一块controlBlock,引用计数为1;

//f1和f2指向两个独立的控制块,但是却指向同一数据内存,为什么会这样?
//因为std::shared_ptr<Foo> f2 = f1.self();相当于std::shared_ptr<Foo> f2(this);//当然,这里的this是指当前的对象指针,只是概念上这么写
//而std::shared_ptr<Foo> f2(一个Object的指针);实际是拷贝构造,shared_ptr会调用拷贝构造,"内含的控制块指针指向一个新的controlBlock,而存储实际数据的指针指向了这个object指针(但这个内存块实际已经有人管理了)"
//所以就出现了上面说的情况
}

顺带说一下一个东西:
Foo object;
std::shared_ptr<Foo> f1(&object);//释放栈上内存(&object),shared_ptr的delete抛出异常

Foo* pObject = new Foo();
std::shared_ptr<Foo> f2(pObject);//正常,但是不建议使用裸指针
*/
};

//本身不是智能指针shared_ptr,调用self会抛出异常
Foo *f = new Foo;
auto f00 = f->self();

//正确用法
std::shared_ptr<Foo> f = std::make_shared<Foo>();
auto f01 = f->self();

再提取该条款中原著的一些有用的东西

1、利用对象的构造析构来开始和结束运转纪录

template<class T>
class LogEntry{
public:
LogEntry(const T& objectToBeModified);//开始记录
~LogEntry();//结束记录
...
};

void editTuple(...)
{
LogEntry<...> entry(*pt); //仅此一行代码,完成开始和结束记录;
...
//do nothing about LogEntry Object
}

2、C++的对象切片(切割问题)

Derived d;
Base b = d;//合法! 派生类赋值给基类对象,丢失派生类对象自己的成员
//等价为 Base b = static_cast<Base&>(d); 属于隐式转型
//这就是C++典型的切割问题的根本原因,在条款13中都有提及,基本都是使用引用来解决这个切割问题

template<class T>
T& SmartPtr<T>::operator*() const
{
...
return *pointee;
}

//如果不反回T&而是返回T,极可能导致继承体系下的“切割问题”
//这是一个启发:在返回类型T对象时,应该思考,是否会产生继承体系下的切割问题,光是使用注释口头禁止错误用法是不够的!

3、abort与exit区别

exit(1);    // 正常终止:调用析构函数,清理资源,返回操作系统
abort(); // 异常终止:不清理,直接崩溃,防止损坏数据扩散到外部世界(数据库、文件、网络); 用进程死亡换取外部系统的安全

4、定义隐式转换,实现类对象可判断是否为null
基于条款5,我们知道隐式转换会带来一些问题,但用于判断的话,我觉得正是隐式判断的用武之地

template<class T>
class SmartPtr{
public:
...
operator void*(); //(类型转换SmartPtr -> void*)如果dumbPtr是null,返回nullptr,否则返回dumbPtr
//operator bool(); 不应该支持转换为bool,bool几乎可以参与所有算术运算和逻辑比较运算,例如: SmartPtr<Type1> pt1; SmartPtr<Type2> pt2; int x = pt1 + pt2;被隐式转换为bool参与无意义的运算,应该禁止
//operator T*(); //转换操作符,转换为T类型的dumb ptr; 但参考条款5,应该提供转换的函数,而不是这种隐式的转换

...
};

SmartPtr<TreeNode> ptn;
...
if(ptn == 0) ... //ptn需要与0比较,编译器检查ptn支持的类型转换,发现operator void*(),可转为void*进行比较,调用ptn.operator void*()
if(ptn) ... // 调用 ptn.operator void*()
if(!ptn) ...// 调用 ptn.operator void*()

实际上,现在的智能指针也是提供专门的函数来获取裸指针,而不是使用隐式转换:
.get()借用
.release()接管


//CASE_1
{
// shared_ptr:get()安全,所有权共享
auto sp = std::make_shared<int>(42);
int* r1 = sp.get(); // sp仍管理对象,安全
}

//CASE_2
{
// unique_ptr:release()危险,必须接管
auto up = std::make_unique<int>(42);
int* r2 = up.release(); // up不再管理,你必须 delete r2!
// 忘记delete = 内存泄漏
}

//CASE_3
{
auto up = std::make_unique<int>(42);
int* r2 = up.get(); // 获取裸指针,up仍拥有所有权
delete r2; // 灾难!双重释放!
// up 析构时再次 delete,未定义行为(崩溃/数据损坏)
}

5、令人眼前一亮的技法,通过成员模板函数实现【模板类】的【模板类型】所在继承体系【向上转型】

template<class T>
class SmartPtr {
T* pointee;
public:
// 成员模板:允许 SmartPtr<Derived> → SmartPtr<Base>
template<class newType>
operator SmartPtr<newType>() {
return SmartPtr<newType>(pointee); // 隐式转换 T* → newType*
}
};

class Base {};
class Derived : public Base {};

SmartPtr<Derived> dptr(new Derived);

// 发生隐式转换
SmartPtr<Base> bptr = dptr; // 成功!

void function(const SmartPtr<Base>& base)//func_1
{
...
}

function(dptr); // 成功!

/*这个过程发生了什么?
1、function需要const SmartPtr<Base>&
2、dptr是SmartPtr<Derived>,类型不匹配
3、先在SmartPtr<Base> class 中寻找单一自变量constructor,且其自变量类型为SmartPtr<Derived>,企图类型转换,没有找到
4、再接再厉,在SmartPtr<Derived> class 中寻找隐式类型转换操作符,希望可用产出一个SmartPtr<Base>,再次失败
5、编译器试图寻找一共“可实例化以导出合宜之转换函数”的member function template,这一次在SmartPtr<Derived>找到了这样一个东西,当它被实例化并令newType绑定至Bases时,产生了所需的函数。于是编译器将该函数实例化,导出以下函数码:
*/

{
SmartPtr<Derived>::operator SmartPtr<Base>(){
return SmartPtr<Base>(pointee);//只是调用了SmartPtr<Base> constructor
}

}

//新问题,重载后出现的二义性
class GrandDerived : public Derived {};

void function(const SmartPtr<Derived>& base)//func_2
{
...
}

SmartPtr<GrandDerived> gdptr(new GrandDerived);
function(gdptr);// 错误! 不知道调用的是func_1还是func_2

//应该避免这种二义性,最好只提供根部的基类作为函数参数类型

//利用member template来转换smart pointer,存在的另外两个问题:

//1、支持member templates编译器不多。(原著所在遥远年代-->现在) 不! 现代C++编译器都支持

//2、其间涵盖的技术并不简单,必须熟悉:(1)函数调用的自变量匹配规则 (2)隐式类型转换函数 (3)template functions的暗自实例化 (4)member function templates等技术;

//原著叹道:怜悯怜悯那些从未见过这些深层技术,缺要求以它们来维护或改善程序的可怜人吧。这项技术很“智能”,母庸质疑,但是太过先进反而可能是件危险的事情

如今的智能指针也支持继承体系的向上转型

#include <memory>

class Base {};
class Derived : public Base {};

std::shared_ptr<Derived> dptr = std::make_shared<Derived>();

// 向上转型:隐式支持
std::shared_ptr<Base> bptr = dptr; // ✅ 直接赋值,无需转换

void func(const std::shared_ptr<Base>& base) {
// ...
}

func(dptr); // ✅ 自动向上转型

6、在类中使用union的设计

template<class T>
class SmartPtrToConst{
...
functions
protected:
union{
const T* constPointee; //提供给 SmartPtrToConst使用的变量
T* pointee; //提供给 SmartPtr使用的变量
};
};

template<class T>
class SmartPtr: public SmartPtrToConst<T>{
...
functions
//没有data members
};

auto_ptr被C++11废弃的原因

auto_ptr 被移除不是因为”独占所有权”的概念错误,而是因为在缺乏语言级移动语义(C++11之前)的时代,被迫用拷贝语法模拟移动语义,导致了严重的安全性和可用性问题。 unique_ptr 借助右值引用和移动语义,以显式、类型安全的方式实现了同样的独占所有权模型。

std::auto_ptr<int> p1(new int(42));
std::auto_ptr<int> p2 = p1; // 看起来是拷贝,实际上是move!

// 现在 p1 变成空指针了!
*p1; // 💥 运行时崩溃:解引用空指针

//p1移交了控制权。 auto_ptr的设计理念是unique_ptr的前身,要求独占,但是没有禁止拷贝操作,这就导致上面的问题,而如今有了移动语义后可以直接禁止拷贝操作,使用move语义来转交资源控制权。