一、可变参数模板
1.基本概念
想要了解C语言的可变参数列表的原理可见:可变参数列表
这个跟C语言的可变参数列表有一定的关系,常用的printf与scanf的参数就包含可变参数列表。
那么可变参数模板是什么呢?举个例子便一目了然。
template<class...Arg>
void ShowList(Arg... arg)
{
//...
}
int main()
{
ShowList();
ShowList(1, 2, 3);
ShowList('a', 'b', 'c');
return 0;
}
这就是可变参数模板,可以传多个参数,这里的Arg称为模板的可变参数包,这里的arg称为形参的可变参数包(包括0-N个参数)。
但是这里有一个问题即如何用这个参数包呢?
- 递归展开
void _ShowList()
{
cout<< endl;
}
template<class T,class...Arg>
void _ShowList(T val, Arg... arg)
{
cout << val << " ";
_ShowList(arg...);
}
template<class...Arg>
void ShowList( Arg... arg)
{
_ShowList(arg...);
}
int main()
{
ShowList();
ShowList(1, 2, 3);
ShowList('a', 'b', 'c');
return 0;
}
- 编译器以类似递归的方式(调用的不是同一个函数,是来自一个模板实例化出的函数)自动推导出参数包的类型然后进行打印。
运行结果:
- 逗号表达式
template<class T>
int PrintArg(T val)
{
cout << val << " ";
return 0;
}
void ShowList()
{
cout << endl;
}
template<class... Arg>
void ShowList(Arg... arg)
{
int arr[] = { PrintArg(arg)... };
//处理成类似与int arr[] = { PrintArg(arg), PrintArg(arg),PrintArg(arg)};这样的形式。
//这是编译链接阶段处理的结果。
cout << endl;
}
int main()
{
ShowList();
ShowList(1, 2, 3);
ShowList('a', 'b', 'c');
return 0;
}
运行结果:
2.应用场景
1.提升传参的灵活性
例:
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
:_year(year)
,_month(month)
,_day(day)
{}
public:
int _year;
int _month;
int _day;
};
template<class ...Args>
Date* CreatDate(Args... args)
{
Date* ret = new Date(args...);
return ret;
}
int main()
{
Date* date1 = CreatDate(1);
Date* date2 = CreatDate(1,2);
Date* date3 = CreatDate(1,2,3);
Date* date4 = CreatDate(Date(1,2,3));
return 0;
}
- 不仅可以调用构造函数,也可调用默认的拷贝构造函数,进而提高了灵活性。
2.接口——emplace_back
参数:
- 说明:这里在可变参数模板的基础上添加了万能引用。
int main()
{
list<pair<int, MY_STL::string>>lt;
//先调用pair的构造函数再调用右值的移动拷贝构造。
lt.push_back(make_pair(1, "张三"));
cout << endl;
lt.push_back({ 2, "李四" });
cout << endl << endl;
//作为参数包(引用)传递下去,直接调用list的构造函数进行构造
lt.emplace_back(3, "王五");
cout << endl;
lt.emplace_back(make_pair(4, "赵六"));//这里编译器进行了优化,也是直接调用构造函数进行初始化。
return 0;
}
-
可见push_back虽然多调用了两次构造但好在是移动构造消耗不大,emplace_back直接将参数包传下去,直接调用构造函数进行初始化,总而言之与push_back有区别但性能上相差不大。
-
举例 : list的emplace_back
//list里面的实现接口
template <class... Args>
void push_back(Args&&... args)
{
insert(end(), args...);
}
template <class... Args>
void emplace_back(Args&&... args)
{
push_back(args...);
}
template <class... Args>
void insert(iterator pos, Args&&... args)
{
Node* newnode = new Node(args...);
Node* cur = pos._node;
Node* prev = cur->_prev;
newnode->_next = cur;
newnode->_prev = prev;
cur->_prev = newnode;
prev->_next = newnode;
_size++;
}
//Node的实现接口
template<class ...Args>
list_node(Args&& ...arg)
:_val(arg...)
, _next(nullptr)
, _prev(nullptr)
{}
//实现逻辑——不断传参数包,将参数包传给构造函数进行初始化。
二、包装器
1. 简单实现
如何用外界控制某段执行逻辑?
- C语言阶段我们可以通过传函数指针
- C++阶段我们可以通过传仿函数或者lambda(底层是仿函数)
这三者有没有什么共同的特性呢?就是可以像函数一样进行使用。如果实现的功能一样那么参数和返回值是一样的。
根据这两个性质,我们可以将这三者抽象成一个模板,以实现减法举例:
template<class T1,class T2,class T3>
double fun(T1 f, const T2& x, const T3& y)
{
return f(x, y);
}
double sub(int x, int y)
{
return x - y;
}
struct Sub
{
double operator()(int x, int y)
{
return x - y;
}
};
int main()
{
auto x = [](int x, int y)->double {return x - y;};//lambda表达式。
cout << fun(x, 1, 2) << endl;
cout << fun(sub, 4, 2) << endl;
Sub s;
cout << fun(s, 9, 2) << endl;
return 0;
}
这样的一个模板就将这三者进行泛化,都可以通过模板实例化使用,也就是实现了包装的作用,当然这只是最简单的用法,下面我们就进入库里面的包装器的使用。
2. 基本语法与使用
重点:class function<Ret()Args...>;
1. Ret 返回值
2. Args... 函数参数
简单使用:
double sub(int x, int y)
{
return x - y;
}
struct Sub
{
double operator()(int x, int y)
{
return x - y;
}
};
int main()
{
auto x = [](int x, int y)->double {return x - y;};
function<double(int, int)> f1 = x;
function<double(int, int)> f2 = s;
function<double(int, int)> f3 = sub;
cout << f1(2 , 3) << endl;
cout << f2(2, 9) << endl;
cout << f3(9, 9) << endl;
return 0;
}
更为实际的使用:
比如:逆波兰表达式的求值
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
map<string,function<int(int,int)>> s = {
{"+",[](int x,int y){return x + y;}},
{"-",[](int x,int y){return x - y;}},
{"*",[](int x,int y){return x * y;}},
{"/",[](int x,int y){return x / y;}}
};
stack<int> st;
int size = tokens.size();
for(int i = 0; i < size; i++)
{
if(tokens[i] == "+" || tokens[i] == "-"
|| tokens[i] == "*" || tokens[i] == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(s[tokens[i]](left,right));
}
else
{
st.push(stoi(tokens[i]));
}
}
return st.top();
}
};
仔细品味这段代码,就会发现包装器的好用之处,其原因在于直接将一类功能类似的函数进行泛化,可以通过具体的反应进行直接调用
,还有我们所写的一类代码统一的出现在一个地方这样便于维护
。
三、bind
bind也是函数的封装,类似与包装器,不过更详细的是对参数的个数和传参顺序的封装,下面我们来看看如何具体使用。
1.基本语法
- 将参数1(函数指针/ 仿函数/lambda),剩余函数参数传进去即可,具体要进行分类进讨论。
2.常见用法
1.函数指针
- 改变参数顺序
double sub(int x, int y)
{
return x - y;
}
int main()
{
function<double(int, int)> f1 = bind(sub, placeholders::_1,\
placeholders::_2);
cout << f1(1, 2) << endl;
function<double(int, int)> f2 = bind(sub, placeholders::_2,\
placeholders::_1);
cout << f2(1, 2) << endl;
return 0;
}
逻辑图解:
- 改变参数个数
double sub1(int x, double rate, int y)
{
return (x - y) * rate;
}
int main()
{
function<double(int, int)> ff1 = bind(sub1, placeholders::_1,\
1.5, placeholders::_2);
//在定义期间固定参数,类似与缺省值的效果。
cout << ff1(2, 1) << endl;
auto ff2 = bind(sub1, placeholders::_1, 1, placeholders::_2);
cout << ff2(3, 1) << endl;
}
逻辑图解:
- 说明:lambda与函数指针的用法相同。
2.仿函数
struct SubType
{
static double sub(int x, int y)//没有this指针,不用进行传参
{
return x - y;
}
double ssub(int x, int y)
{
return x - y;
}
};
int main()
{
function<double(int, int)> pf1 = bind(SubType::sub,\
placeholders::_1, placeholders::_2);
//sub为static函数,没有this指针,只需指定作用域即可。
SubType st; //语法规定普通成员函数传参需加&
auto pf3 = bind(&SubType::ssub, &st, placeholders::_1,\
placeholders::_2);
//ssub为普通成员函数,有this指针,因此需要指定作用域外加传this指针。
auto pf2 = bind(&SubType::ssub, SubType(), placeholders::_1,\
placeholders::_2);
//也可用对象进行调用。
return 0;
}
总之,bind可以改变参数的个数和顺序,改变参数的个数可以固定参数值和减少传参
,改变参数顺序,可以更加符合函数使用者的习惯
(比如在strcpy一般第一个参数是dest,第二个参数为source,如果实现接口的程序员将参数写反了,我们可以通过包装更加符合我们的代码编写习惯。) 。
四、智能指针
为了解决异常的执行流乱跳而导致内存泄漏的问题:
void func()
{
//假设new int 可能会抛出bad_alloc的异常。
int* ptr1 = new int(1);
//当运行到此处ptr1出错时,我们只需抛出异常即可
int* ptr2 = new int(2);
//当运行到此处出ptr2错时,我们需要释放ptr1,然后抛出异常。
int* ptr3 = new int(3);
//当运行到此处ptr3出错时,我们需要释放ptr2,然后向外层抛出异常。
delete ptr1;
delete ptr2;
delete ptr3;
}
第一种方式我们可以通过try catch进行解决:
void func1()
{
//假设new int 也可能会抛出异常。
//根据异常的捕获与处理,我们可以这样解决。
try
{
int* ptr1 = new int(1);
//...
try
{
int* ptr2 = new int(2);
//...
try
{
int* ptr3 = new int(3);
//...
delete ptr3;
}
catch (...)
{
//当运行到此处ptr3出错时,我们需要释放ptr2,然后向外层抛出异常。
delete ptr2;
throw;
}
delete ptr2;
}
catch (...)
{
//当运行到此处出ptr2错时,我们需要释放ptr1,然后抛出异常。
delete ptr1;
throw;
}
delete ptr1;
}
catch (...)
{
//当运行到此处ptr1出错时,我们只需抛出异常即可
throw;
}
}
int main()
{
try
{
func1();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
//...未知异常;
}
return 0;
}
-
很明显,每开辟一次我们需要进行try catch进行手动对异常进行捕获,很麻烦,那有没有简单的方式呢?很明显是有的。
-
RAII思想,资源获取即初始化,也就是用局部对象对资源进行管理,出作用域时会自动调用析构函数对资源进行释放。
就像这样:
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr)
:_ptr(ptr)
{}
~smart_ptr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
void func()
{
//假设new int 可能会抛出bad_alloc的异常。
smart_ptr<int> ptr1 (new int(1));
//当运行到此处ptr1出错时,我们只需抛出异常即可
smart_ptr<int> ptr2(new int(2));
//当运行到此处ptr2出错时,出作用域自动释放ptr1,然后抛出异常。
smart_ptr<int> ptr3(new int(3));
//当运行到此处ptr3出错时,出作用域自动释放ptr1,ptr2,然后向外层抛出异常。
}
这样就简单的使用了RAII思想解决问题,但是如果是赋值问题与拷贝该如何解决呢?在使用默认的成员函数的情况下,可能会出现内存泄漏和重复释放的风险。下面就让我们看看库里面是如何实现的吧!
1.auto_ptr
1.1基本使用
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a = 0;
};
int main()
{
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(new A(2));
auto_ptr<A> ap3(ap1);//资源管理权的转移。
ap1 = ap2;//调用析构函数先释放ap1的资源,再将ap2的资源转移给ap1。
return 0;
}
监视逻辑:从ap3的初始化开始。
- 赋值与拷贝的实现方式是对资源的管理权进行转移。
1.2基本实现
namespace MY_SMART_PTR
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
delete _ptr;
}
//既然是指针就得像指针一样进行访问。
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
auto_ptr( auto_ptr<T>& ptr)
:_ptr(ptr._ptr)
{
ptr._ptr = nullptr;
}
auto_ptr<T>& operator = (auto_ptr& ptr)
{
if(_ptr)
delete _ptr;
_ptr = ptr._ptr;
ptr._ptr = nullptr;
return *this;
}
private:
T* _ptr;
};
}
2.unique_ptr
1.1基本使用
- 对资源的使用权进行转移。
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a = 0;
};
int main()
{
unique_ptr<A> up1(new A(1));
unique_ptr<A> up2(new A(2));
unique_ptr<A> up3(new A(3));
up1 = up2;//直接禁掉了。
unique_ptr<A> up3(up1);//也是被删除了
return 0;
}
会直接进行报错:
1.2基本实现
- 不可进行共享资源
namespace MY_SMART_PTR
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
delete _ptr;
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
//两种方式:
//1.C++98之前构造函数私有+声明进行实现。
//2.C++11沿用之前的default与纯虚函数 == 0的写法,设为delete进行删除。
unique_ptr(const unique_ptr<T>& ptr) = delete;
unique_ptr<T>& operator = (unique_ptr<T>& ptr) = delete;
private:
T* _ptr;
};
}
3.shared_ptr
1.1基本使用
- 可以进行共享资源。
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a = 0;
};
int main()
{
shared_ptr<A> sp1(new A(1));
shared_ptr<A> sp2(new A(2));
shared_ptr<A> sp3 = sp1;//采取引用计数sp3与sp1共享资源,析构采取引用计数。
sp1 = sp2;//实现需注意与自身的赋值
return 0;
}
实现图解:
- 当对象进行拷贝构造时,将对象进行浅拷贝,并进行引用计数。
- 当对象进行赋值时,对被赋值的对象的引用计数进行检查,对引用计数减减,如果引用计数为0。则进行资源的清理。
1.2基本实现
- 开辟一个count的资源对指向资源的对象进行计数。
namespace MY_SMART_PTR
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr )
:_ptr(ptr)
,_pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
shared_ptr(shared_ptr<T>& ptr)
:_ptr(ptr._ptr)
,_pcount(ptr._pcount)
{
if(_pcount)
(*_pcount)++;
}
shared_ptr<T>& operator = (shared_ptr<T>& ptr)
{
//判断是否相等,包括资源指向空的情况
if (ptr._pcount == _pcount)
return *this;
//*this的_pcount的内容进行减减,判断是否为0
if (_pcount && --(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
//对ptr指向的pcount加加
_ptr = ptr._ptr;
_pcount = ptr._pcount;
if(_pcount)
++(*_pcount);
return *this;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
shared_ptr看起来是完美的,那么有没有什么缺陷呢?
思考下面一段代码:
struct Node
{
public:
~Node()
{
cout << "~Node()" << endl;
}
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
};
int main()
{
//循环引用——关键与难点
//画图分析
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
- 运行分析程序的结果:
运行结果看似正确,其实隐含了内存泄漏,因为sp1和sp2的资源没有被释放,这是为什么呢?
我们画图进行分析:
- 关键在于对象析构时sp1和sp2分别只调用了一次析构函数,使引用计数减为1,最后对资源没有进行释放,那么如何解决这个问题呢?就引出了weak_ptr。
- 这种现象我们称之为循环引用。
4.weak_ptr
1.1基本使用
{
public:
~Node()
{
cout << "~Node()" << endl;
}
weak_ptr<Node> _prev;
weak_ptr<Node> _next;
};
int main()
{
//循环引用——关键与难点
//画图分析
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
- 其实现原理也很简单,就是weak_ptr中没有引用计数,且不参与资源的析构。
图解:
- 在析构时调用两次即可对资源进行释放。
1.2基本实现
- 实现shared_ptr转weak_ptr的赋值与拷贝构造。
- 没有引用计数且不需要写析构函数。
namespace MY_SMART_PTR
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
T& operator *()
{
return *_ptr;
}
T* operator ->()
{
return _ptr;
}
weak_ptr(shared_ptr<T>& ptr)
:_ptr(ptr.get())
{}
weak_ptr<T>& operator = (shared_ptr<T>& ptr)
{
_ptr = ptr.get();
return *this;
}
private:
T* _ptr = nullptr;
};
}
- 1.weak_ptr不是
RAII的智能指针
,是专门用来解决循环引用的问题。
2.weak_ptr不增加引用计数,只参与资源使用,不参与资源的释放。
总结
今天的分享就到这里了,如果有所帮助,不妨点个赞鼓励一下,我们下篇文章再见~