C++智能指针(后)

前面一节我们已经大概智能指针是什么东西了,但是可能大家还是会有点懵,我也觉得说的还没很全面,这节就作为上一节的补充继续讲讲。

回顾

我们先回顾一下智能指针是啥?

首先,智能指针的出现主要是为了能更好的管理我们手动申请的内存空间。因为不管是我们人为的忘记释放,亦或者是程序设计不当((内存空间还没释放之前就return返回了)或者(内存空间还没释放,前方的代码就抛异常了,跳过了释放内存空间的步骤))。

这些行为都会导致内存泄露的问题,除此之外还会有悬空指针的问题存在,这些问题我们在上一节已经说的比较详细了,这一节我们主要从智能指针的发展历史来逐步学习智能指针。

std::auto_ptr

在开始之前,有一个极其重要的警告必须放在最前面:

std::auto_ptr 已在 C++11 中被弃用 (deprecated),并在 C++17 中从标准中彻底移除。绝对不应该在新代码中使用它。

学习 std::auto_ptr 的主要目的,是理解它的设计缺陷、为什么它会被淘汰,以及如何将遗留代码中的 auto_ptr 正确地迁移到现代智能指针(主要是 std::unique_ptr)。这是一个了解C++语言演进和设计哲学的反面教材。


第一部分:std::auto_ptr 是什么?它当初要解决什么问题?

std::auto_ptr 是 C++98 标准中引入的第一个尝试实现“自动资源管理”的智能指针。它的核心思想与后来的智能指针一致:基于 RAII 原则,在析构函数中自动 delete 其管理的指针,以解决原生指针导致的内存泄漏问题。

一个简单的例子:

#include <iostream>
#include <memory> // 在C++98中,auto_ptr定义在<memory>中

class MyClass {
public:
    MyClass() { std::cout << "Constructed\n"; }
    ~MyClass() { std::cout << "Destroyed\n"; }
    void doSomething() { std::cout << "Doing something\n"; }
};

int main() {
    std::cout << "Start of main\n";
    {
        std::auto_ptr<MyClass> myPtr(new MyClass());
        myPtr->doSomething();
    } // myPtr 离开作用域,其析构函数会自动调用 delete
    std::cout << "End of main\n";
    return 0;
}

输出:

Start of main
Constructed
Doing something
Destroyed
End of main

从上面的例子看,它似乎完美地解决了问题。那么,它的问题到底出在哪里呢?


第二部分:std::auto_ptr 的致命设计缺陷

std::auto_ptr 的核心缺陷在于其**“复制”语义**。为了模拟“独占所有权”,它的拷贝构造函数和拷贝赋值运算符进行的不是真正的复制,而是所有权的转移

缺陷示例1:令人困惑的所有权转移
#include <iostream>
#include <memory>

int main() {
    std::auto_ptr<int> ap1(new int(42));
    std::cout << "ap1 points to: " << *ap1 << "\n"; // 输出 42

    // “复制”构造函数:所有权从 ap1 转移到 ap2
    std::auto_ptr<int> ap2(ap1);

    // 此时,ap1 已经变为空指针!
    // 这样会导致ap1变成了悬空指针。如果再次访问ap1就会报错。
    std::cout << "ap1 is now: " << (ap1.get() ? "not null" : "null") << "\n"; // 输出 null
    if (ap1.get() == nullptr) {
        std::cout << "ap1 has lost ownership!\n";
    }

    // ap2 现在拥有那个整数
    std::cout << "ap2 points to: " << *ap2 << "\n"; // 输出 42

    return 0;
}

发生了什么?
std::auto_ptr<int> ap2(ap1); 这行代码看起来像是在复制,但实际上它调用了 auto_ptr 的拷贝构造函数,这个构造函数的实现类似于:

auto_ptr(auto_ptr& other) { // 注意:参数是非常量引用
    ptr = other.ptr;   // 偷走指针
    other.ptr = nullptr; // 并将源指针置空
}

这种隐性的、不可控的所有权转移是反直觉和极其危险的根源。

缺陷示例2:在STL容器中使用会导致灾难

STL容器(如 std::vector, std::map)在其内部操作(如排序、插入、重新分配内存)中,严重依赖于元素的可复制且复制后等价的语义。auto_ptr 的“复制”行为会破坏容器的不变量。

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

int main() {
    std::vector<std::auto_ptr<int>> vec;

    vec.push_back(std::auto_ptr<int>(new int(1)));
    vec.push_back(std::auto_ptr<int>(new int(2)));
    vec.push_back(std::auto_ptr<int>(new int(3)));

    // 尝试对vector进行排序或任何可能导致内部复制的操作...
    // std::sort(vec.begin(), vec.end()); // 这行代码如果取消注释,会导致未定义行为!

    // 即使只是访问,也可能因为所有权不明而崩溃
    for (const auto& ptr : vec) { // C++11范围for循环
        // 但此时,你能确定vec里的每个auto_ptr还拥有对象吗?
        // 在之前的push_back过程中,可能已经发生了所有权转移
        // std::cout << *ptr << std::endl; // 极有可能在此处崩溃!
    }
    return 0;
}

为什么? push_back 操作可能会触发 vector 的扩容,需要将旧元素“复制”到新内存中。这个“复制”过程会转移旧内存中 auto_ptr 的所有权,导致旧位置的 auto_ptr 变为 nullptr。之后任何对它们的访问都会导致崩溃。


第三部分:为什么 std::unique_ptr 是完美的替代品?

std::unique_ptr 从设计上就根除了 auto_ptr 的所有问题:

  1. 明确的语义 (Explicit Semantics)

    • unique_ptr 禁止拷贝。任何拷贝的尝试都会导致编译错误,从根本上杜绝了隐性的所有权转移。
    • 所有权的转移必须通过 std::move() 显式地进行,这让代码的意图非常清晰。
    std::unique_ptr<int> up1(new int(42));
    // std::unique_ptr<int> up2 = up1; // 错误!编译不通过!
    std::unique_ptr<int> up2 = std::move(up1); // 正确!显式转移所有权,语义清晰
    
  2. 与STL容器完美兼容

    • 因为 unique_ptr 提供了移动构造函数移动赋值运算符,它可以安全地存放在容器中。容器在内部调整时使用的是移动语义,而非拷贝语义,这不会导致所有权意外丢失。
    std::vector<std::unique_ptr<int>> vec;
    vec.push_back(std::make_unique<int>(1)); // 成功
    vec.push_back(std::make_unique<int>(2)); // 成功,可能会移动之前的元素,但这是安全的
    auto another_vec = std::move(vec);       // 甚至可以移动整个vector!
    
  3. 更小的开销和更多的功能

    • 默认情况下,unique_ptrauto_ptr 一样,几乎没有空间开销。
    • 它支持自定义删除器,可以管理不是 new 分配的资源(如 malloc, 文件句柄等)。
    • 对数组有特化版本(std::unique_ptr<T[]>),会正确地调用 delete[]
特性std::auto_ptr (已废弃)std::unique_ptr (现代替代)
复制语义隐式所有权转移(极其危险)禁止拷贝(编译错误)
移动语义C++98 没有移动语义,用它模拟显式所有权转移std::move
STL兼容性不兼容(导致未定义行为)完全兼容(使用移动语义)
安全性低(运行时潜在崩溃)高(很多错误在编译时发现)
清晰度低(代码行为令人困惑)高(代码意图一目了然)

第四部分:简单实现

虽然它十分不堪,但是我们还是得知道它大概是什么样子的:

Smart.h

#pragma once
#include<iostream>
using namespace std;

template<class T>
class AutoPtr
{
private:
	T* _ptr;
public:
	//基本的智能指针的需求
	//自动管理资源
	AutoPtr(T* ptr)
		:_ptr(ptr)
	{}
	~AutoPtr()
	{
		if (_ptr != nullptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}

	//拷贝构造
	//这是auto_ptr有问题的地方
	AutoPtr(AutoPtr<T>& atp)
		:_ptr(atp._ptr)
	{
		//管理权转移
		atp._ptr = nullptr;
	}

	//赋值重载
	AutoPtr<T>& operator=(AutoPtr<T>& atp)
	{
		if (this != &atp)
		{
			//先释放当前对象中的资源
			if (_ptr != nullptr)
			{
				delete _ptr;
			}

			//然后将atp中的资源转移到当前对象中
			_ptr = atp._ptr;
			atp._ptr = nullptr;
		}
		return *this;
	}

	//指针的基本作用
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
};

std::unique_ptr

第一部分:核心思想与“为什么是它?”

std::unique_ptr 是一个智能指针,它独占(拥有)其所指对象的所有权。

  1. 独占所有权 (Exclusive Ownership)

    • 同一时间内,只有一个 unique_ptr 可以拥有一个给定的对象。
    • 当这个 unique_ptr 被销毁时(例如,离开作用域),它所拥有的对象也会被自动销毁。
  2. 移动语义 (Move Semantics)

    • unique_ptr 不能被复制(拷贝构造和拷贝赋值被禁用)。这保证了所有权的唯一性。
    • 所有权可以从一个 unique_ptr 转移(移动)到另一个。这是通过C++11的移动语义实现的。转移后,源 unique_ptr 变为 nullptr,目标 unique_ptr 获得资源。
  3. 为何取代 auto_ptr

    • auto_ptr 试图通过“拷贝即转移”的诡异语义来实现独占所有权,这在代码中极易引发误解和错误。
    • unique_ptr 通过禁止拷贝强制显式移动std::move)来明确表达所有权的转移,从语言层面杜绝了 auto_ptr 的隐患,并且与STL容器完美兼容。

第二部分:基本用法与代码示例

1. 创建与基本操作

首选方式:使用 std::make_unique (C++14引入)

#include <iostream>
#include <memory> // 必须包含的头文件

class MyClass {
public:
    MyClass(int value) : data(value) { std::cout << "Constructed: " << data << "\n"; }
    ~MyClass() { std::cout << "Destroyed: " << data << "\n"; }
    void print() const { std::cout << "Value: " << data << "\n"; }
    int data;
};

int main() {
    // 1. 创建 - 最推荐的方式,安全且高效
    auto uPtr1 = std::make_unique<MyClass>(42); // 创建一个MyClass对象,初始值为42
    uPtr1->print(); // 使用 -> 操作符访问成员
    (*uPtr1).print(); // 使用 * 操作符解引用

    // 2. 检查是否拥有对象 (since C++17 之前需用 uPtr1.get() != nullptr 判断)
    if (uPtr1) { // 重载了 bool 操作符,可直接在条件中判断
        std::cout << "uPtr1 owns an object.\n";
    }

    // 3. 释放所有权 (release)
    MyClass* rawPtr = uPtr1.release(); // uPtr1 放弃所有权,返回裸指针并将其置为空
    if (!uPtr1) {
        std::cout << "uPtr1 released ownership. Now it's null.\n";
    }
    // 注意:现在你必须手动管理 rawPtr!
    delete rawPtr;

    // 4. 重置 (reset): 销毁当前对象并接管新对象(或置空)
    auto uPtr2 = std::make_unique<MyClass>(100);
    uPtr2.reset(); // 销毁对象,uPtr2变为nullptr
    // uPtr2.reset(new MyClass(200)); // 销毁旧对象,并接管新分配的MyClass(200)

    // 5. 获取底层裸指针 (get): 当你需要传递指针但不转移所有权时使用
    auto uPtr3 = std::make_unique<MyClass>(300);
    someLegacyFunction(uPtr3.get()); // 函数签名: void someLegacyFunction(MyClass* ptr);

    return 0;
} // uPtr3 离开作用域,管理的对象(300)被自动销毁

输出:

Constructed: 42
Value: 42
Value: 42
uPtr1 owns an object.
uPtr1 released ownership. Now it's null.
Constructed: 100
Destroyed: 100
Constructed: 300
Destroyed: 300
2. 所有权的转移

这是 unique_ptr 的核心操作。

void ownershipTransfer() {
    std::cout << "--- Ownership Transfer ---\n";
    std::unique_ptr<MyClass> source = std::make_unique<MyClass>(999);

    // 错误!!拷贝构造是被禁止的
    // std::unique_ptr<MyClass> copy = source;

    // 正确:通过移动构造函数转移所有权
    std::unique_ptr<MyClass> destination = std::move(source);

    if (!source) {
        std::cout << "Source is now empty after move.\n";
    }
    if (destination) {
        std::cout << "Destination now owns the object: " << destination->data << "\n";
    }

    // 同样,移动赋值也可以
    source = std::move(destination); // 所有权又转移回 source
    // 此时 destination 变为空
}

输出:

Constructed: 999
Source is now empty after move.
Destination now owns the object: 999
3. 在函数中使用

unique_ptr 明确了函数参数的语义。

// 1.  sink 函数:接受所有权
//    按值传递。调用者必须使用 std::move,明确表示所有权被此函数夺取。
void takeOwnership(std::unique_ptr<MyClass> ptr) {
    std::cout << "Sink function took ownership of: " << ptr->data << "\n";
} // ptr 离开作用域,对象被销毁

// 2.  源函数:返回所有权
//     工厂函数的经典用法,将新创建的对象的所有权返回给调用者。
std::unique_ptr<MyClass> createObject(int value) {
    return std::make_unique<MyClass>(value); // 无需std::move,编译器会优化(RVO/NRVO)
}

// 3.  借用:不需要所有权,只需要访问
//     使用 const & 或裸指针(get()),避免不必要的所有权转移开销。
void useObject(const std::unique_ptr<MyClass>& ptr) { // 常量引用
    if (ptr) {
        ptr->print();
    }
}
// 或者更推荐的方式:直接使用裸指针或引用,语义更清晰
void useObjectBetter(const MyClass* ptr) {
    if (ptr) {
        ptr->print();
    }
}
void useObjectBest(const MyClass& obj) {
    obj.print();
}

int main() {
    // 使用 sink 函数
    auto ptr = createObject(123); // 从工厂获取所有权
    takeOwnership(std::move(ptr)); // 明确转移所有权给函数

    // 使用借用函数
    auto ptr2 = createObject(456);
    useObject(ptr2); // 只是借用,所有权还在main函数的ptr2中
    useObjectBetter(ptr2.get()); // 更好,明确表示只是用指针
    useObjectBest(*ptr2);        // 最好,明确表示只是用对象引用

    return 0;
}

第三部分:高级特性与底层原理

1. 自定义删除器 (Custom Deleter)

默认情况下,unique_ptr 使用 deletedelete[] 来释放资源。但你可以指定一个自定义的删除器来处理特殊资源(如文件句柄、C库的 malloc/free 等)。

#include <cstdio>

// 1. 函数指针形式的删除器
void FileDeleter(std::FILE* filePtr) {
    if (filePtr) {
        std::fclose(filePtr);
        std::cout << "File closed using function pointer.\n";
    }
}

// 2. 仿函数(Functor)形式的删除器
struct FileCloser {
    void operator()(std::FILE* filePtr) const {
        if (filePtr) {
            std::fclose(filePtr);
            std::cout << "File closed using functor.\n";
        }
    }
};

// 3. Lambda 表达式形式的删除器
auto lambdaDeleter = [](std::FILE* filePtr) {
    if (filePtr) {
        std::fclose(filePtr);
        std::cout << "File closed using lambda.\n";
    }
};

int main() {
    // 使用自定义删除器,模板参数需要指明类型和删除器类型
    std::unique_ptr<std::FILE, decltype(&FileDeleter)> file1(std::fopen("test.txt", "w"), &FileDeleter);
    std::unique_ptr<std::FILE, FileCloser> file2(std::fopen("test.txt", "w"), FileCloser());
    std::unique_ptr<std::FILE, decltype(lambdaDeleter)> file3(std::fopen("test.txt", "w"), lambdaDeleter);

    // 对于有状态的删除器(如捕获变量的lambda),unique_ptr的大小会增加
    // 无状态的删除器(如函数指针、空仿函数、无捕获lambda)不会增加unique_ptr的大小

    return 0;
} // 离开作用域时,自定义删除器会被调用,文件被关闭
2. 管理数组 (Array Specialization)

unique_ptr 对数组进行了特化,使用 delete[] 进行释放。

// 方法一:使用 unique_ptr<T[]> 特化版本
std::unique_ptr<int[]> arrayPtr(new int[10]); // 注意模板类型中的[]
for (int i = 0; i < 10; ++i) {
    arrayPtr[i] = i; // 可以使用下标操作符
}
// 离开作用域时自动调用 delete[]

// 方法二:使用默认的 unique_ptr<T> 但提供自定义删除器(效果相同,但更繁琐)
std::unique_ptr<int, void(*)(int*)> arrayPtr2(new int[10], [](int* p) { delete[] p; });

// 方法三(最佳):优先使用 std::vector 或 std::array,除非有特殊需求。
3. 底层实现简析

unique_ptr 的本质是一个包装了原生指针的类模板,其简化实现如下:

template<class T>
class UniquePtr
{
private:
	T* _ptr;
public:
	//基本的智能指针的需求
	//自动管理资源
	UniquePtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	~UniquePtr()
	{
		if (_ptr != nullptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}

	//拷贝构造
	UniquePtr(const UniquePtr<T>& up) = delete;
	//赋值重载
	UniquePtr<T>& operator=(const UniquePtr<T>& up) = delete;

	//移动构造函数:转移资源管理权
	UniquePtr(UniquePtr<T>&& up)
		:_ptr(up._ptr)
	{
		up._ptr = nullptr;
	}
	//移动赋值重载
	UniquePtr<T>& operator=(UniquePtr<T>&& up)
	{
		if (this != &up)
		{
			delete _ptr;
			_ptr = up._ptr;
			up._ptr = nullptr;
		}
		return *this;
	}
    
	//指针的基本作用
	T& operator*() const
	{
		return *_ptr;
	}
	T* operator->() const
	{
		return _ptr;
	}
};

第四部分:实践

  1. 默认选择:当你需要智能指针时,应优先考虑 std::unique_ptr。它开销最小,语义最清晰。
  2. 使用 std::make_unique:创建 unique_ptr 的首选方法。它更安全(异常安全)、更高效(减少代码生成)。
  3. 明确所有权转移:使用 std::move 来显式转移所有权,让代码意图一目了然。
  4. 函数传参
    • 传递所有权:按值传递 std::unique_ptr
    • 不传递所有权,仅访问:传递裸指针 (ptr.get()) 或引用 (*ptrconst &)。避免使用 const std::unique_ptr<T>& 作为参数,这通常是一种设计异味。
  5. 管理数组:使用 std::unique_ptr<T[]>,但更优先考虑 std::vector
  6. 管理非内存资源:使用自定义删除器,可以安全地管理文件、套接字、互斥锁等任何需要“释放”操作的资源。

std::shared_ptr

它是 C++11 中引入的用于管理共享所有权模型资源的智能指针,与独占所有权的 std::unique_ptr 形成互补。


第一部分:核心思想与解决的问题

std::shared_ptr 的核心是共享所有权。多个 shared_ptr 可以指向同一个对象,系统会通过引用计数来跟踪有多少个“所有者”。只有当最后一个指向该对象的 shared_ptr 被销毁或重置时,该对象才会被销毁。

它解决了什么问题?
想象一下这些场景:

  1. 你有一个对象,需要被多个其他对象使用,但你无法预知哪个对象会是最后一个使用它的。
  2. 你想将对象放入多个容器中(如多个 vectormap)。
  3. 你需要通过多个路径(例如多个线程)访问同一个对象,并希望在其不再被需要时自动释放。

在这些情况下,所有权是共享的、不明确的。std::unique_ptr 因其独占性无法胜任,而原生指针又无法自动管理生命周期。std::shared_ptr 正是为此而生。

一个简单的例子:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass Constructed\n"; }
    ~MyClass() { std::cout << "MyClass Destroyed\n"; }
};

int main() {
    std::cout << "Start of main\n";

    // 创建第一个 shared_ptr,引用计数为 1
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    {
        // 复制构造,共享所有权,引用计数增加为 2
        std::shared_ptr<MyClass> ptr2 = ptr1;
        std::cout << "Inside inner scope. Use count: " << ptr1.use_count() << "\n"; // 输出 2
    } // ptr2 离开作用域,析构,引用计数减为 1

    std::cout << "Outside inner scope. Use count: " << ptr1.use_count() << "\n"; // 输出 1

    // ptr1 离开作用域,引用计数减为 0,对象被销毁
    std::cout << "End of main\n";
    return 0;
}

输出:

Start of main
MyClass Constructed
Inside inner scope. Use count: 2
Outside inner scope. Use count: 1
MyClass Destroyed
End of main

第二部分:基本用法与代码示例

1. 创建与基本操作

首选方式:使用 std::make_shared

#include <iostream>
#include <memory>

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) { std::cout << "Constructor: " << value << "\n"; }
    ~MyClass() { std::cout << "Destructor: " << value << "\n"; }
};

int main() {
    // 1. 创建 - 最推荐的方式,高效且安全
    std::shared_ptr<MyClass> shPtr1 = std::make_shared<MyClass>(10);
    std::cout << "shPtr1 use_count: " << shPtr1.use_count() << "\n"; // 输出 1

    // 2. 拷贝 - 共享所有权,引用计数增加
    std::shared_ptr<MyClass> shPtr2 = shPtr1; // 拷贝构造
    std::shared_ptr<MyClass> shPtr3;
    shPtr3 = shPtr2; // 拷贝赋值
    std::cout << "After copies, use_count: " << shPtr1.use_count() << "\n"; // 输出 3

    // 3. 使用 -> 和 * 操作符访问对象
    shPtr1->value = 20;
    std::cout << "Value via shPtr3: " << shPtr3->value << "\n"; // 输出 20

    // 4. 获取原始指针(不推荐,慎用!仅用于与不支持智能指针的API交互)
    MyClass* rawPtr = shPtr1.get();
    // 注意:绝对不要用这个 rawPtr 去创建另一个智能指针!

    // 5. 重置 (reset): 停止共享当前对象,指向新对象或变为空
    shPtr2.reset(); // 停止共享,引用计数减1
    std::cout << "After shPtr2 reset, use_count: " << shPtr1.use_count() << "\n"; // 输出 2

    shPtr3.reset(new MyClass(30)); // shPtr3 停止共享旧对象(10),开始拥有新对象(30)
    std::cout << "After shPtr3 reset with new obj, use_count for old: " << shPtr1.use_count() << "\n"; // 输出 1
    std::cout << "shPtr3 use_count for new obj: " << shPtr3.use_count() << "\n"; // 输出 1

    // 6. 交换 (swap)
    std::shared_ptr<MyClass> shPtr4 = std::make_shared<MyClass>(40);
    shPtr1.swap(shPtr4); // shPtr1 现在指向 40, shPtr4 指向 10

    return 0;
} // 所有 shared_ptr 离开作用域,各自管理的对象被正确销毁
2. 在函数中的使用
// 1. 按值传递:表示函数参与共享所有权(函数内部会增加引用计数)
void processByValue(std::shared_ptr<MyClass> ptr) {
    std::cout << "In processByValue, use_count: " << ptr.use_count() << "\n";
} // ptr 析构,引用计数减少

// 2. 按引用传递:表示函数只使用对象,但不参与所有权管理,不会增加引用计数
void processByReference(const std::shared_ptr<MyClass>& ptr) {
    if (ptr) { // 使用前最好检查
        std::cout << "In processByReference, value: " << ptr->value << "\n";
    }
    // use_count 保持不变
}

// 3. 工厂函数:返回 shared_ptr
std::shared_ptr<MyClass> createMyClass(int value) {
    return std::make_shared<MyClass>(value); // 返回值优化,很高效
}

int main() {
    auto ptr = createMyClass(50);
    processByValue(ptr); // 内部use_count变为2,函数返回后变回1
    processByReference(ptr); // use_count 始终为1
    return 0;
}

第三部分:原理与底层机制

std::shared_ptr 的实现比 unique_ptr 复杂得多。它通常包含两个指针

  1. 指向被管理对象的指针
  2. 指向控制块 (Control Block) 的指针

控制块是一个动态分配的内含元数据的结构,它包括:

  • 引用计数 (Use Count):记录有多少个 shared_ptr 正拥有这个对象。
  • 弱计数 (Weak Count):记录有多少个 weak_ptr 正指向这个控制块(详见下一部分关于 weak_ptr 的讲解)。
  • 删除器 (Deleter):通常是 std::default_delete,但可以被自定义。
  • 分配器 (Allocator):用于分配控制块和对象内存,通常不使用。

std::make_shared 的优势(非常重要!)
std::make_shared<MyClass>(args...) 通常会执行一次单一的内存分配,这块内存同时容纳了控制块和 MyClass 对象本身

auto p = std::make_shared<MyClass>(); // 一次分配

而直接使用 std::shared_ptr<MyClass>(new MyClass) 会执行两次内存分配:一次 new 分配对象,一次 new 分配控制块。

std::shared_ptr<MyClass> p(new MyClass); // 两次分配

因此,std::make_shared 不仅更安全(避免了内存泄漏的潜在风险),而且更高效(更少的内存分配、更好的局部性)。

简单实现

这里我们的控制块就简单些,就只包含引用计数。

那么我们要怎么设计这个智能指针才能达到我们想要的效果呢?
在这里插入图片描述

我们得这样写:

template<class T>
class SharedPtr
{
private:
	T* _ptr = nullptr;
	//如果想详细实现控制块的话,就创建一个控制块结构体
	//这里换成结构体指针
	int* _count = nullptr;

public:
	//构造函数
	SharedPtr(T* ptr)
		:_ptr(ptr)
		,_count(new int(1))
	{
		//简单的初始化
	}

	//这个函数是方便我们外部调用
	//当其中一个智能指针对象不再指向它原来管理的资源后,我们就将它们共用的*_count--
	//当*_count--后,如果*_count值为0,就释放它管理的空间以及_count指向的空间
	//因为*_count为0,以为着最后一个管理该资源的智能指针对象也不再管理了,所以就要释放空间
	void release()
	{
		if (_count != nullptr && --(*_count) == 0)
		{
			delete _ptr;
			delete _count;
			_ptr = nullptr;
			_count = nullptr;
		}
	}

	~SharedPtr()
	{
		release();
	}

	//拷贝构造
	SharedPtr(const SharedPtr<T>& sp)
	{
		_ptr = sp._ptr;
		_count = sp._count;
		++(*_count);
	}

	//赋值重载
	SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		//避免管理同一块资源的智能指针对象互相赋值
		//我们并不希望管理同一块资源的智能指针对象互相赋值
		//而且这种判断也包含了自己给自己赋值的情况
		if (_ptr != sp._ptr)
		{
			//因为现在要接管新的资源,所以要先将原来管理资源的计数*_count解决一下
			//也就是把*_count--
			//如果*_count==0就释放原资源
			release();

			_ptr = sp._ptr;
			_count = sp._count;
			++(*_count);
		}
		return *this;
	}

	// 移动构造函数
	SharedPtr(SharedPtr<T>&& sp) noexcept {
		_ptr = sp._ptr;
		_count = sp._count;
		sp._ptr = nullptr;
		sp._count = nullptr;
	}

	// 移动赋值运算符
	SharedPtr<T>& operator=(SharedPtr<T>&& sp) noexcept {
		if (_ptr != sp._ptr) {
			release();
			_ptr = sp._ptr;
			_count = sp._count;
			sp._ptr = nullptr;
			sp._count = nullptr;
		}
		return *this;
	}

	//简单的获取计数
	int use_count()
	{
		return *_count;
	}

	//获取管理的资源的地址
	T* get() const
	{
		return _ptr;
	}

	//指针的基本作用
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
};


//这里我顺便把WeakPtr也简单写了一下,大家接着往下看就ok了
template<class T>
class WeakPtr
{
public:
	// RAII
	WeakPtr()
		:_ptr(nullptr)
	{}

	WeakPtr(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
	}

	WeakPtr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;
};


第四部分:陷阱、挑战与 std::weak_ptr

1. 循环引用问题

这是 shared_ptr 最著名的陷阱。如果两个由 shared_ptr 管理的对象互相持有对方的 shared_ptr,它们的引用计数永远无法降到零,导致内存泄漏。

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> bPtr; // A 持有 B 的 shared_ptr
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> aPtr; // B 也持有 A 的 shared_ptr -> 循环引用!
    ~B() { std::cout << "B destroyed\n"; }
};

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

    a->bPtr = b; // A 引用 B,B的引用计数变为2
    b->aPtr = a; // B 引用 A,A的引用计数变为2

    std::cout << "a use_count: " << a.use_count() << "\n"; // 2
    std::cout << "b use_count: " << b.use_count() << "\n"; // 2

    return 0;
} // main 结束时,a 和 b 的引用计数都只减为1,不会为0!
// A 和 B 的对象都不会被销毁!内存泄漏!

输出中不会有 “A destroyed” 和 “B destroyed”。

在这里插入图片描述


在这里插入图片描述

2. 解决方案:std::weak_ptr

std::weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个由 shared_ptr 管理的对象,但不增加其引用计数。它是专门为解决循环引用而设计的。

我们将上面例子中 class A 的成员改为 weak_ptr

class A {
public:
    std::weak_ptr<B> bPtr; // 改为 weak_ptr!打破循环
    ~A() { std::cout << "A destroyed\n"; }
};

// Class B 保持不变
class B {
public:
    std::shared_ptr<A> aPtr;
    ~B() { std::cout << "B destroyed\n"; }
};

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

    a->bPtr = b; // weak_ptr 的赋值不会增加B的引用计数(B的计数仍为1)
    b->aPtr = a; // A的引用计数变为2

    std::cout << "a use_count: " << a.use_count() << "\n"; // 2
    std::cout << "b use_count: " << b.use_count() << "\n"; // 1 (关键!)

    return 0;
} // main 结束时:
  // 1. b 的引用计数从1减为0 -> B对象被销毁 -> B的析构函数被调用,输出 "B destroyed"
  // 2. B被销毁导致其成员 aPtr 被销毁 -> A的引用计数从2减为1
  // 3. a 的引用计数从1减为0 -> A对象被销毁 -> 输出 "A destroyed"

输出:

B destroyed
A destroyed

循环引用被成功打破!weak_ptr 的详细用法我们在下面。


第五部分:总结与最佳实践

  1. 适用场景:仅在需要共享所有权时使用 std::shared_ptr。默认情况下应优先考虑 std::unique_ptr
  2. 性能开销shared_ptr 大小通常是原生指针的两倍(两个指针),且控制块的分配和原子引用计数的操作都有开销。不要滥用。
  3. 优先使用 std::make_shared:除非需要指定自定义删除器,否则总是使用 std::make_shared 来创建 shared_ptr。它更安全、更快速。
  4. 警惕循环引用:在设计对象关系时,如果存在互相持有的情况,果断将其中一方改为 std::weak_ptr
  5. 避免从原始指针创建多个 shared_ptr
    MyClass* rawPtr = new MyClass;
    std::shared_ptr<MyClass> p1(rawPtr);
    // std::shared_ptr<MyClass> p2(rawPtr); // 灾难!两个独立的控制块会尝试删除同一个对象!
    
  6. 线程安全shared_ptr 的引用计数操作是原子的、线程安全的。但对其指向的对象的读写则需要额外的同步机制(如互斥锁)。

std::weak_ptr

好的,我们来深入探讨 std::weak_ptr。它是C++智能指针家族中比较特殊但非常重要的成员,专门设计用来与 std::shared_ptr 协同工作。


第一部分:std::weak_ptr 是什么?为什么需要它?

std::weak_ptr 是一种不控制所指向对象生命周期的智能指针。它指向一个由 std::shared_ptr 管理的对象,但不会增加该对象的引用计数

它解决的两个核心问题:
  1. 打破 std::shared_ptr 的循环引用
    这是 weak_ptr 最主要的设计目的。当两个或多个 shared_ptr 相互引用时,会形成循环引用,导致引用计数永远无法归零,从而引发内存泄漏。weak_ptr 通过不增加引用计数来打破这种循环。

  2. 临时访问共享资源而不影响其生命周期
    当你需要观察一个可能已被释放的对象,但又不想阻止它被释放时,weak_ptr 是理想的选择。它就像一个"安全的观察者"。


第二部分:基本特性与工作原理

1. 关键特性:
  • 不拥有对象:不参与引用计数,不影响所指向对象的生命周期。
  • 必须从 shared_ptr 创建:不能直接指向对象,总是作为 shared_ptr 的"伴侣"存在。
  • 检查对象是否存在:可以检查其观察的对象是否仍然有效。
  • 临时获取所有权:可以通过 lock() 方法获取一个临时的 shared_ptr 来安全地使用对象。
2. 底层原理:

weak_ptrshared_ptr 共享同一个控制块。控制块中除了有强引用计数use_count,由 shared_ptr 管理),还有一个弱引用计数weak_count,由 weak_ptr 管理)。

  • 当最后一个 shared_ptr 被销毁时(use_count 变为 0),对象被销毁,但控制块仍然存在(直到 weak_count 也变为 0)。
  • weak_ptr 通过检查控制块中的信息来判断对象是否还存在。

第三部分:基本用法与代码示例

1. 创建与基本操作

weak_ptr 必须从 shared_ptr 创建或赋值。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int v) : value(v) { std::cout << "Constructed: " << value << "\n"; }
    ~MyClass() { std::cout << "Destroyed: " << value << "\n"; }
    void doSomething() { std::cout << "Doing something with " << value << "\n"; }
    int value;
};

int main() {
    // 1. 创建 shared_ptr
    std::shared_ptr<MyClass> shPtr = std::make_shared<MyClass>(42);
    
    // 2. 从 shared_ptr 创建 weak_ptr
    std::weak_ptr<MyClass> weakPtr = shPtr;
    
    std::cout << "shPtr use_count: " << shPtr.use_count() << "\n"; // 输出 1
    // weak_ptr 不会增加引用计数!
    
    // 3. 使用 lock() 获取临时的 shared_ptr 来访问对象
    if (auto tempShared = weakPtr.lock()) {
        // 如果对象还存在,lock() 返回一个有效的 shared_ptr
        tempShared->doSomething();
        std::cout << "Temp use_count: " << tempShared.use_count() << "\n"; // 输出 2
    } // tempShared 离开作用域,引用计数减回 1
    
    // 4. 释放 shared_ptr
    shPtr.reset();
    std::cout << "shPtr reset. Object should be destroyed.\n";
    
    // 5. 再次尝试访问
    if (auto tempShared = weakPtr.lock()) {
        // 这里不会执行,因为对象已经不存在了
        std::cout << "This won't be printed.\n";
    } else {
        std::cout << "Object is no longer available.\n";
    }
    
    return 0;
}

输出:

Constructed: 42
shPtr use_count: 1
Doing something with 42
Temp use_count: 2
Destroyed: 42
shPtr reset. Object should be destroyed.
Object is no longer available.
2. 解决循环引用问题(核心用途)

这是 weak_ptr 最重要的应用场景。

#include <iostream>
#include <memory>

class Controller; // 前向声明

class Device {
public:
    std::string name;
    // 使用 weak_ptr 而不是 shared_ptr 来打破循环引用
    std::weak_ptr<Controller> controller;
    
    Device(const std::string& n) : name(n) {
        std::cout << "Device " << name << " created\n";
    }
    ~Device() {
        std::cout << "Device " << name << " destroyed\n";
    }
    
    void checkController() {
        if (auto ctrl = controller.lock()) {
            std::cout << name << ": Controller is still alive\n";
        } else {
            std::cout << name << ": Controller has been destroyed\n";
        }
    }
};

class Controller {
public:
    std::string name;
    std::shared_ptr<Device> device;
    
    Controller(const std::string& n) : name(n) {
        std::cout << "Controller " << name << " created\n";
    }
    ~Controller() {
        std::cout << "Controller " << name << " destroyed\n";
    }
};

int main() {
    // 创建相互引用的对象
    auto ctrl = std::make_shared<Controller>("MainCtrl");
    auto dev = std::make_shared<Device>("Sensor1");
    
    // 建立双向关联
    ctrl->device = dev;               // shared_ptr 拷贝,引用计数增加
    dev->controller = ctrl;           // weak_ptr 赋值,不会增加引用计数
    
    std::cout << "Ctrl use_count: " << ctrl.use_count() << "\n"; // 2 (main + device)
    std::cout << "Dev use_count: " << dev.use_count() << "\n";   // 1 (只有ctrl->device)
    
    // 检查控制器状态
    dev->checkController();
    
    // 释放控制器
    ctrl.reset();
    std::cout << "Controller released\n";
    
    // 再次检查
    dev->checkController();
    
    // 现在只有 device 还有一个引用(来自 main 函数的 dev 变量)
    std::cout << "Dev use_count: " << dev.use_count() << "\n"; // 1
    
    return 0;
} // dev 离开作用域,引用计数归零,Device 也被正确销毁

输出:

Controller MainCtrl created
Device Sensor1 created
Ctrl use_count: 2
Dev use_count: 1
Sensor1: Controller is still alive
Controller released
Controller MainCtrl destroyed
Sensor1: Controller has been destroyed
Dev use_count: 1
Device Sensor1 destroyed

注意:循环引用被成功打破!两个对象都能被正确销毁。


第四部分:其他重要方法与用法

1. expired() 方法

快速检查对象是否已被释放,比 lock() 更轻量。

std::weak_ptr<MyClass> weakPtr;

// ... 一些操作后 ...

if (!weakPtr.expired()) {
    // 对象还存在
    auto obj = weakPtr.lock();
    obj->doSomething();
} else {
    // 对象已被释放
    std::cout << "Object no longer exists\n";
}
2. 构造函数与赋值
std::shared_ptr<MyClass> shPtr = std::make_shared<MyClass>(100);

// 各种创建 weak_ptr 的方式
std::weak_ptr<MyClass> w1(shPtr);           // 从 shared_ptr 构造
std::weak_ptr<MyClass> w2 = shPtr;          // 从 shared_ptr 赋值
std::weak_ptr<MyClass> w3 = w2;             // 从另一个 weak_ptr 拷贝
std::weak_ptr<MyClass> w4;
w4 = shPtr;                                 // 从 shared_ptr 赋值
w4 = w3;                                    // 从另一个 weak_ptr 赋值
3. 重置与交换
std::weak_ptr<MyClass> weakPtr;

// 重置为空的 weak_ptr
weakPtr.reset();

// 交换两个 weak_ptr
std::weak_ptr<MyClass> anotherWeakPtr;
weakPtr.swap(anotherWeakPtr);

第五部分:使用场景与最佳实践

1. 主要使用场景:
  • 打破循环引用:如上所示,是主要用途。
  • 缓存实现:缓存可以使用 weak_ptr 来持有对象。如果内存紧张,对象可以被释放,当再次需要时可以从源重新加载。
  • 观察者模式:观察者可以使用 weak_ptr 来观察主题,避免影响主题的生命周期。
  • 避免悬挂指针:比使用原始指针更安全地引用可能已被释放的对象。
2. 缓存示例:
class Cache {
private:
    std::unordered_map<int, std::weak_ptr<LargeObject>> cache;
    
public:
    std::shared_ptr<LargeObject> getObject(int id) {
        auto it = cache.find(id);
        if (it != cache.end()) {
            // 尝试从缓存中获取
            if (auto cachedObj = it->second.lock()) {
                std::cout << "Cache hit for id " << id << "\n";
                return cachedObj;
            } else {
                // 对象已被释放,从缓存中移除
                cache.erase(it);
            }
        }
        
        // 缓存未命中,创建新对象
        std::cout << "Cache miss for id " << id << ", creating new object\n";
        auto newObj = std::make_shared<LargeObject>(id);
        cache[id] = newObj; // 存储 weak_ptr
        return newObj;
    }
};
3. 最佳实践:
  1. 总是检查:在使用 weak_ptr 前,总是使用 lock()expired() 检查对象是否还存在。
  2. 优先使用 lock()lock() 是原子操作,能保证线程安全地获取 shared_ptr
  3. 不要直接解引用:不能直接使用 operator*operator->,必须先转换为 shared_ptr
  4. 理解性能weak_ptr 的操作(特别是 lock())比原始指针访问稍慢,但提供了安全性。
  5. shared_ptr 配合使用weak_ptr 总是与 shared_ptr 成对出现,单独使用没有意义。

总结

std::weak_ptr 是C++智能指针系统中解决特定问题的精巧工具:

特性描述
目的打破 shared_ptr 的循环引用;安全地观察共享资源
所有权不拥有对象,不影响生命周期
创建方式必须从 shared_ptr 或另一个 weak_ptr 创建
核心方法lock():获取临时的 shared_ptr
expired():检查对象是否已被释放
性能比原始指针稍慢,但提供了内存安全性
使用场景循环引用、缓存、观察者模式等

更多推荐