【C++11】智能指针详解

目录

[1. 为什么需要智能指针?](#1. 为什么需要智能指针?)

[2. RAII 设计思想](#2. RAII 设计思想)

[3. auto_ptr 独占型智能指针(废弃)](#3. auto_ptr 独占型智能指针(废弃))

[4. unique_ptr(独占型智能指针)](#4. unique_ptr(独占型智能指针))

[4.1 核心特性](#4.1 核心特性)

[4.2 基本使用](#4.2 基本使用)

[4.3 模拟实现](#4.3 模拟实现)

[5. shared_ptr(共享型智能指针)](#5. shared_ptr(共享型智能指针))

[5.1 核心特性](#5.1 核心特性)

[5.2 基本使用](#5.2 基本使用)

[5.3 模拟实现](#5.3 模拟实现)

[6. operator bool()](#6. operator bool())

[7. 删除器](#7. 删除器)

[7.1 默认删除器](#7.1 默认删除器)

[7.2 为什么需要自定义删除器?](#7.2 为什么需要自定义删除器?)

[7.3 unique_ptr 与 shared_ptr 删除器的设计差异](#7.3 unique_ptr 与 shared_ptr 删除器的设计差异)

[unique_ptr 删除器](#unique_ptr 删除器)

[shared_ptr 删除器](#shared_ptr 删除器)

[7.4 自定义删除器的实现及其使用](#7.4 自定义删除器的实现及其使用)

Lambda表达式删除器

仿函数删除器

函数指针删除器

[7.5 小结](#7.5 小结)

[8. make_unique 和 make_shared](#8. make_unique 和 make_shared)

[8.1 基础定义与引入时间](#8.1 基础定义与引入时间)

[8.2 基本用法](#8.2 基本用法)

[8.3 为什么推荐使用它们?](#8.3 为什么推荐使用它们?)

(1)杜绝裸指针误用

(2)保证异常安全(最重要的原因)

[(3)make_shared 独有优势:一次性内存分配,性能提升](#(3)make_shared 独有优势:一次性内存分配,性能提升)

[8.4 注意事项与局限性](#8.4 注意事项与局限性)

[8.5 最佳实践](#8.5 最佳实践)

[9. shared_ptr 循环引用问题](#9. shared_ptr 循环引用问题)

[10. weak_ptr(弱引用智能指针)](#10. weak_ptr(弱引用智能指针))

[10.1 核心特性](#10.1 核心特性)

[10.2 用 weak_ptr 打破循环引用](#10.2 用 weak_ptr 打破循环引用)

[10.3 常用方法](#10.3 常用方法)

[lock() 函数](#lock() 函数)

[expired() 函数](#expired() 函数)

[10.4 模拟实现](#10.4 模拟实现)

[11. 总结](#11. 总结)


1. 为什么需要智能指针?

使用原始的裸指针(Raw Pointer)和 new/delete 手动管理内存,容易出现以下问题:

  • 内存泄漏 :忘记调用 delete 释放内存。
  • 悬空指针 :在 delete 之后继续访问指针,导致未定义行为。
  • 重复释放 :对同一块内存多次调用 delete,可能导致程序崩溃。
  • 异常不安全 :在函数执行过程中如果抛出异常,可能会跳过 delete 语句,导致内存泄漏。

下面代码展示了 C++ 早期(C++98 之前)程序员面临的困境:为了处理异常安全,必须编写大量的样板代码来释放资源。

复制代码
double Divide(int a, int b)
{
	
	if (b == 0)
	{
		string s("Divide by zero condition!");
		throw s;
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void func()
{
	//如果array1分配成功了,但array2分配失败抛出异常,那么array1就内存泄漏了,就还需要把分配逻辑包在try_catch块里面,释放array1的资源,代码会变得非常臃肿
	
	int* array1 = new int[10];
	int* array2 = new int[10];
	
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)	//如果发生除0错误,Divide函数抛异常,那么array1和array2没有得到释放,所以这里捕获异常,并不处理异常,
	{			//只是手动delete两个数组释放资源,再将异常重新抛出交给上层处理

		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;

		delete[] array1; //释放资源
		delete[] array2;

		throw;//异常重新抛出
	}

	delete[] array1;
	cout << "delete []" << array1 << endl;
	delete[] array2;
	cout << "delete []" << array2 << endl;
}

int main()
{

	try
	{
		func();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}

	return 0;
}

这正是C++引入 智能指针RAII 机制要解决的核心痛点。

2. RAII 设计思想

RAII(Resource Acquisition Is Initialization,资源获取即初始化),RAII核心思想把资源的生命周期绑定到对象的生命周期上,对象构造时获取资源,对象析构时释放资源。
RAII 解决了"异常不安全"导致的资源泄漏,这是 RAII 最大的贡献。在传统的 C++ 代码中,如果资源分配和释放之间发生了异常,程序控制流会跳转,导致释放代码(如 deleteclose)被跳过,从而造成资源泄漏。

  • 没有 RAII 时 :你需要编写大量的 try-catch 块来捕获异常并释放资源,或者在每个 return 语句前手动释放资源,代码极其繁琐且容易遗漏。
  • 使用 RAII 后 :C++ 保证栈上的局部对象在离开作用域时(无论是正常返回还是因异常进行"栈展开"),其析构函数一定会被调用。因此,资源释放代码放在析构函数中是绝对安全的,无需任何额外的异常处理代码。

智能指针是RAII最经典的应用, 也是现代 C++ 管理动态内存的首选方案,它通过封装原生指针并重载运算符,实现了像使用普通指针一样方便,同时又能自动管理。

C++11 标准引入了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr,并废弃了旧的std::auto_ptr。使用智能指针需要包含 <memory> 头文件。智能指针类还重载 operator*/operator->/operator[] 等运算符,方便访问资源。

3. auto_ptr 独占型智能指针(废弃)

auto_ptr 是 C++98 标准中引入的第一代智能指针,旨在通过 RAII 机制自动管理动态内存。然而,由于其设计上存在严重缺陷 ,它在 C++11 标准中被弃用 ,并在 C++17 标准中被正式移除
auto_ptr 号称是 "独占所有权",但受限于 C++98 没有移动语义,它的实现方式是用拷贝 来模拟转移所有权,它的特点是拷贝时把原指针置空,所有权转移给新指针,这导致它极容易写出悬空指针、崩溃代码。

这种设计完全违背了正常拷贝语义,语法语义严重混乱,极易引发难以排查的空指针崩溃。

复制代码
int main()
{
	auto_ptr<int> p1(new int(10));
	auto_ptr<int> p2 = p1;  // 拷贝!管理权转移,被拷贝对象p1悬空

	*p1;  // 未定义行为,直接崩溃

	return 0;
}

4. unique_ptr(独占型智能指针)

std::unique_ptr 在 C++11 中被引入,保留了 "独占所有权" 的核心理念,同时通过显式禁止拷贝、仅支持移动语义 ,彻底解决了 auto_ptr 的安全隐患,是目前 C++ 中默认首选的零开销智能指针。

4.1 核心特性

  • 同一时间只能有一个unique_ptr对象管理这块内存。
  • unique_ptr 禁止拷贝构造和拷贝赋值 ,**只允许移动语义,**这确保了所有权的清晰转移。
  • unique_ptr 离开作用域(或被显式重置)时,它会自动调用删除器释放所管理的对象。
复制代码
void test()
{
	unique_ptr<int> up1(new int(10));
	//unique_ptr<int> up2 = up1;     // 编译错误!禁止拷贝
	unique_ptr<int> up3 = move(up1); // 正确,移动构造,所有权转移给 p3,p1 变为空

	//由此,unique_ptr不允许作为容器元素,
	//vector<unique_ptr<int>> vec;
	//vec.push_back(up1);
}

int main()
{	
	test();
	return 0;
}

4.2 基本使用

复制代码
#include <iostream>   
#include <memory>
using namespace std;

// 创建 unique_ptr
void test1()
{
	//管理单个对象
	unique_ptr<Person> up1(new Person);
	up1->demo();

	//管理动态数组
	unique_ptr<Person[]> up2(new Person[10]);
    //unique_ptr重载了operator* 和 operator->,可以像普通指针一样使用
	up2[0].demo();
	up2[1].demo();	
}

// 常用成员函数的使用
void test2()
{
	unique_ptr<Person> up1(new Person);
	unique_ptr<Person> up2(new Person);
	unique_ptr<Person> up3(new Person);
	unique_ptr<Person> up4(new Person);

	//get():获取内部的裸指针(不改变所有权,用于与C接口交互)
	Person* raw1 = up1.get();
	
	//realease():释放所有权,返回裸指针。注意:调用后unique_ptr变为空,需要手动管理返回的指针
	Person* raw2 = up2.release();

	//reset():重置指针,有两个重载版本(无参和带参),具体如下:
	up3.reset();//无参:释放当前对象,up3指针置空
	up3.reset(new Person);//带参:释放旧对象,接管新对象

	//swap():交换两个unique_ptr对象所管理的对象
	up3.swap(up4);

	//operator bool():隐式转换为bool,判断指针是否为空
	if (!up2)
	{
		cout << "up2管理的指针为空" << endl;
	}
}

int main()
{
	//test1();
	test2();	
	return 0;
}

4.3 模拟实现

复制代码
//模拟实现unique_ptr
template<class T>
class MyUniquePtr
{
public:
	//构造
	explicit MyUniquePtr(T* ptr = nullptr) //加explicit防止隐式转换
		:_ptr(ptr)
	{}

	//析构
	~MyUniquePtr()
	{
		if (_ptr)
		{
			delete _ptr;
		}
	}

	MyUniquePtr(const MyUniquePtr&) = delete; //禁用拷贝构造
	MyUniquePtr& operator=(const MyUniquePtr&) = delete; //禁用拷贝赋值

	//支持移动构造
	MyUniquePtr(MyUniquePtr&& other) noexcept
		:_ptr(other._ptr)
	{
		other._ptr = nullptr; //将对方的指针置空
	}

	//移动赋值
	MyUniquePtr& operator=(MyUniquePtr&& other) noexcept
	{
		if (this != &other)
		{
			delete _ptr;//释放自己原本持有的资源(如果有的话)
			
			_ptr = other._ptr;   //接管对方的资源
			other._ptr = nullptr;//将对方置空
		}
		return *this;
	}

	//访问操作符重载
	T& operator*() const { return *_ptr; }
	T* operator->() const { return _ptr; }

	//获取原始指针
	T* get() const { return _ptr; }

	//释放所有权(不销毁资源,只是交出指针)
	T* release()
	{
		T* temp = _ptr;
		_ptr = nullptr;
		return temp;
	}

	//重置指针(释放旧资源,指向新资源)
	void reset(T* p = nullptr)
	{
		delete _ptr;
		_ptr = p;
	}

    // 显式转换运算符重载 operator bool()
    explicit operator bool() const noexcept 
    {
        return ptr != nullptr; // 检查内部指针是否为空
    }

private:
	T* _ptr; //持有的原始指针
};

数组特化版本:

复制代码
// 数组特化版本(管理数组)
// 注意这里:<T[]>,专门匹配 MyUniquePtr<int[]> 这种写法
template<class T>
class MyUniquePtr<T[]>
{
public:
	explicit MyUniquePtr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	~MyUniquePtr()
	{
		delete[] _ptr; // 关键:使用delete[]
		cout << "delete[] _ptr" << endl;
	}

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

	MyUniquePtr(MyUniquePtr&& other) noexcept
		:_ptr(other._ptr)
	{
		other._ptr = nullptr;
	}

	MyUniquePtr& operator=(MyUniquePtr&& other) noexcept
	{
		if (this != &other)
		{
			delete[] _ptr; // 关键:使用delete[]

			_ptr = other._ptr;
			other._ptr = nullptr;
		}
		return *this;
	}

	//数组特有的访问方式:下标操作符
	T& operator[](size_t index)const
	{
		return _ptr[index];
	}

	T* get()const
	{
		return _ptr;
	}

	T* release()
	{
		T* temp = _ptr;
		_ptr = nullptr;
		return temp;
	}

	void reset(T* p = nullptr)
	{
		delete[] _ptr;
		_ptr = p;
	}
private:
	T* _ptr;
};

5. shared_ptr(共享型智能指针)

std::shared_ptr 实现了共享资源所有权 的语义,允许多个 shared_ptr 实例共享同一块内存 ,通过引用计数 管理内存,所有共享该对象的指针,共用同一个引用计数

5.1 核心特性

  • shared_ptr 支持拷贝构造与移动语义:拷贝shared_ptr时,引用计数原子 + 1 ;析构 / 重置shared_ptr时,引用计数原子 - 1;当计数归 0 时,自动调用删除器释放对象。
  • 当最后一个指向该对象的 shared_ptr 被销毁(即引用计数降为 0)时,对象才会被自动删除。
  • 引用计数的增减操作是原子性的,因此多线程环境下拷贝和销毁 shared_ptr 是线程安全的。但需要注意,它管理的对象本身并不是线程安全的,并发读写仍需加锁。
复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() 
{
    // 创建 shared_ptr
    shared_ptr<int> p1 = make_shared<int>(100);
    cout << "引用计数:" << p1.use_count() << endl; // 1

    // 拷贝,计数+1
    shared_ptr<int> p2 = p1;
    cout << "引用计数:" << p1.use_count() << endl; // 2

    // 所有指针离开作用域,计数归0,自动释放内存
    return 0;
}

5.2 基本使用

复制代码
#include <iostream>   
#include <memory>
using namespace std;

// 创建 shared_ptr
void test1()
{
	//管理单个对象
	shared_ptr<int> sp1(new int);
	*sp1 = 10;

	//管理动态数组
	shared_ptr<int[]> sp2(new int[10]);
	sp2[0] = 1;
}

// 常用成员函数的使用
void test2()
{
	shared_ptr<int> sp1; // 默认构造,空指针

	//从unique_ptr转换构造:从unique_ptr隐式转换为shared_ptr,unique_ptr自动置空,shared_ptr接管对象所有权和删除器
	unique_ptr<int> up(new int(6));
	shared_ptr<int> sp2(std::move(up));

	//get():返回裸指针(仅用于与C接口交互)
	int* raw = sp1.get();	

	//reset():重置指针,释放当前管理的对象(引用计数-1,归0则释放),并可选择接管新对象
	sp1.reset();//无参:释放当前对象,sp1置空
	sp2.reset(new int(8));//带参:释放旧对象,接管新对象

	//use_count():获取引用计数
	cout << sp1.use_count() << endl;

	//swap():交换两个shared_ptr管理的对象和控制块
	sp1.swap(sp2);

	//operator bool():判断shared_ptr是否管理有效对象(非空),支持隐式转换,常用于 if 条件判断
	if (sp1)
	{
		cout << "sp1管理的指针不为空" << endl;
	}
}

int main()
{
	//test1();
	test2();
	return 0;
}

5.3 模拟实现

简单实现:

复制代码
//模拟实现shared_ptr
template<class T>
class MySharedPtr
{
public:
	//构造函数 
	explicit MySharedPtr(T* ptr = nullptr) //使用explicit防止隐式类型转换
		:_ptr(ptr)
		, _refCount(ptr ? new int(1) : nullptr) //指针为空,计数也为空
	{}

	//拷贝构造  核心:引用计数+1
	MySharedPtr(const MySharedPtr& other) 
		:_ptr(other._ptr)
		, _refCount(other._refCount)
	{
		if (_refCount)
			++(*_refCount);
	}

	//析构函数
	~MySharedPtr() 
	{
		release();
	}

	//拷贝赋值
	MySharedPtr& operator=(const MySharedPtr& other)
	{
		//if (this != &other)
		if (_ptr != other._ptr) //更好的判断方式 两个不同的智能指针对象可能指向同一块内存,防止做无用功
		{
			//释放旧资源
			release();

			// 共享对方的资源
			_ptr = other._ptr;
			_refCount = other._refCount;

			// 引用计数+1
			if (_refCount)
				++(*_refCount);
		}
		return *this;
	}

	//移动构造
	MySharedPtr(MySharedPtr&& other) noexcept
		:_ptr(other._ptr)
		, _refCount(other._refCount)
	{
		other._ptr = nullptr;
		other._refCount = nullptr;
	}

	//移动赋值
	MySharedPtr& operator=(MySharedPtr&& other) noexcept
	{
		//if (this != &other)
		if (_ptr != other._ptr)
		{
			release();

			_ptr = other._ptr;
			_refCount = other._refCount;

			other._ptr = nullptr;
			other._refCount = nullptr;
		}
		return *this;
	}

	//获取原始指针
	T* get() const { return _ptr; }

	//获取引用计数
	int use_count() const { return _refCount ? *_refCount : 0; }

	//重置指针
	void reset(T* p = nullptr)
	{
		release();

		_ptr = p;
		_refCount = p ? new int(1) : nullptr;
	}

	//访问操作符重载
	T& operator*() const { return *_ptr; }
	T* operator->() const { return _ptr; }

	//显式转换运算符
	explicit operator bool()const
	{
		return _ptr != nullptr;
	}

private:
	//辅助函数
	//释放资源
	void release()
	{
		if (_refCount)//先判空,否则指针解引用会崩溃
		{
			if (--(*_refCount) == 0)
			{
				delete(_ptr);
				delete _refCount;
			}
		}
		_ptr = nullptr;
		_refCount = nullptr;
	}


	T* _ptr; //指向管理的资源
	int* _refCount; //引用计数
};

带自定义删除器:

复制代码
//模拟实现shared_ptr(带自定义删除器)
template<class T>
class MySharedPtr
{
public:
	//构造函数
	explicit MySharedPtr(T* ptr = nullptr) //即使不传删除器,也会调用其默认构造
		:_ptr(ptr)
		, _refCount(ptr ? new int(1) : nullptr)//指针为空,计数也为空
	{}

	//带自定义删除器的构造函数
	//函数模版,这里接收一个删除器模板参数,自动推导D的类型
	template<class D>
	explicit MySharedPtr(T* ptr, D del)
		:_ptr(ptr)
		, _refCount(ptr ? new int(1) : nullptr)
		, _del(del)
	{}

	//拷贝构造  
	MySharedPtr(const MySharedPtr& other)
		:_ptr(other._ptr)
		, _refCount(other._refCount)
		, _del(other._del)
	{
		if (_refCount)
			++(*_refCount);
	}

	//析构函数
	~MySharedPtr()
	{
		release();
	}

	//拷贝赋值
	MySharedPtr& operator=(const MySharedPtr& other)
	{
		if (_ptr != other._ptr)
		{
			//释放旧资源
			release();

			_ptr = other._ptr;
			_refCount = other._refCount;
			_del = other._del;

			if (_refCount)
				++(*_refCount);
		}
		return *this;
	}

	//移动构造
	MySharedPtr(MySharedPtr&& other) noexcept
		:_ptr(other._ptr)
		, _refCount(other._refCount)
		, _del(std::move(other._del))//移动删除器
	{
		other._ptr = nullptr;
		other._refCount = nullptr;
		//other._del保持默认或原样即可,反正它不再管理资源
	}

	//移动赋值
	MySharedPtr& operator=(MySharedPtr&& other) noexcept
	{
		if (_ptr != other._ptr)
		{
			release();

			_ptr = other._ptr;
			_refCount = other._refCount;
			_del = std::move(other._del);

			other._ptr = nullptr;
			other._refCount = nullptr;
		}
		return *this;
	}

	//获取原始指针
	T* get() const { return _ptr; }

	//获取引用计数
	int use_count() const { return _refCount ? *_refCount : 0; }

	//重置指针
	void reset(T* p = nullptr)
	{
		release();

		_ptr = p;
		_refCount = p ? new int(1) : nullptr;
	}

	//访问操作符重载
	T& operator*() const
	{
		return *_ptr;
	}
	T* operator->() const
	{
		return _ptr;
	}

	//显式转换运算符
	explicit operator bool()const
	{
		return _ptr != nullptr;
	}

private:
	//辅助函数
	//释放资源
	void release()
	{
		if (_refCount)// 先判断指针是否为空,避免空指针解引用;
		{
			if (--(*_refCount) == 0)
			{
				_del(_ptr); //调用删除器
				delete _refCount;
			}
		}
		_ptr = nullptr;
		_refCount = nullptr;
	}

	T* _ptr; //指向管理的资源
	int* _refCount; //引用计数
	function<void(T*)> _del = [](T* ptr) {delete ptr; };//删除器
};

6. operator bool()

operator bool() 是 C++ 中的类型转换运算符 ,作用是:让一个对象可以直接用在 ifwhile 等布尔判断的位置,自动转换成 truefalse

std::unique_ptrstd::shared_ptr 都重载了 operator bool() ,这使得它们可以像普通指针一样,直接在 ifwhile 等条件语句中使用,极大地简化了代码,提高了可读性。

模拟实现

复制代码
template <typename T>
class shared_ptr 
{
    T* ptr; // 内部管理的原始指针
            // ...引用计数等其他成员

public:
    // 重载 operator bool()
    explicit operator bool() const noexcept {
        return ptr != nullptr; // 检查内部指针是否为空
    }
    
    // ... 其他成员函数
};

使用

当写下 if (ptr) 时,编译器会自动调用这个 operator bool() 函数。如果内部的原始指针 ptr 不为 nullptr,函数返回 trueif 条件成立;反之则返回 false

复制代码
shared_ptr<int> ptr = make_shared<int>(10);

if (ptr)  // 这里会自动调用 ptr.operator bool()
{  
	// ...
}

explicit 关键字 防止了智能指针被隐式地转换为 bool 或其他整数类型,保证了类型安全。

复制代码
shared_ptr<int> ptr = make_shared<int>(5);
// bool m = ptr; // 编译错误!禁止隐式转换

7. 删除器

7.1 默认删除器

C++ 标准库为智能指针提供了默认删除器std::default_delete<T>,核心行为分为两类:

  • 普通版本std::default_delete<T>:内部调用delete释放单个对象,适用于unique_ptr<T>/shared_ptr<T>
  • 数组特化版本std::default_delete<T[]>:内部调用delete[]释放动态数组,适用于unique_ptr<T[]>(C++11 起原生支持)、shared_ptr<T[]>(C++17 起原生支持)。
复制代码
// 主模版:用于普通对象(模拟 std::default_delete<T>)
template<class T>
struct DefaultDelete
{
    void operator()(T* ptr) const
    {
        if (ptr == nullptr) return; // 空指针防护,避免野指针释放
        cout << "[DefaultDelete] 普通对象:调用 delete" << endl;
        delete ptr; // 单个对象使用 delete 释放
    }
};

// 偏特化:针对数组对象(模拟 std::default_delete<T[]>)
template<class T>
struct DefaultDelete<T[]>
{
    void operator()(T* ptr) const
    {
        if (ptr == nullptr) return; // 空指针防护
        cout << "[DefaultDelete] 数组对象:调用 delete[]" << endl;
        delete[] ptr; // 数组对象使用 delete[] 释放
    }
};

7.2 为什么需要自定义删除器?

默认情况下,智能指针释放资源只会调用 delete/delete[ ],但在很多场景下资源不一定是new出来的,比如:

  • FILE* 文件句柄 → 要用 fclose
  • malloc 内存 → 要用 free

这时候默认删除器就不管用了,如果只用默认删除器就会导致资源泄漏或未定义行为。
自定义删除器作用:智能指针析构时,不再执行默认 delete,转而执行自定义的释放逻辑,安全管理所有非标资源。

7.3 unique_ptr 与 shared_ptr 删除器的设计差异

unique_ptr 删除器

unique_ptr:删除器是**"模板参数"** (指针类型的一部分

模版定义:

复制代码
template <class T,class Deleter = std::default_delete<T>>
class unique_ptr;

unique_ptr 的设计追求零开销,所以它把删除器作为类型的一部分。

  • 语法 :std::unique_ptr<管理类型, 删除器类型> 指针名(资源,删除器对象);
  • 特点:删除器的类型直接写在尖括号里,参与类型定义。
  • 类型影响: 删除器类型直接决定unique_ptr的最终类型,只要TDeleter有一个不同,就是不同类型,无法互相赋值、存入同一个容器。
  • 零开销设计(EBO 空基类优化)unique_ptr会根据删除器是否为无状态空类 ,自动选择最优存储策略:
    • 无状态空类删除器(空仿函数、无捕获 Lambda):通过私有继承删除器触发空基类优化(EBO) ,编译器不会为空类分配额外空间,最终sizeof(unique_ptr) = 裸指针大小,完全零开销。
    • 有状态删除器(函数指针、带捕获 Lambda、带成员变量的仿函数):EBO 优化失效,删除器作为成员变量存储,最终sizeof(unique_ptr) = 裸指针大小 + 删除器实例大小。
  • 构造规则 :是否需要手动传入删除器对象,完全取决于删除器类型是否支持默认构造:
    • 仿函数空类 :支持默认构造→ 可以不传入删除器对象
    • Lambda、函数指针 :不支持默认构造→ 必须手动传入删除器实例

注:如果一个类继承自一个"空类",编译器可以不为这个基类分配内存空间,这叫空基类优化。(了解)

内存布局

shared_ptr 删除器

shared_ptr:删除器是"构造函数参数"(不参与指针类型定义

模版定义:

复制代码
template <class T> class shared_ptr;

shared_ptr 将删除器存储在堆上的控制块中。

  • 语法 :std::shared_ptr<管理类型> 指针名(资源, 删除器对象);
  • 特点:删除器作为第二个参数传给构造函数,不参与类型定义。
  • 类型影响: 删除器仅在构造时传入,不影响shared_ptr<T>的主类型。无论删除器是函数指针、Lambda 还是仿函数,只要管理的对象类型T一致,就是完全相同的类型,可互相赋值、存入同一个容器。
  • 控制块存储shared_ptr本身固定为2 个指针大小(64 位系统 16 字节),一个指向资源,一个指向堆上的控制块;删除器、强引用计数、弱引用计数、分配器等元数据,全部存储在控制块中。
  • 构造规则: 仅需在构造智能指针时传入删除器实例即可。

内存布局:

7.4 自定义删除器的实现及其使用

我们可以用函数指针、仿函数或Lambda表达式来实现删除器。

Lambda表达式删除器

复制代码
int main()
{
	// 自定义Lambda删除器
	auto my_deleter = [](int* p) {
		cout << "Lambda 删除器:释放int对象" << endl;
		delete p;
		};
	
	//使用 decltype 推导类型
	//unique_ptr<int, decltype(my_deleter)> ptr(new int); //错误写法! Lambda类型没有默认构造函数,必须手动把Lambda对象传给构造函数
	unique_ptr<int, decltype(my_deleter)> ptr(new int, my_deleter); //正确写法 构造时传入Lambda对象

	shared_ptr<int> sp1(new int, my_deleter);
	shared_ptr<int> sp2(new int, [](int* p) {delete p; }); //直接内联Lambda

	return 0;
}

仿函数删除器

复制代码
// 仿函数删除器
struct FileDeleter
{
	void operator()(FILE* fp)const
	{
		if (fp)
		{
			fclose(fp);
			cout << "仿函数删除器:关闭文件\n";
		}
	}
};

int main()
{
	unique_ptr<FILE, FileDeleter> up1(fopen("test.txt", "w"));//空仿函数有默认构造,可以不传删除器
	unique_ptr<FILE, FileDeleter> up2(fopen("test.txt", "w"), FileDeleter());//手动传临时仿函数对象

	shared_ptr<FILE> sp(fopen("test.txt", "w"), FileDeleter());

	return 0;
}

函数指针删除器

复制代码
// 函数指针删除器
void free_malloc(void* p)
{
	cout << "函数指针删除器:释放malloc申请的内存" << endl;
	free(p);
}

int main()
{
	//unique_ptr<void, void (*)(void*)> up1(malloc(100)); //错误写法! 函数指针没有默认构造,必须传删除器
	unique_ptr<void, void(*)(void*)> up2(malloc(100), free_malloc);//正确写法!必须传入函数地址
	unique_ptr<void, decltype(&free_malloc)> up3(malloc(100), free_malloc);//正确写法!使用decltype自动推导类型(更简洁)

	shared_ptr<void> sp(malloc(100), free_malloc);

	return 0;
}

7.5 小结

特性 unique_ptr shared_ptr
删除器位置 模板参数 <T, Del> 构造函数参数 (ptr, del)
类型影响 删除器不同,类型就不同(UP<T, D1>UP<T, D2> 是不同类型)。 删除器不同,类型依然相同(都是 shared_ptr<T>)。
内存开销 无状态删除器零开销;有状态额外占用 固定 2 个指针大小,删除器额外占用控制块
主要用途 独占资源管理,追求极致性能。 共享资源管理,需要灵活指定不同的销毁策略。

实战建议 :尽量使用无状态的仿函数 作为 unique_ptr 的删除器,这样既保持了代码整洁,又确保了零内存开销。对于 shared_ptr,Lambda 是最方便的写法。

8. make_unique 和 make_shared

8.1 基础定义与引入时间

std::make_uniquestd::make_shared 是 C++ 标准库提供的创建智能指针的推荐方式 ,核心目标是更安全、更高效地创建智能指针对象 。它们通过 "一次性分配内存" 和 "隐藏裸指针" 的设计,从根源解决了直接使用 new 构造智能指针带来的安全隐患与性能问题。

函数 引入标准 头文件 核心功能
std::make_shared<T> C++11 <memory> 安全、高效地创建 std::shared_ptr<T>
std::make_unique<T> C++14 <memory> 安全地创建 std::unique_ptr<T>(C++11 遗漏,C++14 补全)

8.2 基本用法

语法:std::make_unique<T>(构造参数...),参数完美转发给 T 的构造函数。

复制代码
int main()
{
	// 1. 管理单个对象
	auto up1 = make_unique<int>(10); // 等价于 unique_ptr<int>(new int(10))
	auto up2 = make_unique<std::string>("hello"); 

	// 2. 管理动态数组(C++14+)
	auto up3 = make_unique<int[]>(10); // 等价于 unique_ptr<int[]>(new int[10])
	up3[0] = 100; // 支持operator[]下标访问

	return 0;
}

语法:std::make_shared<T>(构造参数...),括号内的参数会完美转发给 T 的构造函数。

复制代码
int main()
{
	// 1. 管理单个对象
	auto sp1 = make_shared<int>(10); // 等价于 shared_ptr<int>(new int(10))
	auto sp2 = make_shared<std::string>("world");

	// 2. 管理动态数组(注意C++20 起)
	auto sp3 = make_shared<int[]>(10); // 自动匹配 delete[] 释放
	sp3[0] = 200; //支持operator[]下标访问

	return 0;
}

8.3 为什么推荐使用它们?

make_unique与make_shared可从根源上杜绝裸指针误用、保证异常安全,其中make_shared还额外具备内存分配的性能优势,是现代 C++ 创建智能指针的首选方案。

(1)杜绝裸指针误用

直接用 new 创建智能指针时,裸指针会短暂暴露在代码中,可能被误操作,导致:

  • 手动 delete 裸指针,智能指针析构时触发双重释放,程序直接崩溃;
  • 裸指针被多个主体管理,所有权彻底混乱。

代码示例:双重释放(运行时崩溃)

复制代码
class MyClass 
{
public:
	MyClass() { std::cout << "MyClass 构造\n"; }
	~MyClass() { std::cout << "MyClass 析构\n"; }
};

int main() 
{
	// 手动 new,拿到裸指针
	MyClass* p = new MyClass();

	// 把裸指针传给 shared_ptr,完成所有权接管
	std::shared_ptr<MyClass> sp(p);
	
	delete p;// 致命错误:手动delete裸指针,此时p已经被shared_ptr接管
			 // 导致后续shared_ptr离开作用域,再次析构同一块内存,双重释放,程序直接崩溃(触发未定义行为)

	return 0;
}

安全写法

复制代码
// ✅ 安全写法:使用make_shared,全程不暴露裸指针
auto sp = std::make_shared<MyClass>();

(2)保证异常安全(最重要的原因)

这是 C++ 标准推荐使用make_xxx函数的主要原因,它彻底解决了直接使用new构造智能指针时的内存泄漏隐患。

C++ 标准未规定函数参数的求值顺序 ,编译器可以自由决定先计算哪个参数的值。直接用 new 构造智能指针时,"new 分配内存" 和 "智能指针接管所有权" 是两个独立步骤,中间可能被异常打断,导致内存泄漏。

代码示例(异常安全隐患导致内存泄漏)

复制代码
// 业务函数
void process(std::shared_ptr<int> sp, int param) 
{
	if (param < 0)
		throw std::runtime_error("参数非法");
}

// 一个可能抛出异常的函数
int risky_func() 
{
	throw std::runtime_error("函数执行失败");
	return 0;
}

int main() 
{
	try {
		// ❌ 危险写法:直接用new构造智能指针
		//编译器的执行顺序可能是: 1.new int(42) -> 2.调用risky_func() -> 3.构造shared_ptr
		//如果第 2 步抛异常,程序直接跳转至异常处理逻辑,shared_ptr还没构造出来,没人接管new出来的裸指针,内存就泄漏了!
		process(std::shared_ptr<int>(new int(42)), risky_func());
	}
	catch (...) {}
	return 0;
}

安全写法

复制代码
// ✅ 安全写法:用 make_shared 创建
process(std::make_shared<int>(42), risky_func());

此时,无论编译器先计算哪个参数,都不会发生内存泄漏:

  • 如果先执行 make_shared:函数内部一次性完成**「new 内存」和「构造智能指针」** ,返回一个完整的 shared_ptr 。哪怕后续 risky_func() 抛异常,这个临时智能指针会被自动析构,内存被正确释放,不会泄漏。
  • 如果先执行 risky_func():抛异常后,make_shared 根本不会执行,没有内存分配,自然也不会泄漏。

底层核心原理

复制代码
//简易版 make_unique 实现(仅单个对象)
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) 
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

make_xxx 把「new 内存」和「构造智能指针」封装在函数内部, 相当于 "绑定成了一步",中间不会插入其它代码,从根源上解决了所有场景的异常安全问题。

(3)make_shared 独有优势:一次性内存分配,性能提升

make_shared 除了解决异常安全问题,还额外优化了「对象 + 控制块」的内存分配 ,效率更高,是创建 shared_ptr 的首选方式。

shared_ptr 的底层依赖控制块 (存储引用计数、弱引用计数、删除器等),手动写法和 make_shared 的内存分配差异巨大:

  • shared_ptr<T>(new T):需要两次独立的内存分配。
    1. 第一次 new T:分配对象本身的内存
    2. 第二次 shared_ptr 内部:分配控制块的内存
  • make_shared<T>():仅需**一次内存分配,**一次性分配一块连续内存,同时容纳对象本身和控制块。

优势:

  1. 性能更高减少一次 new/delete 调用, 降低内存分配开销,同时减少堆内存碎片。
  2. 缓存更友好 :对象和控制块在连续内存中,CPU 只需加载一次缓存行,即可同时访问对象和引用计数,大幅提升缓存命中率。

8.4 注意事项与局限性

1. 无法使用自定义删除器

make_uniquemake_shared不支持传入自定义删除器。如果需要自定义释放逻辑,必须放弃工厂函数,手动使用「new + 智能指针构造函数」。

2. make_shared 内存延迟释放问题

这是make_shared 性能优化带来的"副作用",在特定场景下需要注意:

由于 make_shared 将对象和控制块分配在同一块内存上 ,即使强引用计数归 0(对象已析构) ,只要还有 std::weak_ptr 观察(弱引用计数不为 0),整块内存就无法完全释放,只有当强、弱引用计数全部归 0 时,整块内存才会被释放,导致 "对象已析构但内存未归还" 的伪内存泄漏现象。

此类场景避免使用make_shared,直接用裸指针构造shared_ptr,让对象和控制块分开分配,对象析构后可立即释放对象内存,仅保留控制块内存。

8.5 最佳实践

优先使用 make_unique 和 make_shared, 除非有特殊需求(如自定义删除器、处理 make_shared 的控制块延迟问题),否则永远优先用它们创建智能指针。

9. shared_ptr 循环引用问题

std::shared_ptr 通过强引用计数 管理对象生命周期,只有当强引用计数归 0 时,才会析构对象并释放内存。 但当两个或多个shared_ptr 管理的对象,互相持有对方的 shared_ptr 成员 ,形成一个 "引用闭环 ",每个对象的强引用计数都至少为 1,永远无法归 0,导致对象永远不会被释放,内存泄漏

代码示例(循环引用导致内存泄漏):

复制代码
//双向链表节点类
class ListNode
{
public:
	int _data;
	// ❌ 致命设计:next 和 prev 都用 shared_ptr,会形成循环引用
	std::shared_ptr<ListNode> _next; // 强引用:持有下一个节点,增加其强引用计数
	std::shared_ptr<ListNode> _prev; // 强引用:持有上一个节点,增加其强引用计数

	ListNode(int data = 0)
		: _data(data)
	{
		//打印,方便追踪具体节点的构造
		cout << "ListNode: " << _data << " 构造" << endl;
	}

	~ListNode()
	{
		//打印,方便追踪具体节点的析构
		cout << "ListNode: " << _data << " 析构" << endl;
	}	
};

int main()
{
	// 创建两个节点,shared_ptr变量 n1 和 n2 分别持有节点的所有权
	std::shared_ptr<ListNode> n1(new ListNode(10));
	std::shared_ptr<ListNode> n2(new ListNode(20));

	// 构建双向链表,形成循环引用
	n1->_next = n2; // n1 的 _next 强引用 n2 → n2 的强引用计数 +1(变为 2)
	n2->_prev = n1; // n2 的 _prev 强引用 n1 → n1 的强引用计数 +1(变为 2)

	cout << n1.use_count() << endl; // 输出 2(n1自身 + n2->prev)
	cout << n2.use_count() << endl; // 输出 2(n2自身 + n1->next)

	return 0;
}
// 离开main作用域后的释放流程:   
// 1. n2 析构:
//    - n2 释放对节点 20 的所有权 → 节点 20 的强引用计数 -1 → 变为 1(仍被 n1->_next 持有)
//    - 节点 20 暂不析构//
// 2. n1 析构:
//    - n1 释放对节点 10 的所有权 → 节点 10 的强引用计数 -1 → 变为 1(仍被 n2->_prev 持有)
//    - 节点 10 暂不析构
//
// 最终结果:节点 10 和节点 20 互相持有对方的强引用,强引用计数永远为 1,
//           两个节点都无法被析构,内存彻底泄漏!

运行结果: 可以看到,两个节点的析构函数从未被调用,内存彻底泄漏!

循环引用内存布局图解:

10. weak_ptr(弱引用智能指针)

std::weak_ptr 是 C++ 标准专为辅助 shared_ptr 设计的弱引用智能指针 ,主要目的是**解决 shared_ptr 可能导致的循环引用问题,**核心定位是「观察对象,而非拥有对象」。

10.1 核心特性

  • 不增加强引用计数 :仅 "观察 " 对象,不影响 shared_ptr 管理的对象的生命周期,这是打破循环的核心;
  • 无对象所有权 :无法直接解引用(*wp)、箭头访问(wp->)对象,必须通过 lock() 方法获取有效的 shared_ptr 才能访问;
  • 共享控制块 :与关联的 shared_ptr 共享同一个控制块,可安全检测对象是否已被释放;
  • 线程安全:所有状态检查与转换操作均为原子操作,适配多线程场景。

10.2 用 weak_ptr 打破循环引用

我们将其中一个方向的引用(比如 prev)从 shared_ptr 改为 weak_ptr,即可彻底打破循环:

复制代码
//双向链表节点类
class ListNode
{
public:
	int _data;
	// ✅ 修正设计:_prev用 weak_ptr(仅观察)
	std::shared_ptr<ListNode> _next;//强引用:持有下一个节点的所有权,增加其强引用计数
	std::weak_ptr<ListNode> _prev;  //弱引用:仅观察前驱节点,不增加其强引用计数

	ListNode(int data = 0)
		: _data(data)
	{
		cout << "ListNode " << " 构造" << endl;
	}

	~ListNode()
	{
		cout << "ListNode " << " 析构" << endl;
	}
};

int main()
{
	// 创建两个节点,shared_ptr变量 n1 和 n2 分别持有节点的所有权
	std::shared_ptr<ListNode> n1(new ListNode(10));
	std::shared_ptr<ListNode> n2(new ListNode(20));

	// 构建双向链表关系
	n1->_next = n2; // n1 的 _next 强引用 n2 → n2 的强引用计数 +1(变为 2)
	n2->_prev = n1; // n2 的 _prev 弱引用 n1 → n1 的强引用计数不变(仍为 1)

	//打印当前强引用计数
	cout << n1.use_count() << endl; // 输出 1(仅n1持有)
	cout << n2.use_count() << endl; // 输出 2(n2 + n1->next)

	return 0;
}
// 离开main作用域后的释放流程:
// 1. n2 析构:
//	  - n2 释放对节点 20 的所有权 → 节点 20 的强引用计数 - 1 → 变为 1(仍被 n1->_next 持有)
//    - 此时节点 20 暂不析构
// 2. n1 析构:
//    - n1 释放对节点 10 的所有权 → 节点 10 的强引用计数 -1 → 变为 0
//    - 节点 10 开始析构,其成员 _next(shared_ptr)也随之析构
//    - _next 析构 → 释放对节点 20 的所有权 → 节点 20 的强引用计数 -1 → 变为 0
//    - 节点 20 析构
//
// 最终结果:节点 10 和节点 20 均被正确析构,无内存泄漏!

运行结果: 可以看到两个节点的析构函数都被正确调用,内存泄漏问题彻底解决。

修复后内存布局图解:

10.3 常用方法

操作 功能 说明
weak_ptr<T> wp(sp) 构造 shared_ptr 构造 weak_ptr,不增加强引用计数
wp = sp 赋值 shared_ptr 赋值给 weak_ptr,不增加强引用计数
wp.expired() 判断过期 检测观察的对象是否已释放 (等价于 use_count() == 0),返回 true 表示对象已析构
wp.lock() 获取对象 获取观察对象的 shared_ptr,若对象存在,返回有效的 shared_ptr;否则返回空
wp.reset() 重置 清空 weak_ptr,停止观察,不影响对象生命周期

lock() 函数

lock()weak_ptr 唯一能安全访问对象的方式

weak_ptr 不能直接解引用(*wpwp->),必须通过 lock() 获取 shared_ptr 后才能访问,且访问前必须判空,避免对象已释放。使用 lock() 访问对象必须遵循**"先转换,再判空,后访问"**的三步流程,缺一不可:

  1. 调用 wp.lock() 获取 shared_ptr
  2. 检查返回的 shared_ptr 是否为空(if (sp)if (sp != nullptr));
  3. 只有判空通过后,才能通过 shared_ptr 访问对象。

函数行为

lock函数会检查weak_ptr指向的对象是否存活(即检查强引用计数),如果>0,对象存活,它会立刻将引用计数+1 ,并返回一个有效的shared_ptr,如果=0,对象死了,返回一个空的shared_ptr。

基本使用:

复制代码
int main()
{
	weak_ptr<int> wp;

	{
		auto sp = make_shared<int>(40);
		wp = sp; // weak_ptr观察sp

		//对象存在时lock()返回有效shared_ptr
		if (shared_ptr<int> temp = wp.lock())
		{
			cout << "对象存在,值为: " << *temp << endl;
		}
	}//sp析构,对象释放

	//对象释放后,lock()返回空的shared_ptr 
	if (shared_ptr<int> temp = wp.lock()) //先调用lock()赋值给sp,然后if(...)会判断temp是否为空
	{
		cout << "对象存在,值为: " << *temp << endl;
	}
	else
	{
		cout << "对象已释放" << endl;
	}

	return 0;
}

expired() 函数

expired() 的唯一作用是快速判断对象是否已释放 ,它等价于 wp.use_count() == 0,但更高效,返回 true 表示对象已销毁,返回false,表示对象可能还存活。

如果只是想判断对象是否存活,建议使用 wp.expired(),如果既要判断存活又要安全访问对象,直接使用 wp.lock()

10.4 模拟实现

复制代码
//weak_ptr模拟实现
template<class T>
class weak_ptr
{
public:
	//从shared_ptr构造
	weak_ptr(const shared_ptr<T>& sp)
	{
		cb = sp.cb;
		if (cb)
			cb->weak_count++;
	}
	//拷贝构造
	weak_ptr(const weak_ptr<T>& wp)
	{
		cb = wp.cb;
		if (cb)
			cb->weak_count++;
	}
	//析构函数
	~weak_ptr()
	{
		if (--cb->weak_count == 0 && cb->strong_count == 0)
			delete cb;
	}

	shared_ptr<T> lock()const
	{
		if (!cb || cb->strong_count == 0)
			return shared_ptr<T>();//返回空shared_ptr

		shared_ptr<T> sp;
		sp.ptr = cb->ptr;
		sp.cb = cb;
		cb->strong_count++; //引用计数+1
		return sp; //返回有效shared_ptr
	}

private:
	control_block<T>* cb; //控制块指针
};

11. 总结

选择原则

  1. 优先用 unique_ptr :若对象不需要共享所有权,unique_ptr 零开销、更安全,是默认选择。
  2. 仅在需要共享时用 shared_ptr :当对象有多个明确的所有者时,才使用 shared_ptr,接受其固定开销。
  3. weak_ptr 打破循环引用shared_ptr 循环引用会导致内存泄漏,需用 weak_ptr(弱引用,不增加计数)解决。

避坑点

  • 禁止用同一个裸指针初始化多个智能指针:会导致双重释放。
  • 禁止 delete get()/release() 以外的裸指针get() 返回的裸指针仅用于接口交互,不要手动释放。
  • unique_ptr 转移所有权后不要访问原指针move() 后原 unique_ptr 置空,访问会导致未定义行为。
相关推荐
kyriewen111 小时前
Next.js部署:从本地跑得欢,到线上飞得稳
开发语言·前端·javascript·科技·react.js·前端框架·ecmascript
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题】【Java基础篇】第21题:HashMap和Hashtable的区别是什么
java·开发语言·面试·哈希算法·散列表·hash table
不想写代码的星星1 小时前
COW(Copy-on-Write):开抄开抄,哎嘿,我装的
开发语言·c++
慕容卡卡1 小时前
Claude 使用神器(web页面)--CloudCLI UI
java·开发语言·前端·人工智能·ui·spring cloud
咬_咬1 小时前
go语言学习(函数)
开发语言·学习·golang
Sylvia-girl1 小时前
C++内存如何管理?
java·jvm·c++
froginwe111 小时前
PHP MySQL Delete 操作指南
开发语言
凯瑟琳.奥古斯特1 小时前
图论核心考点精讲
开发语言·数据结构·算法·排序算法·哈希算法
charlie1145141912 小时前
嵌入式Linux驱动开发(8)——内存映射 I/O - 别拿物理地址当指针用
linux·开发语言·驱动开发·c·imx6ull