【C++】零基础入门 · 第 16 节:智能指针

在 C++ 中,动态内存管理是一个让人头疼的问题。忘记 delete 会导致内存泄漏,多次 delete 会导致程序崩溃。智能指针的出现,让内存管理变得自动化和安全。

1. 为什么需要智能指针?

1.1 手动内存管理的问题

cpp 复制代码
void problematic() {
    int* ptr = new int(42);
    
    // 如果这里发生异常...
    doSomething();  // 可能抛出异常
    
    delete ptr;  // 这行可能永远执行不到
}

常见问题

  • 内存泄漏 :忘记 delete
  • 悬垂指针delete 后继续使用
  • 重复释放 :多次 delete 同一个指针
  • 异常安全 :异常导致 delete 未执行

1.2 智能指针的解决方案

智能指针利用 RAII(Resource Acquisition Is Initialization)思想:

  • 构造时获取资源(new
  • 析构时释放资源(delete
  • 离开作用域时自动调用析构函数

2. unique_ptr(独占指针)

2.1 基本特性

unique_ptr 表示独占所有权

  • 同一时刻只能有一个 unique_ptr 指向同一个对象
  • 不能复制,只能移动
  • 适用于大多数场景的首选

2.2 基本用法

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 创建 unique_ptr
    unique_ptr<int> ptr1 = make_unique<int>(42);
    
    // 访问值
    cout << *ptr1 << endl;  // 输出:42
    
    // 检查是否为空
    if (ptr1) {
        cout << "ptr1 不为空" << endl;
    }
    
    // 获取原始指针(不推荐频繁使用)
    int* raw = ptr1.get();
    cout << *raw << endl;  // 输出:42
    
    // 离开作用域时自动释放内存
    return 0;
}

2.3 管理数组

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 管理动态数组
    unique_ptr<int[]> arr = make_unique<int[]>(5);
    
    for (int i = 0; i < 5; i++) {
        arr[i] = i * 10;
    }
    
    for (int i = 0; i < 5; i++) {
        cout << arr[i] << " ";  // 输出:0 10 20 30 40
    }
    cout << endl;
    
    // 自动调用 delete[]
    return 0;
}

2.4 转移所有权

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int> ptr1 = make_unique<int>(42);
    
    // unique_ptr<int> ptr2 = ptr1;  // 错误!不能复制
    
    unique_ptr<int> ptr2 = move(ptr1);  // 移动所有权
    
    // ptr1 现在为空
    if (!ptr1) {
        cout << "ptr1 为空" << endl;
    }
    
    cout << *ptr2 << endl;  // 输出:42
    
    return 0;
}

2.5 作为函数参数和返回值

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

// 作为参数(转移所有权)
void takeOwnership(unique_ptr<int> ptr) {
    cout << "接收到:" << *ptr << endl;
    // 函数结束时自动释放
}

// 作为返回值
unique_ptr<int> createObject() {
    return make_unique<int>(100);
}

// 引用传递(不转移所有权)
void useObject(const unique_ptr<int>& ptr) {
    cout << "使用对象:" << *ptr << endl;
}

int main() {
    auto ptr = make_unique<int>(42);
    
    useObject(ptr);  // 引用传递,ptr 仍然有效
    takeOwnership(move(ptr));  // 移动所有权,ptr 变为空
    
    auto newObj = createObject();
    cout << *newObj << endl;  // 输出:100
    
    return 0;
}

2.6 自定义删除器

cpp 复制代码
#include <iostream>
#include <memory>
#include <cstdio>
using namespace std;

int main() {
    // 使用自定义删除器管理文件
    unique_ptr<FILE, decltype(&fclose)> file(
        fopen("test.txt", "w"),
        fclose
    );
    
    if (file) {
        fprintf(file.get(), "Hello, Smart Pointer!\n");
    }
    // 文件会在 unique_ptr 销毁时自动关闭
    
    return 0;
}

3. shared_ptr(共享指针)

3.1 基本特性

shared_ptr 表示共享所有权

  • 多个 shared_ptr 可以指向同一个对象
  • 内部使用引用计数
  • 当最后一个 shared_ptr 销毁时,释放对象

3.2 基本用法

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 创建 shared_ptr
    shared_ptr<int> ptr1 = make_shared<int>(42);
    cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:1
    
    {
        shared_ptr<int> ptr2 = ptr1;  // 复制,引用计数+1
        cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:2
        
        shared_ptr<int> ptr3 = ptr1;
        cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:3
    }  // ptr2 和 ptr3 销毁,引用计数-2
    
    cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:1
    cout << *ptr1 << endl;  // 输出:42
    
    return 0;
}  // ptr1 销毁,对象释放

3.3 引用计数机制

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

class MyClass {
public:
    MyClass(int v) : value(v) {
        cout << "构造:" << value << endl;
    }
    ~MyClass() {
        cout << "析构:" << value << endl;
    }
    int value;
};

int main() {
    cout << "=== 创建对象 ===" << endl;
    shared_ptr<MyClass> ptr1 = make_shared<MyClass>(100);
    
    cout << "\n=== 复制指针 ===" << endl;
    shared_ptr<MyClass> ptr2 = ptr1;
    shared_ptr<MyClass> ptr3 = ptr1;
    
    cout << "引用计数:" << ptr1.use_count() << endl;
    
    cout << "\n=== 重置指针 ===" << endl;
    ptr2.reset();  // 引用计数-1
    cout << "引用计数:" << ptr1.use_count() << endl;
    
    cout << "\n=== 程序结束 ===" << endl;
    return 0;
}

输出

复制代码
=== 创建对象 ===
构造:100

=== 复制指针 ===
引用计数:3

=== 重置指针 ===
引用计数:2

=== 程序结束 ===
析构:100

3.4 make_shared 的优势

cpp 复制代码
// 方式一:分开写(两次内存分配)
shared_ptr<int> ptr1(new int(42));

// 方式二:make_shared(一次内存分配)
shared_ptr<int> ptr2 = make_shared<int>(42);

make_shared 的优势:

  • 性能更好:一次内存分配(对象和控制块一起分配)
  • 异常安全:避免中间状态的内存泄漏
  • 更简洁:不需要写 new

3.5 shared_ptr 的陷阱

陷阱 1:循环引用

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

class B;  // 前向声明

class A {
public:
    shared_ptr<B> b_ptr;
    ~A() { cout << "A 析构" << endl; }
};

class B {
public:
    shared_ptr<A> a_ptr;  // 问题所在!
    ~B() { cout << "B 析构" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a;
    
    cout << "a 引用计数:" << a.use_count() << endl;  // 输出:2
    cout << "b 引用计数:" << b.use_count() << endl;  // 输出:2
    
    return 0;
    // a 和 b 都不会被析构!内存泄漏
}

解决方案 :使用 weak_ptr(见下一节)

陷阱 2:从原始指针创建多个 shared_ptr

cpp 复制代码
int* raw = new int(42);

shared_ptr<int> ptr1(raw);
shared_ptr<int> ptr2(raw);  // 错误!会导致重复释放

陷阱 3:shared_ptr 管理数组(C++17 前)

cpp 复制代码
// C++17 前需要自定义删除器
shared_ptr<int> arr(new int[10], default_delete<int[]>());

// C++17 后可以直接使用
shared_ptr<int[]> arr2(new int[10]);

4. weak_ptr(弱引用指针)

4.1 基本特性

weak_ptrshared_ptr 的助手:

  • 不增加引用计数
  • 不拥有对象的所有权
  • 用于解决循环引用问题
  • 可以检测对象是否还存在

4.2 基本用法

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    weak_ptr<int> weak;
    
    {
        shared_ptr<int> shared = make_shared<int>(42);
        weak = shared;  // weak 指向 shared 管理的对象
        
        cout << "shared 引用计数:" << shared.use_count() << endl;  // 输出:1
        cout << "weak 是否过期:" << weak.expired() << endl;  // 输出:0 (false)
        
        // 通过 weak 获取 shared
        if (auto locked = weak.lock()) {
            cout << "值:" << *locked << endl;  // 输出:42
            cout << "shared 引用计数:" << locked.use_count() << endl;  // 输出:2
        }
    }  // shared 销毁,对象释放
    
    cout << "weak 是否过期:" << weak.expired() << endl;  // 输出:1 (true)
    
    auto locked = weak.lock();
    if (!locked) {
        cout << "对象已不存在" << endl;
    }
    
    return 0;
}

4.3 解决循环引用

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

class B;

class A {
public:
    shared_ptr<B> b_ptr;
    ~A() { cout << "A 析构" << endl; }
};

class B {
public:
    weak_ptr<A> a_ptr;  // 使用 weak_ptr 打破循环
    ~B() { cout << "B 析构" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a;
    
    cout << "a 引用计数:" << a.use_count() << endl;  // 输出:1
    cout << "b 引用计数:" << b.use_count() << endl;  // 输出:2
    
    return 0;
    // a 和 b 都能正常析构
}

4.4 实际应用:缓存系统

cpp 复制代码
#include <iostream>
#include <memory>
#include <unordered_map>
using namespace std;

class Data {
public:
    string content;
    Data(string c) : content(c) {
        cout << "Data 构造:" << content << endl;
    }
    ~Data() {
        cout << "Data 析构:" << content << endl;
    }
};

class Cache {
private:
    unordered_map<string, weak_ptr<Data>> cache;
    
public:
    shared_ptr<Data> get(const string& key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            // 尝试获取 shared_ptr
            if (auto ptr = it->second.lock()) {
                cout << "缓存命中:" << key << endl;
                return ptr;
            } else {
                // 对象已被释放,删除过期条目
                cache.erase(it);
            }
        }
        cout << "缓存未命中:" << key << endl;
        return nullptr;
    }
    
    void put(const string& key, shared_ptr<Data> data) {
        cache[key] = data;
    }
};

int main() {
    Cache cache;
    
    {
        auto data = make_shared<Data>("Hello");
        cache.put("greeting", data);
        
        auto retrieved = cache.get("greeting");
        if (retrieved) {
            cout << "内容:" << retrieved->content << endl;
        }
    }  // data 销毁
    
    auto retrieved = cache.get("greeting");
    if (!retrieved) {
        cout << "数据已被释放" << endl;
    }
    
    return 0;
}

5. 智能指针对比

特性 unique_ptr shared_ptr weak_ptr
所有权 独占 共享
复制
移动 -
引用计数 不影响
内存开销 最小 较大 较小
适用场景 多数场景 共享所有权 打破循环引用

6. 最佳实践

6.1 优先使用 unique_ptr

cpp 复制代码
// 好:明确的所有权
unique_ptr<Widget> createWidget() {
    return make_unique<Widget>();
}

// 只在需要共享时使用 shared_ptr
shared_ptr<Widget> shared = make_shared<Widget>();

6.2 使用 make_unique 和 make_shared

cpp 复制代码
// 好
auto ptr = make_unique<int>(42);

// 避免
unique_ptr<int> ptr(new int(42));

6.3 避免混用原始指针和智能指针

cpp 复制代码
// 危险
int* raw = new int(42);
shared_ptr<int> ptr(raw);
delete raw;  // 重复释放!

// 好
shared_ptr<int> ptr = make_shared<int>(42);

6.4 使用 weak_ptr 打破循环引用

当两个对象需要相互引用时,一方使用 weak_ptr

7. 总结

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

  • unique_ptr:独占所有权,首选方案
  • shared_ptr:共享所有权,引用计数
  • weak_ptr:弱引用,解决循环引用

核心原则

  • 优先使用栈对象,必要时用智能指针
  • 优先使用 unique_ptr
  • 使用 make_uniquemake_shared
  • 避免混用原始指针和智能指针

下一节我们将学习多线程编程,智能指针在其中会发挥重要作用。

相关推荐
basketball6161 小时前
设计模式入门:2. 工厂模式详解 C++实现
开发语言·c++·设计模式
yu85939581 小时前
MATLAB 分支定界法(Branch and Bound)实现
开发语言·matlab
前进吧-程序员1 小时前
CRTP 与静态多态:不用虚函数也能多态
c++
basketball6161 小时前
设计模式入门:1. 单例模式详解 C++实现
c++·单例模式·设计模式
学会去珍惜1 小时前
c语言编程 C语言入门 c语言(C语言程序设计教程 c语言视频教程 c语言零基础
c语言·开发语言
AI 编程助手GPT1 小时前
ChatGPT 新手入门与实战操作指南
开发语言·人工智能·git·python·chatgpt
Brilliantwxx1 小时前
【C++】 红黑树封装 STL set/map 超详细解析
开发语言·c++
程序大视界1 小时前
【C++ 从基础到项目实战】C++(八):运算符重载——让你的类用起来像内置类型
开发语言·c++·cpp
原创小甜甜1 小时前
OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space
java·开发语言·python