目录
一、包装器
1.1 包装器的介绍
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数指针
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
}
这段代码的useF有三种可能,分别是:函数指针、函数对象、lambda表达式。可以看到运行后static变量都是1,说明开始的类模版会被实例化成三份。
问题来了,如果我们想把这些可调用对象存储到容器中,如vector<>,那么里面的类型应该怎么写?
如果我们把它们统一成一份,是不是就可以办到了呢。
统一的工具便是包装器,它可以将函数指针、仿函数、lambda表达式包装,我们来看看它的使用:
#include<functional> //头文件
int main()
{
// 函数指针
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
//包装器
function<double(double)> f1 = f;
function<double(double)> f2 = [](double d)->double { return d / 4; };
function<double(double)> f3 = Functor();
//vector<function<double(double)>> v = { f1,f2,f3 };
vector<function<double(double)>> v =
{ f,[](double d)->double { return d / 4; },Functor() };
double n = 4;
for (auto f : v)
{
cout << f(n++) << endl;
}
return 0;
}
其命名格式为:function<Ret(Args...)>,Ret为返回类型,Args为参数列表的类型。
我们来验证一下:
int main()
{
// 函数名
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lambda表达式
std::function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
可以发现这三个调用对象都被实例化成为了一份,它们共享count变量。
1.2 包装器的绑定
bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
我们可以使用它来进行函数中参数位置的调换:
其中bind调用时首先是一个可调用对象Sub,placeholders是一个命名空间,后面的数字对应的是参数的位置,在调用时我们就可以像图中箭头那样理解,那么如何调换参数位置即传参为10,5但让函数运行的是5-10呢?
相信你也想到了,那就是将bind中placeholders的位置调换一下:
这样10就对应到了第二个参数,5就对应到了第一个参数。
当然我们也可以对参数进行缺省,这样我们就绑定了第三个参数,Plus只需要传两个就可以。
需要注意,要绑定的参数是不会占参数位置的,我们直接忽略它即可,如:
即使rate在中间,它也不会占参数的位置,a仍然对应的是1,按顺序b下来仍然是2,rate我们直接忽略不看。
这里的调用对象都是全局的,那么对于局部的成员函数,我们应该如何绑定?
class SubType
{
public:
static int sub(int a, int b)
{
return a - b;
}
int ssub(int a, int b, int rate)
{
return(a - b) * rate;
}
};
int main()
{
function<double(int, int)> Sub1 =
bind(&SubType::sub, placeholders::_1, placeholders::_2); //&也可以不加
SubType st;
function<double(int, int)> Sub2 =
bind(&SubType::ssub,&st, placeholders::_1, placeholders::_2,3); //多一个参数
//&必须加
function<double(int, int)> Sub3 =
bind(&SubType::ssub,SubType(), placeholders::_1, placeholders::_2,3);
return 0;
}
对于static成员函数,可以在没有创建类的实例的情况下直接调用,因此我们传参只需传它本身即可,但是要加类域,其前面的取地址符可以不加。但是对于非static成员函数,它的取地址符是一定要加的,并且还要再声明一个对象。
二、智能指针
2.1 为什么需要智能指针?
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如果在new与delete之间抛异常了,那么就会出现内存泄漏的问题。当然可以使用try-catch来解决问题,但如果new的对象很多,这麻烦就很大了,因此我们可以通过智能指针来解决这种问题。
2.2 智能指针的使用及原理
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
return 0;
}
智能指针实则完美地运用了构造函数和析构函数的特性,这样就保证了初始化自动调用构造函数,出了作用域自动调用析构函数,这样就不需要我们手动delete了。其设计思想我们称为RAII,它是一种利用对象生命周期来控制程序资源的技术。
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此我们在模板类中还得需要将* 、->重载下,才可以让其像指针一样去使用。
template<class T>
class SmartPtr
{
public:
// RAII
// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
// 1、RAII管控资源释放
// 2、像指针一样
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
2.3 智能指针的拷贝问题
经过上面的学习,我们可以知道,所有的智能指针都具有以下特点:
- 都借助RAII的思想管理资源;
- 都像指针一样
因此有了这些基础我们再来认识几个智能指针并尝试自己写出它们。
2.3.1 auto_ptr
我们自定义一个类A,来试试看auto_ptr是什么效果:
class A
{
public:
A(int a = 0 )
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
auto_ptr<A> ap1(new A(1));
return 0;
}
可以看到,正常的创建并释放了对象。我们再进行拷贝:
可以看到,也正常的创建并释放了对象。
我们再来看看底层的代码:
可以发现在拷贝之后,代码实现了管理权转移,把ap1的管理权给了ap3,ap1悬空了,这样就导致了auto_ptr的一个缺陷:拷贝时会把被拷贝对象的资源管理权转移给拷贝对象,导致被拷贝对象悬空,访问就会出问题:
下面我们来尝试自己写一个auto_ptr
namespace lee
{
template<class T>
class auto_ptr
{
public:
//RAII
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//拷贝问题
//管理权转移
auto_ptr (auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
private:
T* _ptr;
};
}
前面的编写与上述智能指针的原理思想一样,要符合RAII和指针一样的原则,然后就是拷贝问题,拷贝其实就是把自己的指针传给要拷贝的对象的指针,然后将自己置为空。
2.3.2 unique_ptr
知道了auto_ptr的缺陷,我们应该怎样解决呢?
C++11标准库中有一个unique_ptr,它就很好的避免了auto_ptr的问题。我们换成unique_ptr继续尝试运行下拷贝的代码:
可以发现这个智能指针直接禁止了拷贝。这就是它简单粗暴的解决方法。
我们也来自己模拟实现一下吧:
template<class T>
class unique_ptr
{
public:
//RAII
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//拷贝问题
//禁止拷贝
//private:
// unique_ptr(unique_ptr<T>& up);
unique_ptr(unique_ptr<T>& up)=delete;
unique_ptr<T>& operator=(unique_ptr<T>& up)=delete;
private:
T* _ptr;
};
前两块内容与上面代码一致,主要是拷贝问题,如何实现禁止拷贝,有两种方法:
- 只声明不实现,并且设为私有,目的是防止外部再对其进行实现
- 与C++11库内一致,直接将声明delete
对赋值我们也是要实现一下禁用的,否则编译器会默认生成赋值,导致程序崩溃。
目的达成:
2.3.3 shared_ptr
上述智能指针都是不让拷贝的,但有的场景是必须要拷贝的,这就需要用到shared_ptr来完成拷贝的任务了。
sp1和sp3是共同管理一块空间的,那它在析构时不应该会析构两次吗?它底层的拷贝问题是值得我们来探究的:
为了方式它不被析构两次,我们可以采用引用计数的方法来解决:多一个对象对其管理,引用计数就加1,再最后析构时不着急释放资源,而是 -- 计数,当计数为0时再释放资源。
这里对于计数count变量的声明我们要仔细考虑,由于它是多个对象共享的,那我们能不能可以将其设为类的static变量?
答案是不行的,如图,假设sp1、sp3共同管理一块空间,sp2、sp4、sp5共同管理一块空间,如果是staic变量(所有对象都共享),就会有冲突了。
我们期望的是一个资源伴随一个count,因此我们再添加一个指针,让它一个指针指向资源,另一个指针指向计数:
因此在构造时我们就new一个计数,析构时对计数--,直到为0再delete:
//RAII
shared_ptr(T* ptr)
:_ptr(ptr)
:_pcount(new int(1))
{}
~shared_ptr()
{
if (-- (*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
拷贝时就先把原先的计数和指针交给要拷贝的对象,最后计数再加1:
//拷贝问题
shared_ptr(const shared_ptr<T>* sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
目标达成:
接下来继续写一个赋值:
我们要考虑到的点有:
- 如果赋值的两个对象指向的是不同的资源,在赋值前要将自己的count--,当它为0时释放资源,赋值到的新的对象的count++;
- 自己给自己赋值或者原本就指向同一资源的两个对象之间的赋值。
//赋值问题
//在赋值前要将自己的count--,当它为0时释放资源,赋值到的新的对象的count++
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//资源相同直接返回
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
2.3.4 weak_ptr
shared_ptr几乎是没有缺点的,但是对于下面的场景还是会有一些问题:
可以看到,这里的两个shared_ptr节点并没有释放,内存泄漏了,这个问题我们叫做循环引用
sp2和sp1析构后释放的逻辑就死循环了:
要想解决循环引用的问题,就需要把这里的shared_ptr改为weak_ptr:
注意这里的shared_ptr并不是智能指针,它只是专门用来解决循环引用的指针。
代码实现:
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
//没有释放
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
2.4 智能指针的删除器
如果不是new出来的对象我们应该如何通过智能指针管理呢?
我们来看看库中是怎样解决的:
可以看到在构造时,库中又加了一个新的参数,其实是将一个可调用对象作为删除器传入,因此我们可以用函数指针、仿函数、lambda指针的任意一种来做为删除器传给shared_ptr:
下面我们来尝试自己增加一个带删除器的智能指针:
首先遇到了问题:
因为删除器可以是仿函数、函数指针、lambda表达式的任意一种,因此这里删除器我们用一个构造函数的类模版来声明它的类型,那在定义_del这个对象时应该用什么声明?
这就需要我们用到上面讲知识:包装器来解决了。
好啦,至此我们智能指针的版块就学习完成了,感谢观看(#^.^#)