✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/fYaBd
📚专栏简介:在这个专栏中,我将会分享 C++ 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
16. Lambda 表达式
基本用法
lambda 表达式是 C++11 最重要也是最常用的特性之一,这是现代编程语言的一个特点,lambda 表达式有如下的一些优点:
- 声明式的编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象。
- 简洁:避免了代码膨胀和功能分散,让开发更加高效。
- 在需要的时间和地点实现功能闭包,使程序更加灵活。
lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式简单归纳如下:
cpp
[capture](params) opt -> ret {body;};
其中 capture 是捕获列表,params 是参数列表,opt 是函数选项,ret 是返回值类型,body 是函数体。
- 捕获列表 []:捕获一定范围内的变量。
- 参数列表 () : 和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。
auto f = <>{return 1;} // 没有参数, 参数列表为空
auto f = []{return 1;} // 没有参数, 参数列表省略不写 - opt 选项:不需要可以省略。
- mutable :可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
- exception :指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw()。
- 返回值类型:在 C++11 中,lambda 表达式的返回值是通过返回值后置语法来定义的。
- 函数体:函数的实现,这部分不能省略,但函数体可以为空。
捕获列表
lambda 表达式的捕获列表可以捕获一定范围内的变量,具体使用方式如下:
- [] - 不捕捉任何变量
- [&] - 捕获外部作用域中所有变量,并作为引用在函数体内使用 (按引用捕获)
- [=] - 捕获外部作用域中所有变量,并作为副本在函数体内使用 (按值捕获)
- 拷贝的副本在匿名函数体内部是只读的
- [=, &foo] - 按值捕获外部作用域中所有变量,并按照引用捕获外部变量 foo
- [bar] - 按值捕获 bar 变量,同时不捕获其他变量
- [&bar] - 按引用捕获 bar 变量,同时不捕获其他变量
- [this] - 捕获当前类中的 this 指针
- 让 lambda 表达式拥有和当前类成员函数同样的访问权限
- 如果已经使用了 & 或者 = , 默认添加此选项
下面通过一个例子,看一下初始化列表的具体用法:
cpp
#include <iostream>
#include <functional>
using namespace std;
class Test
{
public:
void output(int x, int y)
{
auto x1 = [] {return m_number; }; // error
auto x2 = [=] {return m_number + x + y; }; // ok
auto x3 = [&] {return m_number + x + y; }; // ok
auto x4 = [this] {return m_number; }; // ok
auto x5 = [this] {return m_number + x + y; }; // error
auto x6 = [this, x, y] {return m_number + x + y; }; // ok
auto x7 = [this] {return m_number++; }; // ok
}
int m_number = 100;
};
x1 :错误,没有捕获外部变量,不能使用类成员 m_number
x2 :正确,以值拷贝的方式捕获所有外部变量。
x3 :正确,以引用的方式捕获所有外部变量。
x4 :正确,捕获 this 指针,可访问对象内部成员。
x5 :错误,捕获 this 指针,可访问类内部成员,没有捕获到变量 x,y,因此不能访问。
x6 :正确,捕获 this 指针,x,y 。
x7 :正确,捕获 this 指针,并且可以修改对象内部变量的值。
cpp
int main(void)
{
int a = 10, b = 20;
auto f1 = [] {return a; }; // error
auto f2 = [&] {return a++; }; // ok
auto f3 = [=] {return a; }; // ok
auto f4 = [=] {return a++; }; // error
auto f5 = [a] {return a + b; }; // error
auto f6 = [a, &b] {return a + (b++); }; // ok
auto f7 = [=, &b] {return a + (b++); }; // ok
return 0;
}
f1 :错误,没有捕获外部变量,因此无法访问变量 a 。
f2 :正确,使用引用的方式捕获外部变量,可读写。
f3 :正确,使用值拷贝的方式捕获外部变量,可读。
f4 :错误,使用值拷贝的方式捕获外部变量,可读不能写。
f5 :错误,使用拷贝的方式捕获了外部变量 a ,没有捕获外部变量 b ,因此无法访问变量 b 。
f6 :正确,使用拷贝的方式捕获了外部变量 a ,只读,使用引用的方式捕获外部变量 b ,可读写。
f7 :正确,使用值拷贝的方式捕获所有外部变量以及 b 的引用,b 可读写,其他只读。
在匿名函数内部,需要通过 lambda 表达式的捕获列表控制如何捕获外部变量,以及访问哪些变量。默认状态下 lambda 表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获。
返回值
很多时候,lambda 表达式的返回值是非常明显的,因此在 C++11 中允许省略 lambda 表达式的返回值。
cpp
// 完整的lambda表达式定义
auto f = [](int a) -> int
{
return a+10;
};
// 忽略返回值的lambda表达式定义
auto f = [](int a)
{
return a+10;
};
一般情况下,不指定 lambda 表达式的返回值,编译器会根据 return 语句自动推导返回值的类型,但需要注意的是 labmda 表达式不能通过列表初始化自动推导出返回值类型。
cpp
// ok,可以自动推导出返回值类型
auto f = [](int i)
{
return i;
}
// error,不能推导出返回值类型
auto f1 = []()
{
return {1, 2}; // 基于列表初始化推导返回值,错误
}
函数本质
使用 lambda 表达式捕获列表捕获外部变量,如果希望去修改按值捕获的外部变量,那么应该如何处理呢?这就需要使用 mutable 选项,被 mutable 修改是 lambda 表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的外部变量的只读(const)属性。
cpp
int a = 0;
auto f1 = [=] {return a++; }; // error, 按值捕获外部变量, a是只读的
auto f2 = [=]()mutable {return a++; }; // ok
再举个例子:
cpp
int a = 10, b = 20;
auto f3 = [=]() { //error 常方法里试图改变变量的值
int tmp = a;
a = b;
b = tmp;
};
修改后:
cpp
int a = 10, b = 20;
printf("%p %p\n", &a, &b);
auto f3 = [a, b]() mutable
{
int tmp = a;
a = b;
b = tmp;
printf("%p %p\n", &a, &b);
};
f3();
printf("%d %d\n", a, b);
/*
00CFFCE4 00CFFCD8
00CFFCC8 00CFFCCC
10 20
*/
最后再剖析一下为什么通过值拷贝的方式捕获的外部变量是只读的:
- lambda 表达式的类型在 C++11 中会被看做是一个带 operator() 的类,即仿函数。
- 按照 C++ 标准,lambda 表达式的 operator() 默认是 const 的,一个 const 成员函数是无法修改成员变量值的。
mutable 选项的作用就在于取消 operator() 的 const 属性。
因为 lambda 表达式在 C++ 中会被看做是一个仿函数,因此可以使用 std::function 和 std::bind 来存储和操作。
lambda 表达式:
cpp
#include <iostream>
#include <functional>
using namespace std;
int main(void)
{
// 包装可调用函数
std::function<int(int)> f1 = [](int a) {return a; };
// 绑定可调用函数
std::function<int(int)> f2 = bind([](int a) {return a; }, placeholders::_1);
// 函数调用
cout << f1(100) << endl;
cout << f2(200) << endl;
return 0;
}
对于没有捕获任何变量的 lambda 表达式,还可以转换成一个普通的函数指针:
cpp
using func_ptr = int(*)(int);
// 没有捕获任何外部变量的匿名函数
func_ptr f = [](int a)
{
return a;
};
// 函数调用
f(1314);
自定义 sort
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class A
{
public:
A(int _a,int _b)
{
a=_a;
b=_b;
};
int a;
int b;
};
int main()
{
vector<A> test;
for(int i=0;i<10;i++)
{
A a(i,10-i);
test.emplace_back(a);
}
sort(test.begin(),test.end(),[](A x,A y){return x.a>y.a;});
for(int i=0;i<10;i++)
{
cout<<test[i].a<<endl;
}
return 1;
}
多元组的优先队列
cpp
class Data
{
public:
Data(int val1 = 10, int val2 = 10) :ma(val1), mb(val2) {}
//bool operator>(const Data& data) const { return ma > data.ma; } //方法一
//bool operator<(const Data& data) const { return ma < data.ma; }
int ma;
int mb;
private:
};
//main
//优先级队列
using FUNC = function<bool(Data&, Data&)>; //方法二
priority_queue<Data, vector<Data>, FUNC>
queue([](Data& d1, Data& d2)->bool
{
return d1.ma > d2.ma;
});
queue.push({ 10, 20 });
queue.push({ 15, 25 });
17. 说一下 C++ 左值引用和右值引用
C++11 正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过 move 语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。
- 在 C++11 中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在 C++11 中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c,a 就是左值,其有变量名为 a,通过 &a 可以获取该变量的地址;表达式 b+c、函数 int func() 的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c) 这样的操作则不会通过编译。
- C++11 对 C++98 中的右值进行了扩充。在 C++11 中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在 C++98 标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是 C++11 新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用 T&& 的函数返回值、std::move 的返回值,或者转换为 T&& 的类型转换函数的返回值。将亡值可以理解为通过 "盗取" 其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过 "盗取" 的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
- 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个 "万能" 的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的 "余生" 中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
- 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要 std::move() 将左值强制转换为右值。
左值和右值
- 左值:表示的是可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。但是修饰符 const 的出现使得可以声明如下的标识符,它可以取得地址,但是没办法对其进行赋值:
cpp
const int& a = 10;
- 右值:表示无法获取地址的对象,有常量值、函数返回值、lambda 表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。
左值引用和右值引用
- 左值引用:传统的 C++ 中引用被称为左值引用。
- 右值引用:C++11 中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。
这里主要说一下右值引用的特点:
- 特点 1:通过右值引用的声明,右值又 "重获新生",其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。
- 特点 2:右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值。
- 特点 3:T&& t 在发生自动类型推断的时候,它是左值还是右值取决于它的初始化。
举个例子:
cpp
#include <bits/stdc++.h>
using namespace std;
template<typename T>
void fun(T&& t)
{
cout << t << endl;
}
int getInt()
{
return 5;
}
int main() {
int a = 10;
int& b = a; //b是左值引用
int& c = 10; //错误,c是左值不能使用右值初始化
int&& d = 10; //正确,右值引用用右值初始化
int&& e = a; //错误,e是右值引用不能使用左值初始化
const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int&& h = 10; //正确,右值常引用
const int& aa = h;//正确
int& i = getInt(); //错误,i是左值引用不能使用临时变量(右值)初始化
int&& j = getInt(); //正确,函数返回值是右值
fun(10); //此时fun函数的参数t是右值
fun(a); //此时fun函数的参数t是左值,fun必须用模板才不会报错
return 0;
}
18. 在使用 move 后,之前的变量会失效吗?
在使用 C++ 的移动语义(move semantics)之后,之前的变量不会直接失效,但它的状态会被修改,并且不再保证是有效或可用的。
移动语义是通过将资源的所有权(如内存或文件句柄)从一个对象转移到另一个对象来实现高效的资源管理。使用 std::move() 函数可以将对象转换为右值引用,并且在移动语义的背景下,对右值引用的对象进行移动操作。
移动操作的结果是源对象的状态被修改,通常会使其处于某种无效或未定义的状态。与之对应的是目标对象获得了源对象的资源所有权,并且处于一个有效的状
态。
下面是一个使用移动语义的示例:
cpp
#include <iostream>
#include <string>
int main() {
std::string source = "Hello";
std::string destination = std::move(source);
std::cout << "Source: " << source << std::endl; // 输出为空,原始源对象状态被修改
std::cout << "Destination: " << destination << std::endl; // 输出为 "Hello"
return 0;
}
在上面的示例中,source 对象在使用 std::move() 后即可视为失效,因为它的状态被修改,不再保证是有效的字符串。而 destination 对象获得了 source 对象的资源所有权,并且仍然保持有效的字符串。
需要注意的是,虽然源对象被移动后不再保证是有效的,但并没有强制要求在移动之后立即停止对源对象的使用。在某些情况下,仍然可以安全地使用源对象,例如重新赋值或销毁。但是,需要小心确保源对象没有被再次移动或使用。
综上所述,使用移动语义后,之前的变量不会直接失效,但它们的状态会被修改,并且不再保证是有效或可用的。因此,在使用移动语义后,要注意适当地处理和管理移动操作的源对象和目标对象。