目录
[1.1 这是什么?](#1.1 这是什么?)
[1.2 initializer_list](#1.2 initializer_list)
[1.3 在容器的运用](#1.3 在容器的运用)
[1.4 STL中的变化](#1.4 STL中的变化)
[2.1 是什么?](#2.1 是什么?)
[2.2 这两个东西有啥关系?](#2.2 这两个东西有啥关系?)
[2.3 有啥用?](#2.3 有啥用?)
[3.1 什么是移动构造?](#3.1 什么是移动构造?)
[3.2 移动赋值](#3.2 移动赋值)
[3.3 STL中的变化](#3.3 STL中的变化)
[3.4 完美转发](#3.4 完美转发)
[编辑 3.5 类的新功能](#编辑 3.5 类的新功能)
[3.6 小练习](#3.6 小练习)
[4.1 是什么?](#4.1 是什么?)
[4.2 怎么用](#4.2 怎么用)
[4.2.1 打印参数个数](#4.2.1 打印参数个数)
[4.2.2 递归获取参数](#4.2.2 递归获取参数)
[4.2.3 另一种递归](#4.2.3 另一种递归)
[4.2.4 奇怪的玩法](#4.2.4 奇怪的玩法)
[5.1 是什么?](#5.1 是什么?)
[5.2 lamdba使用场景](#5.2 lamdba使用场景)
[5.3 其他捕捉方法](#5.3 其他捕捉方法)
[6.2 包装器使用](#6.2 包装器使用)
[6.3 逆波兰OJ题](#6.3 逆波兰OJ题)
[6.4 bind](#6.4 bind)
[8.1 auto和decltype](#8.1 auto和decltype)
[8.2 两个被批评的容器](#8.2 两个被批评的容器)
[8.3 emplace系列接口](#8.3 emplace系列接口)
一,列表初始化
1.1 这是什么?
在C++98中,标准允许使用花括号进行统一的列表初始化的设定,下面两种初始化是一样的
cpp
int x1 = 1;
int x2 = { 2 };
而在C++11中,扩大了花括号的初始化列表的使用范围,使其可以用于所有内置类型和用户自定义的类型,其中 "=" 也可以省略了
cpp
int x3{ 2 };
int x4(1); //调用int的构造
int* pa = new int[4] {0};//C++11中列表初始化也可以适用于new表达式中
C++11后自定义类型也支持列表初始化,本质是调用自定义类型的构造函数
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "调用Date的构造函数";
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
void main1()
{
Date d1(2022, 11, 22);
Date d2 = { 2022,11,22 }; //(构造+拷贝,优化成直接构造)
//如果不想让自定义类型这样搞,在构造函数前面加个explicit表示该构造函数不能隐式转换
Date d3{ 2022,11,22 };//C++11
}
1.2 initializer_list
STL可以支持不同数量参数初始化,因为花括号括起来的常量数组,C++把它识别成一个类型initializer_list
文档介绍这个是一个类模板,用来访问C++初始化列表中的的值,且此类型的对象由编译器根据初始化列表声明自动构造,如下演示代码:
cpp
void main(
{
//STL可以支持不同数量参数初始化,因为花括号括起来的常量数组,C++把它识别成一个类型
initializer_list
auto i1 = { 1,2,3,4,5,6 };
auto i2 = { 1,2,5,6 };
cout << typeid(i1).name() << endl;
cout << typeid(i2).name() << endl;
initializer_list<int>::iterator it1 = i1.begin();
initializer_list<int>::iterator it2 = i2.begin();
cout << it1 <<endl;
{
1.3 在容器的运用
cpp
void main()
{
Date d1(2022, 11, 22);
Date d2 = { 2022,11,22 };
Date d3{ 2022,11,22 };//C++11
cout << "---------------------" << endl;
vector<int> v1 = { 1,2,3,4,5,6 }, v2{ 1,2,3,4,5,6 }; //容器都可以用列表初始化
list<int> lt1 = { 1,2,3,4,5,6 }, lt2{ 1,2,3,4,5,6 };
vector<Date> v3 = {d1, d2}; //用对象初始化vector
vector<Date> v4 = { Date(2024,1,29), Date(2024,1,30)}; //用匿名对象初始化
vector<Date>v5 = { {2024,1,1}, {2023,11,11} }; //隐式类型转换
string s1 = "11111"; //单参数的构造函数支持隐式类型的转换
//支持initializer_list的构造
map<string, string> dict = { {"sort","排序"},{"insert","插入"}};
pair<string, string> kv = { "Date","日期" };
//赋值重载
initializer_list<pair<const string,string>> kvil = { { "left", "左边" }, { "left", "左边" } };
dict = kvil;
}
1.4 STL中的变化
C++11后所有STL容器都会增加一个支持类似list(initializer_list<value_type> il)这样的构造函数,下面是vector的新构造函数模拟实现
cpp
vector(initializer_list<T> il)
{
/*initializer_list<T>::iterator it = il.begin();
while (it != li.end())
{
push_back(*it);
++it;
}*/
for(auto& e : il)
{
push_back(e);
}
**总结:**C++11以后一切对象都可以用列表初始化,但是我们建议普通对象还是用以前的方式初始化,容器如果有需求,可以用列表初始化
二,右值引用和左值引用
2.1 是什么?
先回顾下什么是左值 --> 可以获取它的地址的表达式,const修饰的不能赋值,其它大部分都可以给它赋值,如下代码:
cpp
int* p = new int(0);
int b = 1;
const int c = 2;
//下面就是几个常见的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
double x = 1.1, y = 2.2;
所以右值就是 --> 不能取地址的表达式,并且不能出现在赋值符号左边,如下代码:
cpp
10; //cout<<&10<endl; 自变量(无法打印地址)
x + y; //cout<<&(x+y)<<endl; 表达式返回的临时变量(也无法打印地址)
fmin(x, y); //函数返回值
//下面就是几个右值引用的例子
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x,y);
2.2 这两个东西有啥关系?
①首先左值引用不能直接给右值取别名
cpp
//double& r1 = x + y;
//左值不能引用右值,因为这是权限的放大了,x + y传值时是生成一个临时对象,这个临时对象具有常性,所以要加const
const double& r1 = x + y;//const左值可以引用右值
②右值引用不能给左值取别名
cpp
//int&& rr5 = b;不能
int&& rr5 = move(b);//右值引用可以给move以后的左值取别名
③const左值引用可以引用左值也可以引用右值
cpp
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const 左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const 右值引用" << endl; }
void main()
{
//const左值引用可以引用右值,但缺点是无法知道我引用的是左值还是右值
//所以右值引用的第一个意义就是在函数传参的时候进行更好的参数匹配
int a = 0;
int b = 1;
Func(a);
Func(a + b);
}
2.3 有啥用?
引用的核心价值 --> 减少拷贝
左值引用能解决的问题:
1,做参数 -- ①减少拷贝,提高效率 ②做输出型参数
2,做返回值 -- ①减少拷贝,提高效率 ②可以修改返回对象,比如map的operator[],可以修改插入
左值引用不能解决的问题:
1,传值返回时返回局部对象时,局部对象出了函数栈帧作用域时会被销毁,这时候引用对应的值就没了,引用失效,出现类似野指针的野引用
2,容器插入接口的对象拷贝问题,C++以前,是void push_back(const T& x); 不论T是自定义还是内置类型,都会生茶一个对象x,然后再拷贝赋值然后再销毁x,这样做消耗太大
所以C++11以后所有的容器的插入接口都重载了个类似void push_back(T&& x); 的右值引用接口,使自定义类型走右值引用的移动拷贝,实现资源转移不拷贝,提高效率
所以C++11最大的亮点之一就是右值引用和接下来要讲的移动语义,这两个东西加起来可以大大提高拷贝赋值的效率
三,*移动构造和移动赋值
为了方便演示构造和赋值打印,我们先简单模拟实现string
cpp
namespace bit
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造(资源转移)" << endl;
swap(s);
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值 -- 延长了对象内的资源的生命周期 s = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
3.1 什么是移动构造?
cpp
bit::string To_String(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
如上图左图,在bit::string To_String(int value)函数中可以看到,这里只能使用传值返回,所以会有两次拷贝构造(现在的编译器会把连续构造两次的情况优化为只构造一次)
cpp
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造(资源转移)" << endl;
swap(s);
}
bit::string to_string(int value)
{
bit::string str;
//...
return str;
}
int main()
{
bit::string ret = bit::to_string(-1234);
return 0;
}
如上代码,to_string的返回值是一个右值,用这个右值构造ret,如果没有移动构造,编译器就会匹配调用拷贝构造,因为const左值引用可以引用右值,这里就是一个深拷贝
如果同时有拷贝构造和移动构造,那么编译器会选择最匹配的参数调用,这里就是一个移动构造。而这个更好的参数匹配我们前面讲左值和右值引用的关系的第三点提过
3.2 移动赋值
cpp
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
void main()
{
bit::string s1;
s1 = to_string1(1234);
//C++98中,先生成对象s1,然后to_string返回时再生成一个str对象返回,然后拷贝给临时对象,然后再拷贝给s1,消耗太大 --> 两次深拷贝
//C++11后,直接生成s1,然后to_string返回时,将返回时临时生成的str识别成将亡值,把资源给临时对象,然后通过string& operator=(string&& s)再移动给s --> 只是单纯的资源转移,无任何拷贝,大大提高效率
//bit::string("hello world"); //析构函数添加打印~string的话,这一条语句会打印~string
//const bit::string& ref1 = bit::string("hello world"); //不打印~string,
// const引用延长传参时临时对象生命周期,是为了解决void push_back(const string& s);
// 当我们v.push_back(string("1111"));时,string("1111")的生命周期只有这一行,这里用引用的话一旦生命周期过了,就没了,所以这种场景下必须延长匿名对象生命周期
//bit::string s2;
//const bit::string& ref2 = bit::to_string(1234); //如果to_string返回值为const引用,出了作用域后调用析构函数销毁了,这时候再去访问就造成了类似野指针的野引用
//如果to_string是传值返回,就可以用const引用接收了,因为传值返回时返回的就是str的拷贝
}
3.3 STL中的变化
由于移动语义在提高效率上的飞跃,C++11后所有容器的拷贝和赋值都增加了右值引用的移动语义重载
cpp
void main()
{
std::string s1("hello world");
std::string s2(s1);//拷贝构造
//std::string s3(s1+s2);
std::string s3 = s1 + s2;//C++98就是拷贝构造,C++11已经更新,这里现在是移动构造
std::string s4 = move(s1);//s1被move后就成为了一个右值,就被弄走了,所以调试时走完这一行,监视窗口的s1没了
}
并且,所有可插入容器的插入接口也都重载了右值引用版本,如下代码:
cpp
void main()
{
vector<bit::string> v;
bit::string s1("hello");
v.push_back(s1);
cout << "--------------------------" << endl;
v.push_back(bit::string("world"));
//v.push_back("world");//平时喜欢这样写
cout << "==========================" << endl;
list<bit::string> lt;
//bit::string s1("hello");
lt.push_back(s1); //左值
lt.push_back(move(s1)); //右值
cout << "--------------------------" << endl;
//lt.push_back(bit::string("world")); //匿名对象是右值,也是一次移动构造
lt.push_back("world"); //但是我们喜欢这样写,上面那个不推荐
}
3.4 完美转发
cpp
//万能引用/引用折叠 -- t能引用左值,也能引用右值,如果没有完美转发,会被全部搞成左值
template<typename T>
void PerfectForward(T&& t)
{
//完美转发 -- 保持t引用对象属性
//Func(std::forward<T>(t));
Func(t);//没有完美转发时,main()里面就全部搞为左值引用
}
//模板中的&&不代表右值引用,代表万能引用,它既能接收左值又能接收右值
//但是模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力
//但是引用类型的唯一作用就是限制了接收的类型,后续使用的时候会全部退化成左值
//所以我们希望能够在传递过程中保持它的左值或右值的属性,就需要用到完美转发 --> std::forward<T>()
#include"list.h"
void main()
{
//如果没有完美转发,则下面全部搞成了左值引用
PerfectForward(10); // 右值
cout << "------------------------" << endl;
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
cout << "------------------------" << endl;
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
cout << "------------------------" << endl;
bit::list<bit::string> lt;
bit::string s1("hello");
lt.push_back(s1);
cout << "------------------------" << endl;
//如果是确定类型就是走拷贝,模板就可以走右值引用
lt.push_back("world");
}
演示模拟转发,需要在list_node类节点里面添加右值引用版本的构造
cpp
namespace bit
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{}
list_node(T&& x)//这里也提供一个右值引用
:_data(std::forward<T>(x))//完美转发
, _next(nullptr)
, _prev(nullptr)
{}
};
// typedef __list_iterator<T, T&, T*> iterator;
// typedef __list_iterator<T, const T&, const T*> const_iterator;
// 像指针一样的对象
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> iterator;
//typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef ptrdiff_t difference_type;
Node* _node;
// 休息到17:02继续
__list_iterator(Node* node)
:_node(node)
{}
bool operator!=(const iterator& it) const
{
return _node != it._node;
}
bool operator==(const iterator& it) const
{
return _node == it._node;
}
// *it it.operator*()
// const T& operator*()
// T& operator*()
Ref operator*()
{
return _node->_data;
}
//T* operator->()
Ptr operator->()
{
return &(operator*());
}
// ++it
iterator& operator++()
{
_node = _node->_next;
return *this;
}
// it++
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
// --it
iterator& operator--()
{
_node = _node->_prev;
return *this;
}
// it--
iterator operator--(int)
{
iterator tmp(*this);
_node = _node->_prev;
return tmp;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//typedef __reverse_iterator<iterator, T&, T*> reverse_iterator;
//typedef __reverse_iterator<const_iterator, const T&, const T*> //const_reverse_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
/*reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}*/
void empty_init()
{
// 创建并初始化哨兵位头结点
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
list()
{
empty_init();
}
void swap(list<T>& x)
//void swap(list& x)
{
std::swap(_head, x._head);
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
void push_back(const T& x)
{
//Node* tail = _head->_prev;
//Node* newnode = new Node(x);
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
insert(end(), x);
}
void push_back(T&& x)//右值引用 + 完美转发,不加完美转发就会被折叠成左值引用
{
insert(end(), std::forward<T>(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)//右值引用 + 完美转发,不加完美转发就会被折叠成左值引用
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(std::forward<T>(x));
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
}
3.5 类的新功能
原来C++类中,有6个默认成员函数:构造,析构,拷贝构造,拷贝赋值重载,取地址重载,const取地址重载,最重要的是前面四个,后两个用处不大。
而C++11后新增了两个:移动构造和移动赋值运算符重载
注意:在你没有自己实现析构函数,拷贝构造,拷贝赋值重载中的任意一个,编译器会自动生成默认移动构造和默认移动赋值,对于内置类型成员会执行按字节拷贝,对于自定义类型成员,需要看这个成员是否实现了移动构造和重载,没有就调用它拷贝构造和重载
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{}
一个构造函数可以复用其他构造函数
//Person(const char* name)
// :Person(name,18) //委托构造
//{}
/*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
// 如果不想让Person对象拷贝或被拷贝
//Person(const Person& p) = delete; //delete表示禁止生成
//强制生成移动构造和移动赋值
// Person(Person&& p) = default;
// Person& operator=(Person&& p) = default; //default表示强转生成
//
// ~Person()
// {
// cout << "~Person()" << endl;
// }
private:
bit::string _name; //自定义类型
int _age; //内置类型
};
void main()
{
Person s1("张三",18);
Person s2 = s1; //拷贝构造
Person s3 = std::move(s1); //移动构造(没有构造,也可以调用构造)
cout << endl << endl;
Person s4;
s4 = std::move(s2); //调用bit::string的移动拷贝
}
3.6 小练习
题目 -- 要求用delete关键字实现一个类,且只能在堆上创建对象
cpp
class HeapOnly//指类只能在堆上
{
public:
HeapOnly()
{
_str = new char[10];
}
~HeapOnly() = delete;
void Destroy()
{
delete[] _str;
operator delete(this);
}
private:
char* _str;
};
void main15()
{
//HeapOnly hp1;
//static HeapOnly hp2;//无法在堆以外的区域生成对象
HeapOnly* ptr = new HeapOnly;
//delete ptr;析构被限制,调不了
ptr->Destroy(); //把类里的构造函数new的10个char释放
//operator delete(ptr); //把生成的ptr对象释放掉
}
四,可变参数模板
4.1 是什么?
C++11的新特性可变参数模板可以让我们创建接收可变参数的函数模板和类模板,相比C++98,以前的类模板和函数模板只能含固定数量的模板参数。但是这块使用起来比较抽象,容易出bug,所以对于这一块本文章只点到为止,不进行深入学习
cpp
template <class ...Args>
void ShowList(Args ...args)
{}
首先Args是一个模板参数包,args是一个函数形参参数包。
我们把带省略号的参数称为"参数包",里面包含了大于0个模板参数。我们无法直接获取 参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是参数包的一个重要特征,也是最大的难点,因为解析参数包过程比较复杂,一旦代码变多就容易出bug并且不好排查,下面将介绍可变参数模板的解析与使用
4.2 怎么用
4.2.1 打印参数个数
cpp
template<class ...Args>
void ShowList1(Args ...args)//(Args ...args)是函数参数包
{
cout << sizeof ...(args) << endl;//打印参数包内参数的个数
}
void main()
{
string str("hello");
ShowList1();
ShowList1(1);
ShowList1(1,'A');
ShowList1(1,'A',str);
}
4.2.2 递归获取参数
cpp
void ShowList2()//应对0个参数的参数包时
{
cout << endl;
}
//Args... args代表N个参数包(N >= 0)
template<class T, class ...Args>
void ShowList2(const T& val, Args ...args)
{
cout << "ShowList(" << val << ", 包里还剩" << sizeof...(args) << "个参数)" << endl;
ShowList2(args...); //利用类似递归的方式来解析出参数包
}
void main()
{
string str("hello");
ShowList2(1, 'A', str);
ShowList2(1, 'A', str, 2, 3, 5.555);
}
4.2.3 另一种递归
cpp
void _ShowList3()
{
cout << endl;
}
template <class T, class ...Args>
void _ShowList3(const T& val, Args... args)
{
cout << val << " ";
cout << __FUNCTION__ << "(" << sizeof...(args)+1 << ")" << endl;
_ShowList3(args...);
}
template <class ...Args>
void ShowList3(Args... args)
{
_ShowList3(args...);
}
void main()
{
string str("hello");
ShowList3(1, 'A', str);
ShowList3(1, 'A', str, 2, 3, 5.555);
}
4.2.4 奇怪的玩法
cpp
template<class T>
int PrintArg(T t)
{
cout << t << " "; //所有的参数可以在这里获取到
return 0;
}
template<class ...Args>
void ShowList4(Args ...args)
{
int arr[] = { PrintArg(args)... };
//这里利用了编译器自动初始化,这里自动初始化这个数组,编译器编译的时候对这个数组进行大小确认再开空间,就去解析后面这个内容
//就把参数包第一个值传给PrintArg的T,然后又因为后面是...所以参数包有几个值就要生成几个PrintArg表达式,然后生成几个表达式前面的那个数组就开多大
//然后PrintArg就拿到了所有参数包的值,然后main函数调用后就开始打印
cout << endl;
}
//编译器编译推演生成了一下代码
//void ShowList(char a1, char a2, std::string a3)
//{
// int arr[] = { PrintArg(a1),PrintArg(a2),PrintArg(a3) };
// cout << endl;
//}
void main()
{
ShowList4(1, 'A', string("sort"));
ShowList4(1, 2, 3);
}
五,lambda表达式
5.1 是什么?
在C/C++中可以像函数那样使用的对象/类型有:1,函数指针(C) 2,仿函数/函数对象(C++) 3,lamdba(C++)
lamdba表达式又叫匿名函数。具体语法如下:
cpp
lamdba表达式语法:[capture-list](parameters)mutable -> return_type { statrment }
捕捉列表 参数列表 返回值类型 函数体实现
①捕捉列表(capture-list):该部分出现在lamdba最开始,编译器根据[]来判断接下来的代码是否为lamdba函数,捕捉列表能够捕捉上下文的变量供lamdba函数使用
②参数列表(parameyers):与普通函数的参数列表一致,如果不需要传参数,次()可以省略
③mutable:默认情况下,lamdba函数总是一个const函数,mutable可以取消其常量性。(一般用不到,省略)
④返回值类型(retuen_type):用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。但是在返回值类型明确情况下,仍可省略,编译器会自动堆返回类型进行推导
⑤函数体(statement):与普通函数体一样,可以使用参数列表和捕捉列表的变量
下面是lamdba的简单演示:
cpp
void main()
{
//最简单的lamdba表达式,无意义
[] {};
//两个数相加的lambda
auto add1 = [](int a, int b)-> int {return a + b; };//本质来说是一个对象
//cout << [](int x, int y)->int {return x + y; }(1, 2) << endl;
cout << add1(1, 2) << endl;//使对象能像普通函数一样调用
// 省略返回值
auto add2 = [](int a, int b){return a + b; };
cout << add2(1, 2) << endl;
//交换变量的lambda
int x = 0, y = 1;
auto swap1 = [](int& x1, int& x2) //返回值为void,一般省略,而且参数列表和函数参数一样,交换值要用引用,不然交换的仅仅是形参
{
int tmp = x1;
x1 = x2;
x2 = tmp;
};
swap1(x, y);
cout << x << ":" << y << endl;
//捕捉列表
//不传参数来交换x y的lambda -- 捕捉列表
//默认捕捉过来的变量不能被修改
auto swap2 = [x, y]()mutable//()mutable使捕捉过来的参数可修改
{
int tmp = x;
x = y;
y = tmp;
};
swap2();//此处不传参 -- 但是mutable仅仅让形参修改,不修改实参,下面打印后会发现没有交换
cout << x << ":" << y << endl;
//要捕捉只能传引用捕捉
auto swap3 = [&x, &y]
{
int tmp = x;
x = y;
y = tmp;
};
swap3();//此处不传参
cout << x << ":" << y << endl;
}
5.2 lamdba使用场景
假设一个场景:我们自定义一个商品类型,然后这个商品有名字,价格,评价三种属性,然后我们想堆商品进行排序,如下代码:
cpp
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
//...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
//struct ComparePriceLess
struct Compare1
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
//struct ComparePriceGreater
struct Compare2
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
void main24()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
/*sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());*/
//sort(v.begin(), v.end(), Compare1());
//sort(v.begin(), v.end(), Compare2());
//sort的时候需要传一个比较的对象过去,所以需要传一个函数指针或者仿函数过去,而这个函数指针或仿函数又需要定义实现在全局
// 而且在比较大的项目中,有很多人,你用你的名字,我用我的,最后就导致我看不懂你的,你看不懂我的,容易混乱
//所以万一遇到命名或者作用域不一样的问题,就很难解决,所以lamdba就可以很好地解决这个问题
//lamdba是一个局部的匿名函数对象
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._name < g2._name; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._name > g2._name; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });
}
5.3 其他捕捉方法
cpp
void main()
{
int a = 1, b = 2, c = 3, d = 4, e = 5;
auto f1 = [=]()
{ //[]里面加=,全部传值捕捉,=改成&就是全部引用捕捉
cout << a << b << c << d << e << endl;
};//但是不打印,因为f1仅仅只是定义,需要调用才会打印
f1();
//混合捕捉
auto f2 = [=, &a]() //表示除a是引用捕捉以外全部传值捕捉
{
a++;
cout << a << b << c << d << e << endl;
a--;
};
f2();
static int x = 0;
if (a)
{ //a使用传值捕捉,其他全部用引用捕捉
auto f3 = [&, a]() mutable
{
a++;
b++;
c++;
d++;
e++;
x++;//可以捕捉位于静态区的变量
cout << a << b << c << d << e << endl;
};
f3();
}
//捕捉列表本质是在传参,其底层原理和仿函数很相同,类似于范围for和迭代器的关系
}
5.4 函数对象与lamdba表达式
函数对象,又称仿函数,可以像函数一样使用对象,是因为在类中重载了operator()运算符的类对象。现在我们观察下列代码和反汇编指令:
cpp
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
cout << "调用对象的仿函数" << endl;
return money * _rate * year;
}
private:
double _rate;
};
class lambda_xxxx{};
void main() //lambda不能相互赋值
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
//调用汇编看,先call构造一个仿函数的对象,再去call调用operator()
//编译器在编译时会把lambda表达式搞成一个空类,所以lambda表达式的大小是一个字节
cout << sizeof(lambda_xxxx{}) << endl;
// 仿函数lambda_uuid -- uuid是随机生成的不重复唯一标识符,当有很多个lanbda时,汇编代码就通过uuid获取不同的lambda (uuid是算法算出来的一种唯一字符串)
// lambda -> lambda_uuid -- 所以对我们来说是匿名的,对编译器来说是有名的,也导致lambda不能相互赋值
auto r2 = [=](double monty, int year)->double{return monty*rate*year; }; //landba调用时也区call调用一个构造和对象调用是一样的
r2(10000, 2);
auto r3 = [=](double monty, int year)->double{return monty*rate*year; };
r3(10000, 2);
}
可以发现,从使用方式上来看,函数对象和lamdba表达式完全一样,函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lamdba表达式通过捕获列表可以直接将该变量捕获到。
(另外就是代码注释里提到的lamdba_uuid是随机数,截图中我用的是VS2022的反汇编,结果是lamdba在该函数作用域中的顺序编号,这其实是因为编译器的不同,对lamdba的处理方式不同,下面是VS2013调用反汇编的情况:)
六,function包装器
6.1 是什么?
function包装器也叫适配器,在C++中本质是一个类模板
cpp
#include<functional>
template<class T> function
template <class Ret, class... Args>
classs function<Ret(Args...)>
其中Ret表示被调用函数的返回类型,Args...表示被调用函数的形参可变参数包
如下列代码:
cpp
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 Functor3
{
double operator()(double d)
{
return d / 3;
}
};
void main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor3(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
}
可以看到,useF打印count的地址时打印了三个不同的值,代表着useF函数模板被实例化了三份。所以一旦类型过多,函数指针,函数对象,lamdba表达式,这些都是可调用的类型,类型过多就会导致模板的效率低下,所以我们需要用到包装器,这个东东本质是对可调用对象进行再封装适配
如下代码和现象:
cpp
void main()
{
//函数指针
function<double(double)> f1 = f;
cout << useF(f1, 11.11) << endl;
// 函数对象
function<double(double)> f2 = Functor3();
cout << useF(f2, 11.11) << endl;
// lamber表达式对象
function<double(double)> f3 = [](double d)->double{ return d / 4; };
cout << useF(f3, 11.11) << endl;
}
用了包装器之后打印的地址就是一样的了
6.2 包装器使用
cpp
int _f(int a, int b)
{
cout << "int f(int a, int b)" << endl;
return a + b;
}
struct Functor
{
public:
int operator()(int a, int b)
{
cout << "int operator()(int a, int b)" << endl;
return a + b;
}
};
void main()
{
int(*pf1)(int, int) = _f;
//map<string, > 上面两个东东调用起来是一样的都是函数,但是类型完全不同,所以无法用map同时声明两个
function<int(int, int)> f1 = _f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {
cout << "[](int a, int b) { return a + b;}" << endl;
return a + b;
};
cout << f1(1, 2) << endl;
cout << f2(10, 20) << endl;
cout << f3(100, 200) << endl;
cout << "---------------" << endl;
map<string, function<int(int, int)>> opFuncMap;
opFuncMap["函数指针"] = f1;
opFuncMap["函数对象"] = Functor();
opFuncMap["lamdba"] = [](int a, int b) {
cout << "[](int a, int b) { return a + b;}" << endl;
return a + b;
};
cout << opFuncMap["函数指针"](1, 2) << endl;
cout << opFuncMap["函数对象"](1, 2) << endl;
cout << opFuncMap["lamdba"](1, 2) << endl;
}
cpp
class Plus1
{
public:
Plus1(int rate = 2)
:_rate(rate)
{}
static int plusi(int a, int b) { return a + b; }
double plusd(double a, double b) { return a + b; }
private:
int _rate = 2;
};
void main()
{
function<int(int, int)> f1 = Plus1::plusi;//包装类内部静态函数
function<double(Plus1, double, double)> f2 = &Plus1::plusd;//包装内部非静态函数要加&,而且类内部函数还有个this指针,所以要传三个参数
cout << f1(1, 2) << endl;
cout << f2(Plus1(), 20, 20) << endl;
cout << f2(Plus1(), 1.1, 2.2) << endl;
Plus1 pl(3);
cout << f2(pl, 20, 20) << endl;
//为什么不能下面这样用?我传个指针为啥不行?
//function<double(Plus1, double, double)> f2 = &Plus1::plusd;
//cout << f2(&Plus1(), 20, 20) << endl;
//Plus1 pl(3);
//cout << f2(&pl, 20, 20) << endl;
//用指针是可以的,但是后面用的时候只能像这样传指针了不能传匿名对象了,左值可以取地址,右值不能取地址
//如果传的是对象就用对象去调用,如果传的是指针就用指针去调用
//成员函数不能直接调用,需要用对象去调用
}
6.3 逆波兰OJ题
cpp
//逆波兰表达式
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
stack<long long> st; //返回值 参数包
map<string, function<long long(long long, long long)>> opFuncMap = //这里这样写就不用再用which case语句了,简便很多
{
//列表初始化,pair初始化
{"+",[](long long a,long long b) {return a + b; }},
{"-",[](long long a,long long b) {return a - b; }},
{"*",[](long long a,long long b) {return a * b; }},
{"/",[](long long a,long long b) {return a / b; }},
};
for (auto& str : tokens)
{
if (opFuncMap.count(str)) //题目传给我们的只有数字和运算字符,如果str是字符,那么表示这个字符必定在包装器里面,直接出栈,然后opFuncMap是一个map,通过str找到对应关系,然后通过lambda得出值
{
long long right = st.top();
st.pop();
long long left = st.top();
st.pop();
st.push(opFuncMap[str](left, right)); //把上一个运算符算出的结果再入栈
}
else//操作数,直接入栈
{
st.push(stoll(str)); //stoll字符串转数字函数
}
}
return st.top();
}
};
6.4 bind
std::bind定义在头文件中,也是一个函数模板,像一个函数包装器,接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表。简单来说我们可以用它把一个原本接收N个参数的函数fn,沟通过绑定一些参数,返回一个接收M个参数的新函数,bind还可以实现参数顺序调整等操作
cpp
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
6.4.1 bind调整参数顺序:
cpp
//调整顺序
// _1,_2是占位符,代表绑定函数对象的形参,定义在placeholders中
// _1,_2分别代表第一个第二个形参...
using namespace placeholders;
void Print(int a, int b)
{
cout << "void Print(int a, int b)" << endl;
cout << a << " ";
cout << b << endl;
}
void main()
{
Print(10, 20);
auto RPrint = bind(Print, _2, _1);// 是类似占位符,_1是第一个参数,_2就是第二个参数,把第二个参数放到第一个位置去。绑定完后bind会返回一个对象
//function<void(int, int)> RPrint = bind(Print, _2, _1);
RPrint(10, 20);
}
6.4.2 bind调整参数个数
cpp
//调整参数个数
class Sub
{
public:
Sub(int rate = 0)
:_rate(rate)
{}
int sub(int a, int b)
{
return a - b;
}
private:
int _rate;
};
void main()
{
function<int(Sub, int, int)> fSub = &Sub::sub;
fSub(Sub(), 10, 20);//这里要传对象是因为只能通过对象去调用sub
//上面两个有点麻烦,因为我们还要传个对象过去,还有两个参数 --> 麻烦
//包装的时候,可以绑死某个参数,减少调用的时候传多余的参数,最好和function一起用
//function<int(Sub, int, int)> funcSub = &Sub::sub;//这条语句就可以变成下面这个
function<int(int, int)> funcSub1 = bind(&Sub::sub, Sub(), _1, _2);
cout << funcSub1(10, 20) << endl;//这里的sub我们定义在一个类中,这里调用的时候传参就要传四个,但是我们前面用了绑定了之后,就只需要传对应_1和_2的参数了
//上面那个是绑死前面要传的对象参数,如果我们想绑死中间那个参数呢
function<int(Sub, int)> funcSub2 = bind(&Sub::sub, _1, 100, _2);
cout << funcSub2(10, 20) << endl;
map<string, function<int(int, int)>> opFuncMap =
{
{"-",bind(&Sub::sub,Sub(),_1,_2)}
};
cout << opFuncMap["-"](1, 2) << endl;
cout << endl;
}
七,补充
7.1 auto和decltype
在C++98中auto只是一个存储类型的说明符,表示该变量是自动存储类型,但是局部域中定义的局部变量默认就是自动存储,所以auto就没什么用了。C++11中放弃了auto以前的用法,将其用于实现自动类型判断,让编译器将定义对象的类型设置为初始化值的类型
cpp
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
decltype关键字的作用是将变量的类型声明为表达式指定的类型
cpp
void main2()
{
int x = 10;
//typeid不能定义对象,它只是拿到字符串的类型,而且拿到这个类型后也不能直接初始化对象,只能去打印
//typeid(x).name() y = 20;
//decltype可以定义一个和括号内类型一样的,但是和auto不一样哦
decltype(x) y1 = 20.22;
auto y2 = 20.22;
cout << y1 << endl;//打印20
cout << y2 << endl;//打印20.22
//vector存储类型跟x*y表达式返回值类型一致
//decltype推导的表达式类型,然后用这个类型实例化模板参数或者定义对象
vector<decltype(x* y1)> n;
}
7.2 两个被批评的容器
C++11新增了一些容器,其中最有用的两个闪光点容器就是unordered_map和unordered_set,这两个容器我们前面已经详细讲过。
但是也有两个容器是被批评的,也就是几乎没啥用的:array和forward_list
cpp
void main()
{
const size_t N = 100;
int a1[N];
//C语言数组越界检查的缺点:1,越界读根本检查不出来 2,越界写可能会抽查出来
a1[N];
//a1[N] = 1;
//a1[N + 5] = 1;
//C++11新增的array容器,就把上面两个问题全解决了
array<int, N> a2;
//a2[N];
a2[N] = 1;
a2[N + 5] = 1;
//但实际上,array用得非常少,一方面大家用c数组用习惯了,其次用array不如用vector + resize去替代c数组
}
7.3 emplace系列接口
这个系列接口没啥好说的,直接看下面两段代码
cpp
void main()
{
std::list<bit::string> mylist;
bit::string s1("1111");
mylist.push_back(s1);
mylist.emplace_back(s1); //都打印深拷贝
cout <<"----------"<< endl;
bit::string s2("2222");
mylist.push_back(move(s2));
mylist.emplace_back(move(s2)); //都打印移动拷贝
cout <<"----------"<< endl;
//上面的没有区别,下面的就有区别了,和上面两个比起来只有一个移动构造
mylist.push_back("3333"); // 构造匿名对象 + 移动构造
mylist.emplace_back("3333");// 直接构造
vector<int> v1;
v1.push_back(1);
v1.emplace_back(2);
//std::vector::emplace_back
//template<class... Args>
//void emplace_back(Args&&... args);
vector<pair<std::string, int>> v2;
v2.push_back(make_pair("sort", 1));//pair的一次构造,再对象的拷贝构造+移动构造
//std::vector::push_back
//void push_back (const value_type& val);
//void push_back (value_type&& val);
v2.emplace_back(make_pair("sort",1));
v2.emplace_back("sort",1);
//结论:深拷贝 emplace_back和push_back效率差不多相同
}
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
Date& operator=(const Date& d)
{
cout << "Date& operator=(const Date& d))" << endl;
return *this;
}
Date(Date&& d)
{
cout << "Date(Date&& d)";
}
Date& operator=(Date&& d)
{
cout << "Date& operator=(Date&& d)" << endl;
return *this;
}
private:
int _year;
int _month;
int _day;
};
void main()
{
//浅拷贝的类
//没区别
std::list<Date> list2;
Date d1(2023, 5, 28);
list2.push_back(d1);
list2.emplace_back(d1);
cout <<"----------"<< endl;
Date d2(2023, 5, 28);
list2.push_back(move(d1));
list2.emplace_back(move(d2));
cout << endl;
// 有区别
cout << "----------" << endl;
list2.push_back(Date(2023, 5, 28));
list2.push_back({ 2023, 5, 28 }); //多参数隐式类型转换
cout << endl;
cout <<"----------"<< endl;
list2.emplace_back(Date(2023, 5, 28)); // 构造+移动构造
list2.emplace_back(2023, 5, 28); // 直接构造,直接传参数,通过可变参数包一直往下传
}