C++11 智能指针 —— 奇牛+Gemini

C++11 智能指针 —— 奇牛

1. std::auto_ptr<>(C++11已废弃)

1.1 std::auto_ptr<>概述

std::auto_ptr<> 是 C++98 标准中定义的第一个智能指针模板。它的核心思想是RAII(资源获取即初始化):将动态分配(new 操作)获得的内存地址交给一个栈上的对象去管理,当该对象生命周期结束(过期)时,其析构函数会自动调用 delete 来释放内存,从而防止内存泄漏。

  • 历史地位:由于设计上存在严重缺陷,std::auto_ptr<> 在 C++11 中被弃用(deprecated),并在 C++17 中被完全移除(removed)。在现代 C++ 中,应严格使用 std::unique_ptr<> 来替代它。

  • 头文件#include <memory>

  • 语法std::auto_ptr<类型> 变量名(new 类型);

1.2 auto_ptr基本使用

使用智能指针时,可以通过重载的 ->* 运算符,像使用普通裸指针一样操作它。

#include <iostream>
#include <memory>

class Test 
{
public:
    Test() : _x(1) { std::cout << "Test constructor\n";}
    ~Test() { std::cout << "Test destructor\n"; }
     
    int _x;
};

void func()
{
    std::auto_ptr<Test> atp(new Test);
    
    // 使用智能指针时,使用方式和普通指针完全一样
    std::cout << "_x: " << atp->_x << std::endl;
} // 函数结束,atp 离开作用域,自动调用 Test 的析构函数

int main()
{
    func();
    /*
    输出: 
    Test constructor
    _x: 1
    Test destructor
    */
}

1.3 常用成员函数:.get(), .release(), .reset(raw_ptr)

为了更好地管理底层指针,std::auto_ptr<> 提供了几个关键的成员函数:

  • .get():获取其管理的底层裸指针(不释放所有权)。
  • .release():取消智能指针对内存的管理(交出所有权),返回裸指针,将自身内部指针置空。注意:调用后必须手动释放内存。
  • .reset(raw_ptr):重置智能指针。如果传入了新的裸指针,它会先 delete 掉当前管理的内存,然后开始管理新传入的内存。
void api_demo()
{
    // 1. get() 返回裸指针,不交出所有权
    std::auto_ptr<Test> atp1(new Test);
    Test* tp = atp1.get();
    std::cout << "Raw pointer: " << tp << std::endl; 
    
    // 2. release() 取消托管,返回裸指针
    std::auto_ptr<Test> atp2(new Test);
    Test* manual_tp = atp2.release();
    // release 后不再自动释放,必须手动 delete
    delete manual_tp; 
    
    // 3. reset() 重置智能指针
    // 若新/旧指针不同,先析构原指针再托管新指针
    std::auto_ptr<Test> atp3(new Test);
    Test* otherp = new Test;
    
    // 发现指针不同,先析构 atp3 原来管理的 Test,再托管 otherp
    atp3.reset(otherp); 
}

1.4 为什么std::auto_ptr<>会被废弃

1.4.1 拷贝构造/赋值所有权转移

std::auto_ptr<> 采用了控制权转移(破坏性复制)的语义:当发生拷贝构造或赋值时,原指针会交出内存的所有权,并被置为 NULL

void bad_copy_demo1()
{
    std::auto_ptr<Test> apt1(new Test);
    
    // auto_ptr的拷贝构造/赋值会导致裸指针所有权转移
    std::auto_ptr<Test> apt2 = apt1;

    std::cout << apt1.get() << '\n'; // 0
    std::cout << apt2.get() << '\n'; // 0x505368
}

这种设计违背了 C++ 程序员对“拷贝”的常识(拷贝应该产生两个相同的对象),极易引发严重的 Bug。

1.5 std::auto_ptr<>注意事项(其他缺陷)

如果维护的老旧代码库不得不使用 std::auto_ptr<>,需注意以下几点:

  1. 绝对不要用于标准容器(STL Container):因为 STL 容器(如 std::vector)的算法经常进行元素的拷贝和赋值。如果往容器里放 std::auto_ptr<>,一旦发生拷贝,容器里原来的元素就会变成空指针。

    std::vector<std::auto_ptr<int>> vec1;
    // ... 给 vec1 放入一些数据 ...
    std::vector<std::auto_ptr<int>> vec2 = vec1; // vec1元素全指向NULL!
    
  2. 不能管理动态数组std::auto_ptr<> 内部的析构函数使用的是 delete 而不是 delete[]。如果用它管理 new [] 分配的数组,会导致行为未定义(内存泄漏或崩溃)。

  3. 不能作为全局变量:全局变量的初始化和析构顺序难以控制,智能指针的滥用可能导致在不正确的时机释放内存。

  4. 不要定义指向智能指针的指针:例如 new std::auto_ptr<T>(new T)。这是毫无意义的套娃,因为外层的指针本身是一个裸指针,不会被自动析构,从而导致内层的资源也一同泄漏。

  5. 升级替代方案:在 C++11 及以后的代码中,无脑使用 std::unique_ptr 替代 std::auto_ptrstd::unique_ptr 直接在编译期禁用了拷贝和赋值,必须显式使用 std::move 才能转移所有权,从根本上解决了上述问题。

2. std::unique_ptr<> C++11

在 C++11 之前,标准库提供了 std::auto_ptr<> 来管理动态内存,但其存在致命缺陷:在进行拷贝操作时会隐式地转移所有权,极易导致悬空指针错误。C++11 引入了 std::unique_ptr<> 来正式取代它(auto_ptr 在 C++17 中被完全移除)。

  • 排他所有权语义(独占语义)std::unique_ptr<> 严格遵循“一物一主”原则。在任何给定时刻,一块动态分配的内存只能由一个 unique_ptr 实例拥有。
  • RAII 机制的完美体现:它内部封装了原始的裸指针,当 unique_ptr 对象离开其作用域(生命周期结束)时,会自动调用底层的析构函数和 delete/delete[],从根本上杜绝内存泄漏。

2.1 std::unique_ptr<>的创建与初始化

在创建 std::unique_ptr<> 时,必须严格区分管理的是“单个对象”还是“对象数组”,这两者在底层模板实例化和资源释放(delete vs delete[])上采取了完全不同的策略。

2.1.1 std::unique_ptr<>管理单个对象

  • 传统方式:直接将 new 运算符返回的裸指针包裹进智能指针中。

    • 语法std::unique_ptr<T> ptr(new T());

    现代推荐方式(C++14 引入):使用标准库提供的包装函数 std::make_unique<T>()

    • 语法std::unique_ptr<T> ptr = std::make_unique<T>(构造函数参数);
      • 优势:代码更简洁;更重要的是它具备更好的异常安全性,并且在底层只执行一次内存分配操作。

2.1.2 管理对象数组

旧版的 std::auto_ptr<> 无法管理数组,因为它在内部固定使用 delete 来释放资源,导致行为未定义(通常会崩溃或内存泄漏)。而 std::unique_ptr<> 通过模板偏特化(Partial Specialization)完美解决了这个问题。

语法核心:在模板参数中必须加上 [],即 std::unique_ptr<T[]>

  • 传统方式:使用 new T[] 分配数组,并交由智能指针接管。
    • 语法std::unique_ptr<T[]> ptr(new T[数组大小]());
    • 注意:对于内置类型(如 int),如果不显式加 (),这种方式分配的内存中可能包含垃圾值。
      • C++不会对new出来的堆区内存0初始化!
  • 现代推荐方式(C++14 引入):使用专门针对数组重载的 std::make_unique
    • 语法std::unique_ptr<T[]> ptr = std::make_unique<T[]>(数组长度);
    • 【重要特性】值初始化(Value-initialization):使用 make_unique 创建数组时,它会强制对数组内存进行值初始化。对于 intdouble 等内置类型,所有元素都会被安全地自动初始化为 0;对于自定义类型,则会自动调用对应次数的默认构造函数。
    • 【语法限制】:在 C++14 和 C++17 标准下,使用 std::make_unique<T[]>只能指定数组的大小,无法像普通数组那样使用初始化列表(如 {1, 2, 3})来给数组元素赋特定的初值。

2.1.3 std::unique_ptr创建/初始化示例代码

#include <iostream>
#include <string>
#include <memory>

class Element 
{
public:
	Element() { std::cout << "Element constructor\n"; }
	~Element() { std::cout << "Element destructor\n"; }
};

void test_initialization()
{
// ===================================================
// 1. 管理单个对象
	// 1.1 传统方式,直接调用智能指针的构造函数,传递裸指针
	std::unique_ptr<std::string> str_up1(new std::string("traditional nameless"));

	// 1.2 C++14现代方式,传递构造参数给std::make_unique<>()
	std::unique_ptr<std::string> str_up2 = std::make_unique<std::string>("C++14 nameless");

// ===================================================
// 2. 管理对象数组
	// 2.1 传统方式:没有()的话,内置类型可能为垃圾值(未初始化)
	//	直接调用智能指针的构造函数,传递裸指针
	std::unique_ptr<int[]> arr_up1(new int[5]); // 无(), 可能为垃圾值
	std::unique_ptr<double[]> arr_up2(new double[5]()); // 有(), 0初始化

	// 2.2 C++14现代方式:内置类型自动0初始化
	//	传递数组大小给重载的std::make_unique<>()
	std::unique_ptr<int[]> arr_up3 = std::make_unique<int[]>(5);

	// 2.3 补充:管理自定义类的数组,会连续调用3次默认构造函数
	std::unique_ptr<Element[]> arr_obj = std::make_unique<Element[]>(3);
} // 离开作用域时,arr_obj执行delete[],会连续调用3次析构函数

int main()
{
	test_initialization();
}

2.2 对象所有权的控制与转移

为了强行贯彻“独占”语义,std::unique_ptr<> 在类的设计上做了严格的限制。

2.2.1 禁用拷贝操作

  • std::unique_ptr<> 显式删除了拷贝构造函数(unique_ptr(const unique_ptr&) = delete;)和拷贝赋值运算符。

  • 结论:绝对无法使用一个左值的 unique_ptr 去初始化或赋值给另一个。

2.2.2 启用移动语义(Move Semantics)

  • 虽然不能“复制”,但所有权可以“转移”。std::unique_ptr<> 支持右值引用的移动构造和移动赋值。
  • 转移方法:如果必须交出当前左值指针的所有权,必须显式调用 std::move() 将其转换为将亡值(xvalue)。
  • 转移结果:所有权移交后,原智能指针内部的裸指针会被安全地置为 nullptr

2.2.3 与 STL 容器的无缝契合

正因为禁用了隐式拷贝,std::unique_ptr<> 存入 std::vector 等容器是非常安全的。只需在推入容器时使用 std::move(),就能确保指针的所有权安全地转移给容器内的元素,不会出现资源被意外释放的情况。

	std::string p1{ "Reimu" };
	std::string p2{ "Marisa" };
	std::string p3{ "Sakuya" };
	std::string p4{ "Remilia" };

	std::vector<std::string> vec;

	// vec.push_back(p1); // 报错
	vec.push_back(std::move(p1));
	vec.push_back(std::move(p2));
	vec.push_back(std::move(p3));
	vec.push_back(std::move(p4));

2.3 常用方法:生命周期干预与手动控制

在多数情况下,我们依赖它自动析构。但 C++ 提供了丰富的接口供开发者手动介入内存管理。

2.3.1 .get():只看不碰

  • 作用:返回内部包裹的裸指针。
  • 应用场景:用于兼容必须接收裸指针的旧版 C/C++ API。
  • 警告:绝对不要用这个返回的裸指针手动调用 delete

2.3.2 指针对象 = nullptr:立刻销毁

  • 作用:将智能指针置空。
  • 底层行为:这会立即触发析构函数并释放当前管理的对象内存。

2.3.3 .release():交出控制权(不触发析构)

  • 作用:主动放弃对该内存块的管理权,并将智能指针自身置空。

  • 底层行为:它仅仅返回内部的裸指针,绝对不会调用析构函数或删除器!

  • 风险防范:调用该方法后,开发者必须接管返回的裸指针,并亲自负责后续的 delete 操作,否则必定造成内存泄漏。

2.3.4 .reset():替换与重置(先析构,后接管)

  • 作用:重置智能指针内部的裸指针。

  • 执行顺序:如果当前智能指针已经拥有一个对象,调用 reset(新指针)首先释放(析构)原有的对象,然后再接管传入的新指针。

  • 无参调用:如果直接调用 reset()(不传参),则单纯释放原有对象并将指针置空。

2.4 自定义删除器(Custom Deleter)

默认的 unique_ptr 使用 deletedelete[]。但如果我们用它来管理非标准堆内存(如网络 Socket、FILE* 文件句柄)或需要在释放前执行特定清理逻辑,就需要自定义删除器。

语法规范:

删除器的类型必须作为 std::unique_ptr 模板的第二个参数传入。

  • std::make_unique<>()不支持传入删除器
  • 构造函数中必须传入删除器实例
std::unique_ptr<被管理对象类型, 删除器类型> 变量名(裸指针, 删除器实例);

示例代码:

// 1. 仿函数作为删除器
	// 1.1 直接传入删除器类的对象(仿函数)
	MyDeleter md;
	std::unique_ptr<Element, MyDeleter> custom_up1(new Element, md);
	// 1.2 传入 类名() ,用构造函数创建匿名对象(仿函数)
	std::unique_ptr<Element, MyDeleter> cunstom_up2(new Element, MyDeleter());

// 2. Lambda等等,学完Lambda再补充

2.4.1 编写仿函数(Functor)作为删除器

通常推荐编写一个重载了 operator() 的结构体或类作为删除器。

  1. 定义一个类。
  2. 在类中重写 void operator()(T* ptr) const
  3. 在函数体内编写清理逻辑,并显式调用 delete ptr(如果管理的是堆内存)。
class Element 
{
public:
	Element() { std::cout << "Element constructor\n"; }
	~Element() { std::cout << "Element destructor\n"; }

	void say() { std::cout << "GUGUGAGA!\n"; }
};

class MyDeleter
{
public:
	void operator()(Element* elem)
	{
		elem->say();
		delete elem;
	}
};

int main()
{
	std::unique_ptr<Element, MyDeleter> up(new Element);
}
// 程序输出:
//Element constructor
//GUGUGAGA!
//Element destructor

2.4.2-x Lambda等作为删除器

  • 等学完了回来补充

2.5 std::unique_ptr<>全内容示例代码

#include <iostream>
#include <memory>
#include <vector>
#include <string>

class Test
{
public:
	Test() { std::cout << "Test constructor\n"; }
	~Test() { std::cout << "Test destructor\n"; }

	void doSomething() { std::cout << "do something...\n"; }
};

// 自定义删除器(仿函数)
class TestDeleter
{
public:
	void operator()(Test* test)
	{
		test->doSomething();
		delete test;
	}
};

int main()
{
// 1. 通过移动语义,将std::unique_ptr移动给STL容器
	std::unique_ptr<std::string> up1 = std::make_unique<std::string>("Marisa");
	std::unique_ptr<std::string> up2 = std::make_unique<std::string>("Alice");

	std::vector<std::unique_ptr<std::string>> vec;
	vec.push_back(std::move(up1));
	vec.push_back(std::move(up2));

// 2. 数组对象管理,使用重载的std::make_ptr<T[]>(数组长度)
	// 必须带有 [] 模板参数,生命周期结束时自动调用 delete[]
	std::unique_ptr<int[]> arr_up1 = std::make_unique<int[]>(5);

//3. 使用自定义删除器,不能使用工厂函数
	TestDeleter ts;
	std::unique_ptr<Test, TestDeleter> custom_up1(new Test(), ts);
	std::unique_ptr<Test, TestDeleter> custom_up2(new Test, TestDeleter());

// 4. 手动控制生命周期
	// 4.1 .get()返回仅裸指针,不放弃控制权或析构
	{
		auto up = std::make_unique<std::string>("Reimu");
		std::string* raw_p = up.get();
		std::cout << *up << std::endl; // 输出: Reimu
	}

	// 4.2 .reset(raw_p)先析构现有对象,再置空接管新对象;参数为空时指向nullptr
	{
		auto up = std::make_unique<int>(114);
		int* raw_p = new int(514);

		up.reset(raw_p);
		std::cout << *up << std::endl; // 输出:514

		up.reset();
		std::cout << "up.get(): " << up.get() << std::endl; // 输出:up.get(): 00000000
	}

	// 4.3 .release() 交出控制权,不析构——必须手动清理
	{
		auto up = std::make_unique<std::string>("Sakuya");
		std::string* raw_p = up.release();

		std::cout << "up.get(): " << up.get() << std::endl; // 输出:up.get(): 00000000
		std::cout << "*raw_p: " << *raw_p << std::endl; // 输出:*raw_p: Sakuya

		// 必须手动清理!
		delete raw_p;
	}
}

3. std::shared_ptr<>

std::shared_ptr<> 是 C++11 引入的一种智能指针,用于解决资源共享和自动内存管理的问题。它的核心机制是基于引用计数(Reference Counting)的共享所有权模型

在底层实现上,std::shared_ptr<> 实例本身只包含两个原生指针:

  • 一个指向被管理的堆区对象本身。
  • 另一个指向在堆上动态分配的控制块(Control Block)

控制块的核心结构:

  • 强引用计数/use_count(Strong count):记录当前有多少个 std::shared_ptr<> 指向该对象。标准库对外提供的查询接口为 use_count()

  • 弱引用计数(Weak count):与 std::weak_ptr<> 相关。

  • 附加组件:如果初始化时传入了自定义删除器(Deleter)或内存分配器(Allocator),它们的副本也会保存在控制块中。

3.1 std::unique_ptr<>的创建与初始化

3.1.1 使用原生构造函数

1. 创建空std::shared_ptr<>

创建一个空的智能指针。此时指针指向 nullptr,不仅没有托管任何对象,底层也不会为其分配控制块内存。

std::shared_ptr<T> sp;
2. 管理单个堆区对象

传递一个堆区裸指针。智能指针将接管该内存的生命周期,并在其独立分配的控制块中初始化强引用计数为 1。

  • 注意:该构造函数被声明为 explicit,因此不支持隐式类型转换(例如 std::shared_ptr<int> sp = new int(10); 是编译错误的)。
std::shared_ptr<T> sp(new T());
3. 管理对象数组 (C++17 引入原生支持)

在 C++17 及之后的标准中,std::shared_ptr 增加了对数组类型 T[] 的偏特化。此时传递一个指向数组的裸指针,智能指针在生命周期结束时,能自动匹配并调用 delete[] 释放整块数组内存,而不会错误地调用 delete

// 空智能指针,可管理对象数组T[]
std::shared_ptr<T[]> arr_sp;

// 创建智能指针+传递对象数组指针
std::shared_ptr<T[]> arr_sp(new T[]{...});
4. 绑定自定义删除器

当托管的资源不是标准的 C++ 堆内存(例如 C 语言的 FILE*、操作系统的句柄或网络 Socket)时,必须引入自定义删除器以替代默认的 delete 行为。当强引用计数归零时,系统将调用传入的 Deleter 来清理资源(Deleter 可以是仿函数、Lambda 表达式或函数指针)。

  • 底层逻辑:与 std::unique_ptr<> 将删除器作为模板参数不同,std::shared_ptr<> 利用类型擦除(Type Erasure)将删除器保存在控制块中。这使得拥有不同删除器但托管同类型对象的 std::shared_ptr<> 可以被放入同一个容器中。
// 指针本身指向 nullptr,但其控制块中绑定了自定义的删除器
std::shared_ptr<T> sp(nullptr, Deleter());

// 托管裸指针并指定删除器
std::shared_ptr<T> sp(new T(), Deleter());
5. 拷贝构造与移动构造

拷贝构造(共享所有权): 通过拷贝已有的智能指针进行初始化。此操作不会触发新的内存分配,而是让 sp1 指向与 other_sp 相同的对象和控制块,并以原子操作将其底层的强引用计数递增。

std::shared_ptr<T> sp1 = other_sp;
std::shared_ptr<T> sp2(other_sp);

移动构造(所有权转移): 优先使用移动语义。移动构造不会增加强引用计数(避免了原子操作的性能开销),而是直接剥夺原智能指针的控制权,原指针被置空。

std::shared_ptr<T> sp3 = std::move(other_sp);

3.1.2 使用 std::make_shared<T>() (推荐) —— C++11

注意: std::make_shared<>() 在 C++11 就加入了标准库,但 std::make_unique<>() 却是因为当时标准委员会的遗漏,直到 C++14 才被正式补充进去。

std::make_shared<>() 函数在动态内存中分配一个对象并完成初始化,最后返回指向该对象的 std::shared_ptr<T>

std::shared_ptr<int> isp = std::make_shared<int>(10);
std::shared_ptr<std::string> ssp = std::make_shared<std::string>("nameless");

核心优势(推荐原因):

  • 内存分配效率高:相比原生构造函数的两次内存分配,std::make_shared 会在底层一次性申请一块足够大的连续连续内存,将“控制块”和“对象本身”存放在一起。这种连续内存布局大幅减少了内存碎片,并显著提高了 CPU 缓存命中率。
  • 异常安全性强:避免了在 new 对象成功之后、智能指针构造完成之前这一极小的时间窗口内抛出异常,从而杜绝了潜在的内存泄漏风险。

标准演进与功能限制:

  • 对象数组的支持情况: 在 C++11 至 C++17 标准中,std::make_shared 并不支持直接分配对象数组。在这些标准下管理数组,只能退回使用原生构造函数配合 new T[]。从 C++20 标准开始,标准库对该模板函数进行了扩充,允许使用 std::make_shared<T[]>(size) 的语法一次性分配并初始化数组。
  • 自定义删除器的冲突std::make_shared 无法指定自定义删除器。由于其底层的核心优化是将对象与控制块合并在一块连续内存中分配,因此它在内部硬编码了这块整体内存的释放逻辑。如果开发场景中强制要求使用特定的删除器清理非常规资源,必须放弃 make_shared,转而使用 std::shared_ptr 的原生构造函数。
  • Weak_ptr 导致的内存延迟释放陷阱: 由于 make_shared 将对象和控制块分配在同一块内存中,即使所有的 shared_ptr(强引用)都已销毁(对象调用了析构函数),只要还有 weak_ptr(弱引用)指向该控制块,整块连续内存(包括对象原本占据的空间)就无法交还给操作系统。如果对象体积庞大且 weak_ptr 存活时间长,可能会造成“伪内存泄漏”。

3.1.3 示例代码

#include <iostream>
#include <string>
#include <memory>

class Person
{
public:
    Person(int id) : _id(id) 
    { 
        std::cout << "Person constructor: " << _id << "\n"; 
    }
    
    ~Person() 
    { 
        std::cout << "Person destructor: " << _id << "\n"; 
    }

    int getId() const { return _id; }
    
private:
    int _id;
};

struct PersonDeleter
{
    void operator()(Person* p)
    {
        std::cout << "Custom Deleter called for Person: " << p->getId() << std::endl;
        delete p;
    }
};

struct Group 
{
    Person leader{404}; // 内部成员对象
};

int main()
{
    std::cout << "--- 1. 空指针与原生构造 ---\n";
    std::shared_ptr<Person> sp1; // 空指针,无控制块
    std::shared_ptr<Person> sp2(new Person(9527)); // 独立分配控制块
    // std::shared_ptr<Person> err_sp = new Person(1); // 错误:explicit 禁止隐式转换

    std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // 0
    std::cout << "sp2 use_count: " << sp2.use_count() << std::endl; // 1

    std::cout << "\n--- 2. 拷贝与移动语义 ---\n";
    sp1 = sp2; // 拷贝:共享控制块,原子递增引用计数
    std::cout << "After copy, sp1 use_count: " << sp1.use_count() << std::endl; // 2
    std::cout << "After copy, sp2 use_count: " << sp2.use_count() << std::endl; // 2
    
    std::shared_ptr<Person> sp_move = std::move(sp2); // 移动:不增加计数
    std::cout << "After move, sp_move use_count: " << sp_move.use_count() << std::endl; // 2
    std::cout << "After move, sp2 is empty: " << (sp2 == nullptr) << std::endl; // 1 (true)

    std::cout << "\n--- 3. 数组对象管理 (C++17) ---\n";
    // 生命周期结束时会自动调用 delete[]
    std::shared_ptr<Person[]> arr_sp(new Person[3]{ 1, 2, 3 });

    std::cout << "\n--- 4. 自定义删除器 ---\n";
    // 使用仿函数或 Lambda 作为删除器
    std::shared_ptr<Person> sp_del(new Person(233), PersonDeleter());
    std::shared_ptr<Person> sp_lambda(new Person(1024), [](Person* p) 
    {
        std::cout << "Lambda Deleter called for Person: " << p->getId() << std::endl;
        delete p;
    });

    std::cout << "\n--- 5. make_shared ---\n";
    std::shared_ptr<Person> sp_make = std::make_shared<Person>(1001);

    std::cout << "\n--- 6. 别名构造函数 (Aliasing Constructor) ---\n";
    std::shared_ptr<Group> group_sp = std::make_shared<Group>();
    // leader_sp 指向 member,但与 group_sp 共享生命周期
    std::shared_ptr<Person> leader_sp(group_sp, &group_sp->leader); 
    std::cout << "leader_sp use_count: " << leader_sp.use_count() << std::endl; // 2
    
    std::cout << "\n--- 作用域结束,开始析构 ---\n";
}

3.2 常用方法与生命周期管理

std::shared_ptr<> 的生命周期管理依赖于底层控制块中的强引用计数use_count。对智能指针的各种方法调用,本质上是对内部裸指针和控制块状态的操作。

3.2.1 状态获取与资源访问

1. 引用计数:.use_count()

返回当前与该智能指针共享同一控制块的强引用总数。

  • 时间复杂度:通常是 $O(1)$。
  • 多线程陷阱:在多线程环境下,.use_count() 返回的值只是一个“瞬间的快照”。由于其他线程可能随时进行拷贝或析构,返回值在获取到的下一刻可能就已经失效了。因此,它主要用于调试或测试,不建议将其作为程序业务逻辑的严格判断条件。
2. 唯一性判断:.unique() (C++17 废弃,C++20 移除)

判断当前智能指针是否是唯一独占托管对象的指针(即等价于 use_count() == 1)。

  • 标准演进原因:标准委员会在 C++17 中将其标记为废弃([[deprecated]]),并在 C++20 中将其彻底移除。原因是 unique() 在多线程并发环境中极易引发数据竞争(Data Race),容易误导开发者写出非线程安全的代码。现代 C++ 推荐直接使用无锁设计或精确的所有权模型,而非依赖瞬间的唯一性检查。
3. 获取裸指针:.get()

返回智能指针托管的底层堆区裸指针。如果智能指针为空,则返回 nullptr

  • 极度危险的操作
    1. 绝对不能手动 delete.get() 返回的指针,否则会导致重复释放(Double Free)引发程序崩溃。
    2. 绝对不能用 .get() 返回的指针去初始化另一个独立的智能指针。这会为同一个对象创建两个不同的控制块,导致灾难性的后果。
  • .get()唯一正确用途:将裸指针传递给不支持智能指针的遗留 C 语言 API 接口。
4. 解引用与布尔判定
  • operator*operator->:像使用普通指针一样操作托管对象。

  • operator bool():隐式检查智能指针是否为空(即底层指针是否不为 nullptr)。

3.2.2 生命周期管理:重置与主动释放

1. 无参重置:.reset()= nullptr

主动放弃当前智能指针的所有权。

底层逻辑是:将其底层的强引用计数以原子操作递减 1。如果减 1 后引用计数归零,则自动调用 delete(或自定义删除器)释放对象,并销毁控制块。当前智能指针随后被置为空(指向 nullptr)。

  • sp.reset();sp = nullptr; 在语义和底层行为上是完全等价的。
2. 带参重置:.reset(raw_p).reset(raw_p, Deleter)

放弃对当前对象的托管,转而接管新的裸指针 raw_p

  • 执行顺序:首先递减当前托管对象的引用计数(若归零则释放旧内存),然后接管 raw_p,并为其创建一个新的控制块(引用计数初始化为 1)。
  • 异常安全:如果 raw_p 的控制块内存分配失败抛出异常,旧对象的释放动作依然能够正确执行,保证了强异常安全性。

3.2.3 赋值与交换操作

1. 赋值运算符:operator=
  • 拷贝赋值(sp1 = sp2sp1 首先递减原有对象的强引用计数(可能触发旧对象的析构),然后指向 sp2 的对象与控制块,并将新的强引用计数递增。
  • 移动赋值(sp1 = std::move(sp2):优先推荐。直接转移控制权,不涉及原子计数的加减。sp1 原有对象的计数递减,随后剥夺 sp2 的指针,sp2 被置为空。
2. 交换操作:.swap()std::swap()
  • 成员函数sp1.swap(sp2);

  • 非成员函数std::swap(sp1, sp2);(标准库为智能指针提供了偏特化,底层直接调用成员函数 .swap())。

性能优势:交换两个智能指针是一个极其高效的操作。它仅仅在内部交换了两个指针的值(一个指向对象的裸指针,一个指向控制块的指针)。全程不涉及任何原子引用计数的增减,因此不会产生额外的性能开销,是改变智能指针指向的最快方式。

3.2.4 综合示例代码

#include <iostream>
#include <memory>
#include <string>

class Resource
{
public:
    Resource(const std::string& name) : _name(name)
    {
        std::cout << "Resource Acquired: " << _name << "\n";
    }

    ~Resource()
    {
        std::cout << "Resource Destroyed: " << _name << "\n";
    }

    void doSomething() const
    {
        std::cout << _name << " is working.\n";
    }

private:
    std::string _name;
};

int main()
{
    std::cout << "--- 1. 初始化与状态获取 ---\n";
    std::shared_ptr<Resource> sp1 = std::make_shared<Resource>("Res_A");
    std::shared_ptr<Resource> sp2 = sp1; // 发生拷贝,计数为 2

    std::cout << "sp1 use_count: " << sp1.use_count() << "\n"; // 2
    
    // 危险演示(仅展示语法,切勿尝试释放)
    Resource* raw_ptr = sp1.get();
    if (raw_ptr != nullptr)
    {
        raw_ptr->doSomething();
    }

    std::cout << "\n--- 2. 赋值与交换 ---\n";
    std::shared_ptr<Resource> sp3 = std::make_shared<Resource>("Res_B");
    
    // 交换操作:不涉及原子计数的增减,极其高效
    sp1.swap(sp3); 
    // 此时 sp1 指向 Res_B,sp3 指向 Res_A
    std::cout << "After swap, sp1 use_count (Res_B): " << sp1.use_count() << "\n"; // 1
    std::cout << "After swap, sp3 use_count (Res_A): " << sp3.use_count() << "\n"; // 2 (sp2和sp3指向它)

    std::cout << "\n--- 3. 主动释放与重置 ---\n";
    sp2 = nullptr; // sp2 放弃托管 Res_A,计数变为 1
    std::cout << "After sp2 = nullptr, sp3 use_count: " << sp3.use_count() << "\n";

    // sp3 重新接管新的堆内存,原 Res_A 的计数归零,触发析构
    sp3.reset(new Resource("Res_C")); 
    
    // sp1 无参重置,直接释放 Res_B
    sp1.reset(); 
    std::cout << "Is sp1 empty? " << (sp1 == nullptr ? "Yes" : "No") << "\n";

    std::cout << "\n--- 作用域结束,清理剩余资源 (Res_C) ---\n";
    return 0;
}

3.3 std::shared_ptr<>的循环引用问题 (Circular Reference)

3.3.1 机制解析:为什么会发生内存泄漏?

循环引用是基于“引用计数”机制的智能指针最大的致命伤。当两个或多个对象通过 std::shared_ptr 相互指向对方,或者形成环状的引用链时,就会构成一个强引用闭环

内存泄漏的推演过程:

  1. 建立引用:当对象 A 内部的 shared_ptr 指向对象 B,且对象 B 内部的 shared_ptr 指向对象 A 时,A 和 B 所在的控制块的强引用计数都会增加(通常变为 2:一个是外部局部指针,一个是对方对象内部的成员指针)。
  2. 退出作用域:当外部的局部智能指针变量生命周期结束被销毁时,它们各自析构并让 A 和 B 的强引用计数减 1。
  3. 死锁局面:此时,A 和 B 的强引用计数均从 2 降为 1,而不是 0。因为它们内部的成员指针还在互相“死死抱住”对方。
  4. 最终结果:由于引用计数无法归零,A 和 B 的析构函数永远不会被调用,它们所占用的堆内存及其控制块也就永远无法交还给操作系统,从而导致严重的内存泄漏 (Memory Leak)

3.3.2 示例代码:经典的 Boy & Girl 循环引用

#include <iostream>
#include <string>
#include <memory>

class Girl;

class Boy 
{
public:
    Boy() { std::cout << "Boy constructor\n"; }
    ~Boy() { std::cout << "Boy destructor\n"; }

    void set_girl_friend(const std::shared_ptr<Girl>& g)
    {
        _girl_friend = g;
    }
private:
    std::shared_ptr<Girl> _girl_friend; // 强引用指向 Girl
};

class Girl
{
public:
    Girl() { std::cout << "Girl constructor\n"; }
    ~Girl() { std::cout << "Girl destructor\n"; }

    void set_boy_friend(const std::shared_ptr<Boy>& b)
    {
        _boy_friend = b;
    }
private:
    std::shared_ptr<Boy> _boy_friend; // 强引用指向 Boy
};

int main()
{
    // 1. 分别独立创建,分配内存与控制块
    // sxc 指向 Boy,引用计数为 1
    std::shared_ptr<Boy> sxc = std::make_shared<Boy>();
    // dxl 指向 Girl,引用计数为 1
    std::shared_ptr<Girl> dxl = std::make_shared<Girl>();

    std::cout << "Before cross-reference:\n";
    std::cout << "Boy use_count: " << sxc.use_count() << "\n"; // 1
    std::cout << "Girl use_count: " << dxl.use_count() << "\n\n"; // 1

    // 2. 发生交叉引用
    sxc->set_girl_friend(dxl); // Girl 的引用计数 +1,变为 2
    dxl->set_boy_friend(sxc);  // Boy 的引用计数 +1,变为 2

    std::cout << "After cross-reference:\n";
    std::cout << "Boy use_count: " << sxc.use_count() << "\n"; // 2
    std::cout << "Girl use_count: " << dxl.use_count() << "\n\n"; // 2

    // 3. main 函数作用域结束,开始清理局部变量
    // - dxl 销毁:Girl 的引用计数 -1,变为 1 (不为 0,不析构 Girl)
    // - sxc 销毁:Boy 的引用计数 -1,变为 1 (不为 0,不析构 Boy)
    // 最终:屏幕上永远不会打印 "Boy destructor" 和 "Girl destructor"
    
    return 0;
}

3.3.3 解决方案

使用 std::weak_ptr<> 打破强引用闭环

  • 核心思路:在设计相互引用的类时,将其中一方或双方内部的 std::shared_ptr 替换为 std::weak_ptrweak_ptr 能够“观察”托管对象,但不会增加强引用计数。这样,当外部指针销毁时,引用计数能顺利归零,从而正常触发对象的析构函数,解开闭环。

4. std::weak_ptr<>

不增加use_count,增加weak count;

std::weak_ptr<>只能够从一个std::shared_ptr<>或者std::weak_ptr<>对象构造

弱指针不能够解引用!不支持*->

std::weak_ptr<>::lock()可以返回一个std::shared_ptr<>

posted @ 2026-05-12 22:25  学习笔记草稿存放账号  阅读(18)  评论(0)    收藏  举报