目录
- 内存管理问题
- RALL
- 智能指针
- 为什么shared_ptr共享指针会出现循环引用问题,以及怎么解决
- make_shared与显式通过构造函数初始化(new)的shared_ptr区别?
- new 来创建对象额外开销
- make_shared 的优点:
- 智能指针的实现?(share)
内存管理问题
动态内存管理经常会出现两种问题:
① 忘记释放内存,会造成内存泄漏
② 尚有指针引用内存的情况下就释放了它,产生指针悬挂
RALL
RAII是C++中的一个惯用法,即“Resource Acquisition Is Initialization”,翻译为“资源获取就初始化”。**在构造函数中申请分配资源,在析构函数中释放资源。**因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。RAII的核心思想是将资源或者状态与对象的生命周期绑定,通过C++的语言机制,实现资源和状态的安全管理,智能指针是RAII最好的例子。
智能指针
智能指针本质上是一个对象,里面封装了普通指针,就是RALL技术
智能指针就可以方便我们控制指针对象的生命周期。在智能指针中,一个对象什么情况下被析构或被删除,是由指针本身决定的,并不需要用户进行手动管理
unique_ptr独享指针
unique_ptr没有复制构造函数,不支持普通的拷贝和赋值操作
unique最常见的使用场景,就是替代原始指针,为动态申请的资源提供异常安全保证。
只要unique_ptr创建成功,unique_ptr对应的析构函数都能保证被调用,从而保证申请的动态资源能被释放掉。
class MyClass {
public:MyClass() ~MyClass()
};int main() {std::unique_ptr<MyClass> ptr1(new MyClass()); // 创建独占指针// std::unique_ptr<MyClass> ptr2 = ptr1; // 错误,不能复制std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 移动所有权//nuique_ptr是一个模板类,Myclass是指针管理的对象资源是模板类的参数,这两个告诉编译器怎么编译//move(ptr1)将ptr1转换成右值,返回一个右值引用,ptr2 将接管 ptr1 所指向的对象的所有权。之后,ptr2 成为唯一拥有该对象的指针。ptr1指空if (!ptr1) {std::cout << "ptr1 is now null\n"; // ptr1 已经失去所有权}return 0; // 当ptr2超出作用域时,MyClass会被自动销毁
}
shared_ptr共享指针
允许多个指针指向同一个对象。
当对象的所有权需要共享(share)时,share_ptr可以进行赋值拷贝,每一个shared_ptr的拷贝都指向相同的内存
内部使用计数机制维护,每使用他一次,内存的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。
class MyClass {
public:MyClass() ~MyClass()
};int main() {std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); // 使用make_shared创建共享指针 //初始化智能指针ptr1,引用为1{std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权//ptr1 的引用计数从 1 增加到 2。//ptr2 现在也与 ptr1 共享这个计数器,所以它们的引用计数都是 2。std::cout << "Reference count: " << ptr2.use_count() << "\n"; //ptr2是在{}定义的,作用域在{}内} // ptr2超出作用域,它的析构函数被调用,引用计数减少到 1,失去对资源的引用。//ptr1的作用域是整个main函数std::cout << "Reference count after ptr2 is out of scope: " << ptr1.use_count() << "\n"; // ptr1仍保持对资源的引用,技术1return 0; // 当ptr1超出作用域时,MyClass会被自动销毁
}
weak_ptr
weak_ptr
比较特殊,它主要是为了配合shared_ptr
而存在的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。
#include <iostream>
#include <memory>//是 C++ 标准库中的一个头文件,用于提供智能指针的定义和功能。class B; // 前向声明class A {
public:std::shared_ptr<B> b_ptr; // 共享指针指向 B~A()
};class B {
public:std::weak_ptr<A> a_ptr; // 弱指针指向 A~B()
};int main() {// 创建 A 的共享指针std::shared_ptr<A> a = std::make_shared<A>();// 创建 B 的共享指针,并将其保存到 A 的成员中std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;// 将 B 中的 weak_ptr 指向 Ab->a_ptr = a; // 此时 a 和 b 之间建立了弱引用关系std::cout << "Reference count of A: " << a.use_count() << "\n"; // 输出引用计数为 1std::cout << "Reference count of B: " << b.use_count() << "\n"; // 输出引用计数为 1// weak_ptr 不增加引用计数if (auto a_locked = b->a_ptr.lock()) { // 尝试从 weak_ptr 获取 shared_ptrstd::cout << "A is still alive\n";} else {std::cout << "A is already destroyed\n";}/*.lock() 方法:当你调用 lock() 方法时,它会尝试获取一个指向 weak_ptr 管理的对象的 shared_ptr。
如果 weak_ptr 所指向的对象仍然存在(即至少有一个 shared_ptr 指向它,引用计数大于 0),则 lock() 会返回一个有效的 shared_ptr,指向该对象。
如果 weak_ptr 所指向的对象已经被销毁(即引用计数为 0),lock() 会返回一个空的 shared_ptr,可以通过检查返回值来判断对象是否仍然有效。*/// 当 a 和 b 超出作用域时,A 和 B 的析构函数会被调用return 0;
}
那weak_ptr
存在的意义到底是什么呢?
weak指针的出现是为了解决shared指针循环引用造成的内存泄漏的问题。由于shared_ptr
是通过引用计数来管理原生指针的,那么最大的问题就是循环引用(因为它们都在互相等待对方先释放,所以造成内存泄漏。),这样必然会导致内存泄露(无法删除)。而weak_ptr
不会增加引用计数,因此将循环引用的一方修改为弱引用,可以避免内存泄露。
为什么shared_ptr共享指针会出现循环引用问题,以及怎么解决
当两个或多个对象通过shared_ptr相互引用时,它们的引用计数会互相增加,从而导致内存无法被释放。
比如说:
两个类,a和b都被创建,并且它们互相持有对方的shared_ptr。
a的引用计数和b的引用计数都增加到2。
当main结束时,a和b的引用计数不会归零,导致它们永远存在于内存中,造成内存泄漏。
解决:
将其中一个对象的指针改为weak_ptr,以打破循环引用。
a和b的引用计数可以正常减少到0,从而调用它们的析构函数,正确释放内存。
make_shared与显式通过构造函数初始化(new)的shared_ptr区别?
显式通过构造函数初始化:
std::shared_ptr<MyClass> ptr(new MyClass(constructor_args));
这种方式显式地使用new来创建对象并将其传递给shared_ptr。在这种情况下,指针内存的分配和控制块的分配是分开的。
使用 new 来创建对象时,会单独为对象分配内存。之后,shared_ptr 的控制块会进行另一轮内存分配,以存储引用计数和其他管理信息。这意味着至少需要两次内存分配。
new 来创建对象额外开销
性能开销:
由于显式初始化需要两次内存分配,这会增加性能开销,特别是在需要频繁创建和销毁对象的场景中。
每次内存分配都需要调用操作系统的内存分配器,这会增加开销并导致性能下降。
内存溢出问题:
如果在高频率分配内存的情况下,由于系统的内存管理策略,可能会导致内存碎片的增加,进而提高内存溢出的风险。频繁的内存分配和释放会导致内存不可用的情况,从而引发 std::bad_alloc 异常。
make_shared:
auto ptr = std::make_shared<MyClass>(constructor_args);
make_shared会为对象的构造分配内存,并返回一个shared_ptr,同时在单个内存分配中管理对象和控制块(用于引用计数等信息)。
make_shared 的优点:
make_shared 的优点:
只进行一次内存分配,性能更优。
降低了内存碎片的可能性,减少了内存溢出的风险。
提供了更好的异常安全性,确保内存管理更加高效。
因此,在实际开发中,推荐使用 make_shared 来创建 shared_ptr,以提高性能和内存管理的安全性。
智能指针的实现?(share)
原理:智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函
数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程
序)结束时会自动被释放
➢ 智能指针(smart pointer)的通用实现技术是使用引用计数。智能指针类
将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指
针指向同一对象。
+1:
每次创建类的新对象时,初始化指针就将引用计数置为 1;当对象作为另
一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计
数;
-1:
对一个对象进行赋值:(如果引用计数为减至 0,则删除对象),并增加右
操作数所指对象的引用计数;(只能转移,不能拷贝,因此赋值运算会使左
操作数-1,右操作数+1)
调用析构函数时,析构函数减少引用计数(如果引用计数减至 0,则删除
基础对象)
注意:为了实现智能指针的效果,必须借助一个计数器,以便随时获知有多
少智能指针绑定在同一个对象上。显而易见,这个计数器不应该是智能指针
这个类的一部分。(指针内存和控制块分开创建)这是因为,如果计数器是智能指针类的一部分,那么每次增减计数器的值,都必须广播到每一个管理着目标对象的智能指针。这样做的代价太大了。