<C++> 智能指针

1. 引言

在C++编程中,内存管理一直是一个核心且复杂的话题。传统的手动内存管理(使用newdelete)容易导致内存泄漏、悬空指针和双重释放等问题。为了解决这些问题,C++11引入了智能指针(Smart Pointers),它们通过RAII(Resource Acquisition Is Initialization)技术自动管理内存生命周期,大大提高了代码的安全性和可维护性。

2. 智能指针的核心原理

2.1 RAII设计模式

智能指针基于RAII设计模式,其核心思想是:

  • 资源获取即初始化:在对象构造时获取资源
  • 资源释放自动化:在对象析构时自动释放资源
  • 异常安全保证:即使发生异常,资源也能正确释放

2.2 引用计数机制

大多数智能指针使用引用计数来跟踪资源的使用情况:

  • 当智能指针被创建时,引用计数为1
  • 当智能指针被复制时,引用计数增加
  • 当智能指针被销毁或重置时,引用计数减少
  • 当引用计数为0时,自动释放资源

3. C++智能指针类型详解

3.1 std::unique_ptr(独占指针)

std::unique_ptr表示独占所有权的智能指针,同一时间只能有一个unique_ptr指向特定资源。

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructed\n"; }
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
    void sayHello() { std::cout << "Hello from MyClass!\n"; }
};

int main() {
    // 创建unique_ptr
    std::unique_ptr<MyClass> ptr1(new MyClass());
    ptr1->sayHello();
    
    // 使用make_unique(C++14及以上)
    auto ptr2 = std::make_unique<MyClass>();
    
    // 转移所有权(移动语义)
    std::unique_ptr<MyClass> ptr3 = std::move(ptr1);
    
    // ptr1现在为空,ptr3拥有资源
    if (!ptr1) {
        std::cout << "ptr1 is now empty\n";
    }
    
    // 自动释放内存,无需手动delete
    return 0;
}

特点

  • 独占所有权,不可复制
  • 支持移动语义
  • 零额外开销(与裸指针相当)
  • 可自定义删除器

3.2 std::shared_ptr(共享指针)

std::shared_ptr允许多个指针共享同一资源,使用引用计数管理生命周期。

cpp 复制代码
#include <iostream>
#include <memory>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource released\n"; }
    void use() { std::cout << "Resource in use\n"; }
};

int main() {
    // 创建shared_ptr
    std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();
    
    {
        // 共享所有权
        std::shared_ptr<Resource> ptr2 = ptr1;
        std::cout << "Use count: " << ptr1.use_count() << "\n"; // 输出: 2
        
        ptr1->use();
        ptr2->use();
    }
    
    // ptr2离开作用域,引用计数减1
    std::cout << "Use count: " << ptr1.use_count() << "\n"; // 输出: 1
    
    // 循环引用示例(潜在问题)
    class Node {
    public:
        std::shared_ptr<Node> next;
        ~Node() { std::cout << "Node destroyed\n"; }
    };
    
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    node1->next = node2;
    node2->next = node1; // 循环引用!
    
    return 0;
}

特点

  • 共享所有权,支持复制
  • 使用引用计数
  • 可能产生循环引用问题
  • 支持自定义删除器

3.3 std::weak_ptr(弱指针)

std::weak_ptrshared_ptr的观察者,不增加引用计数,用于解决循环引用问题。

cpp 复制代码
#include <iostream>
#include <memory>

class Observer;

class Subject {
public:
    std::shared_ptr<Observer> observer;
    ~Subject() { std::cout << "Subject destroyed\n"; }
};

class Observer {
public:
    std::weak_ptr<Subject> subject; // 使用weak_ptr避免循环引用
    
    void checkSubject() {
        if (auto spt = subject.lock()) { // 尝试获取shared_ptr
            std::cout << "Subject is still alive\n";
        } else {
            std::cout << "Subject has been destroyed\n";
        }
    }
    
    ~Observer() { std::cout << "Observer destroyed\n"; }
};

int main() {
    auto subject = std::make_shared<Subject>();
    auto observer = std::make_shared<Observer>();
    
    subject->observer = observer;
    observer->subject = subject; // 使用weak_ptr,不会增加引用计数
    
    observer->checkSubject();
    
    // 当subject和observer离开作用域时,都能正确销毁
    return 0;
}

特点

  • 不增加引用计数
  • 需要调用lock()获取shared_ptr
  • 用于观察和解决循环引用

4. 智能指针的实用技巧

4.1 自定义删除器

cpp 复制代码
#include <iostream>
#include <memory>
#include <cstdio>

// 文件指针的自定义删除器
struct FileDeleter {
    void operator()(FILE* fp) {
        if (fp) {
            std::cout << "Closing file\n";
            fclose(fp);
        }
    }
};

int main() {
    // 使用自定义删除器管理文件
    std::unique_ptr<FILE, FileDeleter> filePtr(fopen("test.txt", "w"));
    
    if (filePtr) {
        fprintf(filePtr.get(), "Hello, World!\n");
    }
    
    // 使用lambda表达式作为删除器
    auto arrayDeleter = [](int* p) {
        std::cout << "Deleting array\n";
        delete[] p;
    };
    
    std::unique_ptr<int[], decltype(arrayDeleter)> 
        arrPtr(new int[10], arrayDeleter);
    
    return 0;
}

4.2 智能指针与多态

cpp 复制代码
#include <iostream>
#include <memory>

class Base {
public:
    virtual void print() const {
        std::cout << "Base class\n";
    }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void print() const override {
        std::cout << "Derived class\n";
    }
};

int main() {
    // 使用基类智能指针管理派生类对象
    std::unique_ptr<Base> ptr = std::make_unique<Derived>();
    ptr->print(); // 输出: Derived class
    
    // shared_ptr也支持多态
    std::shared_ptr<Base> sharedPtr = std::make_shared<Derived>();
    
    return 0;
}

4.3 智能指针与STL容器

cpp 复制代码
#include <iostream>
#include <memory>
#include <vector>
#include <map>

class Item {
public:
    Item(int id) : id(id) {
        std::cout << "Item " << id << " created\n";
    }
    ~Item() {
        std::cout << "Item " << id << " destroyed\n";
    }
    
    int id;
};

int main() {
    // vector中的unique_ptr
    std::vector<std::unique_ptr<Item>> items;
    items.push_back(std::make_unique<Item>(1));
    items.push_back(std::make_unique<Item>(2));
    
    // map中的shared_ptr
    std::map<std::string, std::shared_ptr<Item>> itemMap;
    itemMap["first"] = std::make_shared<Item>(101);
    itemMap["second"] = std::make_shared<Item>(102);
    
    // 使用emplace_back避免临时对象
    items.emplace_back(std::make_unique<Item>(3));
    
    return 0;
}

5. 性能考虑与最佳实践

5.1 性能对比

指针类型 内存开销 性能开销 适用场景
裸指针 简单场景,明确生命周期
unique_ptr 独占所有权,性能关键
shared_ptr 控制块(约16-32字节) 引用计数操作 共享所有权
weak_ptr 控制块引用 lock()调用开销 观察者,解决循环引用

5.2 最佳实践

  1. 优先使用make_uniquemake_shared

    cpp 复制代码
    // 好:异常安全,一次内存分配
    auto ptr = std::make_shared<MyClass>();
    
    // 不好:可能内存泄漏,两次内存分配
    std::shared_ptr<MyClass> ptr(new MyClass());
  2. 明确所有权语义

    • 使用unique_ptr表示独占所有权
    • 使用shared_ptr表示共享所有权
    • 使用weak_ptr表示弱引用
  3. 避免循环引用

    cpp 复制代码
    // 使用weak_ptr打破循环引用
    class Parent {
        std::shared_ptr<Child> child;
    };
    
    class Child {
        std::weak_ptr<Parent> parent; // 使用weak_ptr
    };
  4. 不要混合使用智能指针和裸指针

    cpp 复制代码
    // 危险:混合使用
    MyClass* rawPtr = new MyClass();
    std::shared_ptr<MyClass> smartPtr(rawPtr);
    
    // 另一个shared_ptr使用同一个rawPtr创建
    std::shared_ptr<MyClass> anotherPtr(rawPtr); // 双重释放!
  5. 在接口中明确传递语义

    cpp 复制代码
    // 接受unique_ptr表示转移所有权
    void takeOwnership(std::unique_ptr<MyClass> ptr);
    
    // 接受shared_ptr表示共享所有权
    void shareResource(std::shared_ptr<MyClass> ptr);
    
    // 接受weak_ptr表示观察
    void observe(std::weak_ptr<MyClass> ptr);

6. 常见问题与解决方案

6.1 循环引用问题

问题 :两个shared_ptr相互引用,导致引用计数永不为0。

解决方案 :使用weak_ptr打破循环。

cpp 复制代码
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 使用weak_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a; // 不会增加引用计数
    
    return 0; // A和B都能正确销毁
}

6.2 this指针问题

问题 :在类成员函数中创建指向自身的shared_ptr

解决方案 :使用std::enable_shared_from_this

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    std::shared_ptr<MyClass> getShared() {
        return shared_from_this(); // 安全获取shared_ptr
    }
};

int main() {
    auto ptr = std::make_shared<MyClass>();
    auto ptr2 = ptr->getShared(); // 安全
    std::cout << "Use count: " << ptr.use_count() << "\n"; // 输出: 2
    
    return 0;
}

6.3 数组管理

cpp 复制代码
// unique_ptr支持数组
std::unique_ptr<int[]> arr(new int[10]);

// shared_ptr需要自定义删除器管理数组
std::shared_ptr<int> arrShared(new int[10], std::default_delete<int[]>());

// 或者使用vector替代
std::vector<int> vec(10);

7. 总结

C++智能指针是现代C++编程中不可或缺的工具,它们通过自动化的内存管理大大减少了内存泄漏和资源管理错误。掌握unique_ptrshared_ptrweak_ptr的特性和适用场景,能够帮助您编写更安全、更健壮的C++代码。

关键要点

  1. 优先使用make_uniquemake_shared创建智能指针
  2. 根据所有权需求选择合适的智能指针类型
  3. 使用weak_ptr解决循环引用问题
  4. 在类设计中考虑智能指针的使用模式
  5. 遵循RAII原则,让资源管理自动化
相关推荐
fox_lht3 小时前
第十四章 一个输入和输出项目:构建一个命令行程序
开发语言·后端·rust
郑州光合科技余经理3 小时前
海外版外卖系统:如何快速搭建国际化外卖平台
java·开发语言·前端·人工智能·小程序·系统架构·php
Cheng小攸3 小时前
协议分析与分析工具(一)
开发语言·php
玖玥拾3 小时前
C/C++ 基础笔记(五)
c语言·c++·指针
fox_lht3 小时前
14.2.读文件
开发语言·后端·rust
codeejun3 小时前
每日一Go-74、Go 云原生可观测性实战之OpenTelemetry 全链路采集:Trace + Metrics + Logs
开发语言·云原生·golang
神仙别闹3 小时前
基于 Python 实现 ANN 与 KNN 的图像分类
开发语言·python·分类
yugi9878383 小时前
基于Qt的实用二维码生成解决方案
开发语言·qt
_小许_3 小时前
Go语言导入与导出excel文件
开发语言·golang·excel