1. 智能指针概述:从手动管理到自动管理

在C++的世界里,内存管理一直是个让人又爱又恨的话题。爱的是它给了我们极大的控制权,恨的是稍不留神就会导致内存泄漏或悬空指针。传统的newdelete就像手动挡汽车,虽然灵活但需要驾驶员全神贯注。而智能指针则像是自动挡,让我们可以更专注于驾驶本身而非换挡操作。

C++11引入了三种主要的智能指针:unique_ptrshared_ptrweak_ptr。它们各自有不同的所有权语义:

  • unique_ptr:独占所有权,轻量高效
  • shared_ptr:共享所有权,引用计数
  • weak_ptr:观察者模式,不增加引用计数

今天我们要重点探讨的是shared_ptr和它的好搭档weak_ptr,以及如何通过定制删除器来扩展它们的功能。

2. shared_ptr的线程安全性分析

shared_ptr的线程安全性是个经常被误解的话题。很多人以为用了shared_ptr就万事大吉了,其实不然。让我们先看一个简单的例子:

#include <iostream>
#include <memory>
#include <thread>

// 技术栈:C++11及以上

void thread_func(std::shared_ptr<int> sp) {
    // 线程内使用shared_ptr是安全的
    std::cout << "Thread count: " << sp.use_count() << std::endl;
}

int main() {
    auto sp = std::make_shared<int>(42);
    
    std::thread t1(thread_func, sp);
    std::thread t2(thread_func, sp);
    
    std::cout << "Main count: " << sp.use_count() << std::endl;
    
    t1.join();
    t2.join();
    
    return 0;
}

在这个例子中,多个线程同时持有同一个shared_ptr的副本是安全的。但是,如果多个线程同时修改同一个shared_ptr对象(不是副本),那就需要额外的同步机制了。

2.1 shared_ptr的线程安全规则

  1. 引用计数的修改是原子的shared_ptr的引用计数增减操作是线程安全的,这保证了对象只会在最后一个引用消失时被销毁。

  2. 控制块是线程安全的shared_ptr的控制块(包含引用计数等元数据)的修改是线程安全的。

  3. 指向的数据不自动保证线程安全shared_ptr只管理指针的生命周期,不保证指向的数据的线程安全性。

  4. 同一个shared_ptr对象的读写需要同步:如果多个线程要读写同一个shared_ptr对象(不是副本),需要使用锁或其他同步机制。

2.2 不安全的shared_ptr使用示例

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

// 不安全的shared_ptr使用示例
std::shared_ptr<int> global_sp = std::make_shared<int>(0);

void unsafe_increment() {
    for (int i = 0; i < 10000; ++i) {
        // 多个线程同时修改同一个shared_ptr对象
        global_sp = std::make_shared<int>(*global_sp + 1);
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(unsafe_increment);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final value: " << *global_sp << std::endl;
    return 0;
}

这个例子展示了多个线程同时修改同一个shared_ptr对象导致的数据竞争问题。运行结果通常不会是我们期望的100000。

2.3 安全的shared_ptr多线程使用方式

#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>

// 安全的shared_ptr多线程使用
std::shared_ptr<int> global_sp = std::make_shared<int>(0);
std::mutex mtx;

void safe_increment() {
    for (int i = 0; i < 10000; ++i) {
        std::shared_ptr<int> local_sp;
        {
            std::lock_guard<std::mutex> lock(mtx);
            local_sp = global_sp;  // 在锁保护下获取副本
        }
        // 在锁外使用副本是安全的
        auto new_val = std::make_shared<int>(*local_sp + 1);
        {
            std::lock_guard<std::mutex> lock(mtx);
            global_sp = new_val;  // 在锁保护下更新
        }
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(safe_increment);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final value: " << *global_sp << std::endl;
    return 0;
}

这个改进版本使用了互斥锁来保护对global_sp的访问,确保了线程安全。

3. weak_ptr:解决shared_ptr循环引用问题

weak_ptrshared_ptr的观察者,它不会增加引用计数,主要用于解决shared_ptr的循环引用问题。让我们看一个典型的使用场景:

#include <iostream>
#include <memory>

class Node {
public:
    std::string name;
    std::shared_ptr<Node> parent;
    std::shared_ptr<Node> child;
    
    Node(const std::string& n) : name(n) {
        std::cout << "Node " << name << " created" << std::endl;
    }
    
    ~Node() {
        std::cout << "Node " << name << " destroyed" << std::endl;
    }
};

void circular_reference_example() {
    auto node1 = std::make_shared<Node>("Parent");
    auto node2 = std::make_shared<Node>("Child");
    
    node1->child = node2;
    node2->parent = node1;
    
    // 即使离开作用域,node1和node2也不会被销毁
    // 因为它们的引用计数永远不会降到0
}

int main() {
    circular_reference_example();
    std::cout << "End of circular_reference_example" << std::endl;
    return 0;
}

运行这个例子,你会发现析构函数永远不会被调用,这就是典型的循环引用导致的内存泄漏。

3.1 使用weak_ptr解决循环引用

#include <iostream>
#include <memory>

class NodeFixed {
public:
    std::string name;
    std::weak_ptr<NodeFixed> parent;  // 使用weak_ptr替代shared_ptr
    std::shared_ptr<NodeFixed> child;
    
    NodeFixed(const std::string& n) : name(n) {
        std::cout << "Node " << name << " created" << std::endl;
    }
    
    ~NodeFixed() {
        std::cout << "Node " << name << " destroyed" << std::endl;
    }
    
    void printParent() {
        if (auto p = parent.lock()) {  // 尝试获取shared_ptr
            std::cout << name << "'s parent is " << p->name << std::endl;
        } else {
            std::cout << name << "'s parent is expired" << std::endl;
        }
    }
};

void weak_ptr_solution() {
    auto node1 = std::make_shared<NodeFixed>("Parent");
    auto node2 = std::make_shared<NodeFixed>("Child");
    
    node1->child = node2;
    node2->parent = node1;  // 这里使用weak_ptr,不会增加引用计数
    
    node2->printParent();
    
    // 离开作用域时,node1和node2都会被正确销毁
}

int main() {
    weak_ptr_solution();
    std::cout << "End of weak_ptr_solution" << std::endl;
    return 0;
}

在这个改进版本中,我们使用weak_ptr来打破循环引用,内存现在可以被正确释放了。

3.2 weak_ptr的过期检查

weak_ptr提供了expired()方法来检查关联的shared_ptr是否还存在,以及lock()方法来尝试获取一个shared_ptr。看一个更复杂的例子:

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

class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() = default;
};

class Subject {
    std::vector<std::weak_ptr<Observer>> observers;
    
public:
    void addObserver(std::weak_ptr<Observer> obs) {
        observers.push_back(obs);
    }
    
    void notify() {
        auto it = observers.begin();
        while (it != observers.end()) {
            if (auto obs = it->lock()) {
                obs->update();
                ++it;
            } else {
                // 观察者已失效,从列表中移除
                it = observers.erase(it);
            }
        }
    }
};

class ConcreteObserver : public Observer {
    int id;
public:
    ConcreteObserver(int i) : id(i) {}
    void update() override {
        std::cout << "Observer " << id << " notified" << std::endl;
    }
};

int main() {
    Subject subject;
    
    {
        auto obs1 = std::make_shared<ConcreteObserver>(1);
        subject.addObserver(obs1);
        
        {
            auto obs2 = std::make_shared<ConcreteObserver>(2);
            subject.addObserver(obs2);
            subject.notify();  // 两个观察者都会收到通知
        }  // obs2离开作用域被销毁
        
        subject.notify();  // 只有obs1会收到通知
    }  // obs1离开作用域被销毁
    
    subject.notify();  // 没有观察者会收到通知
    
    return 0;
}

这个例子展示了weak_ptr在观察者模式中的应用,它可以自动处理观察者对象的生命周期问题。

4. 定制删除器:扩展shared_ptr的功能

shared_ptr允许我们自定义删除器,这在管理非传统资源时特别有用。默认情况下,shared_ptr会使用delete来释放内存,但我们可以改变这一行为。

4.1 基本定制删除器示例

#include <iostream>
#include <memory>
#include <cstdio>

void file_deleter(FILE* fp) {
    if (fp) {
        std::cout << "Closing file..." << std::endl;
        fclose(fp);
    }
}

int main() {
    // 使用自定义删除器管理文件句柄
    std::shared_ptr<FILE> sp(
        fopen("test.txt", "w"), 
        file_deleter  // 指定自定义删除器
    );
    
    if (sp) {
        fprintf(sp.get(), "Hello, World!");
    }
    
    // 文件会在sp离开作用域时自动关闭
    return 0;
}

4.2 更复杂的删除器示例

#include <iostream>
#include <memory>
#include <functional>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
    void use() { std::cout << "Using resource" << std::endl; }
};

void custom_deleter(Resource* res) {
    std::cout << "Custom deleter called" << std::endl;
    delete res;
}

int main() {
    // 使用函数指针作为删除器
    std::shared_ptr<Resource> sp1(new Resource, custom_deleter);
    
    // 使用lambda表达式作为删除器
    std::shared_ptr<Resource> sp2(
        new Resource,
        [](Resource* res) {
            std::cout << "Lambda deleter called" << std::endl;
            delete res;
        }
    );
    
    // 使用std::function作为删除器
    std::function<void(Resource*)> deleter = [](Resource* res) {
        std::cout << "std::function deleter called" << std::endl;
        delete res;
    };
    std::shared_ptr<Resource> sp3(new Resource, deleter);
    
    sp1->use();
    sp2->use();
    sp3->use();
    
    return 0;
}

4.3 删除器在数组上的应用

#include <iostream>
#include <memory>

int main() {
    // 管理数组的shared_ptr需要自定义删除器
    std::shared_ptr<int> sp(
        new int[10],
        [](int* p) { 
            std::cout << "Deleting array" << std::endl;
            delete[] p; 
        }
    );
    
    for (int i = 0; i < 10; ++i) {
        sp.get()[i] = i * i;
    }
    
    // C++17及以上可以使用shared_ptr管理数组的更好方式
    auto sp17 = std::make_shared<int[]>(10);
    for (int i = 0; i < 10; ++i) {
        sp17[i] = i * i;
    }
    
    return 0;
}

5. 应用场景与最佳实践

5.1 shared_ptr的应用场景

  1. 共享所有权:当多个对象需要共享同一个资源时。
  2. 工厂模式:工厂方法返回对象的所有权给调用者。
  3. 缓存系统:需要自动释放不再使用的缓存项。
  4. 观察者模式:如我们前面看到的weak_ptr示例。

5.2 weak_ptr的应用场景

  1. 打破循环引用:如父子节点、双向链表等场景。
  2. 缓存系统:存储对缓存项的弱引用。
  3. 观察者模式:主题不需要控制观察者的生命周期。
  4. 临时对象访问:需要临时访问可能已被释放的资源。

5.3 定制删除器的应用场景

  1. 管理非内存资源:如文件句柄、网络连接、数据库连接等。
  2. 特殊内存分配:如内存池、对齐内存等。
  3. 调试和日志:在释放资源时记录日志。
  4. 数组管理:如前面所示的管理动态数组。

5.4 注意事项

  1. 避免裸指针转换:不要将裸指针转换为shared_ptr,这可能导致多次删除。
  2. 优先使用make_shared:它更高效,能减少内存分配次数。
  3. 循环引用:注意潜在的循环引用问题,及时使用weak_ptr。
  4. 性能开销:shared_ptr比unique_ptr有额外的性能开销。
  5. 线程安全:记住shared_ptr本身的线程安全规则。

6. 总结

C++智能指针是现代C++编程中不可或缺的工具,它们极大地简化了资源管理,减少了内存泄漏的风险。shared_ptr提供了共享所有权的能力,weak_ptr则解决了循环引用问题,而定制删除器则扩展了智能指针的功能,使其能够管理各种类型的资源。

在实际开发中,我们应该:

  1. 优先使用make_shared创建shared_ptr
  2. 在可能产生循环引用的地方使用weak_ptr
  3. 根据资源类型选择合适的删除器
  4. 注意智能指针的线程安全特性
  5. 避免混合使用智能指针和裸指针

智能指针不是银弹,但它们确实能帮助我们编写更安全、更易维护的代码。理解它们的内部机制和使用场景,可以让我们在合适的场合发挥它们的最大价值。