系列文章目录
【C++11】智能指针与动态内存
文章目录
- 系列文章目录
- 简介
- 一、头文件
- 二、初始化及使用
- 1. 使用一个shared_ptr来初始化
- 三、循环引用
- 3.1 循环引用
- 3.2 循环引用 解决方法
简介
在C++编程中,处理循环引用是一个常见的问题。循环引用可能导致内存泄漏和资源管理问题。为了解决这个问题,C++11引入了weak_ptr
智能指针。
弱指针(weak_ptr
)是一种不受控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。允许你共享对象的所有权,但不会增加对象的引用计数。它是一种弱引用,不会阻止对象的销毁。一旦一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也会被释放。
一、头文件
本文出现的关于unique_ptr
的方法都包含在此头文件中
#include <memory>
二、初始化及使用
1. 使用一个shared_ptr来初始化
用一个shared_ptr来初始化,不会改变shared_ptr的引用计数
auto p = std::make_shared<int>(4);
std::weak_ptr<int> wp(p);
由于wp指向的对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。
此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指针指向共享对象的shared_ptr;否则返回一个空的shared_ptr。
if (std::shared_ptr<int> np = wp.lock()) // 如果np不为空则条件成立
{// np与p共享对象
}
三、循环引用
3.1 循环引用
环引用是指两个或多个智能指针相互引用,形成一个闭环,导致它们都无法被释放,从而造成内存泄漏。循环引用通常发生在两个或多个对象之间存在相互依赖关系时,它们需要同时被释放。
下述代码就是循环引用的典型例子,a和b所指向的对象的引用计数永远不会为0
,也就不会释放对象。
class B;
class A
{
public:std::shared_ptr<B> b_ptr;A() {std::cout << "A Constructor Called" << std::endl;};~A() {std::cout << "A Desstructor Called" << std::endl;};
};class B
{
public:std::shared_ptr<A> a_ptr; //这里改成weak_ptr即可解决循环引用B() {std::cout << "B Constructor Called" << std::endl;};~B() {std::cout << "B Desstructor Called" << std::endl;};
};int main()
{ std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();std::cout << "a uses" << " " << a.use_count() << std::endl; // 引用计数输出1std::cout << "b uses" << " " << b.use_count() << std::endl; // 引用计数输出1a->b_ptr = b;b->a_ptr = a;std::cout << "a uses" << " " << a.use_count() << std::endl; // 引用计数输出2std::cout << "b uses" << " " << b.use_count() << std::endl; // 引用计数输出2std::cout << "Hello World!\n";
}
析构规定
在C++中,局部变量的析构顺序与它们的构造顺序相反。也就是说,最后构造的对象会首先被析构。这被称为栈解旋(stack unwinding)。
在C++中,当一个对象被销毁,其成员变量的析构函数会在该对象的析构函数之后被调用。也就是说,首先调用对象的析构函数,然后调用其成员变量的析构函数。确保了在对象的析构函数中,你仍然可以访问和操作其成员变量,因为它们此时还没有被销毁。
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。
上述代码执行完毕,析构函数的调用情况以及引用计数变化情况的说明
①a 和 b 离开作用域之前
a和b->a_ptr指向A的同一个实例对象,该对象的引用计数为2
b和a->b_ptr指向B的同一个实例对象,该对象的引用计数为2
②b调用析构函数
a和b离开作用域时,b先调用析构函数,这会导致b指向对象的引用计数减1,但是,由于a->b_ptr指向同一个对象,所以b指向对象的引用计数仍然为1。
③b->a_ptr调用析构函数
因为b指向对象的引用计数不为0,故其其析构函数没有被执行,对象没有被销毁。这导致其成员变量b->a_ptr的析构函数也不会被调用(对象本身没有被销毁的话,其成员变量的析构函数不会被调用),a指向对象的引用计数不变仍为2。
④a调用析构函数
a和b离开作用域时,b先调用析构函数,然后a调用析构函数,这会导致a指向对象的引用计数减1,但是,由于b->a_ptr也指向同一个对象,所以a指向对象的引用计数仍为1。
⑤a->b_ptr调用析构函数
因为a指向对象的引用计数不为0,故其析构函数没有被执行,对象没有被销毁。这导致其成员变量a->b_ptr的析构函数也不会被调用,b指向对象的引用计数不变仍为1。
最后对象a和b的引用计数都为1,不会销毁对象,释放内存,从而导致内存泄漏。
3.2 循环引用 解决方法
将B类中的shared_ptr改为weak_ptr,这样就可以打破循环引用,当a和b离开作用域时,它们的引用计数会变为0,析构函数会被调用,内存会被释放。
①a和b离开作用域之前
a指向A的一个实例对象,a指向对象的引用计数是1(b->a_ptr弱指针,引用计数不会加1)
b和a->b_ptr指向B的同一个实例对象b,对象b引用计数是2
②b调用析构函数
a和b离开作用域时,b先调用析构函数,这会导致b指向对象的引用计数减1,但是,由于a->b_ptr指向同一个对象,所以b指向对象的引用计数仍然为1。
③b->a_ptr调用析构函数
因为b指向对象的引用计数不为0,故其其析构函数没有被执行,对象没有被销毁。这导致其成员变量b->a_ptr的析构函数也不会被调用(对象本身没有被销毁的话,其成员变量的析构函数不会被调用),a指向对象的引用计数不变仍为1。
④a调用析构函数
a和b离开作用域时,b先调用析构函数,然后a调用析构函数,这会导致a指向对象的引用计数减1。但因为b->a_ptr是弱指针,不会增加引用计数,所以a指向对象的引用计数变为0,会执行析构函数,销毁对象,释放内存。
⑤a->b_ptr调用析构函数
因为a指向对象的引用计数为0,故其析构函数被执行,对象被销毁。从而其成员变量a->b_ptr的析构函数也会被调用,b指向对象的引用计数递减也变为0,销毁对象,释放内存。
最后对象a和b的引用计数都为0,不会导致内存泄漏