1.智能指针的实现原理
智能指针的原理就是将动态分配的内存块与一个或多个智能指针对象相关联,以确保内存块在不再需要时能够自动释放。通常情况下,智能指针会将内存的所有权转移到其自己的对象中,并在其析构函数中释放内存。这样就可以避免常见的内存泄漏和释放非法内存等问题。
智能指针在实现时通常使用引用计数技术,即每个智能指针对象都包含一个计数器,用于记录有多少个智能指针对象引用了同一个内存块。当引用计数器为0时,说明没有任何智能指针对象引用该内存块,此时内存块就可以被释放。
需要注意的是,由于引用计数技术可能存在循环引用的问题,即两个或多个智能指针对象相互引用,导致其引用计数器无法归零,从而导致内存泄漏。为了解决这个问题,C++11标准引入了weak_ptr,可以解决循环引用的问题。
分析《《
1.shared_ptr <<智能指针的本质就是一个模板类,用类来实现对指针对象的管理
template <typename T>
class shared_ptr;
template <typename Y, class Deleter>
shared_ptr(Y* ptr, Deleter d);
template <typename Y, class Deleter, class Alloc>
shared_ptr(Y* ptr, Deleter d, Alloc alloc);
为什么需要用到智能指针 《《《 首先是解决忘记释放资源的内存泄漏 其次是解决当多个指针指向同一个内存块的时候,当某一个指针释放时,导致其他指针变成悬垂指针 ,用户去操作这些指针大概率就会崩溃。悬垂指针的本质 《《 当多个指针同时指向同一个内存资源时,如果通过其中的某一个指针delete释放了资源,其他指针无法感知到。 所以智能指针的引用计数可以解决上述问题。
如上图当某个指针delete时,只是对引用计数减一,当引用计数为0时才会删除对象。
2.make_shared << 是函数模板 ,make_shared 的函数返回值是 shared_ptr
template<typename T>
shared_ptr<T> make_shared(); //make_shared()模板函数,返回一个shared_ptr<T> 类型的返回值
template<typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args);
//例如:
shared_ptr<string> p1 = make_shared<string>(10, '9');
shared_ptr<string> p2 = make_shared<string>("hello");
shared_ptr<string> p3 = make_shared<string>();
make_shared 的优缺点
优点 《《 效率高 安全
shared_ptr需要分配两次内存,控制块和内存是分离的 使用 make_shared 的方式,则只需要一次分配内存,控制块和内存是一起的,分配出的内存结构如图所示:
auto sp1 = make_shared<widget>();
auto sp2(sp1);
可能会出现异常的情况:
//函数F的定义:
void F(shared_ptr<Lhs>& lhs, shared_ptr<Rhs>& rhs) { ... }
//调用F函数:
F(shared_ptr<Lhs>(new Lhs("foo")), shared_ptr<Rhs>(new Rhs("bar")));
C++是不保证参数求值顺序,以及内部表达式的求值顺序的,所以可能的执行顺序如下:
1. new Lhs("foo")
2. new Rhs("bar")
3. shared_ptr<Lhs>
4. shared_ptr<Rhs>
此时,如果程序在第2步时抛出一个异常(比如out of memory等,Rhs的构造函数异常的),那么在第1步中new分配的Lhs对象内存将无法释放,导致内存泄漏。
这个问题的核心在于 shared_ptr 没有立即获得new分配出来的裸指针,shared_ptr与new结合使用时是要分成两步。
修复这个问题的方式有两种:
(1)不要将new操作放到函数形参初始化中,这样将无法保证求值顺序:
//解决方法是先保证两个new分配内存都没有错误,并在new之后立即初始化shared_ptr:
auto lhs = shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);
(2)更推荐的方法,是使用make_shared,一步到位 :
F(make_shared<Lhs>("foo"), make_shared<Rhs>("bar"))
缺点 《《 私有、保护的构造函数 不能使用 内存回收不及时
make_shared的优点是只需申请一次内存,带来了性能上的提升。但这一性能同样也给make_shared带来了缺点:
智能指针的“控制块”中保存着两类关于“引用计数”的信息:
- 强引用;(strong refs)
- 弱引用。(weak refs)
“弱引用计数”用来保存当前正在指向此基础对象的weak_ptr指针的个数,weak_ptr会保持控制块的生命周期,因此有一种特殊情况是:强引用的引用计数已经降为0,没有shared_ptr再持有基础对象,然而由于仍有weak_ptr指向基础对象,弱引用的引用计数非0,原本因为强引用计数已经归0就可以释放的基础对象内存,现在变成了“强引用、弱引用都减为0时才能释放”, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。
3.weak_ptr
- weak_ptr 只能从shared_ptr构建;
- weak_ptr 并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数(强引用的引用计数);
- weak_ptr 没有重载 operator-> 和 operator* 操作符,因此不可以直接通过 weak_ptr 使用对象(必须通过weak_ptr获取到shared_ptr后才能访问基础对象);
- weak_ptr 提供了 expired() 和 lock() 成员函数,分别用于判断基础对象是否已被销毁、返回指向基础对象的shared_ptr指针。
weak_ptr 模板类中常用的成员函数:
use_count : 与 shared_ptr.use_count() 的功能类似,返回指向基础对象的shared_ptr的个数
对于一个“空”weak_ptr(还未指向任何shared_ptr),它的use_count是0
expired : 用于判断weak_ptr所指向的对象是否已经被销毁(即判断use_count是否为0),返回值为bool类型
如果所指对象已被销毁,返回值为1(true)。对于“空”weak_ptr,expired返回值为0
lock : 返回weak_ptr所指向的基础对象上的一个shared_ptr指针
如果对象已经被销毁,则返回“空”shared_ptr
如果成功返回一个shared_ptr,则强引用计数加1
weak_ptr的使用场景:
- 当你想使用对象,但是并不管理对象,并且在需要的时候可以返回对象的shared_ptr时,则使用
- 解决shared_ptr的“循环引用”问题。
#include <iostream>
#include <memory>
using namespace std;
class Parent;
typedef std::shared_ptr<Parent> ParentPtr;
class Child
{
public:
ParentPtr father;
Child() {
cout << "hello Child" << endl;
}
~Child() {
cout << "bye Child\n";
}
};
typedef std::shared_ptr<Child> ChildPtr;
class Parent {
public:
ChildPtr son;
Parent() {
cout << "hello parent\n";
}
~Parent() {
cout << "bye Parent\n";
}
};
void testParentAndChild()
{
ParentPtr p(new Parent());
ChildPtr c(new Child());
p->son = c;
c->father = p;
}
int main()
{
testParentAndChild();
return 0;
}
问题:c只有调用p的析构的时候,才能被释放。p只有调用c的析构的时候,才能被释放。。形成了循环引用,造成了内存泄露
#include <iostream>
#include <cassert>
#include <memory>
using namespace std;
class Parent;
typedef std::shared_ptr<Parent> ParentPtr;
typedef std::weak_ptr<Parent> WeakParentPtr;
class Child
{
public:
WeakParentPtr father; // 只有一环换成 weak_ptr, 即可打破环
Child() {
cout << "hello Child" << endl;
}
~Child() {
cout << "bye Child\n";
}
};
typedef std::shared_ptr<Child> ChildPtr;
typedef std::weak_ptr<Child> WeakChildPtr;
class Parent {
public:
ChildPtr son;
Parent() {
cout << "hello parent\n";
}
~Parent() {
cout << "bye Parent\n";
}
};
void testParentAndChild()
{
ParentPtr p(new Parent());
ChildPtr c(new Child());
p->son = c;
c->father = p;
cout << (c->father).use_count() << endl;
cout << (p->son).use_count() << endl;
}
int main()
{
testParentAndChild();
return 0;
}
析构函数成功调用, 注意析构顺序,谁是weak_ptr对象,就先析构谁。
4.unique_ptr《《独占性 唯一指向,不能赋值给其他指针
unique_ptr的设计主要有如下两点:
- 禁止拷贝构造函数、拷贝赋值运算符,即设置为
=delete
; - 实现了移动构造函数和移动赋值运算符。
unique_ptr必须直接初始化,且不能通过隐式转换来构造,因为unique_ptr的构造函数被声明为explicit。
//unique_ptr 有两个版本:管理单个对象 或 管理动态分配的对象数组:
template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr { /****/ };
template <typename _Tp, typename _Dp>
class unique_ptr<_Tp[], _Dp> { /****/ };
3.3 unique_ptr 的常用操作:
u.get(); //返回unique_ptr中保存的裸指针
u.reset(); //重置unique_ptr
u.release(); //放弃指针的控制权,返回裸指针,并将unique_ptr自身置为空。
u.swap(); //交换两个unique_ptr所指向的对象
unique_ptr<int> uptr1 = make_unique<int>();//新建第一个对象 //unique_ptr<int> uptr2 = uptr1;//错误,唯一指向,不能赋值给其他指针 unique_ptr<int> uptr2 = move(uptr1);//将指针uptr1指向的内存转移给uptr2,uptr1变为空 unique_ptr<int> uptr3= make_unique<int>();//新建第二个对象 int* p4 = uptr3.get();//返回指针uptr3指向的对象,可以修改对象的值,但是不能执行 //delete p4;否则uptr3销毁时释放内存会报错; *p4 = 8;//修改内存保存的值u //delete p4;//会报错,这里销毁一次,uptr3又销毁一次; uptr3.release();//释放uptr3指向内存,但是不销毁内存; unique_ptr<int> uptr4(new int);//新建第三个对象 uptr4.reset();//清空uptr4指向,并且销毁指向内存; unique_ptr<int> uptr5(new int);//新建第四个对象; uptr5.reset(new int);//新建第五个对象,uptr5指向第五个对象,并销毁第四个对象内存; unique_ptr<int> uptr6; uptr6.reset(new int);//新建第六个对象,uptr6指向第六个对象
2.智能指针使用注意事项
在使用shared_ptr管理指针时,有一个原则就是要尽量避免“先new、后用裸指针初始化shared_ptr” 的方式,这是因为当有两个或多个shared_ptr同时管理一个指针时,多个shared_ptr之间无法共享彼此的引用计数,导致可能造成double free。
异常场景示例:(两个shared_ptr共同管理同一个裸指针)
int main() {
int *ptr = new int(42);
shared_ptr<int> sp1(ptr);
shared_ptr<int> sp2(ptr);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
//sp1记录的引用计数是1,sp2记录的引用计数也是1,
//此时有两个智能指针sp1和sp2同时管理ptr,相当于有两个独立的控制块
return 0;
//此时退出作用域,sp1、sp2会分别调用delete去释放基础对象*ptr,
//重复释放,导致程序段错误
}
由此引出一个使用shared_ptr的原则:
当我们使用智能指针管理资源时,必须统一使用智能指针,而不能在某些地方使用智能指针,某些地方使用raw pointer,否则不能保持智能指针管理这个类对象的语义,从而产生各种错误。
给shared_ptr管理的资源必须在分配时立即交给shared_ptr,即:shared_ptr sp(new T());,而不是先new出ptr,再在后面的某个地方将ptr赋给shared_ptr。
在使用shared_ptr管理指针时,有一个原则就是要尽量避免“先new、后用裸指针初始化shared_ptr” 的方式,这是因为当有两个或多个shared_ptr同时管理一个指针时,多个shared_ptr之间无法共享彼此的引用计数,导致可能造成double free。
异常场景示例:(两个shared_ptr共同管理同一个裸指针)
int main() {
int *ptr = new int(42);
shared_ptr<int> sp1(ptr);
shared_ptr<int> sp2(ptr);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
//sp1记录的引用计数是1,sp2记录的引用计数也是1,
//此时有两个智能指针sp1和sp2同时管理ptr,相当于有两个独立的控制块
return 0;
//此时退出作用域,sp1、sp2会分别调用delete去释放基础对象*ptr,
//重复释放,导致程序段错误
}
由此引出一个使用shared_ptr的原则:
当我们使用智能指针管理资源时,必须统一使用智能指针,而不能在某些地方使用智能指针,某些地方使用raw pointer,否则不能保持智能指针管理这个类对象的语义,从而产生各种错误。
给shared_ptr管理的资源必须在分配时立即交给shared_ptr,即:shared_ptr sp(new T());,而不是先new出ptr,再在后面的某个地方将ptr赋给shared_ptr。
1. shared_from_this的使用场景:
上述的情况同样可能会发生在 this指针
上面。
当一个类被shared_ptr管理(当使用shared_ptr管理类对象时,实际上是管理的类对象的 *this指针),且在类的成员函数中需要把当前类对象作为参数传递给其他函数时,就需要返回当前对象的this指针,但是,直接传递this指针(相当于裸指针)到类外,有可能会被多个shared_ptr所管理,造成与上面一样的二次释放的异常错误。
错误示例:
//C是一个可以返回类对象this指针的类:
class C {
public:
C(int b = 10) : a(b) { cout << "constructor" << endl; }
~C() { cout << "destructor" << endl; }
void show() const { cout << "a = " << a << endl; }
C* object_ptr() { return this; } //一个返回*this指针的成员函数
private:
int a;
};
int main() {
shared_ptr<C> sp1(new C(42)); //构造一个C类对象,并由shared_ptr对此对象资源进行管理
shared_ptr<C> sp2(sp1->object_ptr());
//在某种场景下返回类对象的this指针给其他函数,我们的本意是在原有C类对象的基础上累加引用计数
cout << sp1.use_count() << ", " << sp2.use_count() << endl;
//sp1、sp2的引用计数都是 1
return 0;
//在退出程序前sp1、sp2的引用计数都降为0,会分别调用delete去释放C类对象,导致重复释放,段错误
}
出现上述异常的原因很简单,类的成员函数将对象的this指针返回出去,this是一个普通指针,交给智能指针sp2管理,而sp2根本感知不到这个裸指针已经被其他智能指针sp1给管理起来了。
使用shared_ptr直接管理this指针导致“重复释放”的原因在于:
- 使用智能指针管理“类对象”的本质是管理类对象的
this
指针; - this指针与其他的普通裸指针并无区别,当多个shared_ptr同时管理同一个this指针时,相互之间无法感知。
C++11 引入shared_from_this
,使用方式如下:
- 继承
enable_shared_from_this
类; - 调用
shared_from_this()
成员函数先将this指针封装进一个shared_ptr,再将shared_ptr返回到类外供其他人使用。
使用shared_from_this 改写上面的错误示例:
//首先,继承enable_shared_from_this模板类:
//注意继承模板类时需要先将类模板实例化,否则编译器无法知道具体的数据类型
class C : public enable_shared_from_this<C> {
public:
C(int b = 10) : a(b) { cout << "constructor" << endl; }
~C() { cout << "destructor" << endl; }
void show() const { cout << "a = " << a << endl; }
//C* object_ptr() { return this; } //一个返回*this指针的成员函数
//在需要返回类对象this指针的地方,调用shared_from_this成员函数先将this封装成shared_ptr再返回
shared_ptr<C> object_ptr() { return shared_from_this(); }
private:
int a;
};
int main() {
shared_ptr<C> sp1(new C(42));
cout << "sp1.use_count : " << sp1.use_count() << endl;
shared_ptr<C> sp2(sp1->object_ptr());
cout << "sp2.use_count : " << sp2.use_count() << endl;
return 0;
}
------
运行结果:
constructor
sp1.use_count : 1
sp2.use_count : 2
destructor
3.智能指针和管理的对象分别在哪个区
智能指针本身在栈区,托管的资源在堆区,利用了栈对象超出生命周期后自动析构的特征,所以无需手动delete释放资源。