初识C++ · 智能指针
智能指针的引入,我们得先从异常开始说起,异常面临的一个窘境是new了多个对象,抛异常了会导致先new的对象没有析构,从而导致内存泄漏的问题,解决方法是使用RAII。RAII是一种技术,英文的全称是:Resource Acquisition Is Initialization,是利用对象的声明周期来解决内存泄漏的一种技术。throw string("除数为0");
目录
前言:
智能指针的引入,我们得先从异常开始说起,异常面临的一个窘境是new了多个对象,抛异常了会导致先new的对象没有析构,从而导致内存泄漏的问题,解决方法是使用RAII。
RAII是一种技术,英文的全称是:Resource Acquisition Is Initialization,是利用对象的声明周期来解决内存泄漏的一种技术。
就像这样:
template<class T>
class Smart_ptr
{
public:
Smart_ptr(T* ptr)
:_ptr(ptr)
{}
~Smart_ptr()
{
cout << "~Smart_ptr" << endl;
}
private:
T* _ptr;
};
double Division(int a, int b)
{
if (b == 0)
{
throw string("除数为0");
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
Smart_ptr<int> p1 = new int[1000];
Smart_ptr<int> p2 = new int[1000];
int a = 0, b = 0;
cin >> a >> b;
Division(a, b);
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "捕获到未知异常" << endl;
}
return 0;
}
之前的忧虑来源于第二个抛异常之后前面的对象不会析构的问题,那么现在用一个智能指针对象来接受指针,不仅起到了管理空间的作用,还可以调用原本的行为,比如* -> ,这里还没有重载。
上文有些许未介绍的,比如exception,是C++异常体系里面的基类,所以捕捉到了异常我们就可以用exception来捕捉,打印是用的what函数,就和前面提到的示例代码是一样的。
现在就对智能指针有了一个简单的理解了,现在我们就深入了解一下。
1 智能指针的发展历史
提到智能指针,我们首先应该想到的是C++98里面的auto_ptr,早在C++98就引入了智能指针,但是使用确实让人一言难尽,它的本质是转移控制权,所以会导致一个悬空,像move左值一样,导致原来的自定义对象变为空了,这就不是将亡值的施舍了,是活生生的夺舍:
就像文档里面提到的,推荐使用unique_ptr而不是auto_ptr,因为是指针,所以需要重载函数使得该类有指针的对应行为,比如* -> ,这里我们先使用,所在的头文件是memory:
int main()
{
auto_ptr<string> s1(new string("aaaa"));
auto_ptr<string> s2(s1);
return 0;
}
当我们通过监视窗口可以看到:
执行了拷贝构造之后,s1就悬空了,相应的,s2获得了控制权,但是,属实有点鸡肋。
比如之后还要访问s1,那就报错了,反正呢,很多公司是禁止使用auto_ptr的。
那么呢,在03年的时候,引入了一个库boost,这个库是由C++委员会的部分工作人员搞的,在C++11里面引入的很多新特性都是在boost里面借鉴的,其中就涉及到了智能指针,在boost里面有三种智能指针,分别是scoped_ptr,shared_ptr,weak_ptr,其中也有引入数组,但是C++11并没有采纳。
在C++11引入智能指针的时候就借鉴了boost的智能指针,但是有一个指针改了一个名,scoped_ptr改成了unique_str,为什么改呢,咱也不知道,学就完事儿了。
所以今天的重点就是unique_str和shared_ptr和weak_ptr,其中shared_ptr是人人都要会的。
2 unique_ptr和shared_ptr的基本使用
对于使用方面都是很简单,咱们先看一个文档:
模板参数有一个D = 什么什么的,这时定制删除器,本文后面会介绍,现在先不急。
基本的函数使用就是:
这就是所有的成员函数了,get函数是获取原生指针,release是显式的释放指针,但不是显式的析构这个类,同样的,既然是指针,就应该具备指针的行为,比如* ->等,有了stl的基本,这些我们应该是看一下文档就应该知道怎么使用的,这里再看看构造函数:
注意的是unique_ptr是不支持拷贝构造的,重载的第9个函数,拷贝函数被delete修饰了,所以不支持。其中支持auto_ptr 右值构造什么的,咱们先不管,主要了解前面几个就可以了。
class A
{
public:
A(int a1 = 1, int a2 = 1)
:_a1(a1)
,_a2(a2)
{}
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
unique_ptr<A> sp1(new A[10]);
unique_ptr<A> sp2(sp1);
A* p = sp1.get();
cout << p << endl;
sp1->_a1++;
sp1->_a2++;
return 0;
}
找找错误?
第一个 不能拷贝构造,第二析构会报错,因为开辟的是一个数组,基本类型是A,应该是A[],这就和定制删除器有关系了,所以这里的正确代码是:
class A
{
public:
A(int a1 = 1, int a2 = 1)
:_a1(a1)
,_a2(a2)
{}
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
unique_ptr<A[]> sp1(new A[10]);
//unique_ptr<A> sp2(sp1);
A* p = sp1.get();
cout << p << endl;
//sp1->_a1++;
//sp1->_a2++;
sp1[1]._a1++;
sp1[1]._a2++;
return 0;
}
这里的执行结果就是析构了十次:
为什么new了十个空间,基本类型也要是一个数组,这里的解决方案是定制删除器,先不管。
然后就是shared_ptr的基本使用:
相比之下shared_ptr就朴实无华很多了。
成员函数这么多,这里和pair有点像的是,有一个make_shared的东西,在构造的时候会用到:
因为它支持拷贝构造,欸~所以我们可以使用make_shared构造,产生临时对象拷贝一个就行。
简单介绍一下其中的成员函数,use_count是用来计算计数的,因为在智能指针要实现的一个事就是,管理资源更加简单,比如多个对象共同管理一块资源,新加了一个对象管理资源,引入计数就++,反之--,对比auto那里不能拷贝,就是因为如果拷贝了,造成了多次析构的问题,就会报错。
那么要验证计数很简单:
int main()
{
//shared_ptr<int> sp1(new int);
shared_ptr<int> sp1 = make_shared<int>(1);
cout << sp1.use_count() << endl;
{
shared_ptr<int> sp2(sp1);
cout << sp2.use_count() << endl;
}
cout << sp1.use_count() << endl;
return 0;
}
既然是出了局部域就会销毁,那么我们创建一个局部域即可:
简单使用了解一下可以了,咱们这里简单模拟实现一下。
3 shared_ptr的模拟实现
目前对于shared_ptr我们的简单理解有了,那么现在简单捋一下,智能指针防止多次析构的方法是使用计数的方式,那么我们就需要一个计数了,问题是这个变量我们如何创建呢?
template<class T>
class shared_ptr
{
private:
T* ptr;
int count;
};
这样可行吗?显然是不可行的,因为我们创建一个对象,指向空间,就会让这个类多次实例化,每个对象都有一个count,实例化一次count++,然后呢就导致每个对象都有count,count的都是一样的,我们指向一个空间,创建了n个对象,预期是计数为n,但是创建的对象是独立的,析构只会析构某个对象的count,从而count的大小是n * n,所以结果是不可预估的。
那么我们应该用static吗?每创建一个对象就++一次,看起来好像可以,但是我们如果指向的空间不是一个呢?new了两个空间,就会导致两个空间公用一个计数,更不行了。
所以解决办法是创建一个指针,每创建一个对象,指针指向的空间,即计数空间就++:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
这是一般的函数,重点在于赋值重载 和 定制删除器。
对于赋值重载来说,指向的空间修正,指向的计数空间修正,那么原来指向的空间是否需要修正呢?这就要看计数是否到0了,所以需要判断是否到0,到了就析构:
那么析构函数,简单的,new了空间删除就完事了:
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
release();
}
赋值重载判断+ 改定指向:
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (--(sp._pcount) == 0)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
但是,万一有人突发奇想,想自己给自己赋值呢?再万一,有人在指向空间是同一块的基础上相互赋值呢?
所以我们不妨判断一下指向空间的地址是不是一样的:
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
shared_ptr的基本实现就完成了。
现在引入一个线程安全的问题:
mutex mtx;
void Func(Ptr::shared_ptr<list<int>> sp, int n)
{
cout << sp.use_count() << endl;
for (int i = 0; i < n; i++)
{
Ptr::shared_ptr<list<int>> copy1(sp);
Ptr::shared_ptr<list<int>> copy2(sp);
Ptr::shared_ptr<list<int>> copy3(sp);
mtx.lock();
sp->push_back(i);
mtx.unlock();
}
}
int main()
{
Ptr::shared_ptr<list<int>> sp1(new list<int>);
cout << sp1.use_count() << endl;
thread t1(Func, sp1, 100);
thread t2(Func, sp1, 200);
t1.join();
t2.join();
cout << sp1->size() << endl;
cout << sp1.use_count() << endl;
return 0;
}
解决方法是引入atomic,原子操作:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new std::atomic<int>(1))
{}
private:
T* _ptr;
std::atomic<int>* _pcount;
还有一个问题就是,如果是交叉指向,就会导致无法析构:
struct Node
{
//std::shared_ptr<Node> _next;
//std::shared_ptr<Node> _prev;
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
int _val;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
std::shared_ptr<Node> p1(new Node);
std::shared_ptr<Node> p2(new Node);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
cout << p1->_next.use_count() << endl;
cout << p2->_prev.use_count() << endl;
return 0;
}
此时就需要弱指针,weak_ptr,只是指向空间,但是计数不++即可。
4 有关定制删除器
为什么会引入定制删除器呢?
int main()
{
Ptr::shared_ptr<int> sp1((int*)malloc(sizeof(4)));
Ptr::shared_ptr<FILE> sp2(fopen("test.txt", "w"));
return 0;
}
在内存管理章节提及,内存管理的函数不能混合使用,何况是FILE呢,FILE*要使用fclose,所以我们应该定制一个删除器,但是如何传过去呢?传只能传到构造,传不到析构,所以我们不妨:
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new std::atomic<int>(1))
, _del(del)
{}
private:
T* _ptr;
std::atomic<int>* _pcount;
std::function<void(T*)> _del = [](T* ptr) {delete ptr; };
要删除的时候,传对象就可以了,比如:
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
class A
{
public:
A(int a1 = 1, int a2 = 1)
:_a1(a1)
,_a2(a2)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
Ptr::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
Ptr::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());
Ptr::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
Ptr::shared_ptr<A> sp4(new A);
return 0;
}
智能指针就介绍到这里。
感谢阅读!
更多推荐
所有评论(0)