当前位置: 首页 > news >正文

C++11新特性(上)——右值引用,移动语义,引用折叠,完美转发,初始化列表,可变参数模版,lambda,包装器

文章目录

  • 一、右值引用与移动语义
    • 1.左值引用与右值引用
    • 2.移动构造和移动赋值
  • 二、引用折叠
  • 三、完美转发
  • 四、C++11的{}
    • 1.初始化列表
    • 2.initializer_list
  • 五、可变参数模版
    • 1.语法与原理
    • 2.包扩展
    • 3.empalce接口
  • 六、新的类功能
  • 七、lambda
    • 1.语法
    • 2.捕捉列表
    • 3.原理
  • 八、包装器
    • 1.function
    • 2.bind

一、右值引用与移动语义

1.左值引用与右值引用

  • 左值:可以取到地址的值,比如一些变量名,指针等。
  • 右值:不能取到地址的值,比如常量、临时对象、匿名对象、表达式结果等。
  • 左值引用:给左值取别名。&表示左值引用。
  • 右值引用:给右值取别名。&&表示右值引用。
  • 一般情况下左值引用(&)不能引用右值,除非使用const修饰该左值引用。
  • 一般情况下右值引用(&&)不能引用左值,除非用move修饰该左值。

注意:左值引用和右值引用是左值。

示例如下:

int main()
{int a = 8;//其中a为左值。int& pa=a;//pa为左值引用,引用了左值a//8;//8是一个常量为右值int(9);//9是匿名对象为右值5 + 3;//表达式的结果7为右值int&& pb = 8;//pb为右值引用,引用了右值8。int&& pc = int(9);int&& pd = 5 + 3;//const int& pl= 8;//const修饰后的左值引用可引用右值。int&& pr = move(a);//move修饰左值后可被右值引用引用。int& pp = pr;//pr属性是左值return 0;
}

2.移动构造和移动赋值

左值引用可以使得在 函数传参过程中减少拷贝,在函数内直接对实参进行修改 等等。这些可以大大的提高程序的执行效率。但是对于在被调函数内创建的临时对象不能直接传引用返回到原函数。因为在函数结束后这些临时对象会随着 函数栈帧 的销毁而销毁,而引用最底层用的是指针。所以必须得返回对象从而进行拷贝转移资源。这样的话会大大的降低程序的执行效率。

对于以上左值引用的不足之处,右值引用就可以得到一个还好的解决。

右值引用在引用右值的时候实际上是把该右值的资源的 地址保存 。而该资源不会被立即释放掉,相当于延长了生命周期,因为右值都是一些临时对象、常量、匿名对象等这些**“将亡”值**。反正这些资源又没有人使用,那么就不急着释放它可以让右值引用接管它。

而对于把左值move后进行右值引用,相当于“掠夺”资源,这种情况一般都是该左值不再被需要了,从而把它move让右值引用接管,那么原来的那个左值的状态是未定义的,要避免使用它。

当然单上面的内容,还体现不出右值引用的高效之处,接下来我们来看右值引用的两个应用的地方,移动构造和移动赋值。

移动构造和移动赋值的实现是非常简单的就是 交换资源 ,把自己的空资源给别人把别人的拿过来。
如下一个string类的移动构造和移动赋值的简单实现:

string(string&& s)
{swap(s);
}
string& operator=(string&& s)
{swap(s);return *this;
}

移动构造和移动赋值相比拷贝构造和拷贝赋在这里插入代码片值效率是非常之高的,值得我们学习并使用。

二、引用折叠

C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过 模板或typedef
中的类型操作可以构成引⽤的引⽤。
通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。

如下:

template<class T>
void func1(T&& x);template<class T>
void func2(T& x);

对于func1当传入左值时是(折叠成)左值引用,当传入右值时是(折叠成)右值引用

对于func2当传入左值时是(折叠成)左值引用,当传入右值时还是(折叠成)左值引用。

func1这样的函数模板我们通常把它称为“万能引用”。

三、完美转发

当左值与右值在函数之间一直往下传的时候。我们会无法识别它原本是左值还是右值,因为左值引用和右值引用传入下一层后都被视为左值了。会导致移动构造和移动赋值等操作失效。而完美转发就可以解决这个问题。

完美转发需要用到库提供的一个函数模板forward,源码如下:

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvaluereturn static_cast<_Ty&&>(_Arg);
}
void func1(T&& x)
{//......func2(std::forward<T>(x));
}
void func2(T&& x)
{//......
}

完美转发forward它主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中如果传递给func1的实参是右值,T被推导为int,没有折叠,forward内部_Arg被强转为右值引⽤返回;如果传递给func1的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部_Arg被强转为左值引⽤返回

四、C++11的{}

1.初始化列表

  • C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化 {}初始化也叫做列表初始化
  • 关于c++11后的{}初始化,内置类型⽀持,⾃定义类型也⽀持。⾃定义类型使用{}初始化本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
  • {}初始化的过程中, 可以省略掉=
  • C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便。

例如:

struct pt
{int x;int y;
}
int main()
{int a1 = { 7 };double d1 = { 26.3 };set<int> st1= { 71 };pt pos1={ 3, 4 };//......//=可省略int a2{ 7 };double d2{ 26.7 };set<int> st2{ 78 };pt pos2{ 3, 4 };//......return 0;
}

2.initializer_list

上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持。

C++11库中提出了⼀个std::initializer_list的类

  • auto il = { 10, 20, 30 };
    这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
  • std::initializer_list⽀持迭代器遍历。
  • 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3…}
    进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3…}
    进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。

例如

int main()
{vector<int> arr{ 1,5,2,62,4 };unordered_map<char, int> mp{ {'a',1},{'p',8},{'v',8} };//可以任意传多个值,在此之前编译器是不能确定你要传入多少个值的,//所以底层用了std::initializer_list从而支持初始化多个值return 0;
}

五、可变参数模版

1.语法与原理

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。

也就是 模板的参数个数是不确定的 ,当使用它的时候它会根据传入参数的个数自动去推导并生成对应的参数个数的函数或类。

template <class... Args> void Func(Args... x) {}
template <class... Args> void Func(Args&... x) {}
template <class... Args> void Func(Args&&... x) {}

我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
这⾥我们可以使⽤sizeof…运算符 去计算参数包中参数的个数。

2.包扩展

包扩展就是一个将包里的元素取出来的操作,因为考虑到很多因数,在这个取这些元素过程会比较复杂。

注意:包扩展是在编译时完成的。

如下一个包开展过程:

// 递归终止函数
template <class T>
void ShowList(const T& t)
{cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " ";ShowList(args...);
}
int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}

3.empalce接口

C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。

template <class... Args>
void emplace_back (Args&&... args);
int main()
{std::list< std::pair<int, char> > mylist;// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别// 对于有带有拷贝构造和移动构造的的emplace_back是直接构造了,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;
}

六、新的类功能

在原来C++类中6个默认成员函数:构造函数、析构函数、拷⻉构造函数、拷⻉赋值重载、取地址重载、const取地址重载的基础上C++11新增了2个默认成员函数, 移动构造函数和移动赋值运算符重载。

关键字功能:

  • default:强行生成默认成员函数。 只需在需要编译器生成的默认成员函数声明加上=default即可。
    如下:
class student
{
public:student(const string& s, const int& num):_name(s),_age(num){}student() = default;~student() = default;
private:string _name;int _age;
};
  • delete:如果想要限制某些默认函数的⽣成, 在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上 =delete 即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

例如:

class student
{
public:student(const string& s, const int& num) = delete;
private:string _name;int _age;
};
  • final:(1).防止类被继承。(2).防止函数被重写。
  • override:检查函数重写是否正确。

七、lambda

1.语法

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。

lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
lambda表达式的格式:

[ 捕捉列表 ] (参数) ->返回类型 {函数体}
一个简单的lambda表达式:

auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
//捕捉列表和参数可为空
//返回值可以省略,可以通过返回对象自动推导
//如下:
auto add2 = [](int x, int y){return x + y; };
cout << add2(1, 2) << endl;
  • 捕捉列表:该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda函数,所以捕捉列表可为空当[ ]不能省略。 捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉。
  • 参数列表:与普通函数的参数列表功能类似,如果不需要参数传递, 则可以连同()⼀起省略。
  • 返回值类型:⽤追踪返回类型形式声明函数的返回值类型, 没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
  • 函数体:函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略{ }。
auto func1 = []
{
cout << "hello bit" << endl;
return 0;
};func1();

2.捕捉列表

lambda 表达式中默认只能⽤ lambda 函数体和参数列表中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉。其中捕捉方式有三种,如下:

  • 显示捕捉:在捕捉列表中显⽰的 传值捕捉和传引⽤捕捉 ,捕捉的多个变量⽤逗号分割。[x,y,&z]表⽰x和y值捕捉,z引⽤捕捉。
  • 隐式捕捉:在捕捉列表写⼀个=表⽰隐式值捕捉, 即[=] ,在捕捉列表写⼀个&表⽰隐式引⽤捕捉即,即[&] ,这样我们 lambda表达式中⽤了那些变量,编译器就会 ⾃动捕捉 那些变量。
  • 混合捕捉:即有显示捕捉又有隐式捕捉,如[=,&x]表⽰其他变量隐式值捕捉,x引⽤捕捉;[&,x,y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。

lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量, 不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表(注意不是捕捉列表)的后⾯可以取消其常量性 ,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。

3.原理

lambda底层确实是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类。仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量。

所以为了方便或增加可读性我们通常都会用lambda来代替仿函数或函数指针。

八、包装器

1.function

std::function是⼀个类模板,也是⼀个包装器被定义头⽂件中。 std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为std::function的⽬标。若std::function不含⽬标,则称它为空。调⽤空std::function的⽬标导致抛出异常。
函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型。

语法格式如下:

function<返回类型(参数类型1,参数类型2,…)> 对象名 = 函数指针/仿函数/ lambda / bind 表达式。

#include<iostream>
#include<functional>
using namespace std;
int add(int x, int y)
{return x + y;
}
int main()
{function<int(int, int)> fn = [](int x, int y) {return x + y; };function<int(int, int)> fm = add;//......return 0;
}

例如下面这个题可以这么写:
150.逆波兰表达式求值-力扣(LeetCode)

class Solution {
public:int evalRPN(vector<string>& tokens){stack<int> s;map<string,function<int(int,int)>> m{{"+", [](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;}}};for(auto& e : tokens){if(m.count(e)){int right = s.top();s.pop();int left = s.top();s.pop();s.push(m[e](left,right));}else{s.push(stoi(e));}}return s.top();}
};

2.bind

bind的使用语法:

auto f=bind(可调用对象,参数1,参数2,…)

bind是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的可调用对象进⾏处理后返回⼀个可调⽤对象。 bind可以⽤来调整参数个数和参数顺序。
bind 也在这个头⽂件中。

调⽤bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰
newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象
中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3…这些占
位符放到placeholders的⼀个命名空间中。如下:

#include<iostream>
#include<functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
int sub(int x, int y)
{return x - y;
}
int main()
{//可以调换传参顺序auto fn1 = bind(sub, _1, _2);auto fn2 = bind(sub, _2, _1);cout << fn1(2, 3) << endl;//输出-1cout << fn2(2, 3) << endl;//输出1//可以固定某些参数auto fn3 = bind(sub, 2, _1);auto fn4 = bind(sub, _1, 3);cout << fn3(1) << endl;cout << fn4(5) << endl;return 0;
}
http://www.dtcms.com/a/285006.html

相关文章:

  • 通过轮询方式使用LoRa DTU有什么缺点?
  • CMake综合学习2: 构建高效可靠的C++中型服务项目以及现代CMake全链条指南
  • 【CodeTop】每日练习 2025.7.17
  • 面试Redis篇-深入理解Redis缓存雪崩
  • 关于vector中的erase的强调
  • 从一到无穷大 #48:Vector Bucket,S3如何把向量玩成新范式?
  • imx6ull-系统移植篇9——bootz启动 Linux 内核
  • Spark 之 HashJoin
  • Langchain和Faiss搭建本地知识库对比
  • python东方财富api股票数据获取程序
  • Vue3从入门到精通
  • Django `transaction.atomic()` 完整使用指南
  • SWD和JTAG区别
  • 【47】MFC入门到精通——MFC编辑框 按回车键 程序闪退问题 ,关闭 ESC程序退出 问题
  • git merge 和 git rebase 的区别
  • LoRA:大模型低秩适配技术全景——原理、演进与高效微调革命
  • MongoDB社区版安装(windows)
  • 第4.3节 iOS App生成追溯关系
  • 联发科MT6897 5G智能手机应用处理器 软件寄存器表:通用闪存(UFS)
  • Kafka——无消息丢失配置怎么实现?
  • C++:list
  • 博客摘录「 Springboot入门到精通(超详细文档)」2025年7月4日
  • ubuntu 22.02 带外进单用户拯救系统
  • 人工智能之数学基础:概率论和数理统计在机器学习的地位
  • 什么是 M4A 和 WAV?这两种音频互转会导致音质发生变化吗
  • python爬虫入门(小白五分钟从入门到精通)
  • 振石股份闯关上市:业绩连降,资产、负债两端暗藏隐忧
  • leetcode 3202. 找出有效子序列的最大长度 II 中等
  • 18650锂电池点焊机:新能源制造的精密纽带
  • Unreal5从入门到精通之如何实现第一人称和第三人称自由切换