C++ `std::shared_ptr`的控制块(Control Block)结构:引用计数、弱引用与自定义删除器

好的,我们开始今天的讲座。今天的主题是C++ std::shared_ptr 的控制块(Control Block)。我们将深入探讨控制块的结构,包括引用计数、弱引用以及自定义删除器,并通过代码示例来加深理解。

std::shared_ptr 的基本概念

std::shared_ptr 是一种智能指针,它允许多个 shared_ptr 实例共享同一个对象的所有权。当最后一个 shared_ptr 实例销毁时,它会自动释放所管理的对象。这种机制通过引用计数来实现,避免了手动内存管理带来的泄漏风险。

控制块(Control Block)的作用

std::shared_ptr 的核心在于控制块。控制块是一个动态分配的内存区域,用于存储以下关键信息:

  • 强引用计数(Strong Count): 记录当前有多少个 shared_ptr 实例指向这个对象。当强引用计数降为 0 时,表示没有 shared_ptr 再持有该对象的所有权,对象将被销毁。
  • 弱引用计数(Weak Count): 记录当前有多少个 weak_ptr 实例指向这个对象。weak_ptr 不参与对象的所有权管理,它的存在是为了在对象被销毁后仍然可以检测到对象是否有效。
  • 删除器(Deleter): 一个函数对象,用于释放所管理的对象。默认情况下,shared_ptr 使用 delete 运算符来释放对象,但我们可以通过自定义删除器来指定不同的释放策略。
  • 指向被管理对象的指针: 指向实际被 shared_ptr 管理的对象。

控制块的创建时机

控制块的创建时机取决于 shared_ptr 的构造方式:

  1. 从原始指针构造: 当使用原始指针(例如 new T 返回的指针)构造 shared_ptr 时,控制块通常会被创建。这是最常见的情况。

    #include <iostream>
    #include <memory>
    
    struct MyClass {
        int value;
        MyClass(int v) : value(v) { std::cout << "MyClass constructed with value: " << value << std::endl; }
        ~MyClass() { std::cout << "MyClass destructed with value: " << value << std::endl; }
    };
    
    int main() {
        std::shared_ptr<MyClass> ptr(new MyClass(10)); // 控制块在这里创建
        return 0;
    }
  2. unique_ptr 移动构造: 当使用 unique_ptr 移动构造 shared_ptr 时,unique_ptr 会释放其所有权,shared_ptr 会接管该对象并创建控制块。

    #include <iostream>
    #include <memory>
    
    struct MyClass {
        int value;
        MyClass(int v) : value(v) { std::cout << "MyClass constructed with value: " << value << std::endl; }
        ~MyClass() { std::cout << "MyClass destructed with value: " << value << std::endl; }
    };
    
    int main() {
        std::unique_ptr<MyClass> uptr(new MyClass(20));
        std::shared_ptr<MyClass> sptr(std::move(uptr)); // 控制块在这里创建 (如果之前没有)
    
        return 0;
    }
  3. 从另一个 shared_ptr 拷贝构造或赋值: 在这种情况下,不会创建新的控制块。新的 shared_ptr 实例会增加现有控制块的强引用计数,共享同一个控制块。

    #include <iostream>
    #include <memory>
    
    struct MyClass {
        int value;
        MyClass(int v) : value(v) { std::cout << "MyClass constructed with value: " << value << std::endl; }
        ~MyClass() { std::cout << "MyClass destructed with value: " << value << std::endl; }
    };
    
    int main() {
        std::shared_ptr<MyClass> ptr1(new MyClass(30)); // 控制块在这里创建
        std::shared_ptr<MyClass> ptr2 = ptr1; // 不创建新的控制块,增加 ptr1 的控制块的强引用计数
    
        return 0;
    }
  4. 使用 std::make_shared std::make_shared 是创建 shared_ptr 的推荐方式。它会一次性分配对象和控制块,减少内存分配的次数,提高性能。

    #include <iostream>
    #include <memory>
    
    struct MyClass {
        int value;
        MyClass(int v) : value(v) { std::cout << "MyClass constructed with value: " << value << std::endl; }
        ~MyClass() { std::cout << "MyClass destructed with value: " << value << std::endl; }
    };
    
    int main() {
        std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(40); // 对象和控制块一次性分配
        return 0;
    }

控制块的结构(理论上的描述)

虽然 C++ 标准没有明确规定控制块的实现细节,但我们可以从概念上理解它的结构。一种可能的实现方式如下:

template <typename T>
struct ControlBlock {
    std::atomic<long> strong_count;
    std::atomic<long> weak_count;
    T* ptr;
    std::function<void(T*)> deleter; // 函数对象,用于删除对象

    ControlBlock(T* p, std::function<void(T*)> d) : strong_count(1), weak_count(0), ptr(p), deleter(d) {}

    ~ControlBlock() {
        if (ptr) {
            deleter(ptr); // 使用删除器释放对象
        }
    }
};

注意: 这只是一个概念性的示例。实际的控制块实现可能更复杂,并且依赖于编译器和标准库的优化。标准库的具体实现是隐藏的。

引用计数的工作原理

  • 强引用计数: 每当创建一个新的 shared_ptr 实例指向同一个对象时,强引用计数就会增加。当一个 shared_ptr 实例销毁时,强引用计数就会减少。当强引用计数降为 0 时,表示没有 shared_ptr 持有该对象的所有权,对象将被销毁,控制块也被销毁。

  • 弱引用计数: weak_ptr 不参与对象的所有权管理。当从 shared_ptr 创建 weak_ptr 时,弱引用计数会增加。当 weak_ptr 实例销毁时,弱引用计数会减少。即使强引用计数降为 0,只要弱引用计数大于 0,控制块就不会被销毁。这允许 weak_ptr 在对象被销毁后仍然可以检测到对象是否有效。

代码示例:引用计数

#include <iostream>
#include <memory>

struct MyClass {
    int value;
    MyClass(int v) : value(v) { std::cout << "MyClass constructed with value: " << value << std::endl; }
    ~MyClass() { std::cout << "MyClass destructed with value: " << value << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(50);
    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出 1

    std::shared_ptr<MyClass> ptr2 = ptr1; // 拷贝构造,增加引用计数
    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出 2
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 输出 2

    {
        std::shared_ptr<MyClass> ptr3 = ptr1; // 增加引用计数
        std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出 3
        std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 输出 3
        std::cout << "ptr3 use_count: " << ptr3.use_count() << std::endl; // 输出 3
    } // ptr3 销毁,减少引用计数

    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出 2
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 输出 2

    ptr1.reset(); // 释放 ptr1 的所有权,减少引用计数
    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出 0
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 输出 1

    ptr2.reset(); // 释放 ptr2 的所有权,减少引用计数,对象被销毁
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 输出 0

    return 0;
}

弱引用 std::weak_ptr

std::weak_ptr 是一种不拥有对象所有权的智能指针。它可以从 shared_ptr 创建,用于观察对象是否仍然有效。

  • weak_ptr 不会增加强引用计数。
  • shared_ptr 管理的对象被销毁后,weak_ptr 会失效,但它仍然存在,可以用来检测对象是否已经被销毁。
  • 可以使用 weak_ptr::lock() 方法尝试获取一个 shared_ptr。如果对象仍然有效,lock() 方法会返回一个指向该对象的 shared_ptr;否则,返回一个空的 shared_ptr

代码示例:weak_ptr

#include <iostream>
#include <memory>

struct MyClass {
    int value;
    MyClass(int v) : value(v) { std::cout << "MyClass constructed with value: " << value << std::endl; }
    ~MyClass() { std::cout << "MyClass destructed with value: " << value << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>(60);
    std::weak_ptr<MyClass> wptr = sptr;

    {
        std::shared_ptr<MyClass> locked_sptr = wptr.lock(); // 尝试获取 shared_ptr
        if (locked_sptr) {
            std::cout << "Object is still alive, value: " << locked_sptr->value << std::endl;
            std::cout << "sptr use_count: " << sptr.use_count() << std::endl; // use_count 为 1
            std::cout << "locked_sptr use_count: " << locked_sptr.use_count() << std::endl; // use_count 为 2
        } else {
            std::cout << "Object has been destroyed." << std::endl;
        }
    } // locked_sptr 销毁,use_count 恢复为 1

    sptr.reset(); // 释放 sptr 的所有权,对象被销毁

    std::shared_ptr<MyClass> locked_sptr = wptr.lock(); // 尝试获取 shared_ptr
    if (locked_sptr) {
        std::cout << "Object is still alive." << std::endl;
    } else {
        std::cout << "Object has been destroyed." << std::endl; // 输出此行
    }

    return 0;
}

自定义删除器(Custom Deleter)

shared_ptr 允许我们自定义删除器,以便在对象被销毁时执行特定的清理操作。这在以下情况下非常有用:

  • 对象不是通过 new 运算符分配的,而是通过其他方式分配的(例如,使用 C 风格的 malloc 函数)。
  • 需要执行一些额外的清理操作,例如关闭文件句柄或释放系统资源。
  • 需要使用特定的删除函数,例如 delete[] 运算符来释放数组。

代码示例:自定义删除器

#include <iostream>
#include <memory>

// 使用 malloc 分配内存
void* allocate_memory(size_t size) {
    void* ptr = std::malloc(size);
    if (ptr == nullptr) {
        throw std::bad_alloc();
    }
    return ptr;
}

// 自定义删除器,使用 free 释放内存
void free_memory(void* ptr) {
    std::cout << "Freeing memory allocated with malloc." << std::endl;
    std::free(ptr);
}

int main() {
    // 使用 malloc 分配内存
    void* raw_ptr = allocate_memory(sizeof(int));

    // 使用 shared_ptr 管理 malloc 分配的内存,并指定自定义删除器
    std::shared_ptr<int> sptr(static_cast<int*>(raw_ptr), free_memory);

    *sptr = 70;
    std::cout << "Value: " << *sptr << std::endl;

    // 当 sptr 销毁时,free_memory 函数会被调用,释放 malloc 分配的内存

    return 0;
}

代码示例:使用 lambda 表达式作为删除器

#include <iostream>
#include <memory>
#include <fstream>

int main() {
    // 打开文件
    std::ofstream* file = new std::ofstream("test.txt");

    // 使用 shared_ptr 管理文件句柄,并使用 lambda 表达式作为删除器
    std::shared_ptr<std::ofstream> file_ptr(file, [](std::ofstream* f) {
        std::cout << "Closing file." << std::endl;
        f->close();
        delete f;
    });

    *file_ptr << "Hello, world!" << std::endl;

    // 当 file_ptr 销毁时,lambda 表达式会被调用,关闭文件句柄

    return 0;
}

代码示例:使用 delete[] 运算符释放数组

#include <iostream>
#include <memory>

int main() {
    // 分配一个 int 数组
    int* arr = new int[10];

    // 使用 shared_ptr 管理数组,并指定 delete[] 运算符作为删除器
    std::shared_ptr<int> arr_ptr(arr, [](int* p) {
        std::cout << "Deleting array." << std::endl;
        delete[] p;
    });

    for (int i = 0; i < 10; ++i) {
        arr_ptr.get()[i] = i * 10;
        std::cout << arr_ptr.get()[i] << " ";
    }
    std::cout << std::endl;

    // 当 arr_ptr 销毁时,delete[] 运算符会被调用,释放数组

    return 0;
}

std::enable_shared_from_this

std::enable_shared_from_this 是一个非常有用的工具类,它可以让一个类安全地创建指向自身实例的 shared_ptr。这在以下情况下非常有用:

  • 类的方法需要返回指向自身实例的 shared_ptr
  • 避免多个 shared_ptr 实例管理同一个对象,导致 double free 的问题。

代码示例:enable_shared_from_this

#include <iostream>
#include <memory>

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    int value;

    MyClass(int v) : value(v) {
        std::cout << "MyClass constructed with value: " << value << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructed with value: " << value << std::endl;
    }

    std::shared_ptr<MyClass> get_shared_ptr() {
        return shared_from_this(); // 返回指向自身实例的 shared_ptr
    }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(80);
    std::shared_ptr<MyClass> ptr2 = ptr1->get_shared_ptr(); // 使用 shared_from_this 获取 shared_ptr

    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出 2
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 输出 2

    // 如果不使用 enable_shared_from_this,直接使用 std::shared_ptr<MyClass>(this) 构造,
    // 会导致多个 shared_ptr 管理同一个对象,从而导致 double free 的问题。

    return 0;
}

总结

std::shared_ptr 的控制块是实现引用计数和自定义删除器的关键。理解控制块的结构和工作原理对于正确使用 shared_ptr 至关重要。 通过掌握引用计数、弱引用和自定义删除器,能够更好地管理内存,避免内存泄漏,并编写更安全、更可靠的 C++ 代码。

控制块,引用计数,弱引用与删除器的关系

控制块保存了引用计数(强引用和弱引用)和删除器。强引用计数决定了对象的生命周期,而弱引用允许观察对象的状态。 删除器决定了对象如何被释放。它们共同保证了 shared_ptr 的正确性和安全性。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注