告别手动管理内存:C++智能指针完全指南

/ 0评 / 0

  如果你曾经在C++的海洋中遨游,那么你一定遇到过那些令人头疼的内存管理问题:忘记 delete 导致的内存泄漏,或者对已经释放的内存进行操作(悬空指针)导致的程序崩溃。这些问题不仅难以调试,而且是C++程序稳定性的主要威胁。
  幸运的是,C++标准库为我们提供了强大的工具来自动化内存管理,它们就是——智能指针。今天,我们就来深入了解这些现代C++编程的基石。

为什么我们需要智能指针?(传统指针的痛点)

在智能指针出现之前,我们通常这样管理动态分配的内存:

void old_school_memory_management(){

        int *raw_ptr = new int(10); //分配内存
        // ... 使用raw_ptr ...
        if (*raw_ptr>5){
            std::cout<< "Value is "<< *raw_ptr <<std::endl;
            //如果在这里提前返回或者发生异常,delete就不会被执行!
            //return;
        }
        delete raw_ptr; //手动释放内存
        raw_ptr = nullptr; //好习惯;放置悬空指针

}

这种方式存在几个显而易见的问题:

1.内存泄露:如果忘记delete,或者在delete执行前因为异常、提前返回等原因跳出了作用域,那么分配的内存就永远无法回收。

2.悬空指针:当一个指针指向的内存已经被释放,但指针本省没有被置空,此时它就成了悬空指针。再次使用它就会导致未定义行为。

3.重复释放:对同一块内存执行两次delete也会导致程序崩溃。

4.所有权混乱:当多个指针指向同一块内存时,谁负责释放它?这很容易造成混乱。

智能指针的出现,正是为了解决这些问题。它们利用了C++的RAII(Resource Acquisition Is Initialization)技术,即资源(这里是动态内存)在对象构造时获取,在对象析构时自动释放。

智能指针的核心成员

C++11标准库引入了三种主要的智能指针,它们都定义在<memory>头文件中:

1.std::unique_ptr:独占所有权的智能指针。

2.std::shared_ptr:共享所有权的智能指针。

3.std::weak_ptr:一种弱引用,配合std::shared_ptr使用,用于解决循环应用的问题。

让我们逐一深入的了解它们。

1.std::unique_ptr(独占指针)

std::unique_ptr 对其指向的对象拥有独占所有权。这意味着在任何时刻,最多只有一个unique_ptr指向给定的对象。当unique_ptr被销毁时(例如离开作用域),它所指向的对象也会被自动删除。

特点:

轻量级:性能开销几乎与原始指针相同。

不可复制:你不能通过拷贝构造函数或赋值操作符来复制一个unique_ptr。

std::unique_ptr<int> ptrl(new int(10));
//std::unique_ptr<int> ptr2 = ptr1; //编译错误!
//std::unique_ptr<int> ptr3;
//ptr3 = ptr1; //编译错误!

可移动:所有权可以通过std::move从一个unique_ptr 转移到另一个。

std::unique_ptr<int> ptr1(new int(10));
std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1 失去所有权,变为 nullptr
                                           // ptr2 现在拥有对象
if (ptr1) {
    std::cout << "ptr1 still valid (this won't happen)\n";
}
if (ptr2) {
    std::cout << "ptr2 has value: " << *ptr2 << std::endl; // 输出 10
}

如何创建和使用?

推荐使用std::make_unique(C++14及以后版本)来创建unique_ptr,因为它更安全且简洁:

#include <iostram>
#include <memory> //必须包含

class MyClass{
public:
    MyClass(int v): value(v) {std<<cout << "MyClass(" << value << "constructed. \n";}
    ~MyClass() {std::cout<< "MyClass(" << value << ") destructed. \n";}
    void print() const {std::cout << "Value: " << value << std::endl;}
private:
    int value;

};


int main(){
    //使用std::make_unique (推荐,C++14+)
    std::unique_ptr<MyClass> u_ptr1 = std::make_unique<MyClass>(100);
    u_ptr1->print(); //像普通指针一样使用 -> 和 *
    

    // 如果没有C++14,可以这样创建(但不推荐直接使用 new)
    // std::unique_ptr<MyClass> u_ptr2(new MyClass(200));
    // u_ptr2->print();


    // 转移所有权
    std::unique_ptr<MyClass> u_ptr3 = std::move(u_ptr1);
    if(!u_ptr1){
        std::cout << "u_ptr1 is now null. \n";
    }
    if(u_ptr3) {
        u_ptr3->print();
    }


    // u_ptr3 在main函数结束时离开作用域,MyClass(100)对象会自动析构
    // u_ptr1 已经是nullptr, 不会做任何事
    return 0;
}

//输出:
// MyClass(100) constructed.
// Value: 100
// u_ptr1 is now null.
// Value: 100
// MyClass(100) destructed.

unique_ptr也支持数组:

std::unique_ptr<int[]> arr_ptr = std::make_unique<int[]>(5); // 创建一个包含5个整数的数组
for (int i = 0; i < 5; ++i) {
    arr_ptr[i] = i * 10;
    std::cout << arr_ptr[i] << " "; // 0 10 20 30 40
}
std::cout << std::endl;
// arr_ptr 离开作用域时,数组会被 delete[] 自动删除

自定义删除器:

unique_ptr还可以指定一个自定义的删除器,用于在对象不再需要时执行特定的清理操作,而不仅仅是delete.

struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp) {
            std::cout << "Closing file.\n";
            fclose(fp);
        }
    }
};

// 使用 fopen 打开文件,并用 unique_ptr 管理
std::unique_ptr<FILE, FileCloser> file_ptr(fopen("test.txt", "w"), FileCloser());
if (file_ptr) {
    fprintf(file_ptr.get(), "Hello unique_ptr with custom deleter!\n");
}
// file_ptr 离开作用域时,FileCloser::operator() 会被调用,文件自动关闭

何时使用unique_ptr?

当你需要一个指针来管理动态分配的资源,并且明确这个资源只有一个所有者时,unique_ptr是首选。例如,工厂函数返回的对象,或者类成员指向的独占资源。

2.std::shared_ptr(共享指针)

std::shared_ptr实现了共享所有权。多个shared_ptr可以指向同一个对象。它内部维护了一个引用计数,记录有多少个shared_ptr指向该对象。当最后一个指向对象的shared_ptr被销毁或重置时,对象才会被删除。

特点:

引用计数:这是shared_ptr 的核心机制

可复制:复制一个shared_ptr 会增加引用计数

std::shared_ptr<int> s_ptr1 = std::make_shared<int>(20);
std::shared_ptr<int> s_ptr2 = s_ptr1; // 复制,引用计数变为 2
std::cout << "s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 输出 2
std::cout << "s_ptr2 use_count: " << s_ptr2.use_count() << std::endl; // 输出 2

线程安全:引用计数本身的增减是原子操作,因此在多线程环境下,对引用计数的操作是安全的。但对所管理对象的访问不是线程安全的,需要用户自己同步。

如何创建和使用?

同样,推荐使用std::make_shared 来创建shared_ptr:

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

// 使用上面定义的 MyClass

void process_widget(std::shared_ptr<MyClass> sp) {
    std::cout << "In process_widget, use_count: " << sp.use_count() << std::endl;
    sp->print();
    // sp 离开作用域,引用计数减 1
}

int main() {
    // 使用 std::make_shared (推荐)
    std::shared_ptr<MyClass> s_ptr1 = std::make_shared<MyClass>(200);
    std::cout << "After make_shared, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 1

    {
        std::shared_ptr<MyClass> s_ptr2 = s_ptr1; // 复制
        std::cout << "s_ptr2 created, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 2
        s_ptr2->print();
        process_widget(s_ptr1); // 传递 shared_ptr,引用计数会增加
        std::cout << "After process_widget, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 2
    } // s_ptr2 离开作用域,引用计数减 1

    std::cout << "After s_ptr2 scope, s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // 1
    
    // s_ptr1 在 main 函数结束时离开作用域,引用计数变为 0,MyClass(200) 对象会自动析构
    return 0;
}
//输出:
// MyClass(200) constructed.
// After make_shared, s_ptr1 use_count: 1
// s_ptr2 created, s_ptr1 use_count: 2
// Value: 200
// In process_widget, use_count: 3
// Value: 200
// After process_widget, s_ptr1 use_count: 2
// After s_ptr2 scope, s_ptr1 use_count: 1
// MyClass(200) destructed.

为什么make_shared比shared_ptr<T>(new T())更好?

make_shared 通常更高效,因此它能一次性分配对象本身和shared_ptr控制块(包含引用计数等)所需的内存。而shared_ptr<T>(new T())会有两次内存分配(一次new T(),一次为控制块),这可能导致性能稍差,并且在某些复杂表达式中存在微小的异常安全风险。

何时使用shared_ptr?

当你需要多个指针共享同一个对象的所有权,例如,在数据结构中(如链表、图的节点,但要注意循环引用),或者当对象的生命周期需要多个独立的观察者共同决定时。

3.std::weak_ptr(弱指针)

std::weak_ptr是一种非拥有型智能指针。它指向由shared_ptr管理的对象,但不会增加对象的引用计数。weak_ptr主要用于解决shared_ptr可能导致的循环引用的问题。

什么是循环引用?

考虑两个对象A和B,它们都使用shared_ptr互相指向对方:

struct B; // 前向声明

struct A {
    std::shared_ptr<B> b_ptr;
    A() { std::cout << "A constructed\n"; }
    ~A() { std::cout << "A destructed\n"; }
};

struct B {
    std::shared_ptr<A> a_ptr;
    B() { std::cout << "B constructed\n"; }
    ~B() { std::cout << "B destructed\n"; }
};

void cycle_example() {
    std::shared_ptr<A> spa = std::make_shared<A>(); // A的引用计数为1
    std::shared_ptr<B> spb = std::make_shared<B>(); // B的引用计数为1

    spa->b_ptr = spb; // B的引用计数变为2 (spb 和 spa->b_ptr)
    spb->a_ptr = spa; // A的引用计数变为2 (spa 和 spb->a_ptr)

    std::cout << "spa use_count: " << spa.use_count() << std::endl; // 2
    std::cout << "spb use_count: " << spb.use_count() << std::endl; // 2
} // spa 和 spb 离开作用域,A 和 B 的引用计数都从 2 减为 1
  // 但由于 A 持有 B,B 持有 A,它们的引用计数永远不会降到 0
  // 结果:A 和 B 都不会被析构,造成内存泄漏!

在这个例子中,当spa和spb离开作用域时,A和B对象的引用计数都变成了1(因为它们内部互相只有对方的shared_ptr)。由于引用计数不为0,它们的析构函数永远不会被调用,导致内存泄露。

weak_ptr如何解决?

如果其中一个(或两个)指针使用weak_ptr,循环就被打破了。

#include <iostream>
#include <memory>

struct BetterB;

struct BetterA {
    std::shared_ptr<BetterB> b_ptr_shared; // A 强引用 B
    // 或者,如果 A 只需要观察 B,不拥有 B
    // std::weak_ptr<BetterB> b_ptr_weak; 
    BetterA() { std::cout << "BetterA constructed\n"; }
    ~BetterA() { std::cout << "BetterA destructed\n"; }
    void set_b(std::shared_ptr<BetterB> b) { b_ptr_shared = b; }
};

struct BetterB {
    std::weak_ptr<BetterA> a_ptr_weak; // B 弱引用 A,打破循环
    BetterB() { std::cout << "BetterB constructed\n"; }
    ~BetterB() { std::cout << "BetterB destructed\n"; }
    void set_a(std::shared_ptr<BetterA> a) { a_ptr_weak = a; }

    void check_a() {
        if (auto locked_a = a_ptr_weak.lock()) { // 尝试获取一个 shared_ptr
            std::cout << "BetterA is still alive. Use count: " << locked_a.use_count() << std::endl;
            // 可以安全使用 locked_a
        } else {
            std::cout << "BetterA has been destroyed.\n";
        }
    }
};

int main() {
    std::shared_ptr<BetterA> spa = std::make_shared<BetterA>();
    std::shared_ptr<BetterB> spb = std::make_shared<BetterB>();

    spa->set_b(spb);    // A 指向 B (shared_ptr), B 的引用计数 +1
    spb->set_a(spa);    // B 指向 A (weak_ptr),   A 的引用计数不变

    std::cout << "spa use_count: " << spa.use_count() << std::endl; // 1 (只有 spa 自己)
    std::cout << "spb use_count: " << spb.use_count() << std::endl; // 2 (spb 和 spa->b_ptr_shared)

    spb->check_a();

    // 当 spa 和 spb 离开作用域:
    // 1. spa 析构,A 的引用计数变为 0,A 被销毁。
    // 2. A 销毁时,其成员 spa->b_ptr_shared 析构,B 的引用计数减 1。
    // 3. spb 析构,B 的引用计数再减 1,变为 0,B 被销毁。
    // 没有内存泄漏!
    return 0;
}
// BetterA constructed
// BetterB constructed
// spa use_count: 1
// spb use_count: 2
// BetterA is still alive. Use count: 2
// BetterA destructed
// BetterB destructed

如何使用weak_ptr?

weak_ptr不能直接访问对象,因为它不保证对象仍然存在。要访问对象,必须调用lock()方法。lock()会检查对象是否存在,如果存在,它返回一个指向该对象的shared_ptr(增加引用计数,保证在shared_ptr存在期间对象有效)。如果对象已被销毁,他返回一个空的shared_ptr。相对应的也可以用expired()方法检查对象是否已被销毁,但通常直接用lock()的结果判断更方便。

何时使用weak_ptr?

        1.打破shared_ptr的循环引用:这是最常见的用途。

        2.观察对象:当你需要一个指向对象的指针,但不希望影响其生命周期时。例如,在缓存实现中,缓存项可以被weak_ptr引用,当对象不再被其他地方需要时,缓存项可以安全地失效。

选择合适的智能指针

默认使用std::unique_ptr,它是最轻量级且最能清晰表达所有权的。只有当你确定需要共享所有权时,才考虑其他选项。

当你需要多个地方共享对象的所有权,并且对象的生命周期由最后一个引用者决定时,使用std::shared_ptr。

当你有一个shared_ptr指向的对象,但你需要一个非拥有型的指针来观察它(通常是为了打破循环引用),使用std::weak_ptr。

智能指针使用最佳实践

优先使用std::make_unique和std::make_shared:

不要混用原始指针和智能指针来管理同一个对象的所有权:

MyClass* raw_ptr = new MyClass(1);
std::shared_ptr<MyClass> sp1(raw_ptr);
// std::shared_ptr<MyClass> sp2(raw_ptr); // 错误!会导致 double free
                                     // sp1 和 sp2 会有各自的引用计数,都以为自己是唯一从原始指针创建的

正确的做法是从一个已有的shared_ptr创建另一个shared_ptr:

std::shared_ptr<MyClass> sp2 = sp1;

将new 的结果立即传递给智能指针构造函数:

// 好:
std::shared_ptr<MyClass> sp(new MyClass());
// 或者更好:
auto sp = std::make_shared<MyClass>();

// 不好(有风险):
// process_widget(std::shared_ptr<Widget>(new Widget()), compute_priority());
// 如果 compute_priority() 抛出异常,new Widget() 分配的内存可能泄漏。
// make_shared 可以避免这个问题。

函数参数传递:

如果函数需要取得对象的所有权,按值传递unqiue_ptr(使用std::move)或直接传递unique_ptr&&。

void take_ownership(std::unique_ptr<MyClass> ptr) { /* ... */ }
std::unique_ptr<MyClass> p = std::make_unique<MyClass>(1);
take_ownership(std::move(p));

如果函数需要共享对象的所有权,按值传递shared_ptr(会增加引用计数)。

void share_ownership(std::shared_ptr<MyClass> ptr) { /* ... */ }

如果函数只是使用/观察对象,并且不影响其生命周期,传递原始指针(T*)或引用(T&)。可以从智能指针通过.get()(获取原始指针)或*(解引用)获得。

void observe_object(const MyClass* obj) { if(obj) obj->print(); }
void observe_object_ref(const MyClass& obj) { obj.print(); }

std::unique_ptr<MyClass> u_ptr = std::make_unique<MyClass>(10);
observe_object(u_ptr.get());
observe_object_ref(*u_ptr);

std::shared_ptr<MyClass> s_ptr = std::make_shared<MyClass>(20);
observe_object(s_ptr.get());
observe_object_ref(*s_ptr);

注意:传递.get()获取的原始指针时,要确保原始指针的生命周期长于函数调用。

对于类成员,如果该成员是类独有的,使用unique_ptr。如果需要与其他对象共享,使用shared_ptr。如果可能形成循环引用,则考虑使用weak_ptr.

std::enable_shared_from_this:如果一个类的方法需要返回指向当前对象(this)的shared_ptr,那么这个类应该公有继承自std::enable_shared_from_this<YourClass>,然后使用其shared_from_this()方法。

class MyObject : public std::enable_shared_from_this<MyObject> {
public:
    std::shared_ptr<MyObject> get_shared_this() {
        return shared_from_this();
    }
    // 注意:不能在构造函数中调用 shared_from_this(),
    // 因为此时 shared_ptr 的控制块还未完全建立。
};

结语

智能指针是现代C++编程中不可或缺的一部分。它们通过自动化内存管理,极大地减少了内存泄露和悬空指针等常见错误,使我们能够编写更安全、更健壮、更易于维护的代码。

掌握unique_ptr、shared_ptr 和 weak_ptr 的特性和使用场景,将使你的C++技能更上一层楼。开始在你的项目中拥抱它们吧!

希望这篇博客能帮助你更好地理解C++智能指针。如果你有任何问题或想法,欢迎在评论区分享! ​

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注