第二章:智能指针

📚 章节概述

智能指针是现代C++中最重要的特性之一,它们通过RAII(Resource Acquisition Is Initialization)原则自动管理内存,避免了传统裸指针可能导致的内存泄漏、野指针等问题。本章将深入介绍三种主要的智能指针:unique_ptrshared_ptrweak_ptr

🎯 学习目标

学完本章后,您将能够:

  • 理解智能指针的设计原理和RAII概念
  • 掌握unique_ptr的使用场景和最佳实践
  • 理解shared_ptr的引用计数机制
  • 使用weak_ptr解决循环引用问题
  • 掌握自定义删除器的用法
  • 了解智能指针与STL容器的配合使用
  • 遵循智能指针的最佳实践原则

1. 智能指针基础概念

📖 理论讲解

智能指针是封装了裸指针的类模板,通过RAII原则自动管理动态分配的内存。当智能指针离开作用域时,会自动释放其管理的内存。

💡 核心概念

  • RAII原则:Resource Acquisition Is Initialization,资源获取即初始化
  • 自动内存管理:无需手动调用delete
  • 异常安全:即使发生异常也能正确释放资源
  • 所有权语义:明确表达资源的所有权关系

🔍 传统指针的问题

// 传统指针的问题示例
void problematic_function() {
    int* ptr = new int(42);
    
    // 如果这里抛出异常,内存就泄漏了
    if (some_condition()) {
        throw std::runtime_error("Error occurred");
    }
    
    delete ptr;  // 可能永远执行不到
}

// 智能指针的解决方案
void safe_function() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    
    // 即使抛出异常,ptr也会自动释放内存
    if (some_condition()) {
        throw std::runtime_error("Error occurred");
    }
    
    // 无需手动delete
}

2. unique_ptr - 独占所有权智能指针

📖 理论讲解

unique_ptr提供对动态分配对象的独占所有权。它不能被拷贝,只能被移动,确保任何时候只有一个unique_ptr指向特定的对象。

💡 核心特性

  • 独占所有权:同一时间只能有一个unique_ptr拥有对象
  • 移动语义:支持移动但不支持拷贝
  • 零开销:几乎没有性能开销
  • 自定义删除器:可以自定义资源释放方式

🔍 基本用法

#include <memory>

// 1. 创建unique_ptr
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2(new int(100));  // 也可以,但不推荐

// 2. 访问对象
std::cout << *ptr1 << std::endl;  // 解引用
if (ptr1) {  // 检查是否为空
    std::cout << "ptr1 is not null" << std::endl;
}

// 3. 移动所有权
std::unique_ptr<int> ptr3 = std::move(ptr1);
// 现在ptr1为空,ptr3拥有对象

// 4. 重置和释放
ptr3.reset();  // 删除对象,ptr3变为空
ptr3.reset(new int(200));  // 删除旧对象,指向新对象

// 5. 释放所有权(不删除对象)
int* raw_ptr = ptr3.release();
delete raw_ptr;  // 需要手动删除

🚀 高级用法

// 1. 数组的unique_ptr
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
arr[0] = 42;  // 支持数组索引操作

// 2. 自定义删除器
auto custom_deleter = [](FILE* f) {
    if (f) {
        std::cout << "Closing file" << std::endl;
        fclose(f);
    }
};

std::unique_ptr<FILE, decltype(custom_deleter)> file_ptr(
    fopen("test.txt", "w"), custom_deleter);

⚠️ 注意事项

  • 不能直接赋值拷贝,只能移动
  • 使用make_unique比直接使用new更安全
  • 可以存储在容器中(通过移动)

🎯 使用场景

  1. 独占资源管理:文件句柄、网络连接等
  2. 工厂函数返回值:返回动态创建的对象
  3. 容器元素:存储多态对象
  4. PIMPL惯用法:隐藏实现细节

3. shared_ptr - 共享所有权智能指针

📖 理论讲解

shared_ptr使用引用计数来管理对象的生命周期。多个shared_ptr可以指向同一个对象,当最后一个shared_ptr销毁时,对象才会被删除。

💡 核心特性

  • 共享所有权:多个shared_ptr可以共享同一对象
  • 引用计数:自动跟踪指向对象的shared_ptr数量
  • 线程安全:引用计数操作是线程安全的
  • 支持拷贝:可以像普通对象一样拷贝和赋值

🔍 基本用法

// 1. 创建shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2(new int(100));  // 不推荐

// 2. 共享所有权
std::shared_ptr<int> ptr3 = ptr1;  // 引用计数变为2
std::cout << "Use count: " << ptr1.use_count() << std::endl;  // 输出: 2

// 3. 检查引用计数
if (ptr1.use_count() == 1) {
    std::cout << "Only one reference" << std::endl;
}

// 4. 重置
ptr3.reset();  // 引用计数减1
ptr1.reset();  // 引用计数变为0,对象被删除

🚀 高级特性

// 1. 弱引用转换
std::weak_ptr<int> weak = ptr1;
if (auto locked = weak.lock()) {  // 尝试获取shared_ptr
    std::cout << *locked << std::endl;
}

// 2. 别名构造
struct Node {
    std::shared_ptr<Node> child;
    int value;
};

std::shared_ptr<Node> node = std::make_shared<Node>();
std::shared_ptr<int> value_ptr(node, &node->value);  // 指向value但共享node的生命周期

// 3. 自定义删除器
std::shared_ptr<FILE> file(fopen("test.txt", "r"), 
                          [](FILE* f) { if(f) fclose(f); });

⚠️ 注意事项

  • 有一定的内存和性能开销
  • 避免循环引用(使用weak_ptr解决)
  • 优先使用make_shared而不是new

🎯 使用场景

  1. 共享资源:多个对象需要访问同一资源
  2. 缓存系统:多个地方引用同一对象
  3. 观察者模式:多个观察者共享被观察对象
  4. 图结构:节点之间的复杂引用关系

4. weak_ptr - 弱引用智能指针

📖 理论讲解

weak_ptr提供对shared_ptr管理对象的弱引用,不影响对象的生命周期。主要用于解决shared_ptr的循环引用问题。

💡 核心特性

  • 不影响引用计数:不会增加shared_ptr的引用计数
  • 避免循环引用:打破shared_ptr循环引用
  • 安全检查:可以检查对象是否还存在
  • 临时访问:可以临时获得shared_ptr来访问对象

🔍 基本用法

// 1. 创建weak_ptr
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

// 2. 检查对象是否存在
if (!weak.expired()) {
    std::cout << "Object still exists" << std::endl;
}

// 3. 安全访问对象
if (auto locked = weak.lock()) {
    std::cout << *locked << std::endl;
} else {
    std::cout << "Object has been destroyed" << std::endl;
}

// 4. 获取引用计数
std::cout << "Use count: " << weak.use_count() << std::endl;

🚀 解决循环引用

// 循环引用问题示例
class Node {
public:
    std::shared_ptr<Node> child;
    std::weak_ptr<Node> parent;  // 使用weak_ptr避免循环引用
    std::string name;
    
    Node(const std::string& n) : name(n) {
        std::cout << "Node " << name << " created" << std::endl;
    }
    
    ~Node() {
        std::cout << "Node " << name << " destroyed" << std::endl;
    }
    
    void set_child(std::shared_ptr<Node> c) {
        child = c;
        if (c) {
            c->parent = shared_from_this();  // 需要继承enable_shared_from_this
        }
    }
};

// 正确的使用方式
auto root = std::make_shared<Node>("Root");
auto child = std::make_shared<Node>("Child");
root->set_child(child);
// 当root和child离开作用域时,都会被正确销毁

🎯 使用场景

  1. 父子关系:子对象持有父对象的弱引用
  2. 观察者模式:观察者持有被观察对象的弱引用
  3. 缓存系统:缓存对象的弱引用,允许对象被自动清理
  4. 回调函数:避免回调中的循环引用

5. 自定义删除器

📖 理论讲解

智能指针允许自定义删除器来处理特殊的资源释放需求,如文件句柄、网络连接等非内存资源。

💡 核心概念

  • 资源多样性:不仅仅管理内存,还可以管理其他资源
  • RAII扩展:将RAII原则应用到各种资源
  • 类型安全:编译时确定删除器类型

🔍 使用示例

// 1. 函数指针删除器
void close_file(FILE* f) {
    if (f) {
        std::cout << "Closing file" << std::endl;
        fclose(f);
    }
}

std::unique_ptr<FILE, void(*)(FILE*)> file_ptr(fopen("test.txt", "w"), close_file);

// 2. Lambda删除器
auto file_closer = [](FILE* f) {
    if (f) {
        std::cout << "Lambda closing file" << std::endl;
        fclose(f);
    }
};

std::unique_ptr<FILE, decltype(file_closer)> file_ptr2(
    fopen("test2.txt", "w"), file_closer);

// 3. 函数对象删除器
struct ArrayDeleter {
    void operator()(int* p) {
        std::cout << "Deleting array" << std::endl;
        delete[] p;
    }
};

std::unique_ptr<int, ArrayDeleter> arr_ptr(new int[10]);

// 4. shared_ptr的删除器(类型擦除)
std::shared_ptr<FILE> shared_file(
    fopen("test3.txt", "w"),
    [](FILE* f) { if(f) fclose(f); }
);

🚀 高级应用

// 管理系统资源
class SocketDeleter {
public:
    void operator()(int* socket_fd) {
        if (socket_fd && *socket_fd != -1) {
            std::cout << "Closing socket " << *socket_fd << std::endl;
            close(*socket_fd);
        }
        delete socket_fd;
    }
};

std::unique_ptr<int, SocketDeleter> socket_ptr(new int(socket(AF_INET, SOCK_STREAM, 0)));

// 管理Windows句柄(示例)
#ifdef _WIN32
struct HandleDeleter {
    void operator()(HANDLE handle) {
        if (handle != INVALID_HANDLE_VALUE) {
            CloseHandle(handle);
        }
    }
};

std::unique_ptr<void, HandleDeleter> handle_ptr(CreateFile(...));
#endif

🎯 最佳实践

  1. Lambda优先:对于简单的删除逻辑,使用Lambda
  2. 函数对象复用:复杂逻辑可以创建专门的删除器类
  3. 异常安全:删除器不应该抛出异常
  4. null检查:删除器应该检查指针是否为null

6. 智能指针与STL容器

📖 理论讲解

智能指针与STL容器的结合使用是现代C++中管理动态对象集合的标准方式。

🔍 容器中的智能指针

// 1. unique_ptr容器
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));

// 遍历多态对象
for (const auto& shape : shapes) {
    shape->draw();  // 多态调用
}

// 2. shared_ptr容器
std::vector<std::shared_ptr<Resource>> resources;
auto res = std::make_shared<Resource>("resource1");
resources.push_back(res);
resources.push_back(res);  // 同一资源可以被多次引用

// 3. 容器操作
// 移动元素
auto moved_shape = std::move(shapes[0]);
shapes.erase(shapes.begin());

// 查找和操作
auto it = std::find_if(shapes.begin(), shapes.end(),
    [](const std::unique_ptr<Shape>& s) {
        return s->area() > 10.0;
    });

🚀 高级模式

// 1. 工厂模式与容器
class ShapeFactory {
public:
    static std::unique_ptr<Shape> createShape(const std::string& type) {
        if (type == "circle") {
            return std::make_unique<Circle>(1.0);
        } else if (type == "rectangle") {
            return std::make_unique<Rectangle>(1.0, 1.0);
        }
        return nullptr;
    }
};

std::vector<std::unique_ptr<Shape>> shapes;
std::vector<std::string> types = {"circle", "rectangle", "circle"};
for (const auto& type : types) {
    if (auto shape = ShapeFactory::createShape(type)) {
        shapes.push_back(std::move(shape));
    }
}

// 2. 缓存模式
class ResourceCache {
private:
    std::unordered_map<std::string, std::weak_ptr<Resource>> cache_;
    
public:
    std::shared_ptr<Resource> getResource(const std::string& name) {
        auto it = cache_.find(name);
        if (it != cache_.end()) {
            if (auto locked = it->second.lock()) {
                return locked;  // 返回缓存的资源
            } else {
                cache_.erase(it);  // 清理过期的弱引用
            }
        }
        
        // 创建新资源
        auto resource = std::make_shared<Resource>(name);
        cache_[name] = resource;
        return resource;
    }
};

7. 智能指针最佳实践

🎯 选择指南

场景 推荐指针 原因
独占资源 unique_ptr 性能最好,语义清晰
共享资源 shared_ptr 支持多重所有权
避免循环引用 weak_ptr 不影响生命周期
管理数组 unique_ptr<T[]> 自动调用delete[]
函数返回值 unique_ptr 明确所有权转移
函数参数 原始指针或引用 不转移所有权

🚀 编程指南

// 1. 优先使用make_unique和make_shared
auto ptr1 = std::make_unique<MyClass>(args);  // 推荐
auto ptr2 = std::unique_ptr<MyClass>(new MyClass(args));  // 不推荐

auto sptr1 = std::make_shared<MyClass>(args);  // 推荐
auto sptr2 = std::shared_ptr<MyClass>(new MyClass(args));  // 不推荐

// 2. 函数参数设计
void process_object(const MyClass& obj);  // 不转移所有权,使用引用
void process_object(MyClass* obj);        // 可能为null,使用指针
std::unique_ptr<MyClass> create_object(); // 转移所有权,返回unique_ptr

// 3. 避免从裸指针创建多个智能指针
MyClass* raw_ptr = new MyClass();
auto ptr1 = std::shared_ptr<MyClass>(raw_ptr);  // 危险!
auto ptr2 = std::shared_ptr<MyClass>(raw_ptr);  // 会导致双重删除

// 正确做法
auto ptr1 = std::make_shared<MyClass>();
auto ptr2 = ptr1;  // 安全的共享

// 4. 使用enable_shared_from_this
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    void register_callback() {
        callback_manager.register_callback(shared_from_this());
    }
};

⚠️ 常见陷阱

  1. 双重删除:从同一个裸指针创建多个智能指针
  2. 循环引用:shared_ptr相互引用导致内存泄漏
  3. 悬挂引用:weak_ptr.lock()后不检查返回值
  4. 过度使用shared_ptr:能用unique_ptr就不要用shared_ptr

🧪 实践练习

练习1:设计一个简单的文件管理器

class FileManager {
public:
    // 使用适当的智能指针设计以下接口:
    // 1. 打开文件(独占访问)
    // 2. 共享只读文件
    // 3. 管理文件缓存
};

练习2:实现观察者模式

class Subject; // 前向声明
class Observer {
public:
    // 使用weak_ptr避免循环引用
    virtual void update(/* 参数 */) = 0;
};

class Subject {
    // 管理观察者列表,避免循环引用
};

练习3:设计图数据结构

class GraphNode {
public:
    // 如何设计节点之间的连接关系?
    // 考虑:父子关系、邻接关系、避免循环引用
};

📝 本章小结

智能指针是现代C++内存管理的核心工具:

  • unique_ptr:独占所有权,性能最优,适合大多数场景
  • shared_ptr:共享所有权,支持多重引用,适合共享资源
  • weak_ptr:弱引用,解决循环引用,适合观察者模式

🎯 选择原则

  1. 默认选择unique_ptr:除非确实需要共享所有权
  2. 需要共享时使用shared_ptr:多个对象需要访问同一资源
  3. 使用weak_ptr打破循环:避免shared_ptr循环引用
  4. 自定义删除器管理特殊资源:文件、socket、句柄等

🚀 进阶提示

  • 结合移动语义提高性能
  • 理解enable_shared_from_this的用法
  • 掌握智能指针的类型转换
  • 学习智能指针与异常安全的关系

智能指针不仅解决了内存管理问题,更重要的是让代码表达了清晰的所有权语义,这是现代C++设计的重要理念。


🔗 相关资源

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐