C++11 智能指针详解:告别内存泄漏,轻松管理动态内存

引入

在传统的C++中,动态内存的管理是一个棘手的问题,完全依赖于程序员手动使用 new 和 delete(或 new[] 和 delete[])。这种方式非常容易出错,是许多程序Bug的根源。

普通指针的问题

  • 必须手动释放new 和 new[] 分配的内存必须严格对应地使用 delete 和 delete[] 来释放。

  • 容易忘记释放:在复杂的代码逻辑或异常发生时,很容易忘记释放已分配的内存。

  • 不知何时释放:在多个函数或对象之间传递指针时,很难确定在哪个时间点、哪个作用域下应该释放内存。

这些问题会导致内存泄漏重复释放访问已释放内存等严重问题。

智能指针的优点

C++11引入了智能指针,它们位于 <memory> 头文件中,通过RAII(Resource Acquisition Is Initialization) 机制来管理动态分配的对象。其核心思想是:将内存资源与对象的生命周期绑定。当智能指针对象被销毁时(例如离开作用域),它的析构函数会自动释放其所管理的内存。

这带来了巨大的优势:

  • 自动释放:无需手动调用 delete,避免了忘记释放的问题。

  • 异常安全:即使代码中抛出异常,智能指针也能保证内存被正确释放。

  • 清晰的所有权语义:明确了谁拥有内存、谁负责释放。

C++11主要有三种智能指针:unique_ptrshared_ptr 和 weak_ptrauto_ptr 已被弃用,应避免使用。

C++11 智能指针类型及特点

1. std::unique_ptr

unique_ptr 独占其所指向对象的所有权。它不能被复制,但可以通过 std::move 转移所有权。

代码举例

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "MyClass constructed with value: " << data << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destroyed with value: " << data << std::endl;
    }
    void print() const { std::cout << "Value: " << data << std::endl; }
private:
    int data;
};

int main() {
    std::cout << "--- unique_ptr Demo ---" << std::endl;
    {
        // 创建一个 unique_ptr,管理一个 MyClass 对象
        std::unique_ptr<MyClass> ptr1(new MyClass(42));
        // 更推荐使用 std::make_unique (C++14)
        // auto ptr1 = std::make_unique<MyClass>(42); 

        ptr1->print(); // 使用 -> 操作符访问成员

        // 编译错误!unique_ptr 不能复制
        // std::unique_ptr<MyClass> ptr2 = ptr1;

        // 所有权转移:ptr1 将所有权交给 ptr2,ptr1 变为 nullptr
        std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
        if (ptr1) {
            ptr1->print(); // 这行不会执行,因为 ptr1 已是空指针
        }
        if (ptr2) {
            ptr2->print(); // 这行会执行
        }
    } // 离开作用域,ptr2 被销毁,它管理的对象也随之自动销毁

    std::cout << "--- End of scope ---" << std::endl;
    return 0;
}

输出:

--- unique_ptr Demo ---
MyClass constructed with value: 42
Value: 42
Value: 42
MyClass destroyed with value: 42
--- End of scope ---

2. std::shared_ptr

shared_ptr 通过引用计数实现共享所有权。每当一个新的 shared_ptr 指向同一个对象时,引用计数增加。当一个 shared_ptr 被销毁或重置时,引用计数减少。当计数变为零时,管理的对象被自动删除。

代码举例

#include <iostream>
#include <memory>

void shared_ptr_demo() {
    std::cout << "\n--- shared_ptr Demo ---" << std::endl;
    {
        // 创建 shared_ptr (推荐使用 std::make_shared)
        auto ptr1 = std::make_shared<MyClass>(100);
        std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出:1

        {
            // 复制构造,引用计数增加
            auto ptr2 = ptr1;
            std::cout << "After ptr2 copy, use_count: " << ptr1.use_count() << std::endl; // 输出:2
            ptr2->print();
        } // ptr2 离开作用域被销毁,引用计数减为 1

        std::cout << "After ptr2 destroyed, use_count: " << ptr1.use_count() << std::endl; // 输出:1
    } // ptr1 离开作用域,引用计数减为 0,对象被销毁
    std::cout << "--- End of shared_ptr scope ---" << std::endl;
}

int main() {
    shared_ptr_demo();
    return 0;
}

输出:

--- shared_ptr Demo ---
MyClass constructed with value: 100
ptr1 use_count: 1
After ptr2 copy, use_count: 2
Value: 100
After ptr2 destroyed, use_count: 1
MyClass destroyed with value: 100
--- End of shared_ptr scope ---

3. std::weak_ptr 与循环引用问题

weak_ptr 是为了解决 shared_ptr 最大的陷阱——循环引用而设计的。

循环引用问题

#include <iostream>
#include <memory>

struct Node {
    ~Node() { std::cout << "Node " << id << " destroyed." << std::endl; }
    int id;
    std::shared_ptr<Node> next; // 指向下一个节点的强引用
    // std::weak_ptr<Node> next; // 正确的做法应该是使用 weak_ptr
};

void circular_reference_demo() {
    std::cout << "\n--- Circular Reference Demo ---" << std::endl;
    auto nodeA = std::make_shared<Node>();
    auto nodeB = std::make_shared<Node>();
    nodeA->id = 1;
    nodeB->id = 2;

    // 形成循环引用:nodeA->next 指向 nodeB,nodeB->next 指向 nodeA
    nodeA->next = nodeB;
    nodeB->next = nodeA; // 这里导致循环引用!

    std::cout << "nodeA use_count: " << nodeA.use_count() << std::endl; // 输出:2
    std::cout << "nodeB use_count: " << nodeB.use_count() << std::endl; // 输出:2
} // 函数结束,nodeA和nodeB的局部shared_ptr被销毁,但引用计数只减1,不为0,对象无法释放!内存泄漏!

int main() {
    circular_reference_demo();
    std::cout << "Program ended, but Nodes were not destroyed due to circular reference!" << std::endl;
    return 0;
}

输出:

--- Circular Reference Demo ---
nodeA use_count: 2
nodeB use_count: 2
Program ended, but Nodes were not destroyed due to circular reference!
// 注意:没有看到 Node 的析构函数被调用!

使用 weak_ptr 解决循环引用

weak_ptr 只观察对象,而不拥有它。它不会增加引用计数。

#include <iostream>
#include <memory>

struct SafeNode {
    ~SafeNode() { std::cout << "SafeNode " << id << " destroyed." << std::endl; }
    int id;
    std::weak_ptr<SafeNode> next; // 使用弱引用打破循环

    void printNext() {
        // 使用 lock() 来获取一个临时的 shared_ptr 以访问对象
        if (auto tempSharedPtr = next.lock()) {
            std::cout << "Next node id: " << tempSharedPtr->id << std::endl;
        } else {
            std::cout << "Next node is expired." << std::endl;
        }
    }
};

void weak_ptr_demo() {
    std::cout << "\n--- weak_ptr Solution Demo ---" << std::endl;
    auto nodeA = std::make_shared<SafeNode>();
    auto nodeB = std::make_shared<SafeNode>();
    nodeA->id = 1;
    nodeB->id = 2;

    // 形成引用,但使用的是 weak_ptr,不会增加引用计数
    nodeA->next = nodeB; // nodeB 的引用计数还是 1
    nodeB->next = nodeA; // nodeA 的引用计数还是 1

    nodeA->printNext();

    std::cout << "nodeA use_count: " << nodeA.use_count() << std::endl; // 输出:1
    std::cout << "nodeB use_count: " << nodeB.use_count() << std::endl; // 输出:1
} // 离开作用域,nodeA和nodeB的引用计数都减为0,对象被正确销毁

int main() {
    weak_ptr_demo();
    std::cout << "Program ended correctly." << std::endl;
    return 0;
}

输出:

--- weak_ptr Solution Demo ---
Next node id: 2
nodeA use_count: 1
nodeB use_count: 1
SafeNode 2 destroyed.
SafeNode 1 destroyed.
Program ended correctly.

总结与最佳实践

  1. 首选 unique_ptr:默认情况下使用 unique_ptr。它开销小,语义清晰,能满足大部分需求。

  2. 需要共享时再用 shared_ptr:只有在确实需要多个指针共享同一个对象的所有权时,才使用 shared_ptr

  3. 使用 make_unique 和 make_shared:优先使用 std::make_unique<T>(args...) (C++14) 和 std::make_shared<T>(args...) 来创建智能指针。它们更安全(避免内存泄漏)、更高效(make_shared 可能将引用计数和对象分配在同一块内存)。

  4. 警惕循环引用:当设计可能形成循环引用的数据结构(如链表、树、图)时,使用 weak_ptr 来打破循环。

  5. 不要混合使用原始指针和智能指针:一旦将资源交给智能指针管理,就尽量全程使用智能指针,避免手动 delete 或用原始指针创建另一个智能指针,这可能导致重复释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值