C++学习笔记:
各位于晏,亦菲们,请点赞关注!
我的个人主页:
目录
1、前言
书接上回,我们主要讲了关于C++11的 列表初始化,右值引用和移动语义,以及类新增的移动构造和移动赋值等成员函数 大家简单复习下后 步入本文正题。
2、类型分类
在C++11后,进一步对类型进行了划分,右值被划分为纯右值(prvalue)和 将亡值 (xvalue)。C++98中的右值就等价于C++11中的纯右值。
泛左值(glvalue),泛左值包含将亡值(xvalue)和 左值(lvalue)。
2.1、纯右值:(prvalue)
纯右值是表达式求值过程中产生的临时值,或者用于初始化对象的字面量等。它们通常没有持久的存储,只在表达式求值期间存在。
2.2、将亡值:(xvalue)
将亡值是一种特殊的右值,它与对象的资源获取和转移相关。通常是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达。
std::move
函数的返回值就是将亡值,move(x)。转换到对象的右值引用类型的类型转换表达式,如 static_cast<char&&>(x)。
总的来说,有变量名,就是泛左值;有变量名,且不能被move,就是左值;有变量名,且可以被move,就是将亡值;没有名字,且可以被移动,则是纯右值。不是左值和纯右值就是将亡值。
3、引用折叠
引用折叠是 C++ 中的一个重要概念,它主要出现在模板编程和类型推导的上下文中。在C++中如果我们想定义一个引用的引用,是不能直接定义的。但是通过模板或者typedef对类型重命名可以构成引用的引用。
3.1 、引用折叠的规则
typedef实现引用折叠
1、左值引用绑定到左值引用,折叠为左值引用。T& & 折叠为 T&
void Test()
{
typedef int& lref;
typedef int&& rref;
int a = 10; //a的类型为int
lref & b = a; //b的类型为int& & 折叠为int&
}
2、左值引用绑定到右值引用, 折叠为左值引用。T& && 折叠为 T&
void Test()
{
typedef int& lref;
typedef int&& rref;
int a = 10; //a的类型为int
lref && b = a; //b的类型为int& && 折叠为int&
}
3、右值引用绑定右值引用 折叠为右值引用。 T&& && 折叠为 T&&
void Test()
{
typedef int& lref;
typedef int&& rref;
int a = 10; //a的类型为int
rref && b = move(a); //b的类型为int&& && 折叠为int&&
rref && c=10; //c的类型也是 int&& && 折叠为 int&&
}
4、右值引用绑定左值引用 折叠为左值引用 。 T& && 折叠为 T&
void Test()
{
typedef int& lref;
typedef int&& rref;
int a = 10;//a的类型为int
rref & b = a; //b的类型为 int&& & 折叠为int&
}
3.2、函数模板的引用折叠
3.2.1、左值引用模板和万能引用模板
//由于引⽤折叠限定,func1实例化后只能是左值引⽤,左值引用模板
template<class T>
void func1(T& x)
{
}
// 由于引⽤折叠限定,func2实例化后可以是左值引⽤,也可以是右值引⽤,也叫万能引用模板
template<class T>
void func2(T&& x)
{
}
下面我们通过具体的例子验证。
3.2.2、func1的折叠
//由于引⽤折叠限定,func1实例化后只能是左值引⽤
template<class T>
void func1(T& x)
{
}
void Test()
{
int n = 10;
func1<int>(n);
//没有引用折叠 实例化为 void func1(int& x)
func1<int&>(n);
//int& & 折叠为int& 实例化为 void func1(int& x)
func1<int&&>(n);
//int& && 折叠为int& 实例化为 void func1(int& x)
func1<const int&>(n);
//const int& & 折叠为 const int& 实例化为 void func1(const int& x)
func1<const int&&>(n);
//const int&& & 折叠为 const int& 实例化为 void func1(const int& x)
}
无论如何实例化 func1只能折叠为 左值引用或者const 左值引用
3.2.1、func2的折叠
// 由于引⽤折叠限定,func2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void func2(T&& x)
{
}
void Test()
{
int n = 10;
func2<int>(10);
//没有引用折叠 实例化为 void func1(int&& x)
func2<int&>(n);
//int&& & 折叠为int& 实例化为 void func1(int& x)
func2<int&&>(10);
//int&& && 折叠为int&& 实例化为 void func1(int&& x)
func2<const int&>(n);
//const int&& & 折叠为 const int& 实例化为 void func1(const int& x)
func2<const int&&>(10);
//const int&& && 折叠为 const int&& 实例化为 void func1(const int&& x)
}
func2实例化后既可以折叠为左值引用,也可以右值引用。
4、完美转发
4.1、使用场景
上节课我们讲过,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量在用于表达式时的属性是左值!!!(这是理解完美转发的关键),结合下面这个场景。
void func(int& x)
{
cout << "void func(int& x)" << endl;
}
void func(int&& x)
{
cout << "void func(int&& x)" << endl;
}
template<class T>
void Function(T&& x)
{
//这里无论x是左值还是右值,传递给func时只会匹配左值引用的版本
//因为右值变量在用于表达式时的属性是左值!!
func(x);
}
void Test()
{
int a = 10;
//实例化为左值引用 传左值
Function<int&>(a);
//实例化为右值引用 传右值
Function<int&&>(10);
}
int main()
{
Test();
return 0;
}

4.2、forward
完美转发forward本质是⼀个函数模板,主要还是通过引用折叠的方式实现。
4.2.1、函数模板
4.2.2、返回值

否则,函数返回一个右值引用(T&&),该引用指向可用于传递右值的arg。
4.2.3、使用方法
还是上面那个例子我们结合完美转发就可以解决。
#include<utility>
void func(int& x)
{
cout << "void func(int& x)" << endl;
}
void func(int&& x)
{
cout << "void func(int&& x)" << endl;
}
template<class T>
void Function(T&& x)
{
//这里我们将x使用forward完美转发,不改变它的属性,就可以使其匹配到对应的func版本
func(forward<T>(x));
}
void Test()
{
int a = 10;
//实例化为左值引用 传左值
Function<int&>(a);
//实例化为右值引用 传右值
Function<int&&>(10);
}
int main()
{
Test();
return 0;
}
左值匹配到左值引用版本,右值匹配到右值引用的版本。
5、lambda
5.1、lambda表达式的基本语法
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。 lambda表达式在使用层而言没有类型,我们一般使用auto来接收。
lambda表达式的格式:
[capture-list] (parameters) -> return type { function boby }
[capture-list]:捕捉列表
该列表总是出现在 lambda 函数的开始位置,编译器根据[ ]来判断接下来的代码是否为 lambda 函数,关于捕捉列表我们下面详细讲解。
(parameters):参数列表。
与普通函数的参数列表相似,如果不需要参数,可以连同()一起省略。
-> return type:返回值类型。
用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{ function boby }:函数体。
与普通函数的函数体类似,函数体内除了可以使用参数以外,还可以使用捕捉列表所捕获的变量,函数体为空也不能省略。
使用:和普通函数一样使用。
5.2、简单的lambda表达式
void Test()
{
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回类型可以省略,可以通过返回对象⾃动推导
// 4、函数体不能省略
//一个简单的lambda表达式
auto add = [](int x, int y)-> int {return x + y; };
cout << add(1, 2) << endl;
//也可以写成普通函数的形式
auto print = [] //省略参数列表 省略返回类型 编译器自动推导
{
cout << "我是一个简单的lambda表达式" << endl;
};
print();
int a = 0, b = 1;
auto swap= [] (int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap(a, b);
cout << a << ":" << b << endl;
}
5.3、捕捉列表
lambda 表达式中默认只能用 lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要通过捕捉列表进行捕捉。注意:全局变量不需要捕捉即可使用。
5.3.1、显示捕捉
显示捕捉分为传值捕捉和传引用捕捉。捕捉的多个变量中间使用逗号分隔,例如:[x , y,& z]x , y为传值捕捉,z为传引用捕捉。
//全局变量
int a = 15;
void Test()
{
//显示捕捉
int b= 5;
int c = 10;
auto func = [b, &c]
{
//传值捕捉的变量不可以修改
//b = 10;
//传引用捕捉的变量可以修改
++c;
//全局变量可以修改
++a;
};
func();
cout << a <<" " << b <<" " << c;
}
5.3.2、隐式捕捉
在捕捉列表写⼀个=表示隐式传值捕捉,在捕捉列表写⼀个&表示隐式传引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。不需要我们显示的写。
//全局变量 捕捉到可以修改
int a = 10;
void Test()
{
int b = 15;
int c = 20;
// 隐式值捕捉 不能修改
//用了哪些变量编译器自动捕捉
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
// 隐式引⽤捕捉 可以修改
// 用了哪些变量编译器自动捕捉
auto func3 = [&]
{
a++;
b++;
c++; };
func3();
cout << a << " " << b << " " << c << " " << endl;
}
5.3.3、混合捕捉
在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第⼀个元素必须是 &或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
void Test()
{
int a = 10;
int b = 15;
int c = 20;
int d = 25;
// 混合捕捉1
//b和c值捕捉,其他引用捕捉
auto func1 = [&,b,c]
{
a++;
//b++;
//c++;
d++;
return a + b + c + d;
};
func1();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉2
// a和b引用捕捉 其他值捕捉
auto func2 = [=, &a, &b]
{
a++;
b++;
//c++;
//d++;
return a + b + c + d;
};
func2();
cout << a << " " << b << " " << c << " " << d << endl;
}
5.4、注意
lambda 表达式如果在函数局部域中,可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, 可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
//全局变量
int a = 1;
// 定义在全局的lambda捕捉列表必须为空,因为全局变量不⽤捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
{
a++;
};
void Test()
{
func1();
//静态局部变量 也不需要捕捉
static int b = 2;
int c = 3;
int d = 4;
//定义在局部的lambda表达式可以捕捉局部变量,不能捕捉全局变量和静态局部变量
auto func2 = [&c,d]()
{
++a;
++c;
++b;
//++d c传值捕捉不能修改
return a + b + c + d;
};
cout << func2() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
}
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以 修改了,但是修改还是形参对象,不会影响实参。本质上是一种拷贝。使用该修饰符后,参数列表不可省略(即使参数为空)。
void Test()
{
// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷贝,修改形参不影响实参
int a = 5;
int b = 10;
string s("helloworld");
auto func = [=]()mutable //全部值捕捉
{
//传值捕捉也可以修改,但是不影响实参
a = 10;
b = 20;
s[5] = 'x';
cout <<"形参修改:"<< a << " " << b << " " << s << endl;
};
func();
cout <<"实参不变"<< a << " " << b << " " << s << endl;
}
5.5、lambda的应用
在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,使用起来也不方便。仿函数要定义⼀个类,使用起来也不便捷。使用lambda 去定义可调用对象,既简单又方便。 lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,在智能指针那节课我们模拟实现shared_ptr就用到了lambda,lambda的使用场景还有很多,后面我们会经常接触到。
//描述商品信息的类
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& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
//按商品价格升序的仿函数
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
void Test()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙子", 2.2, 3
}, { "菠萝", 1.5, 4 },{"蓝莓",5,10}};
// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
// 不同项的⽐较,需要写不同的仿函数或者函数指针,相对还是比较麻烦的,那么这里lambda就很好⽤了
//通过仿函数控制排序逻辑
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
//通过lambda控制按照价格排序
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;
});
//通过lambda控制按照评价排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
}
5.6、lambda的原理
实际上经过编译器编译后,来到汇编层,根本就没有lambda这个东西,lambda的底层就是仿函数对象,也就是说我们写了一个lambda表达式后,编译器会自动生成一个对应的仿函数的类。底层仿函数的类名是编译器按⼀定规则生成的,保证不同的 lambda生成的类名不同,lambda参数、返回类型、函数体就是 仿函数operator()的参数、返回类型、函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传哪些对象构造。
分别写一个仿函数和lambda来计算利息。
//计算年利率的仿函数
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
void Test()
{
double rate = 0.49;
//仿函数对象
Rate r1(rate);
//lambda表达式
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
r1(10000, 2);
r2(10000, 2);
}
lambda底层的仿函数类名: 编译器按照一定的规则生成,防止重复.

lambda调用本质就是调用底层编译器生成的仿函数的operator():
6、总结
本章重点是引用折叠和lambda表达式,语法比较复杂,lambda表达式是C++11中非常重要的部分,大家要结合例子自己手动敲一遍,理解lambda的语法、使用以及lambda的原理!
下篇文章,我们会讲解C++11剩余的所有内容,完成对C++11的收尾。
创作不易,希望大家点赞收藏支持一下,关注博主,为你带来更多优质内容!