C++学习:C++11介绍及其新特性学习
在之前学习的过程中,我们学习的都是C++最早的版本——C++98的内容。C++98特性固然不错,但是为了迎合更加现代的C++开发方式,我们必须要学习C++更加现代化的内容。
本篇就让我们以C++的转折点,也是C++迈向现代化的第一步——C++11开始讲起。
相关代码已经上传至作者个人gitee:楼田莉子/CPP代码学习喜欢请点个赞谢谢
目录
C++11的介绍
列表初始化
右值引用和移动语义
左值和右值
左值引用和右值引用
引用延长生命周期
左值和右值的参数匹配
右值引用和移动语义的使用场景
左值引用主要使用场景回顾
移动构造和移动赋值
右值引用和移动语义引发的传值问题
右值对象构造,只有拷贝没有移动的场景
右值对象构造,有拷贝有移动的场景
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
右值对象赋值,拷贝构造和拷贝赋值,有移动构造和移动赋值的场景
类型分类
引用折叠
完美转发
可变参数模板
基本原理
包扩展
empalce系列接口
类与对象新增的功能
默认的移动构造和移动赋值
成员变量声明时给缺省值
委托构造函数
继承构造函数
defult和delete
final与override
final
override
组合应用
强制性规则
编译期保证
lambda表达式
lambda表达式语法
捕捉列表
lambda表达式的应用
lambda表达式的原理
包装器
function
function示例:逆波兰表达式
bind
C++11的介绍
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。
列表初始化
C++98中⼀般数组和结构体可以⽤{}进⾏初始化
C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
{}初始化的过程中,可以省略掉=
C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便
#include<iostream>
#include<vector>
using namespace std;
//C++98的{}初始化
struct Point
{int _x;int _y;
};
class Date
{public :Date(int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;} Date(const Date& d): _year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}// 一切皆可用列表初始化,且可以不加=
private:int _year;int _month;int _day;
};
int main()
{//C++98的{}初始化int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };//C++11{}初始化int x1 = { 1 };int x2{ 3 };Date(2025,9,23);Date d1{ 2022,12,31 };Date d2 = { 2022,12,31 };Date d3 = Date{ 2022,12,31 };const Date d4{ 2022,12,31 };return 0;
}
列表初始化官方文档:initializer_list - C++参考
⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持
vector<int> v1 ={1,2,3};
vector<int> v2 = {1,2,3,4,5};
C++11库中提出了⼀个std::initializer_list的类
auto v2 = { 1,2,3,4,5 ,6,7,8,9,10 };//initializer_list类型
cout<<sizeof(v2)<<endl;
cout<<typeid(v2).name()<<endl;
这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束
std::initializer_list⽀持迭代器遍历
容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的
{x1,x2,x3...}
进⾏初始化。STL中的容器⽀持任意多个值构成的
{x1,x2,x3...}
进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。
这里是一些列表初始化的方式,希望看到不要奇怪
//新型的{}初始化
string func()
{return {};//表示返回一个空字符串
}
void test3()
{string s1("111111");string s2 = "111111";string s3={ "111111" };string s4 { "111111" };string s5(10,'f');string s6 = { 10, 'f' };string s7 = {};//空字符串int a=0;int b = {};int c{};}
右值引用和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
左值和右值
左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left
value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
int main()
{//左值特点是可以取地址int a = 10; //左值,可以取地址,可以赋值int b = 20; //左值,可以取地址,可以赋值int *p = new int(10); //左值,可以取地址,可以赋值string s = ("hello"); //左值,可以取地址,可以赋值s[0] = "x";//右值特点是不能取地址,不能赋值a + b;10;fmin(a, b);string("world");return 0;
}
左值引用和右值引用
Type& r1 = x; Type&& rr1 = y;
第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
template <class T> typename remove_reference<T>::type&& move (T&&
arg);
move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识
需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
int i = 10;
int &r=i;
int &&r2 = 10;
//无法将左值绑定到右值引用
int &&r3 = r2;//这行代码会报错
//要move一下才可以
int &&r4 = move(r2);
//或者类型强转
int&& r5 = (int&&)r2;
语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。
底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
int main()
{//左值特点是可以取地址int a = 10; //左值,可以取地址,可以赋值int b = 20; //左值,可以取地址,可以赋值int *p = new int(10); //左值,可以取地址,可以赋值string s = ("hello"); //左值,可以取地址,可以赋值//s[0] = "x";//右值特点是不能取地址,不能赋值a + b;10;fmin(a, b);string("world");return 0;//左值引用int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];//右值引用int&& rr1 = 10;double&& rr2 = a+b;double&& rr3 = fmin(a, b);string&& rr4 = string("11111");// 左值引用不能直接引用右值,但是const左值引用可以引用右值const int& rx1 = 10;const double& rx2 = a + b;const double& rx3 = fmin(a, b);const string& rx4 = string("11111");// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);string&& rrx5 = (string&&)s;// b、r1、rr1都是变量表达式,都是左值cout << &b << endl;cout << &r1 << endl;cout << &rr1 << endl;// 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下int& r6 = r1;// int&& rrx6 = rr1;int&& rrx6 = move(rr1);return 0;
}
引用延长生命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
int main()
{std::string s1 = "Test";// std::string&& r1 = s1; // 错误:不能绑定到左值const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改std::cout << r3 << '\n';return 0;
}
左值和右值的参数匹配
C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的使⽤场景时,就能体会这样设计的价值了
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
void f(int& x)
{std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{std::cout << "右值引用重载 f(" << x << ")\n";
}
void test3()
{int i = 1;const int ci = 2;f(i); // 调用 f(int&)f(ci); // 调用 f(const int&)f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)f(std::move(i)); // 调用f(int&&)// 右值引用变量在用于表达式时是左值int&& x = 1;f(x); // 调用 f(int& x)f(std::move(x)); // 调用 f(int&& x)
}
int main()
{test3();return 0;
}
右值引用和移动语义的使用场景
左值引用主要使用场景回顾
左值引⽤主要使⽤场景是在函数中左值引用传参和左值引用传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。
左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。
那么C++11以后这⾥可以使用右值引用做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引用返回也⽆法概念对象已经析构销毁的事实。
namespace Boogle1
{class Solution {public:// 传值返回需要拷贝string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;// 进位int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);} if(next == 1)str += '1';reverse(str.begin(), str.end());return str;}};
}
namespace Boogle2
{class Solution {public:// 这里的传值返回拷贝代价太大了vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}//应该这么改void generate(int numRows,vector<vector<int>>& vv){vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}};
}
移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的样例实现了移动构造和移动赋值来提高我们的理解
//string.h
#pragma once
#include <iostream>
#include <cstring>
using namespace std;
namespace string_h
{//string类实现class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{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);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){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';}char& operator[](size_t pos){//assert(pos < _size);return _str[pos];}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};string addStrings(string num1, string num2){string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}//test.h
void test4()
{string_h::string s1("xxxxxxx");// 拷贝构造string_h::string s2 = s1;//右值拷贝,马上就要销毁了//调用移动构造函数string_h::string s3 = string_h::string("yyyyyyy");//不要轻易对左值movestring_h::string s4 = move(s1);string_h::string ret = string_h::addStrings("11111", "2222");//cout << ret.c_str() << endl;
}
下面两个图,左面是左值引用的过程理解,右面是右值引用的过程理解
对于左值拷贝而言,拷贝字面量需要创建临时对象,将数据拷贝进去才能传值;而右值引用则是直接将字面量赋值给新值。极大的提升了拷贝效率
右值引用和移动语义引发的传值问题
namespace string_h
{string addStrings(string num1, string num2){string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}
//场景1
int main()
{string_h::string ret = string_h::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}
//场景2
int main()
{string_h::string ret;ret = string_h::addStrings("11111", "2222");cout << ret.c_str() << endl;
return 0;
}
从上到下依次为不优化、优化方式一、优化方式二。但是优化方式一、二均为个人行为
右值对象构造,只有拷贝没有移动的场景
图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造。
需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
Linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤
g++ test.cpp -fno-elideconstructors
的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉
图1
右值对象构造,有拷贝有移动的场景
图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
Linux系统下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤
g++ test.cpp -fno-elideconstructors
的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。
图2
图3
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
图4左边展⽰了vs2019 debug和
g++ test.cpp -fno-elide-constructors
关闭优化环境下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。
需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
图4
右值对象赋值,拷贝构造和拷贝赋值,有移动构造和移动赋值的场景
图5左边展⽰了vs2019 debug和
g++ test.cpp -fno-elide-constructors
关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。
需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
类型分类
C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如:
42、true、nullptr
或者类似
str.substr(1, 2)、str1 + str2
传值返回函数调⽤,或者整形
a、b,a++,a+b
等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如
move(x)、static_cast<X&&>(x)
泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值
值类型的官方文档:值类别 - cppreference.com,感兴趣可以自行了解
引用折叠
C++中不能直接定义引⽤的引⽤如
int& && r = i;
这样写会直接报错,通过模板或 typedef中的类型操作可以构成引⽤的引⽤。
通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:只有没有折叠的右值引用和右值引用的右值引用折叠构成右值引用,所有其他组合均折叠构成左值引用。
下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则
// 引⽤折叠
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{
}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{
}
void test6()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&// 没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)f2<int>(n); // 报错f2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // 报错// 折叠->实例化为void f2(int&& x)f2<int&&>(n); // 报错f2<int&&>(0);
}
引用折叠是为了泛型编程而准备的。有一种叫作“万能引用”的东西。以下代码是一种“万能引用”的应用
//万能引用,既可以实例化出左值版本又可以实例化出右值版本
template<class T>
void Function(T&& t)
{int a = 0;T x = a;cout<<"&x=" << &x << endl;cout<<"&a=" << &a << endl;
}
测试文档:
void test7()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10); // 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)// 所以Function内部会编译报错,x不能++// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)// 所以Function内部会编译报错,x不能++Function(std::move(b)); // const 右值Function(b);
}
结果为:
原因为:
当 T
推导为引用类型时,x
是 a
的别名 → 地址相同
当 T
推导为非引用类型时,x
是独立变量 → 地址不同
万能引用有且仅有在有引用折叠的时候才能实现
以下就不是万能引用
//不是万能引用
template<class T>
class A
{...void Function(T&& t){int a = 0;T x = a;cout<<"&x=" << &x << endl;cout<<"&a=" << &a << endl;}
}
如果想写万能引用最好这样
//万能引用
template<class T>
class A
{...template<class X>void Function(X&& t){int a = 0;T x = a;cout<<"&x=" << &x << endl;cout<<"&a=" << &a << endl;}
}
完美转发
Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现
forward函数就是完美转发函数,是C++14开始支持的新特性函数,功能是:保持参数的值类别(左值/右值属性)在转发过程中不变。主要应用如下:
template <class T> T&& forward (typename remove_reference<T>::type&arg);template <class T> T&& forward (typename remove_reference<T>::type&& arg);
forward官方资料:forward - C++ 参考
完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现。
下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。
//万能转发
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 Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void Function(T&& t)
{Fun(t);//Fun(forward<T>(t));
}
//测试代码
void test8()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10); // 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)Function(b); // const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)Function(std::move(b)); // const 右值
}
结果为:
可变参数模板
基本原理
C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包。存在两种参数包:
1、模板参数包,表⽰零或多个模板参数
2、函数参数包:表⽰零或多个函数参数。
语法形式是这样的:
template <class ...Args> void Func(Args... args) {}template <class ...Args> void Func(Args&... args) {}template <class ...Args> void Func(Args&&... args) {}
我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或
typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。
template <class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}
void test1()
{double x = 2.2;Print(); // 包⾥有0个参数Print(1); // 包⾥有1个参数Print(1, string("xxxxx")); // 包⾥有2个参数Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
}
结果为:
可变参数的底层原理
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持
// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,
// 他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ...
包扩展
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如图1所⽰。
C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
以以下代码为例,
void showList()
{cout <<endl;
}
template <class T1, class... Args>
void showList(T1&& x, Args&&... args)
{cout<<x<<endl;showList(args...);
}
template <class... Args>
void Print(Args... args)
{//args是N个参数的参数包//调用showList,参数包第一个传递给x//剩下N-1个参数传递给argsshowList(args...);
}
int main()
{Print(1, string{"xxxxx"}, 2.2);return 0;
}
结果为:
其底层代码的调用顺序为:
//模板元编程
void showList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout <<endl;
}
template <class T1, class... Args>
void showList(T1&& x, Args&&... args)
{cout<<x<<endl;showList(args...);
}
template <class... Args>
// 编译时递归推导解析参数
void Print(Args... args)
{//args是N个参数的参数包//调用showList,参数包第一个传递给x//剩下N-1个参数传递给argsshowList(args...);
}
void test2()
{Print();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);
}
示例2
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}
template <class ...Args>
void Arguments(Args... args)
{
}
template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
}
// 本质可以理解为编译器编译时,包的扩展模式
// 将上⾯的函数模板扩展实例化为下⾯的函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}
void test3()
{Print(1, string("xxxxx"), 2.2);
}
empalce系列接口
template <class... Args> void emplace_back (Args&&... args);template <class... Args> iterator emplace (const_iterator position,Args&&... args);
C++11以后STL容器新增了empalce系列的接⼝(之前关于STL的接口介绍中有简单介绍其结构和功能,可以细看)。empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法。
假设容器为container<T>,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列。对于浅拷贝类型emplace系列更有优势,深拷贝系列相对来说有些性能差距但是并不是很明显
第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
递参数包过程中,如果是
Args&&... args
的参数包,要⽤完美转发参数包。
方式如下:
std::forward<Args>(args)...
否则编译时包扩展后右值引⽤变量表达式就变成了左值。
类与对象新增的功能
默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
class Person
{
public:Person(const char* name = "", int age = 0): _name(name), _age(age){}Person(const Person& p):_name(p._name),_age(p._age){}Person& operator=(const Person& p){if (this != &p){_name = std::move(p._name);_age = p._age;}return *this;}~Person(){}
private:string_h::string _name;int _age;
};
void test1()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = move(s2);
}
成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化。
委托构造函数
C++中的委托构造函数(Delegating Constructor)是C++11引入的特性,允许一个构造函数调用同类中的其他构造函数,从而减少代码重复并提高可维护性。
被委托的构造函数必须初始化所有成员变量,因为委托构造函数后不能再重复初始化。
语法规则:
-
委托必须在成员初始化列表中执行
-
委托构造函数不能同时初始化其他成员
-
委托链不能形成循环
-
委托构造函数的执行顺序严格定义
举例说明
class Example
{
public:Example(int x,int y):_x(x), _y(y){cout<<"目标构造函数"<<endl;}Example(int a):Example(a, 0){cout << "委托构造函数" << endl;}
private:int _x;int _y;
};
但是不可以分开
//不可以这样
class Time
{
public:Time(int hour, int minute):_hour(hour), _minute(minute){}Time(int hour, int minute,int second):Time(hour, minute), _second(second)//这里会报错//委托构造函数仅为成员初始值设定项{}private:int _hour;int _minute;int _second;
};
继承构造函数
继承构造函数是 C++11C++11 引入的一项特性,它允许派生类直接继承基类的构造函数,而不需要手动重新定义它们。这一特性显著简化了派生类的编写,特别是在基类有多个构造函数的情况下。
派生类继承基类的普通构造函数,特殊的拷贝构造函数/移动构造函数不继承。
继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,内置类型成员不确定,自定义类型成员使用默认构造初始化。
适用场景:
1、没有成员变量的派生类
2、成员变量均有缺省值且均用缺省值初始化
以下举例说明:
//继承构造函数
class Base {
public:Base(int x, double d):_x(x), _d(d){}Base(int x):_x(x){}Base(double d):_x(d){}protected:int _x = 0;double _d = 0;
};
// 传统的派生类实现构造
class Derived : public Base {
public:Derived(int x) : Base(x) {}Derived(double d) : Base(d) {}Derived(int x, double d) : Base(x, d) {}
};/*C++11继承基类的所有构造函数1、没有成员变量的派生类2、成员变量都有缺省值,并且我们就想用这个缺省值初始化*/class Derived : public Base {
public:using Base::Base;protected:int _i;string _s;
};
void test3()
{Derived d1(1);Derived d2(1.1);Derived d3(2, 2.2);
}
defult和delete
C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。
如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:Person(const char* name = "", int age = 0): _name(name), _age(age){}Person(const Person& p):_name(p._name), _age(p._age){}Person(Person&& p) = default;//Person(const Person& p) = delete;private:string_h::string _name;int _age;
};
void test2()
{Person s1;Person s2 = s1;Person s3 = move(s1);
}
final与override
之前在继承与多态的内容中我们简单介绍过这两个特性,不过没有详细阐述,本期我们来针对这两个特性详细阐述
往期博客:
继承:https://blog.csdn.net/2401_89119815/article/details/149670984?fromshare=blogdetail&sharetype=blogdetail&sharerId=149670984&sharerefer=PC&sharesource=2401_89119815&sharefrom=from_link
多态:https://blog.csdn.net/2401_89119815/article/details/149767472?fromshare=blogdetail&sharetype=blogdetail&sharerId=149767472&sharerefer=PC&sharesource=2401_89119815&sharefrom=from_link
final
定义与语法
override
是一个上下文关键字,用于显式标识成员函数旨在重写基类中的虚函数。
要求:
-
只能用于虚函数重写
-
函数签名必须与基类虚函数精确匹配
-
访问权限可以不同(但通常保持一致)
作用:
1、编译期契约验证
struct Base {virtual void interface_func(int x);virtual void const_func() const;
};struct Derived : Base {// ✅ 正确重写 - 编译器验证通过void interface_func(int x) override;// ❌ 编译错误 - 签名不匹配(缺少const限定符)void const_func() override;// ❌ 编译错误 - 基类无此虚函数void non_existent() override;
};
2、防止意外行为
class DatabaseHandler {
public:virtual void execute_query(const std::string& sql);
};class MySQLHandler : public DatabaseHandler {
private:// 没有override时:意外创建新虚函数而非重写void executeQuery(const std::string& sql); // 大小写错误// 使用override:立即发现拼写错误void executeQuery(const std::string& sql) override; // ❌ 编译错误
};
3、保证接口明确性
class AbstractShape {
public:virtual double calculate_area() const = 0;virtual void transform(const Matrix& m) = 0;
};class Circle : public AbstractShape {
public:double calculate_area() const override; // 明确重写意图void transform(const Matrix& m) override; // 接口实现清晰
};
override
定义与语法
final
用于阻止派生类进一步重写虚函数或阻止类被继承。
// 类级别final
class NonInheritable final {// ...
};// 函数级别final
class Base {
public:virtual void sealed_method() final;
};
要求
-
类级别
final
阻止任何形式的继承 -
函数级别
final
阻止任何进一步的重写 -
final
可同时用于虚函数和非虚函数
作用:
1、设计意图强制
// 工具类设计:明确禁止继承
class MathUtilities final {
public:static double pi() { return 3.141592653589793; }static int gcd(int a, int b);
};// ❌ 编译错误:final类不能被继承
// class ExtendedMath : public MathUtilities {};
2、关键算法保护
class SecurityProvider {
public:virtual void authenticate() = 0;virtual void encrypt_data() final { // 加密算法固定,禁止修改// 标准加密实现}
};class CustomSecurity : public SecurityProvider {
public:void authenticate() override; // ✅ 允许重写// ❌ 编译错误:final方法不能被重写// void encrypt_data() override;
};
3、性能优化
class Vector3D final { // 数学向量,固定实现以保证性能
private:float x, y, z;
public:// 内联操作,无虚函数开销Vector3D operator+(const Vector3D& other) const;
};
组合应用
1、框架设计模式
class FrameworkBase {
public:virtual void initialize() = 0; // 必须由派生类实现virtual void shutdown() final { // 框架控制关闭流程// 标准资源清理do_shutdown();}private:virtual void do_shutdown() {} // 可选的扩展点
};class Application : public FrameworkBase {
public:void initialize() override final { // 应用初始化,禁止进一步修改// 应用特定初始化}private:void do_shutdown() override { // 允许自定义清理// 应用特定清理逻辑}
};
2、接口版本控制
class IVersionedInterface {
public:virtual void method_v1() = 0;virtual void method_v2() final = 0; // 固定v2接口实现
};class Implementation : public IVersionedInterface {
public:void method_v1() override; // 可自定义实现void method_v2() override; // 必须符合固定接口
};
强制性规则
-
所有虚函数重写必须使用
override
-
设计为不可继承的类必须标记
final
-
关键算法和安全性相关方法应考虑使用
final
推荐案例:
// ✅ 良好实践
class Base {
public:virtual ~Base() = default;virtual void polymorphic_interface() = 0;
};class Derived final : public Base { // 明确为最终实现
public:void polymorphic_interface() override final { // 明确重写并密封// 实现}
};// ❌ 应避免的模式
class Base {
public:virtual void ambiguous(); // 未明确是否期望被重写
};class Derived : public Base {
public:void ambiguous(); // 意图不明确,是否重写?
};
编译期保证
class Base {
public:virtual void process(int value);
};class Derived : public Base {
public:// 传统C++:静默创建新虚函数,难以调试void process(float value); // 非预期的新虚函数// C++11:编译期错误检测void process(float value) override; // ❌ 立即报错
};
lambda表达式
lambda表达式语法
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接
收 lambda 对象。
lambda表达式的格式:
[capture-list] (parameters)-> return type {function boby }
[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使
⽤,捕捉列表可以传值和传引⽤捕捉。捕捉列表为空也不能省略
(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略
1、捕捉为空也不能省略
2、参数为空可以省略
3、返回值可以省略,可以通过返回对象⾃动推导
4、函数题不能省略
//lambda表达式
void test4()
{//一个简单的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;// 1、捕捉为空也不能省略// 2、参数为空可以省略// 3、返回值可以省略,可以通过返回对象⾃动推导// 4、函数体不能省略
}
void test5()
{auto func1 = []{cout << "hello shihua" << endl;return 0;};func1();int a = 0, b = 1;cout <<"比较前>" << a << ":" << b << endl;auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};swap1(a, b);cout << "比较后>" << a << ":" << b << endl;
}
捕捉列表
lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉
//捕获列表
int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto func0 = [](){x++;};
第⼀种捕捉⽅式是在捕捉列表中显式的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z] 表⽰x和y值捕捉,z引⽤捕捉。
void test8()
{// 只能⽤当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;auto func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << func1() << endl;
}
第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。
void test9()
{int a = 0, b = 1, c = 2, d = 3;// 隐式值捕捉// ⽤了哪些变量就捕捉哪些变量auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;// 隐式引⽤捕捉// ⽤了哪些变量就捕捉哪些变量auto func3 = [&]{a++;c++;d++;};func3();cout << a << ":" << b << ":" << c << ":" << d << endl;
}
第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
void test10()
{int a = 0, b = 1, c = 2, d = 3;// 混合捕捉1auto func4 = [&, a, b]{//a++;//b++;c++;d++;return a + b + c + d;};func4();cout << a << " " << b << " " << c << " " << d << endl;// 混合捕捉2auto func5 = [=, &a, &b]{a++;b++;/*c++;d++;*/return a + b + c + d;};func5();cout << a << " " << b << " " << c << " " << d << endl;
}
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
void test11()
{int a = 0, b = 1, c = 2, d = 3;// 局部的静态和全局变量不能捕捉,也不需要捕捉static int m = 0;auto func6 = []{int ret = x + m;return ret;};// 传值捕捉本质是⼀种拷⻉,并且被const修饰了// mutable相当于去掉const属性,可以修改了// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};cout << func7() << endl;cout << a << " " << b << " " << c << " " << d << endl;}
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
lambda表达式的应用
下面我们以商品排序为例,阐述lambda表达式的实际应用,代码如下
//lambda表达式的应用:商品排序
struct Goods
{string _names;//商品名称double _price;//商品价格int _count;//商品数量double _evaluate;//评价Goods(string name, double price, int count, double evaluate):_names(name), _price(price), _count(count), _evaluate(evaluate){}
};
// 仿函数小于比较 - 修正类型处理
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr) const{// 明确处理double类型的比较return gl._price < gr._price;}
};// 仿函数大于比较 - 修正类型处理
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr) const{// 明确处理double类型的比较return gl._price > gr._price;}
};
void Print(vector<Goods>& VG)
{for (auto& vg : VG){cout << vg._names << ":" << vg._price << ":" << vg._count << ":" << vg._evaluate << endl;}cout << "*********************************************" << endl;
}
//仿函数排序
void test6()
{vector<Goods> goods ={ {"苹果",1.0,10,2.3},{"香蕉",2.0,20,3.5},{"橘子",3.0,30,4.7},{"可乐",4.0,40,5.9},{"巧克力饼干",5.0,50,7.1},{"奶油面包",6.0,60,8.3},{"橘汁",3.0,70,9.5},{"雪碧",2.0,80,10.7},{"牛奶",4.0,90,11.9} };// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了Print(goods);sort(goods.begin(), goods.end(), ComparePriceLess());Print(goods);sort(goods.begin(), goods.end(), ComparePriceGreater());Print(goods);
}
//lambda表达式排序
void test7()
{vector<Goods> goods ={ {"苹果",1.0,10,2.3},{"香蕉",2.0,20,3.5},{"橘子",3.0,30,4.7},{"可乐",4.0,40,5.9},{"巧克力饼干",5.0,50,7.1},{"奶油面包",6.0,60,8.3},{"橘汁",3.0,70,9.5},{"雪碧",2.0,80,10.7},{"牛奶",4.0,90,11.9} };Print(goods);sort(goods.begin(), goods.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price;});Print(goods);sort(goods.begin(), goods.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price;});Print(goods);sort(goods.begin(), goods.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate;});Print(goods);sort(goods.begin(), goods.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate;});Print(goods);
}
test6函数的结果为:
test7函数的结果为:
lambda表达式的原理
lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for
这样的东西。范围for底层是迭代器,⽽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 test12()
{double rate = 0.49;// lambdaauto r2 = [rate](double money, int year) {return money * rate * year;};// 函数对象Rate r1(rate);r1(10000, 2);r2(10000, 2);auto func1 = [] {cout << "hello world" << endl;};func1();
}
从底层汇编角度为:
// lambda
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 捕捉列表的rate,可以看到作为lambda_1类构造函数的参数传递了,这样要拿去初始化成员变量
// 下⾯operator()中才能使⽤
00D8295C lea eax,[rate]
00D8295F push eax
00D82960 lea ecx,[r2]
00D82963 call `main'::`2'::<lambda_1>::<lambda_1> (0D81F80h)
// 函数对象
Rate r1(rate);
00D82968 sub esp,8
00D8296B movsd xmm0,mmword ptr [rate]
00D82970 movsd mmword ptr [esp],xmm0
00D82975 lea ecx,[r1]
00D82978 call Rate::Rate (0D81438h)
r1(10000, 2);
00D8297D push 2
00D8297F sub esp,8
00D82982 movsd xmm0,mmword ptr [__real@40c3880000000000 (0D89B50h)]
00D8298A movsd mmword ptr [esp],xmm0
00D8298F lea ecx,[r1]
00D82992 call Rate::operator() (0D81212h)
// 汇编层可以看到r2 lambda对象调⽤本质还是调⽤operator(),类型是lambda_1,这个类型名
// 的规则是编译器⾃⼰定制的,保证不同的lambda不冲突
r2(10000, 2);
00D82999 push 2
00D8299B sub esp,8
00D8299E movsd xmm0,mmword ptr [__real@40c3880000000000 (0D89B50h)]
00D829A6 movsd mmword ptr [esp],xmm0
00D829AB lea ecx,[r2]
00D829AE call `main'::`2'::<lambda_1>::operator() (0D824C0h)
包装器
包装器是对可包装对象进行包装的
function
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
std::function 是⼀个类模板,也是⼀个包装器。
std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标。
若 std::function 不含⽬标,则称它为空。调⽤空 std::function 的⽬标导致抛出 std::bad_function_call 异常。
以上是 function 的原型,他被定义<functional>头⽂件中。std::function - cppreference.com
是function的官⽅⽂件链接。
函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,下⾯的第⼆个代码样例展⽰了 std::function 作为map的参数,实现字符串和可调⽤对象的映射表功能。
function示例:逆波兰表达式
接下来我们以力扣习题:逆波兰表达式来展示function的使用。这个题之前写过因此这里只展示题解答案。
习题链接:150. 逆波兰表达式求值 - 力扣(LeetCode)
题解:
//传统方法:栈
// class Solution
//{
// public:
// int evalRPN(vector<string>& tokens)
// {
// stack<int> st;
// for(auto& str:tokens)
// {
// if(str=="+"||str=="-"||str=="*"||str=="/")
// {
// //遇到运算符要出栈两个运算数然后运算后入栈
// int right=st.top();
// st.pop();
// int left=st.top();
// st.pop();
// switch(str[0])
// {
// case '+':
// st.push(left+right);
// break;// case '-':
// st.push(left-right);
// break;
// case '*':
// st.push(left*right);
// break;
// case '/':
// st.push(left/right);
// break;
// default:
// break;
// }
// }
// else
// {
// //运算数入栈
// st.push(stoi(str));
// }
// }
// return st.top();
// }
// };
// 使⽤map映射string和function的⽅式实现
// 这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution
{
public:int evalRPN(vector<string>& tokens) {stack<int> st;// function作为map的映射可调⽤对象的类型map<string, function<int(int, int)>> opFuncMap = {{"+", [](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& str : tokens){if(opFuncMap.count(str)) // 操作符{int right = st.top();st.pop();int left = st.top();st.pop();int ret = opFuncMap[str](left, right);st.push(ret);} else{st.push(stoi(str));}} return st.top();}
};
bind
simple(1)
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);
bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。
bind 也在<functional>这个头⽂件中。
调⽤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的⼀个命名空间中。
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{return (a - b) * 10;
}
int SubX(int a, int b, int c)
{return (a - b - c) * 10;
}
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
void test2()
{auto sub1 = bind(Sub, _1, _2);cout << sub1(10, 5) << endl;// bind 本质返回的⼀个仿函数对象// 调整参数顺序(不常⽤)// _1代表第⼀个实参// _2代表第⼆个实参// ...auto sub2 = bind(Sub, _2, _1);cout << sub2(10, 5) << endl;// 调整参数个数 (常⽤)auto sub3 = bind(Sub, 100, _1);cout << sub3(5) << endl;auto sub4 = bind(Sub, _1, 100);cout << sub4(5) << endl;// 分别绑死第123个参数auto sub5 = bind(SubX, 100, _1, _2);cout << sub5(5, 1) << endl;auto sub6 = bind(SubX, _1, 100, _2);cout << sub6(5, 1) << endl;auto sub7 = bind(SubX, _1, _2, 100);cout << sub7(5, 1) << endl;// 成员函数对象进⾏绑死,就不需要每次都传递了function<double(Plus&&, double, double)> f6 = &Plus::plusd;Plus pd;cout << f6(move(pd), 1.1, 1.1) << endl;cout << f6(Plus(), 1.1, 1.1) << endl;// bind⼀般⽤于,绑死⼀些固定参数function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);cout << f7(1.1, 1.1) << endl;// 计算复利的lambdaauto func1 = [](double rate, double money, int year)->double {double ret = money;for (int i = 0; i < year; i++){ret += ret * rate;}return ret - money;};// 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);cout << func3_1_5(1000000) << endl;cout << func5_1_5(1000000) << endl;cout << func10_2_5(1000000) << endl;cout << func20_3_5(1000000) << endl;
}
本期关于C++11的特性介绍先到这里了。C++11是C++历史发展的转折点,期好用的特性和功能是非常多的,后续的内容我们会接着来了解
感谢看到这里,喜欢请点个赞谢谢
封面图自取: