在 C++ 的编程世界里,智能指针是个好东西,能帮我们自动管理内存,避免很多内存泄漏的问题。不过呢,它也有个小陷阱,就是循环引用问题。今天咱就来好好聊聊这个事儿,看看怎么避免它。

一、啥是智能指针

在说循环引用之前,咱先搞清楚啥是智能指针。其实啊,智能指针就是一种特殊的类对象,它能像普通指针那样指向对象,但是又能自动管理对象的生命周期。简单来说,就是当这个对象没人用的时候,它能自动把对象占的内存给释放掉,省得咱手动去释放,减少了很多麻烦。

在 C++ 里,常见的智能指针有三种:std::unique_ptrstd::shared_ptrstd::weak_ptr。咱先简单了解一下它们。

1. std::unique_ptr

这个指针比较霸道,它独占所指向的对象。也就是说,同一时间只能有一个 unique_ptr 指向同一个对象。当这个 unique_ptr 被销毁或者指向别的对象时,它所指向的对象也会被自动销毁。

下面是一个简单的示例:

// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数" << std::endl; }
    ~MyClass() { std::cout << "MyClass 析构函数" << std::endl; }
};

int main() {
    // 创建一个 unique_ptr 指向 MyClass 对象
    std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>();

    // 当 uniquePtr 离开作用域时,它所指向的对象会被自动销毁
    return 0;
}

在这个例子里,uniquePtr 是一个 std::unique_ptr,它指向一个 MyClass 对象。当 main 函数结束时,uniquePtr 离开作用域,它所指向的 MyClass 对象就会被自动销毁。

2. std::shared_ptr

这个指针就比较大方了,它可以和其他 shared_ptr 共享同一个对象。每个 shared_ptr 都有一个引用计数,记录有多少个 shared_ptr 指向同一个对象。当引用计数为 0 时,也就是没有 shared_ptr 指向这个对象了,对象就会被自动销毁。

看个示例:

// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数" << std::endl; }
    ~MyClass() { std::cout << "MyClass 析构函数" << std::endl; }
};

int main() {
    // 创建一个 shared_ptr 指向 MyClass 对象
    std::shared_ptr<MyClass> sharedPtr1 = std::make_shared<MyClass>();
    std::cout << "引用计数: " << sharedPtr1.use_count() << std::endl;

    // 再创建一个 shared_ptr 指向同一个对象
    std::shared_ptr<MyClass> sharedPtr2 = sharedPtr1;
    std::cout << "引用计数: " << sharedPtr1.use_count() << std::endl;

    // 当 sharedPtr1 和 sharedPtr2 离开作用域时,引用计数变为 0,对象会被自动销毁
    return 0;
}

在这个例子里,sharedPtr1sharedPtr2 共享同一个 MyClass 对象,它们的引用计数一开始是 1,当 sharedPtr2 也指向这个对象时,引用计数变为 2。当 main 函数结束,sharedPtr1sharedPtr2 都离开作用域,引用计数变为 0,对象就被自动销毁了。

3. std::weak_ptr

这个指针有点像旁观者,它可以指向一个由 shared_ptr 管理的对象,但不会增加对象的引用计数。也就是说,它不会影响对象的生命周期。它主要用来解决 shared_ptr 的循环引用问题,后面我们会详细说。

二、循环引用问题是咋回事

现在我们知道了 std::shared_ptr 的引用计数原理,那循环引用问题是怎么产生的呢?简单来说,就是两个或多个 shared_ptr 互相引用,导致它们的引用计数永远不会变为 0,对象也就无法被自动销毁,从而造成内存泄漏。

看个具体的例子:

// C++ 技术栈
#include <iostream>
#include <memory>

// 定义两个类
class ClassB;

class ClassA {
public:
    // 定义一个 shared_ptr 指向 ClassB 对象
    std::shared_ptr<ClassB> bPtr;
    ClassA() { std::cout << "ClassA 构造函数" << std::endl; }
    ~ClassA() { std::cout << "ClassA 析构函数" << std::endl; }
};

class ClassB {
public:
    // 定义一个 shared_ptr 指向 ClassA 对象
    std::shared_ptr<ClassA> aPtr;
    ClassB() { std::cout << "ClassB 构造函数" << std::endl; }
    ~ClassB() { std::cout << "ClassB 析构函数" << std::endl; }
};

int main() {
    // 创建两个 shared_ptr 分别指向 ClassA 和 ClassB 对象
    std::shared_ptr<ClassA> a = std::make_shared<ClassA>();
    std::shared_ptr<ClassB> b = std::make_shared<ClassB>();

    // 让 a 指向 b,b 指向 a
    a->bPtr = b;
    b->aPtr = a;

    // 当 a 和 b 离开作用域时,由于循环引用,它们的引用计数不会变为 0,对象不会被销毁
    return 0;
}

在这个例子里,ClassA 有一个 shared_ptr 指向 ClassBClassB 也有一个 shared_ptr 指向 ClassA。当 main 函数结束,ab 离开作用域时,它们的引用计数不会变为 0,因为它们互相引用着对方。这样,ClassAClassB 对象就无法被自动销毁,造成了内存泄漏。

三、怎么避免循环引用问题

既然知道了循环引用问题的危害,那怎么避免它呢?这时候 std::weak_ptr 就派上用场了。我们可以把其中一个 shared_ptr 换成 std::weak_ptr,这样就不会增加对象的引用计数,从而打破循环引用。

还是用上面的例子,我们把 ClassB 里的 shared_ptr 换成 std::weak_ptr

// C++ 技术栈
#include <iostream>
#include <memory>

// 定义两个类
class ClassB;

class ClassA {
public:
    // 定义一个 shared_ptr 指向 ClassB 对象
    std::shared_ptr<ClassB> bPtr;
    ClassA() { std::cout << "ClassA 构造函数" << std::endl; }
    ~ClassA() { std::cout << "ClassA 析构函数" << std::endl; }
};

class ClassB {
public:
    // 定义一个 weak_ptr 指向 ClassA 对象
    std::weak_ptr<ClassA> aPtr;
    ClassB() { std::cout << "ClassB 构造函数" << std::endl; }
    ~ClassB() { std::cout << "ClassB 析构函数" << std::endl; }
};

int main() {
    // 创建两个 shared_ptr 分别指向 ClassA 和 ClassB 对象
    std::shared_ptr<ClassA> a = std::make_shared<ClassA>();
    std::shared_ptr<ClassB> b = std::make_shared<ClassB>();

    // 让 a 指向 b,b 指向 a
    a->bPtr = b;
    b->aPtr = a;

    // 当 a 和 b 离开作用域时,由于 b 对 a 的引用是 weak_ptr,不会影响 a 的引用计数,对象会被正常销毁
    return 0;
}

在这个修改后的例子里,ClassB 里的 aPtr 是一个 std::weak_ptr,它指向 ClassA 对象,但不会增加 ClassA 对象的引用计数。当 main 函数结束,ab 离开作用域时,a 的引用计数变为 0,ClassA 对象被销毁,然后 b 的引用计数也变为 0,ClassB 对象也被销毁,这样就避免了循环引用问题。

四、智能指针的应用场景

1. std::unique_ptr 的应用场景

  • 当你需要一个指针独占一个对象时,就可以用 std::unique_ptr。比如,在一个函数里创建一个临时对象,这个对象只在函数内部使用,函数结束后就不需要了,这时候就可以用 std::unique_ptr 来管理这个对象。
// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class MyClass {
public:
    void doSomething() { std::cout << "MyClass 做了一些事情" << std::endl; }
};

void myFunction() {
    // 创建一个 unique_ptr 指向 MyClass 对象
    std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>();
    uniquePtr->doSomething();
    // 当函数结束时,uniquePtr 离开作用域,对象自动销毁
}

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

2. std::shared_ptr 的应用场景

  • 当多个地方需要共享一个对象时,就可以用 std::shared_ptr。比如,在一个程序里有多个模块都需要访问同一个数据对象,这时候就可以用 std::shared_ptr 来管理这个数据对象。
// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class Data {
public:
    int value;
    Data(int v) : value(v) {}
};

void module1(std::shared_ptr<Data> data) {
    std::cout << "module1 访问数据: " << data->value << std::endl;
}

void module2(std::shared_ptr<Data> data) {
    std::cout << "module2 访问数据: " << data->value << std::endl;
}

int main() {
    // 创建一个 shared_ptr 指向 Data 对象
    std::shared_ptr<Data> data = std::make_shared<Data>(42);

    // 多个模块共享这个数据对象
    module1(data);
    module2(data);

    // 当所有模块都不再使用这个对象时,对象自动销毁
    return 0;
}

3. std::weak_ptr 的应用场景

  • 主要就是用来解决 std::shared_ptr 的循环引用问题,就像我们前面说的那样。另外,当你需要一个指针指向一个由 std::shared_ptr 管理的对象,但又不想影响对象的生命周期时,也可以用 std::weak_ptr
// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class MyClass {
public:
    void doSomething() { std::cout << "MyClass 做了一些事情" << std::endl; }
};

int main() {
    // 创建一个 shared_ptr 指向 MyClass 对象
    std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>();

    // 创建一个 weak_ptr 指向同一个对象
    std::weak_ptr<MyClass> weakPtr = sharedPtr;

    // 通过 weak_ptr 检查对象是否还存在
    if (auto lockedPtr = weakPtr.lock()) {
        lockedPtr->doSomething();
    }

    // 当 sharedPtr 被销毁后,对象就不存在了
    sharedPtr.reset();

    // 再次检查对象是否还存在
    if (auto lockedPtr = weakPtr.lock()) {
        lockedPtr->doSomething();
    } else {
        std::cout << "对象已经不存在了" << std::endl;
    }

    return 0;
}

五、智能指针的优缺点

1. 优点

  • 自动内存管理:这是智能指针最大的优点,它能自动管理对象的生命周期,避免了手动管理内存带来的很多问题,比如内存泄漏、悬空指针等。
  • 提高代码安全性:使用智能指针可以减少很多潜在的内存错误,提高代码的安全性和可靠性。
  • 方便使用:智能指针的使用方式和普通指针很相似,学习成本比较低。

2. 缺点

  • 性能开销:智能指针需要维护引用计数等信息,这会带来一定的性能开销,尤其是在频繁创建和销毁对象的场景下。
  • 循环引用问题std::shared_ptr 存在循环引用问题,需要开发者手动处理。

六、使用智能指针的注意事项

1. 避免手动删除智能指针管理的对象

智能指针会自动管理对象的生命周期,如果你手动删除了智能指针管理的对象,会导致程序崩溃。

// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class MyClass {
public:
    ~MyClass() { std::cout << "MyClass 析构函数" << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>();
    // 不要手动删除 sharedPtr 管理的对象
    // delete sharedPtr.get(); // 这会导致程序崩溃
    return 0;
}

2. 注意 std::weak_ptr 的使用

std::weak_ptr 不能直接访问对象,需要通过 lock() 方法获取一个 std::shared_ptr 后才能访问对象。而且在使用 lock() 方法时,需要检查返回的 std::shared_ptr 是否为空,因为对象可能已经被销毁了。

// C++ 技术栈
#include <iostream>
#include <memory>

// 定义一个简单的类
class MyClass {
public:
    void doSomething() { std::cout << "MyClass 做了一些事情" << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>();
    std::weak_ptr<MyClass> weakPtr = sharedPtr;

    // 通过 weak_ptr 访问对象
    if (auto lockedPtr = weakPtr.lock()) {
        lockedPtr->doSomething();
    }

    // 销毁对象
    sharedPtr.reset();

    // 再次访问对象
    if (auto lockedPtr = weakPtr.lock()) {
        lockedPtr->doSomething();
    } else {
        std::cout << "对象已经不存在了" << std::endl;
    }

    return 0;
}

七、文章总结

在 C++ 编程中,智能指针是一个非常有用的工具,它能帮助我们自动管理内存,提高代码的安全性和可靠性。但是,std::shared_ptr 存在循环引用问题,会导致内存泄漏。为了避免这个问题,我们可以使用 std::weak_ptr 来打破循环引用。

不同类型的智能指针有不同的应用场景,std::unique_ptr 适用于独占对象的情况,std::shared_ptr 适用于多个地方共享对象的情况,std::weak_ptr 主要用于解决循环引用问题和在不影响对象生命周期的情况下引用对象。

在使用智能指针时,我们要注意避免手动删除智能指针管理的对象,以及正确使用 std::weak_ptr。同时,我们也要了解智能指针的优缺点,在合适的场景下选择合适的智能指针。