网络上大多有关智能指针的解析只停留于简单的字面理解,今天来详细解析一下三种智能指针的用法以及具体的代码。
目录
概念
RAII机制介绍
智能指针雏形
unique_ptr
weak_ptr
智能指针不是一个指针,它其实是一个对象。它是通过C++的RAII机制实现的。主要是利用C++中对象在释放的时候,会自动调用析构函数这一特性。
所以,当智能指针对象释放的时候,在智能指针对象的析构函数中来释放其管理的内存资源。这样,开发人员就不需要手动去释放已经分配的内存空间。
C++17标准之后,C++标准中还有三种智能指针:shared_ptr、unique_ptr、weak_ptr。下面我们将一一介绍。
RAII
是Resource Acquisition Is Initialization
的简称,其翻译过来就是“资源获取即初始化”,即在构造函数中申请分配资源,在析构函数中释放资源,它是C++
语言中的一种管理资源、避免泄漏的良好方法。
C++
语言的机制保证了,当创建一个类对象时,会自动调用构造函数,当对象超出作用域时会自动调用析构函数。RAII
正是利用这种机制,利用类来管理资源,将资源与类对象的生命周期绑定,即在对象创建时获取对应的资源,在对象生命周期内控制对资源的访问,使之始终保持有效,最后在对象析构时,释放所获取的资源。
RAII
技术被认为是C++
中管理资源的最佳方法,更进一步来说,使用RAII
技术也可以实现安全、简洁的状态管理。
智能指针雏形代码:
template < typename T>
class SmallSmartPtr {
public:
explicit SmallSmartPtr(T* ptr = nullptr)
: ptr_(ptr)
{}
~SmallSmartPtr()
{
delete ptr_;
}
T* get() const { return ptr_; }
private:
T* ptr_;
};
测试代码:
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
};
int main()
{
{
SmallSmartPtr t(new Test());
}
system("pause");
}
测试结果:
shared_ptr是智能指针的一种,不仅通过RAII机制来管理内存资源,还引入了引用计数来解决当多个智能指针指向同一块内存空间的时候,何时释放这块内存空间的问题。也就是说,同一时刻可以有多个shared_ptr拥有一块内存空间的所有权,当最后一个shared_ptr被销毁时,这块内存空间的引用计数为0时,这块内存空间将被释放。
shared_ptr对象有两个指针,一个是指向管理的内存空间,一个是指向内存控制块,内存控制块中包含引用计数和其他的一些信息(删除器和分配器)。
代码示例:
shared_ptr
用图表示如下:
t1释放的时候,引用计数减一,然后释放t1的内存空间,如下:
当t2释放的时候,引用计数会再减一,这时引用计数就会变成0,这时就会释放Object的内存空间和内存控制块的空间,同时t2对象的空间也会被释放。
1)初始化
//通过构造函数初始化
shared_ptr t(new Test());
//使用make_shared来初始化智能指针
shared_ptr t = make_shared();
注意:不要使用裸指针进行初始化 如:
//尽量不要使用裸指针来初始化智能指针
Test* pTest = new Test();
shared_ptr t(pTest);
因为使用裸指针初始化智能指针,容易导致多次使用同一个裸指针对多个智能对象进行初始化。这样就会导致两个智能指针在销毁的时候会去释放同一片内存空间。会造成程序异常崩溃。 如:
Test* pTest = new Test();
shared_ptr t(pTest);
//t1释放的时候会导致程序异常
shared_ptr t1(pTest);
2)支持拷贝构造、赋值
shared_ptr t(new Test());
shared_ptr t1(t);
t1 = t;
3)获取原始指针
shared_ptr t = make_shared();
Test* pTest = t.get();
unique_ptr是独占型智能指针。独占性,就是不允许多个智能指针指向同一块内存空间。也不支持拷贝、赋值,即不能通过赋值将一个unique_ptr赋值给另一个unique_ptr。
1)初始化
//通过构造函数初始化
unique_ptr t(new Test());
//使用make_unique来初始化智能指针
unique_ptr t = make_unique();
2)获取原始指针
unique_ptr t(new Test());
Test* pTest = t.get();
3)支持所有权转移
unique_ptr不支持拷贝、赋值,但支持所有权转移,即智能指针将当前所指的内存空间的所有权交给另一个智能指针。被管理的内存空间永远只有一个智能指针指向它。
unique_ptr t(new Test());
unique_ptr t1 = move(t);
weak_ptr和shared_ptr、unique_ptr不同,weak_ptr不能单独作为智能指针使用。weak_ptr是用来辅助shared_ptr来解决循环引用的问题。
先看一个示例:
class Test;
class Object
{
public:
Object()
{
cout << "Object()" << endl;
}
~Object()
{
cout << "~Object()" << endl;
}
shared_ptr m_pTest;
};
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
shared_ptr m_pObject;
};
int main()
{
{
shared_ptr t1(new Object());
shared_ptr t2(new Test());
t1->m_pTest = t2;
t2->m_pObject = t1;
}//离开作用域,释放t1、t2
system("pause");
}
执行结果如下:
发现问题没?为什么没有调用两个对象的析构函数?,也就是说,两个智能指针释放了,但是管理的内存空间没有被释放,那不就是内存泄漏吗,我们看下这一切是怎么发生的:
这个示例的重点是,Test和Object对象中各有一个智能指针类型的成员,而且这两个智能指针指向的都是对方的内存空间。
如图所示,每块内存空间都有两个智能指针指向。当t1释放的时候,释放t1对象的内存空间,Test的内存块引用计数减一:
同理,t2对象释放的时候,释放t2对象的内存空间,Object的内存块引用计数减一:
此时,已经没有智能指针管理这块内存空间,但是这两块内存空间还未被释放,这个时候,就造成了内存泄漏。
问题的根因是,Test和Object内部的成员指针指向对方的时候,也造成了引用计数加1。
要解决这个问题就需要用到两个东西:weak_ptr和弱引用计数。
也就是说,内存控制块中有一个特殊的引用计数,叫弱引用计数。就是上图中的weak count。为区分,也为了方便表达,我们把原来的引用计数叫强引用计数 。
weak_ptr指向一个对象,不会造成这个对象的内存控制块中的强引用计数加1,只会让弱引用计数加1。而内存空间什么时候释放,是取决于对应的强引用计数什么时候变成0。所以,当t1、t2都被释放之后,两个对象的强引用计数都会变成0。所以,内存空间会被释放。
这个示例的重点是,Test和Object对象中各有一个智能指针类型的成员,而且这两个智能指针指向的都是对方的内存空间。
class Test;
class Object
{
public:
Object()
{
cout << "Object()" << endl;
}
~Object()
{
cout << "~Object()" << endl;
}
//修改成weak_ptr
weak_ptr m_pTest;
};
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
//修改成weak_ptr
weak_ptr m_pObject;
};
int main()
{
{
shared_ptr t1(new Object());
shared_ptr t2(new Test());
t1->m_pTest = t2;
t2->m_pObject = t1;
}
system("pause");
}
运行结果: