大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。
重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。争取让你每天用5-10分钟,了解一些以前没有注意到的细节。
智能指针是C++中的一种高级内存管理工具,用来提高程序的安全性和可维护性。它可以进行自动化内存管理,可以在一定程度上减少内存泄漏和悬挂指针等问题。C++11中的智能指针不只有shared_ptr
,还有与之配套的weak_ptr
,以及独占所有权的unique_ptr
等,本文我们来一起学习下C++11中的智能指针。
1. std::shared_ptr
允许多个指针共享同一个对象,内部使用引用计数来跟踪对象的引用次数。当引用计数为0时,内存会被释放。
1.1 基本使用
写以下测试程序:
#include <iostream>
#include <string>
#include <memory>
class X1 {
public:
int* i_p;
X1(int a) {
i_p = new int(a);
std::printf("call x1 constructor\n----------------------\n");
}
void print()
{
std::printf("int value is %d, this: %p\n", *i_p, this);
}
};
int main(int argc, char *argv[])
{
std::shared_ptr<X1> x1_1 = std::make_shared<X1>(10);
x1_1->print();
std::printf("1 use count: %d\n", x1_1.use_count());
std::shared_ptr<X1> x1_2 = x1_1;
x1_2->print();
std::printf("2 use count: %d\n", x1_2.use_count());
x1_1.reset();
std::printf("x1 after reset: %p\n", x1_1.get());
std::printf("3 use count: %d\n", x1_2.use_count());
x1_2->print();
return 0;
}
执行后输出结果如下:
1.2 代码解释
(1)x1_1
为智能指针,因为现在只有它自己指向X1
的实例,因此其 use_count
为1
(2)x1_2
为智能指针,与x1_1
相等,即x1_2
与x1_1
指向相同的位置,之后该实例的引用计数 use_count
变为了2,同时,根据打印也可以看到,其打印的 this
地址与 x1_1
相同,指向相同位置。
(3)x1_1.reset()
表示清除该智能指针,清除之后x1_1
为空了,原来实例的引用计数 use_count
也变为了1。
(4)因为引用计数还不是0,因此这块的内存空间还是在的,原来的实例还是在的,因此还是能够使用 x1_2
打印出这个实例的数据和地址。
(5)注意智能指针在打印输出时,使用的是 get
方法,get
方法是将智能指针转换成裸指针的方法:x1_1.get()
1.3 线程安全性
指针和引用计数是线程安全的(引用计数的加减是原子的),但指针所指对象中的操作就需要自己做控制,并不是线程安全的。
1.4 补充知识 - 使用注意事项
1.4.1 reset函数
-
reset()不带参数时,若该智能指针是唯一指向该对象的指针,则释放,并置空。若智能指针不是唯一指向该对象的指针,则引用计数减一,同时将自身置空。
-
reset()带参数时,若智能指针是唯一指向该对象的指针,则释放并指向新的对象。若智能指针不是唯一指向该对象的指针,则引用计数减一,并指向新的对象。
std::shared_ptr<int> ptr = make_shared<int>(10);
ptr.reset(new int(20));
1.4.2 优先使用make_shared来构造智能指针,因为它更高效。
优先使用make_shared来构造智能指针,因为它更高效。
1.4.3 慎用 get 方法得到的裸指针
(1)不要delete get到的这个裸指针,否则会导致2次delete
(2)get 到的裸指针,不会增加或减少原智能指针的引用计数,是完全独立的,但是由于都对应一块内存,所以对内存的操作会很难控制。所以,一般不要使用get到的原始指针操作内存。
1.4.4 不要用一个原始指针初始化多个shared_ptr
int *ptr=new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);//逻辑错误
1.4.5 通过shared_from_this返回this指针
不要将this
指针作为shared_ptr
返回回来,因为this
指针本质上是一个裸指针,如下代码错误:
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
public:
shared_ptr<MyClass> GetSelf() {
return shared_ptr<MyClass>(this);//不要这样做
}
MyClass() {
cout << "MyClass()" << endl;
};
~MyClass() {
cout << "~MyClass()" << endl;
};
};
int main()
{
shared_ptr<MyClass> sp1(new MyClass);
shared_ptr<MyClass> sp2 = sp1->GetSelf();
return 0;
}
运行后结果:
MyClass()
~MyClass()
~MyClass()
free(): double free detected in tcache 2
因为这个 sp1 和 sp2之间没有任何关系,会各自析构,从而导致二次释放。应该改成下面这样,继承std::enable_shared_from_this
类,然后使用shared_from_this()
返回this
的shared_ptr
:
#include <iostream>
#include <memory>
using namespace std;
class MyClass: public std::enable_shared_from_this<MyClass>
{
public:
shared_ptr<MyClass> GetSelf() {
return shared_from_this();//不要这样做
}
MyClass() {
cout << "MyClass()" << endl;
};
~MyClass() {
cout << "~MyClass()" << endl;
};
};
int main()
{
shared_ptr<MyClass> sp1(new MyClass);
shared_ptr<MyClass> sp2 = sp1->GetSelf();
return 0;
}
2. std::unique_ptr
独占所有权,确保在其生命周期内只有一个指针可以指向该对象。不能进行拷贝构造或赋值操作,但可以进行移动操作。
2.1 基本使用
在上一段代码基础上,我们增加以下测试代码:
// 创建一个 std::unique_ptr,它将独占所有权一个 X1 对象
std::unique_ptr<X1> x1_3 = std::make_unique<X1>(10);
// std::unique_ptr<X1> x1_3(new X1(10)); // ok 也可以这样定义
x1_3->print();
// 尝试创建另一个指向同一对象的 std::unique_ptr 会导致编译错误
// std::unique_ptr<X1> x1_4 = x1_3; // 错误!不能进行拷贝构造或赋值操作
std::unique_ptr<X1> x1_4 = std::move(x1_3);
// 现在 x1_3 不再指向任何对象
if (!x1_3) {
std::cout << "x1_3 is now empty." << std::endl;
}
x1_4->print();
// 使用 release() 方法释放指针所有权,并将其指向的对象的指针返回
X1* rawPtr = x1_4.release();
// 现在 x1_4 不再指向任何对象
if (!x1_4) {
std::cout << "x1_4 is now empty." << std::endl;
}
// 在不再需要时手动释放内存
delete rawPtr;
输出结果如下:
2.2 代码解释
(1)刚开始创建了一个 x1_3
作为 unique_ptr
指向X1
的一个实例,后面无法通过赋值或拷贝构造来定义 x1_4
,这就是 unique_ptr 的独占性。报错如下:
(2)注意下 unique_ptr
的初始化方式,从C++14开始,unique_ptr
可以使用方法 std::make_unique
来初始化
(3)unique_ptr
无法通过赋值和复制的方式构造,但可以通过 std::move
来构造。因为 std::move
是将内存的所有权转移,还是能保证这块内存只有一个指针在访问。通过输出可以看到,在std::move
之后,x1_3
为空了,x1_4
拿走了所有权。
(4)unique_ptr的释放通过 release()
方法来实现。注意:这里的释放不是指释放内存,而是释放独占属性,表示这块区域可以由其它指针来使用了。release
方法返回的是这块内存区域的裸指针:X1* rawPtr = x1_4.release()
。释放之后,x1_4
就为空了。
(5)注意最后需要手动释放这块内存区域:delete rawPtr;
3. std::weak_ptr
是
std::shared_ptr
的观察者,不会增加引用计数。主要用于解决std::shared_ptr
的循环引用问题,提供一种非拥有的观察方式。
3.1 基本使用
再写下面的测试代码:
std::shared_ptr<X1> x1_1 = std::make_shared<X1>(10);
x1_1->print();
std::printf("1 use count: %d\n", x1_1.use_count());
std::weak_ptr<X1> x1_weak = x1_1;
std::printf("2 use count: %d\n", x1_1.use_count());
std::printf("3 use count: %d\n", x1_weak.use_count());
// 检查 x1_weak 是否指向有效的对象
if (auto ptr = x1_weak.lock()) {
ptr->print();
} else {
std::cout << "Weak pointer is expired." << std::endl;
}
// 销毁 std::shared_ptr,释放资源
x1_1.reset();
// 再次检查 weakPtr 是否指向有效的对象
if (auto ptr = x1_weak.lock()) {
ptr->print();
} else {
std::cout << "Weak pointer is expired." << std::endl;
}
运行结果如下:
3.2 代码解释
(1)根据引用计数的打印,weak_ptr
是不会新增引用计数的。
(2)weak_ptr
在使用时,需要 x1_weak.lock()
之后才能访问内存数据。lock
方法是将 weak_ptr
转化成 shared_ptr
。为了验证这一点,可以在 lock
后再打印 x1_weak
的引用计数,你会看到引用计数增加了:
std::printf("4 use count: %d\n", x1_weak.use_count()); // 引用计数为 2
3.3 解决shared_ptr的循环引用导致内存泄露问题
本文中我们一直在说 shared_ptr 有循环引用问题,weak_ptr 是为了解决 shared_ptr 的循环引用而设计的。
3.3.1 shared_ptr 循环引用导致内存泄露
看下面的示例代码:
#include <iostream>
#include <memory>
class A;
class B;
class A {
public:
std::shared_ptr<B> ptr_b;
~A() { std::cout << "A is destroyed." << std::endl; }
};
class B {
public:
std::shared_ptr<A> ptr_a;
~B() { std::cout << "B is destroyed." << std::endl; }
};
int main() {
std::shared_ptr<A> ptr_a = std::make_shared<A>();
std::shared_ptr<B> ptr_b = std::make_shared<B>();
ptr_a->ptr_b = ptr_b;
ptr_b->ptr_a = ptr_a;
// 当ptr_a和ptr_b离开作用域时,它们的引用计数将变为1,而不是0
// 因此,它们的析构函数不会被调用,发生内存泄漏
return 0;
}
代码中,ptr_a
中有 ptr_b
,ptr_b
中有 ptr_a
,这就是循环引用,这时候在 return 0
之前,ptr_a
和 ptr_b
的引用计数都是2
,而离开二者的作用域后,循环计数只会下降到1
,不会到0
,所以导致 ptr_a
和 ptr_b
的内存一直无法被自动释放,从而会造成内存泄露的问题。
3.3.2 weak_ptr 解决 shared_ptr 循环引用的问题
将其中一个shared_ptr
修改成weak_ptr
:
class B {
public:
std::weak_ptr<A> ptr_a; // 使用 std::weak_ptr
~B() { std::cout << "B is destroyed." << std::endl; }
};
这样修改后,再执行下面的main函数:
int main() {
std::shared_ptr<A> ptr_a = std::make_shared<A>();
std::shared_ptr<B> ptr_b = std::make_shared<B>();
ptr_a->ptr_b = ptr_b;
ptr_b->ptr_a = ptr_a; // 使用 std::weak_ptr
std::printf("ptr_a 的引用计数: %d\n", ptr_a.use_count());
std::printf("ptr_b 的引用计数:%d\n", ptr_b.use_count());
return 0;
}
输出如下:
可以看到循环引用已经没有了,ptr_a
和ptr_b
都被释放了。原因是:weak_ptr
不会增加引用计数,因此ptr_a
的引用计数为1,离开作用域后引用计数变为0,调用析构函数销毁。销毁后,其引用的ptr_b
引用计数减一,变为1,而ptr_b
离开作用域引用计数再减一,变为0,所以ptr_b
也被销毁。
4. std::auto_ptr(C++98已废弃)
独占所有权,不能被复制,但可以被移动。已被
std::unique_ptr
取代。
这个ptr已经不用了,所以在这里就不介绍了,需要的可以自己去查查。
5. 总结
本文介绍了三种智能指针:unique_ptr
、shared_ptr
和 weak_ptr
。 每种智能指针都有各自的使用场景:
(1)unique_ptr
独占对象的所有权,由于没有引用计数,性能较好。
(2)share_ptr
共享对象的所有权,但性能略差。
(3)weak_ptr
配合share_ptr
,解决循环引用问题。
同时,本文也对智能指针的一些使用注意事项做了总结,以帮助大家规避一些使用中常见的坑。
6. 参考
- https://mp.weixin.qq.com/s/EQpghtU8IfV8CN4o0t8-9g
- https://mp.weixin.qq.com/s/9kqUjR2dF98eysk-9pG4mQ
提醒一句:一定要动手去实践一下!没有任何一篇文章看了之后就能彻底搞懂指针,必须亲身体验才能加深印象!
如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~
- 大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例
- 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
- +v: jasper_8017 一起交流💬,一起进步💪。
- 微信公众号也可搜【同学小张】 🙏
本站文章一览: