对象生存期和资源管理(RAII)
RAII:Resource Acquisition Is Initialization,资源获取即初始化。是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。我们常常利用栈对象(栈对象在进程结束时会被自动回收)的生命周期来管理资源(内存资源、文件描述符、文件、锁)。
四个基本特征
- 在构造函数中初始化资源,或者称为托管资源
- 在析构函数中释放资源
- 提供若干访问资源的方法(比如:读写文件)
- 一般不允许复制或者赋值(对象语义)
有关语义的知识,可以查看这篇文章:https://www.cnblogs.com/MyXjil/p/17300170.html
智能指针
在现代 C++ 编程中,标准库包含智能指针,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它对RAII思想的体现是至关重要的。
智能指针的基本框架
智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
#include <iostream>
#include <string>
using namespace std;
// 类模板
template<class T>
class Smartptr // 自定义的智能指针
{
public:
Smartptr(T* _ptr)
: ptr(_ptr)
{
}
~Smartptr()
{
// 析构函数中释放指针所指向的空间
if(ptr != nullptr)
{
delete ptr;
ptr = nullptr;
}
}
// 智能指针需要能够像普通指针一样使用,所以需要重载 *运算符和 ->运算符
T& operator*()
{
return *ptr;
}
T& operator->()
{
return ptr;
}
private:
T* ptr;
};
void test1()
{
Smartptr<int> pInt(new int(10));
Smartptr<string> pStr(new string("hello"));
cout << "*pInt = " << *pInt << endl;
cout << "Str = " << *pStr << endl;
}
void test2()
{
// 我们的类中没有自定义拷贝构造函数和赋值运算符函数,于是下面的代码会调用类中原始的拷贝构造函数
// 这时,pInt1和pInt2会指向同一片区域,于是程序结束时会对这一资源对象释放两次,程序进而会崩溃
Smartptr<int> pInt1(new int(10));
Smartptr<int> pInt2(pInt1); // error
}
int main()
{
test1();
return 0;
}
综上所述,我们不能使用原生的拷贝构造函数和赋值运算符函数,并且在定义的拷贝构造函数和赋值运算符函数时需要考虑只能释放一次资源对象。
智能指针内存模型
C++库中的智能指针
auto_ptr(一般不会使用)
// 源码
class auto_ptr
{
public:
auto_ptr(_Tp* __p = 0)
: _M_ptr(__p)
{
}
auto_ptr(auto_ptr& __a)
: _M_ptr(__a.release())
{
}
_Tp* release()
{
_Tp* __tmp = _M_ptr;
_M_ptr = nullptr;
return __tmp;
}
private:
_Tp* _M_ptr;
};
// 使用实例
#include <iostream>
#include <memory>
using std::cout;
using std::endl;
using std::auto_ptr;
void test()
{
//pInt称为裸指针(原生的指针)
int *pInt = new int(10);
auto_ptr<int> ap(pInt);//智能指针
cout << "*ap = " << *ap << endl;
cout << "*pInt = " << *pInt << endl;
cout << endl;
//表面上执行的是拷贝操作,但是底层已经将右操作数ap所托管
//的堆空间的控制权已经交给了左操作数ap2,并且ap底层的数据
//成员已经被置为空了,该拷贝操作存在隐患,所以该拷贝操作是存在缺陷的
auto_ptr<int> ap2(ap);
cout << "*ap2 = " << *ap2 << endl;
/* cout << "*ap = " << *ap << endl;//error 会出现段错误*/
}
int main(int argc, char **argv)
{
test();
return 0;
}
unique_ptr
具有移动语义,不允许复制或者赋值(源码中将拷贝构造函数和赋值运算符函数=delete,禁用了)。可以作为容器的元素(容器中可以存unique_ptr类型元素)
// 使用实例
#include <iostream>
#include <memory>
#include <vector>
using std::cout;
using std::endl;
using std::unique_ptr;
using std::vector;
class Point
{
public:
Point(int ix = 0, int iy = 0)
: _ix(ix)
, _iy(iy)
{
cout << "Point(int = 0, int = 0)" << endl;
}
void print() const
{
cout << "(" << _ix
<< ", " << _iy
<< ")" << endl;
}
~Point()
{
cout << "~Point()" << endl;
}
private:
int _ix;
int _iy;
};
void test()
{
unique_ptr<int> up(new int(10));
cout << "*up = " << *up << endl;
cout << "up.get() = " << up.get() << endl;
//独享所有权的智能指针,独立拥有托管的空间
//在语法层面将auto_ptr的缺陷摒弃了
/* unique_ptr<int> up2 = up;//error */
unique_ptr<int> up3(new int(20));
/* up3 = up;//error */
cout << endl;
vector<unique_ptr<Point>> vec;
unique_ptr<Point> up4(new Point(10, 20));
vec.push_back(std::move(up4)); // 不能传左值,但是可以传右值
vec.push_back(unique_ptr<Point>(new Point(1, 3))); // 或者直接构建右值,显式调用unique_ptr的构造函数
}
int main()
{
test();
return 0;
}
对于第52行和53行的代码解释:
unique_ptr已经将拷贝构造和赋值运算符函数这类复制的函数删除掉了,如果传左值,会有复制的操作,是会出错的。因此只能传右值作为参数
share_ptr(强引用智能指针)
是一个共享所有权的智能指针,允许对象之间进行复制或者赋值,展示出来的就是值语义。使用引用计数的观点。当对象之间进行复制或者赋值的时候,引用计数会加+1,当最后一个对象销毁的时候,引用计数减为0,此时会回收托管的空间。
share_ptr的原理
shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:
- shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
- 当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
- 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
- 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
shared_ptr常用函数
s.get()
:返回shared_ptr中保存的裸指针s.reset()
:重置shared_ptr- 不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针s不是唯一指向该对象的指针,则引用计数减少1,同时将P置空
- 带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若s不是唯一的指针,则只减少引用计数,并指向新的对象
s.use_count()
:返回shared_ptr的强引用计数s.unique()
:若use_count为1,返回true,否则返回false
shared_ptr的构造
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
// 用make_shared来构建智能指针,因为make_shared比较高效
auto sp1 = make_shared<int>(100);
//或
shared_ptr<int> sp1 = make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100));
// 注意:不能将一个原始指针直接赋值给一个智能指针
std::shared_ptr<int> p = new int(1);
使用时要注意的问题
1、不要使用不同的智能指针托管同一片堆空间(会出现double free的问题)
// 这种情况实际是用同一个裸指针ptr构建了两个智能指针p1和p2,但它们实际上没有任何联系。因此,在他们离开作用域之后,前面申请的堆空间会被释放两次
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // error
2、不要在函数实参中创建shared_ptr
//因为c++的函数参数的计算顺序在不同的编译器不同的约定下可能不一样的。 一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建,则int内存就泄露了
func(shared_ptr<int>(new int),g()); // 有缺陷
// 正确写法
shared_ptr<int> p(new int);
func(p,g());
3、不要将this指针作为shared_ptr 返回出来
class A
{
public:
shared_ptr<A> GetSelf()
{
return shared_ptr<A>(this); // 不要这么做
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
// 实际上是用同一个this指针构造了两个智能指针sp1和sp2,但他们实际上是没有任何关系的。在他们离开作用域之后,会调用两次析构函数,本质上和问题1是类似的
正确返回this指针和shared_ptr的做法是:让目标类继承std::enable_shared_from_this类,然后使用基类的成员函数 shared_from_this()来返回this的shared_ptr
#include <iostream>
#include <memory>
using namespace std;
class A:
public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
return shared_from_this();
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}
4、避免循环引用
问题:存在循环引用导致内存泄漏的风险
案例:
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
}
int main()
{
shared_ptr<ListNode> n1(new ListNode); // 1
shared_ptr<ListNode> n2(new ListNode); // 2
n1.get()->_next = n2; // 3(n1结点的下一个指向n2)
n2.get()->_next = n1; // 4(n2结点的下一个指向n1)
return 0;
}
此时运行程序会发现析构函数并没有被调用,因为在这种情况下,两个智能指针相互托管导致对方的资源无法释放,于是就出现了循环引用的问题:
首先,程序释放n2,此时n2所指向对象的引用计数自减
然后程序释放n1,此时n1所指向对象的引用计数自减
至此,程序结束,两个对象的引用计数始终不会减到0,所以两者都不会调用析构函数
小结:循环引用就是两个智能指针互相托管对方的资源,导致单独靠对象的销毁无法使引用计数减为0,于是堆空间无法回收,因而产生内存泄漏。
解决循环引用的方法:weak_ptr
C++库中存在weak_ptr类型的智能指针。weak_ptr类的对象可以指向shared_ptr,并且不会改变shared_ptr的引用计数。一旦最后一个shared_ptr被销毁时,对象就会被释放。
所以为了解决上述循环引用的问题,我们可以将结构体内的_next指针定义为weak_ptr类型指针,这样可以防止循环引用的出现。
weak_ptr常用函数
-
expired()
:用于判断所观察资源是否已经释放- 如果引用计数
use_count()==0
,那么该函数的返回值就是true - 如果
use_count() != 0
,那么该函数的返回值就是false
- 如果引用计数
-
lock()
:将weak_ptr提升为一个share_ptr,然后再来判断shared_ptr,进而知道weak_ptr指向的空间还在不在
weak_ptr<int> wp;
shared_ptr<int> sp = wp.lock();
if(sp)
{
cout << "提升成功" << endl;
cout << "*sp = " << *sp << endl;
}
else
{
cout << "提升失败,wp所指向的空间已经被回收" << endl;
}
删除器
默认情况下,智能指针释放时,会用delete释放资源。所以针对FILE或者socket这种资源,需要自定义删除器,使用fclose或者close来释放资源。
#include <iostream>
#include <memory>
#include <string>
using std::cout;
using std::endl;
using std::unique_ptr;
using std::shared_ptr;
using std::string;
void test00()
{
string msg = "hello,world\n";
FILE *fp = fopen("wd.txt", "a+");
fwrite(msg.c_str(), 1, msg.size(), fp);
fclose(fp);
}
//删除器
struct FILECloser
{
void operator()(FILE *fp)
{
if(fp)
{
fclose(fp);
cout << "fclose(fp)" << endl;
}
}
};
void test()
{
string msg = "hello,world\n";
/* FILE *fp = fopen("wd.txt", "a+"); */
unique_ptr<FILE, FILECloser> up(fopen("wd.txt", "a+"));
//get函数可以从智能指针获取到裸指针
fwrite(msg.c_str(), 1, msg.size(), up.get());
/* fclose(up.get()); */
}
void test2()
{
FILECloser fc;
//要想从类型调用到对象,可以直接显示使用构造函数,比如:
//使用无参构造函数
string msg = "hello,world\n";
/* shared_ptr<FILE> sp(fopen("wuhan.txt", "a+"), FILECloser()); */
shared_ptr<FILE> sp(fopen("wuhan.txt", "a+"), fc);
fwrite(msg.c_str(), 1, msg.size(), sp.get());
/* fclose(sp.get()); */
}
int main(int argc, char **argv)
{
test2();
return 0;
}
智能指针源码剖析
https://www.bilibili.com/read/cv13410407/
标签:std,int,RAII,智能,new,shared,ptr,指针 From: https://www.cnblogs.com/MyXjil/p/17403807.html