相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习。
C++98时,允许使用花括号{}对结构体或者数组进行统一的初始化设定。
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
而在C++11中,对这一个规则进行了更进一步的规定,扩大了它的使用范围,使其可以为所有内置类型或者自定义类型初始化值,使用初始化列表时,可添加等号(=),也可不添加。
{}可以初始化内置类型变量。
int main()
{
int a = 0;
int b = { 1 };
int c{ 1 };//建议使用上一行的初始化方式
int arr1[]={1,2,3,4,5};
int arr2[]{ 1,2,3,4,5 };
vector<int> v{1,2,3,4,5};
}
{}更重要的是对自定义类型的初始化。
(1)单参数构造
struct Point
{
int _x;
int _y;
};
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Point p{ 1,2 };
//多参数
//C++ 构造
Date d1(2021, 10, 1);
//C++11
//构造+拷贝构造-》优化:直接构造
//多参数的隐式类型转换
Date d2 = { 2024,10,12 };
Date d3{ 2021,10,1 };
//单参数
//构造
string s1("1111");
//构造+拷贝构造-》优化:直接构造
string s2 = "1111";
//Date d4 = (2021, 10, 21); 不支持
Date* d7 = new Date(2021, 10, 1);
Date* d8 = new Date{ 2012,12,1 };
return 0;
}
(2)多参数构造
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* d5 = new Date[3]{ d1,d2,d3 };
Date* d6 = new Date[3]{ {2021,10,1},{2021,10,2},{2021,10,3} };
//C++11为标准的容器都添加了initializer_list构造参数
vector<Date> vd = { { 2022, 1, 17 }, Date{ 2022, 1, 17 }, { 2022, 1, 17 } };
return 0;
}
为什么我们能用{}初始化,是因为有着initializer_list这位大哥存在。
initializer_list是C++11新增的一个模板参数。其介绍为:
在容器中的应用:
我们平时在定义变量的时候,必须先知道他的变量类型才能定义,但是有时候我们并不知道其具体的类型,或者变量的类型太复杂了,这时就需要类型推导。
int main()
{
//发生函数嵌套引用的时候,如果每个函数里面代码太多,此时并不能直接知道函数返回类型
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
//此时用auto就节省了很多
auto it = dict.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
注意:不能将auto作为函数返回值,不然会导致混淆。
decltype的出现是为了补齐auto 不支持对于表达式的类型推导的缺陷的, 经常适用于后置返回类型的推导.
int main()
{
const int x = 1;
double y = 2.2;
cout << typeid(x).name() << endl;
cout << typeid(string).name() << endl;
decltype(x) z = 1;
cout << typeid(z).name() << endl;
const int* p1 = &x;
cout << typeid(p1).name() << endl;
decltype(p1) p2 = nullptr;
cout << typeid(p2).name() << endl;
return 0;
}
注意:这里出现了一个叫typeid(变量名).name()的东西,这个也是显示一个变量的类型名,但是与decltype不一样的是,typeid(变量名).name()返回的是一个字符串。而decltype返回的是一个具体的类型,可以用于变量的定义。
上面圈起来的就是C++11出现的新容器,但是真正有用的是unordered_map和unordered_set两个容器,其他两个容器显得很鸡肋,这也算是C++11的一个败笔 。
C++11中还新增了一些新方法,比如cbegin和cend返回迭代器的const版本。
但是这些都是一些锦上添花的操作,实际意义不大,因为begin和end本来就可以返回const版本。
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性。无论左值引用还是右值引用,都是给对象取别名。
所谓左值,就是一个表示数据的表达式。如:变量名和解引用的指针。我们可以获取他的地址并且为他赋新值。左值可以出现在赋值符号的左边,不能出现在右边。对于const修饰的左值,不能对他赋新值。
那么所谓左值引用就是对左值进行引用,给左值起别名。
int& func2()
{
static int x = 0;
return x;
}
int main()
{
//以下的p,b,c,*p,func2()返回值 都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
//可以对左值取地址
const int* ptr1 = &c;
int* ptr2 = &func2();
//对左值进行左值引用
int*& rp=p;
int& bb=b;
const int& cc=c;
return 0;
}
所谓右值,也是一个表示数据的表达式。如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回)等等。右值可以出现在赋值符号的右边,不能出现在左边,所以不能对其赋值。并且不能对右值取地址。
所谓右值引用就是对右值进行引用,对右值起别名。
int func1()
{
static int x = 0;
return x;
}
int main()
{
double x = 1.1, y = 2.2;
double& r1 = x;
//以下都是常见的右值
10;
x + y;
func1();
//对右值进行右值引用
int&& rr1 = 10;//给右值取别名后,会导致右值被存储到特定位置,可以取到该位置的地址
double&& rr2 = x + y;
int&& rr3 = func1();
return 0;
}
左值引用总结: 1. 左值引用只能引用左值,不能引用右值。 2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra1为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结: 1. 右值引用只能右值,不能引用左值。 2. 但是右值引用可以move以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
语法上,左值引用与右值引用都是起别名,不开空间。 底层,引用是存指针的。左值引用直接存储左值的地址,右值引用是把当前右值拷贝到栈上的一个临时空间,存的是这个临时空间的地址。
我们知道既然左值引用可以引用左值,又可以引用右值,那我们还要右值引用来干嘛呢?
我们来看看下面的示例:
namespace yjy
{
class string
{
public:
typedef char* iterator;
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
string tmp(s);
swap(tmp);
return *this;
}
//...
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
string to_string(int value)//不能用左值引用,因为已经析构了
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
//...
std::reverse(str.begin(), str.end());
return str;
}
}
void fun1(yjy::string s)
{}
void fun2(yjy::string& s)
{}
int main()
{
yjy::string s("1234");
//fun1(s);
//fun2(s);//左值引用提高了效率,不存在拷贝临时对象的问题
//可以使用左值引用返回,这个对象还在
s += 'a';
//不能使用左值引用返回,这个就是左值引用的一个短板
//函数返回对象出了作用域就不在了,就不能用左值引用返回(因为返回的是本身地址,栈帧已销毁)
yjy::string ret = yjy::to_string(1234);
return 0;
}
那么右值引用就可以弥补这个短板。这就需要移动语义。
上面的to_string函数在返回str这个右值时,会创建一个临时变量,这时出了作用域str局部变量就销毁了。C++11称str这种变量为将亡值(还是右值),我们对将亡值传值拷贝返回定义了专门的移动构造,对将亡值的赋值定义了专门的移动赋值。我们的移动语义就包括移动构造和移动赋值。
在str拷贝到临时对象的时候,会调用拷贝构造,将str内的资源转移到临时对象中。再将临时对象拷贝到ret变量的时候,会调用移动构造,将临时对象的资源转移到ret中,发生两次拷贝(这里某些先进版本的编译器会优化此过程,不会产生临时对象,直接将str内的资源转移到ret中,只有一次移动构造)。
C++11之前拷贝构造:
C++11移动构造:
上面是进行构造,如果我们是进行赋值呢?
yjy::string ret;
ret = yjy::to_string(1234);//赋值重载多了一次拷贝构造
同样的我们也为将亡值引进了移动赋值。
在str拷贝到临时对象的时候,会调用拷贝构造,将str内的资源转移到临时对象中。再将临时对象拷贝到ret变量的时候,会调用移动赋值,将临时对象的资源转移到ret中,发生一次拷贝加一次赋值(这里不会进行优化)。
C++11之前拷贝构造+拷贝赋值:
C++11拷贝构造+移动赋值:
要注意的是:
以上就是右值引用的一个使用场景。
还有一种使用场景是在容器的插入操作的时候引入右值引用实参,则可以转移他的资源,减少拷贝。
int main()
{ yjy::list<yjy::string> lt;
yjy::string s1("11111");
lt.push_back(s1);
cout << "----------" << endl;
//以下插入的都是右值,使用移动构造
lt.push_back(yjy::string("2222"));
cout << "----------" << endl;
lt.push_back("2222");
cout << "----------" << endl;
return 0;
}
这是特别需要注意的一点!!!
我们先来看一个规则:有现成的吃现成的。
void func(const int& x)
{
cout << "void func(const int& x)" << endl;
}
void func(int&& x)
{
cout << "void func(int&& x)" << endl;
}
int main()
{
const int x = 0;
func(x);//左值
func(10);//右值
return 0;
}
可以看见是先去找有没有现成的,有现成的就用现成的。
为了证明“右值被右值引用之后,其属性为左值”这一规则,我们用之前的list容器来演示。
namespace yjy
{
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
ListNode(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), 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);
return newnode;
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
//return iterator(newnode);
return newnode;
}
private:
Node* _head;
};
}
int main()
{
yjy::list<yjy::string> lt;
yjy::string s1("11111");
lt.push_back(s1);
cout << "----------" << endl;
lt.push_back(yjy::string("2222"));
cout << "----------" << endl;
lt.push_back("2222");
cout << "----------" << endl;
return 0;
}
我们对左值引用与右值引用不同的时候定义了不同的代码。
运行结果为:
是不是懵逼了,按理来说应该是移动构造才对,怎么是深拷贝呢?
就是因为我们在push_back的右值引用版本之后,我们的属性已经变成了左值,所以以后一直照着左值的版本运行。我们需要在每次右值引用之后,手动move一下,让其属性继续保持右值。
namespace yjy
{
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
ListNode(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(move(x))//手动move,让其属性保持右值
{}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));//手动move,让其属性保持右值
}
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);
return newnode;
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(move(x));//手动move,让其属性保持右值
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
//return iterator(newnode);
return newnode;
}
private:
Node* _head;
};
}
int main()
{
yjy::list<yjy::string> lt;
yjy::string s1("11111");
lt.push_back(s1);
cout << "----------" << endl;
lt.push_back(yjy::string("2222"));
cout << "----------" << endl;
lt.push_back("2222");
cout << "----------" << endl;
return 0;
}
运行结果为:
这样是不是就对啦!
所以右值被右值引用之后,其属性为左值,要让其属性继续保持右值,必须在每个右值引用的地方手动move一下。
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
int main()
{
yjy::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
yjy::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
yjy::string s3(move(s1));
return 0;
}
所以不要轻易的将左值move为右值,除非你准备让这个左值资源被拿走。
STL也增加了右值引用版本:
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Func(int x)
{
cout << x << endl;
}
template<typename T>
void Perfect_Forward(T&& t)
{
Func(t);
}
完美转发的核心概念在于,如果Perfect_Forward() 函数接收到的参数t为左值,那么该函数传递给 Func() 的参数 t 也应该是左值;反之,如果 Perfect_Forward() 函数接收到的参数 t 为右值,那么传递给 Func() 函数的参数 t 也必须是右值。这样参数的原始类型就得到了保留。
我们再来了解一下万能引用:
1、模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。 2、模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。 3、但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值, 4、我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
C++11我们用forward函数来实现万能引用:
//void fun(int& x) { cout << "void fun(int& x) 左值" << endl; }
//void fun(int&& x) { cout << "void fun(int&& x) 右值" << endl; }
//void fun(const int& x) { cout << "void fun(const int& x) const左值" << endl; }
//void fun(const int&& x) { cout << "void fun(const int&& x) const右值" << endl; }
//万能引用
template<typename T>
void PerfectForward(T&& t)
{
//forward
// forward 保持属性->本身是左值,就不变;
// 本身是右值,右值引用后,属性是左值,转成右值,相当于move一下
fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
右值引用右值之后,会导致属性变为左值,使用完美转发之后可以保持其属性。
默认成员函数:
原来C++类中,有6个默认成员函数: 1. 构造函数 2. 析构函数 3. 拷贝构造函数 4. 拷贝赋值重载 5. 取地址重载 6. const 取地址重载
重要的是前面四个函数,后面的两个并不是很重要。
C++11之后,新增了两个成员函数,移动构造和移动赋值函数。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
在C++11之前,我们要让一个类不能被拷贝,除了只定义不实现拷贝构造函数之外,还要私有化拷贝构造函数。因为如果我们只定义不实现,还可以通过类外实现的方式使其能被拷贝。所以还要使拷贝构造函数私有化。
但是C++11添加了关键词delete,作用是:禁止生成默认构造函数。称被delete修饰的函数为删除函数。
//不希望A对象被拷贝
class A
{
public:
A() = default;
private:
//C++98
//只声明不实现
//放到私有
A(const A& a);
int a = 0;
};
int main()
{
A aa1;
//A aa2 = aa1;
return 0;
}
//C++11
class A
{
public:
A() = default;
A(const A& a)=delete;
private:
int a = 0;
};
int main()
{
A aa1;
//A aa2 = aa1;
return 0;
}
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。
下面是一个基本的可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
编译时递归返回条件
void _Showlist()
{
cout << endl;
}
//如果想依次拿到每个参数类型和值,编译时递归解析
template<class t,class ...args>
void _Showlist(const t& val,args... args)
{
cout << val << " ";
_Showlist(args...);
}
template<class ...args>
void Showlist(args... args)
{
_Showlist(args...);
}
//实例化以后,推演生成的过程
//void Showlist(int val1, char ch, std::string s)
//{
// _Showlist(val1, ch, s);
//}
//void _Showlist(const int& val, char ch, std::string s)
//{
// cout << val << " ";
// _Showlist(ch, s);
//}
//void _Showlist(const char& val, std::string s)
//{
// cout << val << " ";
// _Showlist(s);
//}
//void _Showlist(const std::string& val)
//{
// cout << val << " ";
// _Showlist();
//}
int main()
{
ShowList(1, 'A', std::string("sort"));
return 0;
}
需要说明的是,递归 展开参数包并不是可以一直递归下去。
template<size_t N>
void func()
{
cout << N << " ";
func<N - 1>();
}
template<>
void func<1>()
{
cout << 1 << endl;
}
int main()
{
func<2000>();
return 0;
}
以VS2022为例,差不多递归2000次就到极限了。
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和
emplace系列接口的优势到底在哪里呢?
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, bit::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort"});
return 0;
}
emplace系列,直接给插入对象参数的时候:
我们在比较自定义类的大小的时候,需要自己定义一个类来进行比较(也叫仿函数)。
struct Goods
{
string _name; //名字
double _price; //价格
int _evaluate; //评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
int main()
{
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());
return 0;
}
但是这样太复杂了,所以C++11发明了lambda表达式来简化代码。
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) ->bool {
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) ->bool {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) ->bool {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) ->bool {
return g1._evaluate > g2._evaluate;
});
return 0;
}
我们来看看lambda表达式的具体语法:
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
各部分说明:
int main()
{
[]{} //最简单的lambda表达式,不能做任何事情
//局部的匿名函数对象
//auto add = [](int a, int b) ->int{return a + b; };
/*auto add = [](int a, int b) {return a + b; };
cout << add(1, 2) << endl;*/
auto swap1 = [](int& a, int& b) ->void {
int tmp = a;
a = b;
b = tmp;
};
int x = 1;
int y = 2;
swap1(x, y);
auto func1 = [] {
cout << "hello world" << endl;
};
func1();
return 0;
}
要注意的是:必须用auto接受lambda表达式的返回值,因为表达式没有具体的类型,是在每次编译时才确定的。
此处说明一下mutable的作用:
int x = 1, y = 2;
//每次输入一个值跟x换
//传值捕捉,捕捉到的是当前对象的拷贝
auto swap1 = [x, y](){
int tmp = x;
cin >> x;
y = tmp;
//此处是错的,因为不能改变x和y,这时就需要加上mutable
}
};
下面的代码就是对的:
auto swap1 = [x, y]()mutable {
int tmp = x;
cin >> x;
y = tmp;
};
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
int main()
{
int x = 1, y = 2;
//每次输入一个值跟x换
//传值捕捉,捕捉到的是当前对象的拷贝
auto swap1 = [x, y](){
int tmp = x;
//cin >> x;
//y = tmp;
};
auto swap1 = [x, y]()mutable {
int tmp = x;
cin >> x;
y = tmp;
};
//传引用捕捉
auto swap1 = [&x, &y]()mutable {
int tmp = x;
//cin >> x;
x = y;
y = tmp;
};
swap1();
cout << x << endl;
cout << y << endl;
int m = 3, n = 4;
//传值捕捉当前域的所有对象
auto func1 = [=]() {
return x + y * m + n;
};
cout << func1() << endl;
//传引用捕捉当前域的所有对象
auto func2 = [&]() {
x++;
m++;
return x + y * m + n;
};
cout << func2() << endl;
cout << x << endl;
cout << m << endl;
//语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
//传引用捕捉当前域的所有对象,某些对象传值捕捉
auto func3 = [&, n] {
x++;
m++;
//n++; 不行
return x + y * m - n;
};
cout << func3() << endl;
cout << x << endl;
cout << m << endl;
return 0;
}
注意:
a. 父作用域指包含lambda函数的语句块。
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者 非局部变量都 会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同。
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的 类对象。
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
从使用方式来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可 以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如 果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
所以这也验证了:lambda表达式实质上就是仿函数。
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
//是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
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;
// lamber表达式
cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
return 0;
}
通过上面的代码,我们发现useF函数实例化了三份。这样就很浪费资源,显得很赘余。
我们的function就能很好的解决这种问题。
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
//模板参数说明:
//Ret: 被调用函数的返回类型
//Args…:被调用函数的形参
int main()
{
//函数指针
function<double(double)> fc1 = f;
fc1(11.11);
cout << usef(fc1, 11.11) << endl;
//函数对象
function<double(double)> fc2 = Functor();
fc2(11.11);
cout << usef(fc2, 11.11) << endl;
//lambda表达式
function<double(double)> fc3 = [](double d)->double {
return d / 4;
};
fc3(11.11);
cout << usef(fc3, 11.11) << endl;
return 0;
}
我们要注意的是特殊函数的情况:
int f(int a, int b)
{
return a + b;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
//普通函数
function<int(int, int)> fc1 = f;
cout << fc1(1, 1) << endl;
//静态成员函数
function<int(int, int)> fc2 = Plus::plusi;
cout << fc2(1, 1) << endl;
//非静态成员函数
//非静态成员函数需要对象的指针或者对象进行调用
/*Plus plus;
function<int(Plus*,double,double)> fc3 = &Plus::plusd;
cout << fc3(&plus, 1, 1);*/
function<double(Plus, double, double)> fc3 = &Plus::plusd;
cout << fc3(Plus(), 1, 1) << endl;
return 0;
}
我们来看一道老题:. - 力扣(LeetCode)
传统做法是利用栈。我们来看看function包装器是如何简化代码的:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
map<string,function<int(int,int)>> m={
{"+",[](int a,int b){return a+b;}},
{"-",[](int a,int b){return a-b;}},
{"*",[](int a,int b){return a*b;}},
{"/",[](int a,int b){return a/b;}}};
stack<int> s;
int number=0;
for(auto& e:tokens)
{
if(m.count(e))//运算符
{
int right=s.top();
s.pop();
int left=s.top();
s.pop();
number=m[e](left,right);
s.push(number);
}
else
{
s.push(stoi(e));
}
}
number=s.top();
return number;
}
};
我们把四个操作函数用包装器包装起来作为map的value,这样就可以根据字符的情况,直接对map进行[],就可以得到对应的函数。
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可 调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
int sub(int a, int b)
{
return a - b;
}
class plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a - b;
}
};
int main()
{
//调整参数顺序,了解一下,意义不大
int x = 10;
int y = 20;
auto f1 = bind(sub, placeholders::_2, placeholders::_1);
cout << f1(x, y) << endl;
function<double(plus, double, double)> fc3 = &plus::plusd;
cout << fc3(plus(), x, y) << endl;
//调整参数的个数
//某些参数绑死
function<double(double, double)> fc4 = bind(&plus::plusd, plus(), placeholders::_1, placeholders::_2);
cout << fc4(2, 3) << endl;
function<double(double)> fc5 = bind(&plus::plusd, plus(), placeholders::_1, 20);
cout << fc5(2) << endl;
return 0;
}
总结
好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。
祝大家越来越好,不用关注我(疯狂暗示)