一,列表初始化
1.1,c++98传统的{}
c++98中一般数组和结构体可以用{}进行初始化。
cpp
#include<iostream>
using namespace std;
struct Point {
int _x;
int _y;
};
int main() {
char array1[] = { 'a','b','c','d','e','f','g' };
int array2[8] = { 0 };
Point p = { 3,4 };
return 0;
}
1.2,c++11中的{}
c++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
内置类 型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
{}初始化的过程中,可以省略掉= 。
c++11列表初始化的本意是想实现一个大统一的初始化方式,其次它在有些场景下带来不少便利,如容器push / insert多参数构造的对象时,{}初始化会很方便。
cpp
#include<iostream>
#include<vector>
using namespace std;
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 = 1, int month = 1, int day = 1)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
Date(Date&& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(Date&& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//c++98支持的
char array1[] = { 'a','b','c','d','e','f','g' };
int array2[8] = { 0 };
Point p = { 3,4 };
//c++11支持的
//内置类型支持
int x1 = { 2 };
//自定义类型支持
// 这里本质是用{2026,8,19}构造一个Date临时对象临时对象再去拷
//贝构造d1,编译器优化后合二为一变成{2026,8,19}直接构造初始化。
Date d1 = { 2026,8,19 };//调用构造
//这里d2引用的是{2026,4,13}构造临时对象。
const Date& d2 = { 2026,4,13 };
//c++98以后支持单参数时类型转换,也可以不用{}
Date d3 = { 2026 };
Date d4 = 2026;
//可以省略掉=
Point p1{ 4,9 };
int x2{ 13 };
Date d5{ 2026,1,5 };
const Date& d6{ 2026,6,6 };
//只有大括号初始化才能省略掉=
//Date d7 2026; X
vector<Date> v;
v.push_back(d1); //有名对象传参
v.push_back(Date(2025, 1, 1)); //匿名对象传参
//使用{}初始化更方便
v.push_back({ 2024,3,17 });
return 0;
}
1.3,c++11中的std::initializer_list
1,上面的初始化已经很方便,但是对象容器的初始化还是不太方便,比如一个vector对象,要用N个值去构造初始化,那么要实现多个构造函数才能支持。
2,c++11库中提出了一个std::initializer_list的类,auto i1 = { 10, 20 , 30 };// the type of i1 is an initializer_list ,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
3,有了std::initializer_list,就可以像下面这样初始化对象容器了。
cpp
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main() {
initializer_list<int> i1{ 1,2,3,4,5 };
initializer_list<int>::iterator it1 = i1.begin();
while (it1 != i1.end()) {
cout << *it1 << " ";
++it1;
}//1 2 3 4 5
//有了initializer_list,就可以这样初始化了。
vector<string> v{ {"insert"},{"sort"},{"erase"},{"empty"},{"hello" } };
for (auto& ev : v)//insert sort erase empty hello
cout << ev << " ";
return 0;
}
二,右值引用和移动语义
c++98的c++语法中就有引用的语法,而c++11中新增了右值引用语法特性,我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
2.1,左值和右值
1,左值是一个表示数据的表达式 (如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
2,右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
3,值得一提的是,左值的英文简写为 lvalue,右值的英文简写为 rvalue。传统认为它们分别是 left value、right value 的缩写。现代 C++ 中,lvalue 被解释为 loactor value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
cpp
#include <iostream>
using namespace std;
int main() {
//左值,可以取地址
int x = 4;
int* p1 = &x;
int** pp1 = &p1;
int& x1 = x;
const int a = 10;
const int* pa1 = &a;
pa1 = p1;//权限变小。
string s1 = ("float");
s1[0] = 'd';
//右值,不能取地址
int num1 = 3; int num2 = 4;
//常见的右值
4,
fmin(num1, num2);
string("double");//匿名对象
//右值取地址都会报错
//cout << &4 << endl;
//cout << &fmin(num1, num2) << endl;
//cout << &string("double") << endl;
//cout << &(num1 + num2) << endl;
return 0;
}
2.2,左值引用和右值引用
1,Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
2,左值引用不能直接引用右值,但是 const 左值引用可以引用右值。
10是一个右值,const int& x = 10;
3,右值引用不能直接引用左值,但是右值引用可以引用 move (左值)。 int x = 5;int&& a = move(x);
4,
5,move 是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识,这个我们后面会细讲。
6,需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
7,语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下⾯代码中 r1 和 rr1 汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反而是陷入迷途。
cpp
#include<iostream>
using namespace std;
template<class _Ty>
remove_reference_t<_Ty>&& Move(_Ty&& _Arg)
{
return static_cast<remove_reference_t<_Ty>>(_Arg);
}
int main() {
//左值,可以取地址
int x = 4;
int* p1 = &x;
int** pp1 = &p1;
int& x1 = x;
const int a = 10;
const int* pa1 = &a;
pa1 = p1;//权限变小。
string s1 = ("float");
s1[0] = 'd';
//左值引用给左值取别名
int& r1 = x;
int*& r2 = p1;
int& r3 = *p1;
string& r4 = s1;
char& r5 = s1[0];
//右值,不能取地址
int num1 = 3; int num2 = 4;
//常见的右值
4,
fmin(num1, num2);
string("double");//匿名对象
num1 + num2;
//右值引用给右值取别名
int&& rr1 = 4;
double&& rr2 = fmin(num1, num2);
string&& rr3 = string("double");
int&& rr4 = num1 + num2;
//左值引用不能直接引用右值,但是const左值引用可以引用右值
const int& rc1 = 10;
const double& rc2 = fmin(num1, num2);
const string& rc3 = string("double");
const double& rc4 = num1 + num2;
//右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int&& rm1 = move(x);
int*&& rm2 = move(p1);
int&& rm3 = move(*p1);
string&& rm4 = move(s1);
char&& rm5 = move(s1[0]);
//需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,
//右值引用变量变量表达式的属性是左值。
cout << &rm4 << endl;//00CFF77C
cout << &s1 << endl; //00CFF77C
cout << &rm1 << endl;//00B1FAEC
cout << &x << endl; //00B1FAEC
//注意,rm1变量的属性是左值,所以不能被右值引用绑定,在move一下。
int&& rmx1 = move(rm1);
cout << &rmx1 << endl;//00B1FAEC
return 0;
}
2.3,引用延长生命周期
右值引用可以为临时对象延长生命周期,const的左值引用也能延长临时对象声明周期,但这些对象无法被修改。
cpp
#include<iostream>
#include<string>
using namespace std;
int main() {
string s1 = "abcd";
string s2 = "1234";
//string& r1 = s1 + s2; //右值不能绑定到左值引用。
const string& r1 = s1 + s2; //const左值引用延长生命周期
//r1 += "xyz"; //const不能修改
string&& rr1 = s1 + s2; //右值延长生命周期
rr1 += "xyz"; //可以修改
cout << rr1 << endl;
return 0;
}
2.4,左值和右值的参数匹配
1,C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
2,C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f (左值引用),实参是 const 左值会匹配 f (const 左值引用),实参是右值会匹配 f (右值引用)。
3,右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,下一小节我们讲右值引用的使用场景时,就能体会这样设计的价值了。
cpp
#include<iostream>
using namespace std;
void f(int& a) {
cout << "void f(int& a)---->左值引用" << a << endl;
}
void f(const int& a) {
cout << "void f(const int& a)---->const左值引用" << a << endl;
}
void f(int&& a) {
cout << "void f(int&& a)---->右值引用" << a << endl;
}
int main() {
int x = 5;
const int y = 20;
f(x); //调用f(int& a)
f(11); //调用f(int&& a)
f(y); //调用f(const int& a)
f(move(x)); //调用f(int&& a)
return 0;
}
2.5,右值引用和移动语义的使用场景
**2.5.1,左值引用使用场景回顾**
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。
cpp
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
class Solution {
public:
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1; int end2 = num2.size() - 1;
str.reserve(max(end1 + 1, end2 + 1) + 1);
int carry = 0;
while (end1 >= 0 || end2 >= 0) {
int x = end1 >= 0 ? num1[end1--] - '0' : 0;
int y = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = x + y + carry;
carry = ret / 10;
ret %= 10;
str += (ret + '0');
}
if (carry == 1) {
str += '1';
}
reverse(str.begin(), str.end());
return str;
}
};
int main() {
cout << Solution().addStrings(string("1234"), string("9999")) << endl;
return 0;
}
**2.5.2,移动构造和移动赋值**
1,移动构造函数是一个构造函数,与其他构造函数构成函数重载,它的第一个默认参数是这个类类型的引用,不同的是要求它的参数是右值引用,如果有其他参数,必须要有缺省值。
2,移动赋值是一个赋值运算符的重载,与拷贝赋值构成函数重载,它的第一个默认参数是这个类类型的引用,不同的是要求它的参数是右值引用。
3,对于像 string/vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的参数都是右值引用的类型,他的本质是要 "窃取" 引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。下面的 zsw::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
**2.5.3,右值引用和移动语义在传参中的提效**
查看 STL 文档我们发现 C++11 以后容器的 push 和 insert 系列的接口都增加了右值引用版本。当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。当实参是一个右值,容器内部则调用移动构造,窃取右值对象的资源到容器空间的对象上。把我们之前模拟实现的 bit::list 拷贝过来,支持右值引用参数版本的 push_back 和 insert。其实这里还有一个 emplace 系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解 emplace 系列的接口。(gitee)
2.6,类型分类
1,c++11以后,对类型进行了进一步划分,右值被划分为纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
2,纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr (1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值 C++11 中提出的,C++11 中的纯右值概念划分等价于 C++98 中的右值。
3,将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如 move (x)、static_cast<X&&>(x)。对一个左值强转成右值。
4,泛左值 (generalized value,简称 glvalue),泛左值包含将亡值和左值。
2.7,引用折叠
1,C++ 中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引用的引用。
2,通过模板或 typedef 中的类型操作可以构成引用的引用时,这时 C++11 给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
cpp
#include<iostream>
using namespace std;
int main() {
typedef int& lref;
typedef int&& rref;
//int&&& x;//这样写会报错
int n = 10;
lref& r1 = n; //r1的类型是int& ( 左值引用碰到左值引用:左值引用 )
lref&& r2 = n; //r2的类型是int& ( 左值引用碰到右值引用:左值引用 )
rref& r3 = n; //r3的类型是int& ( 右值引用碰到左值引用:左值引用 )
rref&& r4 = 1; //r4的类型是int&&( 右值引用碰到右值引用:右值引用 )
rref&& r5 = move(n);//r5的类型是int&&( 右值引用碰到右值引用:右值引用)
return 0;
}
3,像 f2 这样的函数模板中,T&& x 参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
cpp
#include<iostream>
using namespace std;
template<class T>
void f1(T& x)
{ }
template<class T>
void f2(T&& x)
{ }
int main() {
int n = 0;
// void f1(T& x)
//没有折叠实例化为 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)
#pragma warning(push)
#pragma warning(disable:26800)
f1<const int>(n);
#pragma warning(pop)
f1<const 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(T&& x)
//没有折叠实例化为 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);
return 0;
}
4,Function (T&& t) 函数模板程序中,假设实参是 int 右值,模板参数 T 的推导 int,实参是 int 左值,模板参数 T 的推导 int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的 Function。
cpp
#include<iostream>
using namespace std;
template<class T>
void func(T&& y) {
int a = 0;
T x = a;
//x++;
cout << a << endl;
cout << x << endl;
cout << &a << endl;
cout << &x << endl;
}
int main() {
//10是右值,推导出T为int,模板实例化为void func(int&& y)
func(10); //右值
//a是左值,推导出t为int&,引⽤折叠,模板实例化为void func(int& y)
int a;
func(a); //左值
//std::move(b)是右值,推导出t为int,模板实例化为void func(int&& y)
int b;
func(std::move(b)); //右值
//c是左值,推导出T为const int&,引⽤折叠,模板实例化为void func(const int& y)
const int c = 5;
func(c);//会报错,func函数内部 x不能++ //const左值
//std::move(c)是右值,推导出T为const int,模板实例化为void func(const int&& y)
func(std::move(c));//会报错,func函数内部 x不能++ //const右值
return 0;
}
2.8,完美转发
1,func (T&& y) 函数模板程序中,传左值实例化以后是左值引用的 func 函数,传右值实例化以后是右值引用的 func 函数。
2,但是结合我们前面的讲解,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说 func 函数中 y 的属性是左值,那么我们把 y 传递给下一层函数 Fun,那么匹配的都是左值引用版本的 Fun 函数。这里我们想要保持 y 对象的属性,就需要使用完美转发实现。
3,template T&& forward (typename remove_reference::type& arg);
4,template T&& forward (typename remove_reference::type&& arg);
5,完美转发 forward 本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给 func 的实参是右值,T 被推导为 int,没有折叠,forward 内部 y 被强转为右值引用返回;传递给 func 的实参是左值,T 被推导为 int&,引用折叠为左值引用,forward 内部 y 被强转为左值引用返回。(是左值引用,传过去还是左值属性;是右值引用,传过去还是右值属性)。
cpp
#include<iostream>
using namespace std;
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 func(T&& t)
{
Fun(t);
cout << "~完美转发~" << endl;
Fun(forward<T>(t));
cout << "**************************" << endl;
}
int main()
{
//10是右值,推导出T为int,模板实例化为void func(int&& y)
func(10); //右值
//a是左值,推导出t为int&,引⽤折叠,模板实例化为void func(int& y)
int a;
func(a); //左值
//std::move(b)是右值,推导出t为int,模板实例化为void func(int&& y)
int b;
func(std::move(b)); //右值
//c是左值,推导出T为const int&,引⽤折叠,模板实例化为void func(const int& y)
const int c = 5;
func(c);// //const左值
//std::move(c)是右值,推导出T为const int,模板实例化为void func(const int&& y)
func(std::move(c));// //const右值
return 0;
}
三,可变参数模版
3.1,基本语法及原理
c++11支持可变参数模版,也就是支持可变参数的数量的函数模版和类模版,可变数目的参数被称为参数包,存在两种参数包:模版参数包,表示零或多个模版参数;函数参数包,表示零或多个函数参数。
template<class ...Args> void func(Args... args);
template<class ...Args> void func(Args&... args);
template<class ...Args> void func(Args&&... args); 万能引用
我们用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟... 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
可以使用sizeof...运算符去计算参数包中参数的个数。
cpp
#include"list.h"
#include"string.h"
template<class... Args>
void Print(Args&&... ag)
{
cout << sizeof...(ag) << endl;
}
int main()
{
double pai = 3.14;
Print(); //包里有0个参数
Print(1.3); //包里有1个参数
Print(pai, 5, zsw::string("abcd")); //包里有3个参数
//包里有6个参数
Print(1.9, 'A', pai, zsw::string("xyz"), zsw::list<int>{5, 4, 3, 2, 1}, "www");
return 0;
}
//原理1:编译本质这里会结合引用折叠规则实例化出以下四个函数。
void Print();
void Print(double&& ag1);
void Print(double& ag1, int&& ag2, zsw::string&& ag3);
void Print(double&& ag1, char&& ag2, double& ag3, zsw::string&& ag4, zsw::list<int>&& ag5, const char& ag6);
//原理2:可变参数模版,他是类型泛化基础上叠加数量变化,让泛型编程更灵活。
3.2,包扩展实现细节
1,可以直接将参数包依次展开依次作为实参给一个函数去处理。
2,编译时递归推导,类似以下过程。
cpp
void Show()
{
cout << endl;
}
template<class T, class... Arg>
void Show(T t, Arg... ags)
{
cout << t << " ";
Show(forward<Arg>(ags)...);
}
template<class... Args>
void Print(Args... ag)
{
Show(forward<Args>(ag)...);
}
int main()
{
double pai = 3.14;
Print(); //包里有0个参数
Print(1.3); //包里有1个参数
Print(pai, 5, zsw::string("abcd")); //包里有3个参数
//包里有6个参数
Print(1.9, 'A', pai, zsw::string("xyz"), "www");
return 0;
}
具体是如何传参的:Print(1.9, 'A', pai, zsw::string("xyz"), "www");
首先将参数包 1.9, 'A', pai, zsw::string("xyz"), "www"这一坨传给Print函数,
再将这个包传给Show,Show函数第一次调用时,会将包里第一个参数传给 t ,剩下的参数 'A', pai, zsw::string("xyz"), "www"作为一个包再传给ags,打印 t 的值也就是1.9,
第二次调用Show函数将参数包 'A', pai, zsw::string("xyz"), "www" 递归再次调用,会将 'A'传给 t ,剩下的 pai, zsw::string("xyz"), "www"作为一个包再传给ags,打印 t 的值也就是A,
第三次调用Show函数将参数包 pai, zsw::string("xyz"), "www" 递归再次调用,会将 pai 传给 t ,剩下的 zsw::string("xyz"), "www"作为一个包再传给ags,打印 t 的值也就是3.14,
第四次调用Show函数将参数包 zsw::string("xyz"), "www" 递归再次调用,会将 zsw::string("xyz")传给 t ,剩下的 "www"作为一个包再传给ags,打印 t 的值也就是xyz,
第五次调用Show函数将参数包 "www" 递归再次调用,会将 "www"传给 t ,打印 t 的值也就是 www,参数包里没有参数了,就调用void Show()函数,如果没有这个函数就会报错,没有匹配的重载函数。
这些都是编译时递归推导的。
3,
cpp
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必须返回或者得到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
本质可以理解为编译器编译时,包的扩展模式,将上面的函数模板扩展实例化为下面的函数。
cpp
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}
3.3,emplace系列接口

C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为模板可变参数,功能上兼容 push 和 insert 系列,但是 emplace 还支持新玩法,假设容器为 container,emplace 还支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
emplace_back 总体而言是更高效,推荐以后使用 emplace 系列替代 insert 和 push 系列。
第二个程序中我们模拟实现了 list 的 emplace 和 emplace_back 接口,这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型 T 的构造,所以达到了前面说的 emplace 支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下 std::forward(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。

std::forward(val)... 是为了让包里每个参数都保持属性,对每个参数完美转发。
结论:总体而言,综合各个场景,推荐使用emplace系列接口,综合效率更高,更推荐使用构造容器存储的参数包,去插入数据,这样可以实现直接构造(函数模版:万能引用+完美转发+可变参数模版),效率更高。
四,新的类功能
4.1,默认的移动构造和移动赋值
(1),原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 /const 取地址重载,最后重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
(2),如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。深拷贝类型,自己实现移动构造。
(3),如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
(4),如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
cpp
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 = p._name;
// _age = p._age;
// }
// return *this;
//}
//~Person()
//{ }
private:
zsw::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);//编译器为Person默认生成移动构造
Person s4;
s4 = std::move(s2); //编译器为Person默认生成移动赋值
Person s5;
s5 = s4;
return 0;
}
4.2,成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显式在初始化列表初始化,就会在初始化列表用这个缺省值初始化。
4.3,default和delete
C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显式指定移动构造生成。
如果想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private,并且只声明不定义,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数为删除函数。
4.4,委托构造函数
c++中的委托构造函数是c++11引入的特性,允许一个构造函数调用同类中的其他构造函数,从而减少代码重复提高可维护性。
被委托的构造函数必须初始化所有成员变量,因为委托构造函数后不能再重复初始化。
cpp
#include<iostream>
using namespace std;
class Emx
{
public:
Emx(int x, int y)
:_x(x)
,_y(y)
{
cout << "构造函数" << endl;
}
Emx(int x)
:Emx(x,15)
{
cout << "委托构造函数" << endl;
}
private:
int _x;
int _y;
};
int main()
{
Emx e1(3, 4);
Emx e2(1);
return 0;
}
4.5,继承构造函数
继承构造函数是c++11引入的一项特性,它允许派生类直接继承基类的构造函数,而不需要手动重新定义它们,这一特性显著简化了派生类的编写,特别是在基类有多个构造函数的情况下。
派生类继承基类的普通构造函数,特殊的拷贝构造函数,移动构造函数不继承。
继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,内置类型成员不确定,自定义类型使用默认构造初始化。
cpp
#include<iostream>
using namespace std;
class Base
{
public:
Base(int x) :_x(x) {}
Base(double y) :_y(y) {}
Base(int x, double y) :_x(x), _y(y) {}
private:
int _x;
double _y;
};
//class Device :public Base
//{
//public:
// Device(int x) :Base(x) {}
// Device(double y) :Base(y){}
// Device(int x, double y) :Base(x, y){}
//};
class Device :public Base
{
public:
using Base::Base;
};
int main()
{
Device d1(1);
Device d2(1.2);
Device d3(8, 3.14);
return 0;
}
五,lambda
5.1,lambda表达式语法
1,lambda表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
2,lambda 表达式从语法使用层面而言没有类型,所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象。
3,lambda 表达式的格式: capture-list (parameters)-> return type { function boby }
4,capture‑list:捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,具体细节 7.2 中我们再细讲。捕捉列表为空也不能省略。
5,(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 () 一起省略。
6,->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
7,{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
cpp
#include<iostream>
using namespace std;
int add1(int x, int y) {
return x + y;
}
int main()
{
//计算两个数字相加,普通函数写法
cout << add1(3, 5) << endl;// 8
//lambda表达式
auto add2 = [](int x, int y)->int {return x + y; };
cout << add2(2, 5) << endl;// 7
//cout << typeid(add2).name() << endl;//class `int __cdecl main(void)'::`2'::<lambda_1>
//捕捉列表为空也不可以省略
//参数列表可以省略
//返回类型可以省略
//函数体为空也不可以省略
auto Print = [] {cout << "lambda表达式" << endl; };
Print();//打印 lambda表达式
//用法示例
int a = 5; int b = 12;
auto Swap = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
cout << "a=" << a << " " << "b=" << b << endl;// a = 5 b = 12
Swap(a, b);
cout << "a=" << a << " " << "b=" << b << endl;// a = 12 b = 5
return 0;
}
(int& x, int& y) { int tmp = x; x = y; y = tmp; };代表一个对象,把这个对象赋值给Swap,推出Swap的类型。
5.2,lambda的原理(本质是仿函数)
lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数 / 返回类型 / 函数体就是仿函数 operator () 的参数 / 返回类型 / 函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
cpp
class Rate
{
public:
Rate(double y)
:_y(y) { }
double operator()(int _x, double _z)
{
return _x + _y + _z;
}
private:
double _y;
};
int main()
{
double y = 3.14;
auto r2 = [y](int x, double z) {return x + y + z; };
Rate r1(y);
r1(2, 1.4);
r2(2, 1.4);
return 0;
}
通过汇编可以看到,lambda对象本质还是调用operator(),类型是lambda_1,这个类型名的规则是编译器自己定制的,保证不同的lambda类型名不冲突。

5.3,捕捉列表
1,lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用其外层作用域中的变量需要进行捕捉。
2,第一种捕捉方式是在捕捉列表中显式的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。x,y, \&z 表示 x 和 y 值捕捉,z 引用捕捉。
3,第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个 = 表示隐式值捕捉,在捕捉列表写一个 & 表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
4,第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显式捕捉。=, \&x 表示其他变量隐式值捕捉,x 引用捕捉;\&, x, y 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 & 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
5,lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
6,默认情况下, lambda 捕捉列表是被 const 修饰的,也就是说传值捕捉的过来的对象不能修改,mutable 加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略 (即使参数为空)
cpp
#include<iostream>
using namespace std;
int g = 10;
int main()
{
int a = 0, b = 0, c = 0, d = 0;
//第一种
auto fun1 = [a, &b]
{
//a++;//值捕捉的变量不能修改
b++; //引用捕捉的变量可以修改
return a + b;
};
cout << fun1() << endl;
//隐式值捕捉
auto fun2 = [=]
{
//a++; b++; c++; d++;值捕捉的变量不能修改
return a + b + c + d;
};
cout << fun2() << endl;
//隐式引用捕捉
auto fun3 = [&]
{
a++; b++; c++; d = 4;//引用捕捉的变量可以修改
return a + b + c + d;
};
cout << fun3() << endl;
//混合捕捉
auto fun4 = [&, d, c]
{
//a,b是引用捕捉;c,d是值捕捉
a = 3, b = 2; //引用捕捉的变量可以修改
//c = 5; d = 7; //值捕捉的变量不可以修改
return a + b + c + d;
};
cout << fun4() << endl;
auto fun5 = [=, &d, &c]
{
//a,b是值捕捉;c,d是引用捕捉
//a = 2; b = 4; //值捕捉的变量不可以修改
c = 3; d = 6; //引用捕捉的变量可以修改
return a + b + c + d;
};
cout << fun5() << endl;
//静态局部变量和全局变量不能捕捉
static int st = 5;
//auto fun6 = [g, st] {};会报错
auto fun6 = []
{
//可以直接使用,因为静态局部变量和全局变量具有全局属性
return g + st;
};
cout << fun6() << endl;
//传值捕捉本质是一种拷贝,并且被const修饰了
//mutable相当于去掉了const属性,可以修改了
//但是修改不会影响外面被捕捉的值,因为是一种拷贝。
auto fun7 = [=]()mutable
{
a = 1; b = 2; c = 3; d = 4;
return a * b * c * d;
};
cout << fun7() << endl;
return 0;
}
5.4,lambda的应用
以下代码中Student类是描述了一个学生的属性,按照其属性对其进行排序。
cpp
#include<iostream>
#include<string>
#include<algorithm>
#include<vector>
using namespace std;
struct Student
{
Student(string name, int ch, int ma)
:_name(name)
, _ch(ch)
, _ma(ma)
{ }
//bool operator<(const Student& s)const
//{
// return this->_ch < s._ch;
//}
string _name; //姓名
int _ch; //语文
int _ma; //数学
};
struct CompareChLess
{
bool operator()(const Student& s1, const Student& s2)
{
return s1._ch < s2._ch;
}
};
struct CompareChGreater
{
bool operator()(const Student& s1, const Student& s2)
{
return s1._ch > s2._ch;
}
};
int main()
{
vector<Student> v{ {"张三",81,74},{"李四",95,70},{"王五",84,99},{"赵六",73,65},{"杨七",92,87} };
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
//(1),仿函数写法
//按照语文成绩升序排列
sort(v.begin(), v.end(), CompareChLess());
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
//按照语文成绩降序排列
sort(v.begin(), v.end(), CompareChGreater());
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
//(2),lambda
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
//按照数学成绩升序排列
auto func = [](const Student& s1, const Student& s2)->bool {return s1._ma < s2._ma; };
sort(v.begin(), v.end(), func);
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
//按照数学成绩降序排列
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2) {return s1._ma > s2._ma; });
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
//总分降序排列
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2) {return (s1._ch + s1._ma) > (s2._ch + s2._ma); });
for (auto& e : v)
cout << e._name << " " << e._ch << " " << e._ma << " " << endl;
cout << endl;
return 0;
}
六,包装器
6.1,function

类模板 std::function 是一种通用且多态的函数包装器。 std::function 的实例可以存储、复制调用任何可复制构造的 callable 函数------这些函数可以通过指向相应的指针来实现,比如 lambda 表达式、绑定表达式,以及其他函数对象,以及指向成员函数和成员数据的指针。
被存储的可调用对象被称为 std::function 的目标。如果 std::function 中没有目标,那么它就被视为空目标。当尝试调用空目标 std::function 时,将会引发 std::bad_function_call 异常。
cpp
#include<iostream>
#include<functional>
using namespace std;
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
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 = Functor();
function<int(int, int)> f3 = [](int x, int y)->int {return x + y; };
cout << f1(2, 4) << endl;
cout << f2(2, 4) << endl;
cout << f3(2, 4) << endl;
//包含静态成员函数,建议还是加上&
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(4, 5) << endl;
//成员函数要指定类域并且前面加上&才能获取地址
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus s1;
cout << f5(&s1, 3.15, 4.4) << endl;
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(Plus(), 2.86, 3.11) << endl;
cout << f6(s1, 1.43, 4.65) << endl;
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
Plus s2;
cout << f7(move(s2), 6.23, 3.51) << endl;
cout << f7(Plus(), 3.14, 4.13) << endl;
return 0;
}
包装普通成员函数,普通成员函数还有一个隐含的this指针,所以绑定时传对象或者对象的指针过去都可以。
Leetcode题目:150,逆波兰表达式求值
原本写法:
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(auto& vt : tokens)
{
if(vt != "+" && vt != "-" && vt != "*" && vt != "/")
{
st.push(stoi(vt));
}
else
{
int right=st.top();
st.pop();
int left=st.top();
st.pop();
switch(vt[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;
}
}
}
return st.top();
}
};
新写法:
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>> m={
{"+",[](int left,int right){return left+right;}},
{"-",[](int left,int right){return left-right;}},
{"*",[](int left,int right){return left*right;}},
{"/",[](int left,int right){return left/right;}}
};
for(auto& vt : tokens)
{
if(m.count(vt))
{
int right=st.top();
st.pop();
int left=st.top();
st.pop();
int ret=m[vt](left,right);
st.push(ret);
}
else
{
st.push(stoi(vt));
}
}
return st.top();
}
};
七,bind
1,bind 是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收的 fn 可调用对象进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。bind 也在这个头文件中。
2, 调用 bind 的一般形式: auto newCallable = bind (callable,arg_list); 其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。
3,arg_list 中的参数可能包含形如_n 的名字,其中 n 是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1/_2/_3... 这些占位符放到 placeholders 的一个命名空间中。
cpp
#include<iostream>
#include<functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub1(int a, int b)
{
return (a - b) * 10;
}
int Sub2(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;
}
};
int main()
{
auto sub1 = bind(Sub1, _1, _2);
cout << sub1(4, 5) << endl; //-10
//bind本质是返回一个仿函数对象
// 调整参数顺序不常用
//_1代表第一个实参
//_2代表第二个实参。
auto sub2 = bind(Sub1, _2, _1);
cout << sub2(4, 5) << endl; //10
//调整参数个数
auto sub3 = bind(Sub1, 20, _1);//第一个参数绑定20
cout << sub3(10) << endl;
auto sub4 = bind(Sub1, _1, 20);//第二个参数绑定20
cout << sub4(10) << endl;
auto sub5 = bind(Sub1, 0, 7);
cout << sub5() << endl;
//绑定Sub2,三个参数
auto sub2_1 = bind(Sub2, 30, _1, _2);
cout << sub2_1(10, 5) << endl;
auto sub2_2 = bind(Sub2, _1, 30, _2);
cout << sub2_2(10, 5) << endl;
auto sub2_3 = bind(Sub2, _1, _2, 30);
cout << sub2_3(10, 5) << endl;
auto sub2_4 = bind(Sub2, 20, 5, _1);
cout << sub2_4(3) << endl;
//可以把成员函数对象进行绑死,就不需要每次都传递了。
function<int(int, int)> f1 = &Plus::plusi;
cout << f1(6, 9) << endl;
function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus p1;
cout << f2(&p1, 2.4, 3.14) << endl;
function<double(Plus&&, double, double)> f3 = &Plus::plusd;
cout << f3(Plus(), 1.25, 3.1) << endl;
//bind一般用于绑定一些固定参数
function<double(double, double)> f4 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f4(2.31, 7.48) << endl;
return 0;
}
用于计算复利的lambda:
cpp
#include<iostream>
#include<functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{
auto func = [](double rate, double money, int year)->double{
double ret = money;
for (int i = 1; i <= year; ++i)
{
ret += (ret * rate);
}
return ret - money;
};
cout << func(0.03, 1000, 5) << endl;
//可以把利率和年份固定,输入money就可以看利息了。
function<double(double)> fun_3_1_5 = bind(func, 0.015, _1, 3);
function<double(double)> fun_5_2_0 = bind(func, 0.02, _1, 5);
function<double(double)> fun_10_2_6 = bind(func, 0.026, _1, 10);
function<double(double)> fun_30_3_6 = bind(func, 0.035, _1, 30);
cout << fun_3_1_5(1000000) << endl;
cout << fun_5_2_0(1000000) << endl;
cout << fun_10_2_6(1000000) << endl;
cout << fun_30_3_6(1000000) << endl;
return 0;
}
以上就是c++11新特性的常用语法,后面还会更新c++11及后续版本的语法。
换一个编辑器再发一遍,原来的文章,Makedown下载不了!!!
版权声明:本文为CSDN博主「芝麻开门292」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_71009995/article/details/164184346