从入门到了解C++系列-----C++11 新语法
本章将会讲解 c++11 引入的新的特性,列表初始化、左值右值、可变参数模板 ,包装器,lambda表达式六大部分。
目录
一、列表初始化
1.1 是什么
1.2 怎么用
1.3 std::initializer_list
二、左值,右值
2.1 是什么
2.2 左值引用与右值引用
2.3 生命周期问题
2.4 参数匹配问题
2.5 右值引⽤和移动语义的使⽤场景
2.5.1 左值引用的使用的使用场景
2.5.2 移动构造和移动赋值
2.5.3 编译器中关于移动构造与移动赋值
2.6 类型分类
2.7 引用折叠
2.7.1 是什么
2.7.2 为什么
2.8 完美转发
2.8.1 是什么
三、可变参数模板
3.1 是什么、如何理解
3.2 代码案例
3.3 empalce系列接⼝
四、新的类的功能
4.1 default 与 delete
五、lambda 表达式
5.1 语法
5.2 捕捉列表
5.3 应用以及原理
六、包装器
6.1 语法与实际应用
6.2 bind
七、总结
一、列表初始化
1.1 是什么
列表初始化就是使用 { } 的方式进行初始化。很重要的一个特点就是可以省略 = 。c++11 设立这个目的在于想要让初始化变得更加简单,看一看下面的例子就可以理解。
1.2 怎么用
其中 Point 与 Date 都是自定义类型,有了列表初始化就可以不使用 = ,也可实现初始化,并且使用 { } 还可以提高效率。
// 可以省略掉=Point p1 { 1, 2 };int x2 { 2 };Date d6 { 2024, 7, 25 };const Date& d7 { 2024, 7, 25 };// 不⽀持,只有{}初始化,才能省略=
// Date d8 2025;vector<Date> v;v.push_back(d1);v.push_back(Date(2025, 1, 1));// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐v.push_back({ 2025, 1, 1 });
1.3 std::initializer_list
针对于容器类型,想要多个值进行初始化时,需要写好多的构造函数,但是使用 initializer_list 类型就可以只写一个构造函数即可实现初始化工作。
这个就是使用 initializer_list 类型构建的初始化函数,然后 initializer_list 这个类型中,_start, _end 指向的这个类型的头部与尾部
template<class T>
class vector {
public:typedef T* iterator;vector(initializer_list<T> l){for (auto e : l)push_back(e)}
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _endofstorage = nullptr;
}
下面是它的一些具体使用。
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;
int main()
{std::initializer_list<int> list;list = { 1,2,3,4 };std::cout << *list.begin() << std::endl;std::cout << *list.end() << std::endl;vector<int> v1({ 1,2,3,4 });vector<int> v2 = { 1,2,3,4 };map<string, string> dict = { {"sort", "排序"} };// 使用initializer_listv1 = { 2,3,4,5 };return 0;
}
二、左值,右值
2.1 是什么
左值:我的理解为可以进行修改的变量名或者是解引用的指针,可以得到这个变量的地址。
右值 :常量、数据的表达式、临时创建的对象。不可以取地址
2.2 左值引用与右值引用
左值引用:之前学过很简单,就是一个变量的别名。在底层是使用指针的方式进行使用。
右值引用:就是对于右值起别名。
// 左值引用int x = 0;int& x1 = x;cout << x << endl;cout << x1 << endl;int y = 0;// 右值引用int&& y1 = 1;cout << y1 << endl;int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");
右值引用不可引用左值,但是 const 引用可以引用左值,当一个左值进行 move 之后可以被右值引用!(move 改变引用的实质就是进行类型的强转)
2.3 生命周期问题
我们的右值可以实现临时对象声明周期的延长(我的理解就是一个临时对象,马上就要销毁了,但是通过右值又将这个对象的资源进行了交换,将即将丢去的资源给保存下来),那么既然const int 也是可以保存右值,那就一定有一些区别:const 的引用的对象不可以进行修改,但是 右值引用的对象不可以进行修改。
string s1 = "1234";const string& s2 = s1 + s1;cout << "s1:" << s1 << endl;cout << "s2:" << s2 << endl;//s2 = "1234"; // 这里不可进行修改string&& s3 = s1 + s1;cout << "s3:" << s3 << endl;s3 = "1231";cout << "change s3:" << s3 << endl;
2.4 参数匹配问题
记住一句话:当我们重载函数需要类型匹配的时候,会优先选择最适合自己的,就比如说我的重载函数的类型具有右值引用的类型就会优先使用右值引用,然后如果是没有右值引用就是去找 const 类型的引用。
2.5 右值引⽤和移动语义的使⽤场景
2.5.1 左值引用的使用的使用场景
左值引用的出现通常是为了减少函数调用的拷贝以及引用传参,即我们对于一个变量虽然想要改变他,并且这个变量当我们出了作用域我不希望它被销毁,那么可以使用左值引用,但是并非这样的场景我们都是符合我们的预期的。同时还可以修改实参和修改返回对象的价值。C++ 98 的解决方法是通过输出型参数的方式进行参数的改变,从而提高效率。
我们刚刚学习了右值引用,那么我们可不可以使用右值,来解决这个问题?显然是不可以的,因为函数的调用出了栈帧就会销毁对象,考虑到这一点,右值引用是没有用的。
2.5.2 移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引用。(值得一提的是这样的方法,在对类似于vector 这样的深拷贝的类型才有意义)
如果是想要使用与编译器默认生成的移动构造函数,需要满足两个条件:1.没有⾃⼰实现移动构造函数,2.且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀种,只有这两种情况同时满足才会自动生成 默认的构造函数。对于默认生成的移动构造函数,需要注意:对于内置类型使用按照字节的方式进行拷贝,对于自定义类型则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
同样的默认的移动赋值也是同样的道理,与默认的移动拷贝函数完全相同。
2.5.3 编译器中关于移动构造与移动赋值
由于 C++ 11 迟迟不出来,当时的编译器的研究人员们,就对于拷贝构造的问题,进行了一定的优化,我们通过 vs 2019 的 release 版本可以看到具体的优化过程,就是说不在采用临时变量的方式通过拷贝,然后再次进行拷贝的方式进行使用一个构造方法,而是通过一次非常简单的拷贝构造,直接将函数当中的对象分配给我们定义的变量!下面的图片是相关的配列组合的方案。
在 vs2022的debug和release 下的优化更加的恐怖,直接全部都进行了优化,让函数里面的创建的对象直接就变成了我要进行拷贝的对象!后面拷贝赋值、移动构造、移动赋值 都是同样的道理,第一次优化是将:构造 + 拷贝\移动构造合二为一,后面的进一步优化就是一步到位。其他的方法就不再过的进行描述。
2.6 类型分类
这里的分类是我对于左值,右值的特点结合文档的一个归类的总结,可能不是很准确。在这里引入几个名词:右值包括纯右值和将亡值,左值包括:泛左值(将亡值,左值)。
按照类型我认为就是 2 类:纯右值、将亡值。根据名字进行分类,在将亡值中有名字是泛左值,有名字且没有move 的是左值,有名字并且有 move 的是 将亡值,无名字的是 纯右值。
2.7 引用折叠
2.7.1 是什么
顾名思义就是说 将两个引用进行折叠,规则是传递一个右值引用函数参数变成右值,然后其他的都是进行左值引用。
在我们的 c++ 当中,如果是进行 int && 的会报错,不允许这样定义一个类型,但是可以通过 typedef 的方式来进行定义这样是可以的。
2.7.2 为什么
因为通过这样的方式可以实现万能引用。所谓的万能引用就是无论你传递的是什么我都会自动的推断出来你的类型是右值还是左值。那么我们就可以不必使用两份代码,就是可以完成一份操作!这就是引用折叠的作用。举个例子:当我传递的值是 int 类型就是左值,然后就会调用 左值的引用。如此实现代码的复用!
2.8 完美转发
2.8.1 是什么
完美转发就是使用 forward<T>(t) 这样的语法是 t 的类型是 左值引用传递给下面的函数(如果还有函数调用的话)就是保持左值,如果是右值的话也会继续保持右值.
我们为什么需要使用完美转发呢?因为我们规定变量无论是左值还是右值都是具有左值的属性,也就是说但我们在第一层函数通过万能引用推导出他的类型的时候,又需要调用函数传递给下一层就会全部变成 左值的属性,就不符合我们的预期所以需要完美转发。
三、可变参数模板
3.1 是什么、如何理解
它是可是对于同一个函数,可以传递 n 多个参数,然后都是在编译的时候生成相应的类似于函数重载的方式,去实现同一个需求。比如:打印操作,我可以打印 int ,double ,flost,string 通过这一个可变参数模板就可以实现!下面就是常用的一个模板。
template <class ...Args> void Func(Args... args) {}
其实可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数,然后可使用 sizeof 去判断包中的参数的个数。
3.2 代码案例
#include <iostream>
#include <string>
using namespace std;
template <class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}
int main()
{double x = 2.2;Print();Print(1); // 一定要注意如果是使用没名字的右值一定要看是不是使用万能引用Print(1, x);Print(12.3, 32, string("12321"));// 其实这个语法的原理就是在底层生成了对应的函数Print();template <class T1>Print(T1&& a1);template <class T1, class T2>Print(T1&& a1, T2&& a2);template <class T1, class T2, class T3>Print(T1&& a1, T2 && a2, T3 && a3);return 0;
}
3.3 empalce系列接⼝
可以认为 emplace_back 和 push_back 是一样的都是进行插入的函数,唯一的不同就是emplace_back 更加高效,推荐使用 emplace 替代 insert 与 push 系列,函数代码如下。
list<int> lt;lt.emplace_back(1);lt.emplace_back(2);lt.emplace_back(3);lt.emplace_back(4);map<int, string> mp;mp.emplace(1, "apple");mp.emplace(2, "banan");for (auto x : lt) cout << x << endl;for (auto x : mp) {cout << x.first << "->" << x.second << endl;}
四、新的类的功能
4.1 default 与 delete
default 的作用在于如果我们想要使用类的默认函数,但是呢这个函数还没有生成,就比如说我们的默认移动构造函数的条件非常的苛刻,我们可以在后面加上 default 关键字,就会指定移动构造⽣成。
delete 为删除函数,表示我的类中我不让你使用这个函数。在函数后面加上 =delete 即可。
五、lambda 表达式
5.1 语法
lambda 表示式具有四部分构成:捕捉列表、函数参数、函数返回值、函数体。常常使用 auto 的类型进行推到函数,具体格式如下:
auto fun = [](int x, int y)->int {return x + y; };int c = fun(1, 2);cout << c << endl;
5.2 捕捉列表
捕捉列表在我看来就是:进行参数捕捉的,可以将一个表达式的前面的变量进行捕捉,那么在我的函数体中就可以使用。
具体有如下的三种捕捉方法:
- 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z] 表⽰x和y值捕捉,z引⽤捕捉。
- 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。
- 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
- 他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
- 一般来说捕捉到的东西需要使用使用 const 进行修饰,不可进行改变。
int a = 1;auto add = [a](int x, int y)->int {return a + y + x; };int z = add(1, 2);cout << z << endl;
5.3 应用以及原理
这是一个具体的使用案例。底层的原理就是通过生成⼀个对应的仿函数的类。
#include <algorithm>
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
int main()
{vector<Goods> totol{ { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3
}, { "菠萝", 1.5, 4 } };sort(totol.begin(), totol.end(), [](const Goods& a1, const Goods& a2) {return a1._price > a2._price;});for (auto x : totol){cout << x._name << " " << x._price << " " << x._evaluate << endl;}cout << endl << endl;sort(totol.begin(), totol.end(), [](const Goods& a1, const Goods& a2) {return a1._price < a2._price;});for (auto x : totol){cout << x._name << " " << x._price << " " << x._evaluate << endl;}return 0;
}
六、包装器
6.1 语法与实际应用
function:是一个类的模板,也就是包装器,跟 vector 一样的类。但是传递的其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等。
需要注意的是使用静态的类的成员函数必须使用指针的方式。普通方法必须传递this指针!
class function<Ret(Args...)>
#include <functional>
int f(int x, int y)
{return x + y;
}
struct Function
{int operator() (int a, int b){return a + b;}
};
class Plus
{public :Plus(int n = 10): _n(n){}static int plusi(int a, int b){return a + b;} double plusd(double a, double b){return (a + b) * _n;}
private:int _n;
};
int main()
{function<int(int, int)> f1 = f;function<int(int, int)> f2 = Function();function<int(int, int)> f3 = [](int x, int y) {return x + y; };cout << f(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;// 调用静态成员函数需要使用引用function<int(int, int)> f4 = &Plus::plusi;cout << f4(1, 1) << endl;// 包装普通成员函数// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以function<double(Plus*, double, double)> f5 = &Plus::plusd;Plus pd;cout << f5(&pd, 1.1, 1.1) << endl;return 0;
};
6.2 bind
bind:是一个函数模板,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。其中参数是必须要弄懂的 _1 , _2 分别代表第一个值与第二个值,也可以对于一个函数里面的特定值进行绑定。
还有标记位的概念,需要进行命名空间的展开,以及函数的定义方式。
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{// 使用 function 调用类的成员函数需要传递一个指针function<double(Plus*, double, double)> f1 = &Plus::plusd;Plus pl;cout << f1(&pl, 1.1, 2.22) << endl;// 如果使用 bind 进行绑定可以对于 pl 进行规定,就不需要重复书写// 需要注意的是,类的成员函数必须通过指针的方式获得。// 然后 function 的定义是通过实际传入的参数与实际返回的参数的关系规定的。function<double(double, double)> f2 = bind(&Plus::plusd, &pl, _1, _2);cout << f2(1.1, 2.22) << 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;return 0;
}
七、总结
以上是对于c++11部分常用语法的总结。这个文章用于我的学习记录,如果是有其他的错误还请批评指正。如果对你有帮助还请给我点个赞👍👍👍。