万字详解C++11列表初始化与移动语义
目录
列表初始化
C++11中的{}
C++11中的std::initializer_list
右值引用和移动语义
引用延长生命周期
左值和右值的参数匹配
左值引用的不足:
移动构造和移动赋值
移动赋值
类型分类
引用折叠
完美转发
可变参数模板
包扩展
emplace系列接口
lambda
捕捉列表
新的类功能
defult和delete
包装器
bind
列表初始化
C++98传统的{}
// C++98中⼀般数组和结构体可以⽤{}进⾏初始化。struct Point{int _x;int _y;};int main(){int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0;}
C++11中的{}
-
C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化
-
c++11开始,自定义类型支持用初始化列表,c++98只有内置类型支持初始化列表
#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, 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 a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };// C++11⽀持的// 内置类型⽀持int x1 = { 2 };// ⾃定义类型⽀持// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化Date d1 = { 2025, 1, 1};//C++98⽀持单参数时类型转换,也可以不⽤{}Date d3 = { 2025};//c++11Date d4 = 2025;//c++98//可以省略掉= Point p1 { 1, 2 };int x2 { 2 };Date d6 { 2024, 7, 25 };const Date& d7 { 2024, 7, 25 };//只有初始化列表才支持省略=Date d8 2025//会报错}
C++11中的std::initializer_list
-
上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如⼀个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持:vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};
-
C++11库中提出了⼀个std::initializerlist的类, auto il = { 10, 20, 30 }; // the type of il is an initializerlist ,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
vector<int> v1={1,2,3,4};
initializer_list<int>l1={10,20,30};//本质是底层在栈上开一个数组,
//这里在语义上表示构造+拷贝构造+优化,,,但编译器会优化成直接构造
//本质也可以理解为隐式类型转换
vector<int> v1={1,2,3,4};
vector<int> v2{1,2,3,4};
//这里在语义上表示直接进行构造
vector<int> v3({1,2,3,4});//调用initializer_list进行构造
//上述两种方式在语义上表示的意思不同,但最后的结果是相同的
右值引用和移动语义
-
Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
-
左值引用不能直接引用右值,但是const左值引用可以引用右值
-
右值引用不能直接引用左值,但是右值引用可以引用move(左值)
-
move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及⼀些引用折叠的知识
-
是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值
double x = 1.1, y = 2.2;
const int& rx1 = 10;
const double& rx2 = x + y;int* p = new int(0);
int b = 1;
string s("111111");
int&& rr1 = move(b);
int*&& rr2 = move(p);
string&& rr3 = move(s);
string&& rr4 = (string&&)s;//move本质是进行强转int& tt1 = rr1;//用左值引用来引用右值引用表达式
左值引用与右值引用在底层其实就是指针
引用延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
如果想用引用来延长被调用的函数内部局部变量的生命周期,这是不被允许的。第一点:引用不会改变变量的存储位置。第二点:局部变量是创建在函数栈帧中的,当函数调用结束栈帧销毁,局部变量也会随之销毁。
string s1 = "test";
//string&& r1 = s1;//右值引用无法引用左值const string& r2 = s1 + s1;
//r2 += s1;//const左值可以引用右值,但无法进行修改string&& r3 = s1 + s1;
r3 += s1;
cout << r3 << endl;
左值和右值的参数匹配
-
C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
-
C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会 匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。
void func(int& x)
{cout << "左值引用" << x <<endl;
}void func(const int& x)
{cout << "const左值引用" << x << endl;
}void func(int&& x)
{cout << "右值引用" << x<<endl;
}int main()
{int i = 1;const int ci = 2;func(i);func(ci);func(3);func(move(i));int&& x = 1;func(x);func(move(x));return 0;
}
左值引用与右值引用最终目的是减少拷贝、提高效率。
左值引用还可以修改参数或者返回值,方便使用
左值引用的不足:
在部分函数场景,只能传值返回,不能传引用返回。比如:当前函数的局部对象,出了当前函数的作用域就销毁
移动构造和移动赋值
- 移动构造函数是一种构造函数,类似拷贝构造函数,要求第一个参数是该类类型的引用,不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的bit::string样例实现了移动构造和移动赋值,我们需要结合场景理解。
移动构造是进行指针交换,其本质是“掠夺资源”。被掠夺的右值的指针则指向”空“
所以一个左值不能轻易的去move,因为这会导致左值的资源被掠夺
右值对象构造,只有拷贝构造,没有移动构造的场景
vs2019debug环境下编译器对拷贝进行了优化。左边为优化前的场景,右边为优化后的场景。看到编译器直接将两次拷贝构造合二为一了。
- 图1展示了vs2019debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
- linux下可以将下面代码拷贝到test.cpp⽂件,编译时用g++ test.cpp -fno-elide-
constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。
右值对象构造,有拷贝构造,也有移动构造的场景
vs2019debug环境下编译器对拷贝进行了优化。当移动构造与拷贝构造同时存在时,编译器会选择代价小的移动构造。优化前,需要进行两次移动构造,优化后只需进行一次移动构造
- 图2展示了vs2019debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
- linux下可以将下面代码拷贝到test.cpp⽂件,编译时用g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次移动
如果是在2019的release或者2022的环境下,则只进行一次构造。在2019的release或者2022的环境下str直接变成了ret的引用。
如果想看未优化的场景,在Linux下通过:g++ test.cpp -fno-elide-constructors关闭构造优化来观察。
移动赋值
-
移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
-
移动赋值也是对资源进行掠夺
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
- 图4左边展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
str本质是对临时对象的引用,修改str,临时对象也会被修改。
- 图5左边展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
左值调用拷贝构造,因为左值数据不能轻易改动,可能会影响到后面的程序。
右值调用移动构造,因为右值生命周期极短,比起拷贝构造,用移动构造付出的代价更小,并且效率更高
右值引用和移动语义在传参中的提效
- 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本
- 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
- 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
- 把我们之前模拟实现的bit::list拷贝过来,支持右值引用参数版本的push_back和insert
- 其实这里还有一个emplace系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接口。
类型分类
右值:
-
纯右值
-
将亡值
泛左值:
-
左值
-
将亡值
- C++11以后,进⼀步对类型进行了划分,右值被划分纯右值(purevalue,简称prvalue)和将亡值(expiringvalue,简称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和Valuecategories这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节
引用折叠
-
C++中不能直接定义引用的引用如 int& && r = i,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引用的引用。
-
通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
-
下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家需要一个一个仔细理解一下。
-
像f2这样的函数模板中,T&&?x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
-
Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
typedef int& lref;typedef int&& rref;int n = 0;//只有当是右值右值时,才是右值引用。有左则是左lref& r1 = n;lref&& r2 = n;rref& r3 = n;rref&& r4 = 1;
------------------------------------------------------------template<class T>void f1(T& x){}template<class T>void f2(T&& x){ }int main(){typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n;lref&& r2 = n;rref& r3 = n;rref&& r4 = 1;f1<int>(n);//这里没有引用折叠//f1<int>(0);//是左值类型,所以不能引用右值f1<int&>(n);//f1<int&>(0);//调用的是f1,因为引用折叠,所以f1只可能是左值f1<int&&>(n);//f1<int&&>(0);////调用的是f1,因为引用折叠,所以f1只可能是左值f1<const int&>(n);f1<const int&>(0);//const 左值可以引用右值f1<const int&&>(n);f1<const int&&>(0);//因为引用折叠,所以推出是左值类型,const左值可以引用右值//f2<int>(n);//这里没有引用折叠,所以推出类型是右值,右值不能引用左值f2<int>(0);f2<int&>(n);//f2<int&>(0);存在引用折叠,所以有左则是左,所以推出是左值类型,不能引用右值//f2<int&&>(n);//存在引用折叠,全右则右,所以推出是右值类型,右值引用不能引用左值f2<int&&>(0);return 0;}
完美转发
-
Function(T&&t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化 以后是右值引用的Function函数。
-
变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传 递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发实现
-
template <class T> T&& forward (typename remove_reference<T>::type&
arg); -
template <class T> T&& forward (typename
remove_reference<T>::type&& arg); -
完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
template<class T>
void func2(T&& z)
{cout << z << endl;
}template<class T>
void func1(T&& t)
{//func2(t);因为右值引用表达式属性是左值,如果没调用完美转发,则传给func2的参数的属性是左值func2(forward<T>(t));//调用完美转发,则会维护参数的属性,传递给func2的参数的属性则保持为右值cout << t << endl;
}int main()
{int x = 1;func1(1);return 0;
}
- 在没有调用完美转发时,函数调用的结果:
在Function函数内部推出了参数的类型,但是由于右值引用表达式是左值属性,所以在调用Func函数时,调用的是左值引用的函数。
- 在调用完美转发后,函数调用结果:
完美转发维护了Function函数内部传给Func函数参数的属性,所以右值能调用到右值引用的函数
可变参数模板
-
C++11⽀持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包。我们用省略号来指出模板参数或函数参数包。
-
在模板参数列表中,class…或 typename…指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟…指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则
-
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
-
这里我们可以使用sizeof…运算符去计算参数包中参数的个数
-
template <class ...Args> void Func(Args... args) {}
-
template <class ...Args> void Func(Args&... args) {}
-
template <class ...Args> void Func(Args&&... args) {}
-
模板参数包:表示零或多个模板参数
-
函数参数包:表示零或多个函数参数
//前面是模板参数包,后面是函数参数包
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
计算参数包中参数的个数:
template<class ...Args>
//前提:有模板语法支持
//这里是一个万能引用+可变参数模板+引用折叠。编译器会根据传入的参数实例化出对应的函数
void Print(Args&&...args)
{cout << sizeof...(args) << endl;
}
int main()
{int x = 1;string s1("1");double y = 1.1;Print();Print(1);Print(1,s1,y);return 0;
}
------------------------------
在使用可变参数模板时,从语法层面我们可以理解为:
函数模板:一个函数模板,可以实例化出多个不同类型的函数
可变参数模板:一个可变参数模板函数,可以实例化出多个参数个数不同的函数模板
包扩展
包扩展:解析一个包就叫做包扩展
void ShowList()//当包中的参数个数为0时,调用这个函数
{cout << endl;
}template<class T,class ...Args>
void ShowList(T x,Args...args)//将参数包的第一个参数匹配给x,再将参数个数为n-1的参数包匹配给args
{/*这种写法是错误的,因为包展开匹配解析是在编译时进行的,而if判断是在程序运行时进行的if(sizeof...(args)==0)return;*/cout << x << " ";ShowList(args...);
}template<class ...Args>
void Print(Args&&...args)
{//cout << sizeof...(args) << endl;ShowList(args...);}int main()
{Print(1,string("22222"),2.2);return 0;
}
包扩展详细过程第一种方法:
编译时递归
将包扩展的过程展开写就是:
void ShowList()
{cout << endl;
}void ShowList( double z)
{cout << z << " ";ShowList();
}void ShowList(string y, double z)
{cout << y<< " ";ShowList( z);
}void ShowList(int x,string y,double z)
{cout << x << " ";ShowList(y,z);
}template<class ...Args>
void Print(Args&&...args)
{//cout << sizeof...(args) << endl;ShowList(args...);
}int main()
{Print(1,string("22222"),2.2);return 0;
}
模板是写给编译器的,模板实例化的过程交给编译器来完成,方便程序员
包扩展详细过程第二种方法:
template<class T>
const T& GetArs(const T& x)
{cout << x << " ";return x;
}template<class ...Args>
void Arguments(Args...args)
{ }template<class ...Args>
void Print(Args&&...args)
{Arguments(GetArs(args)...);、/*void Print(int x,string y,double z){Arguments(GetArs(x),GetArs(y),GetArs(z));}*/
}int main()
{Print(1,string("22222"),2.2);return 0;
}-----------------------------------/*不能写成如下这样,因为这不符合c++语法规则。参数包展开(args...)必须发生在允许的上下文环境中如:函数调用参数、初始化列表或模板参数列表等地方。单独的GetArs(args)...;会被解析为试图展开多个表达式语句,这在语法上是不合法的。*/
template<class ...Args>
void Print(Args&&...args)
{GetArs(args)...;
}
emplace系列接口
- template <class... Args> void emplace_back (Args&&... args);
- template <class... Args> iterator emplace (const_iterator position,
Args&&... args); - C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
- emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
- 第二个程序中我们模拟实现了list的emplace和emplace_back接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
- 传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下
std::forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左
值。
emplace_back:
-
传入左值,调用构造和拷贝构造
-
传入右值,调用构造和移动构造
push_back:
-
传入左值,调用构造和拷贝构造
-
传入右值,调用构造和移动构造
两者区别:
当是以隐式类型转换的方式传入值时:
emplace_back是直接构造
push_back是调用构造在调用移动构造。
当插入的参数是多参数时,push_back()须使用make_pair,而emplace_back则不需要使用make_pair
emplace系列兼容push系列和insert的功能,部分场景下emplace可以直接构造,push和insert则是调用构造+移动构造/拷贝构造。
所以emplace系列接口综合而言更强大
lambda
-
lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
-
lambda 表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收lambda对象
lambda表达式的格式:
[capture-list] (parameters)-> return type {function boby }
-
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置, 编译器根据[]来判断接下来的代码是否为lambda 函数。
-
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
-
->return type::返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
-
{function boby}::函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
/*捕捉列表不能省略参数列表与返回值可以省略函数体不能省略
*/
int main()
{auto add1 = [](int x, int y)->int {return x + y; };cout << add1(1,2) << endl;auto func1 = []{cout << "hello world" << endl;return 0;};func1();return 0;
}
捕捉列表
lambda 表达式中默认只能用lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
捕捉方法:
-
显示的传值捕捉和传引用捕捉,[x, y,&z]表示x和y传值捕捉,z传引用捕捉。
-
捕捉列表中隐式捕捉:捕捉列表写一个=表示隐式传值捕捉,捕捉列表写一个&表示隐式传引用捕捉。并不是捕捉全部的变量,而是用哪个变量捕捉哪个变量
-
捕捉列表中混合使用隐式捕捉和显示捕捉,[=,&x]表示其他变量隐式值捕捉, x引用捕捉;[&,x,y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是 &或=。
-
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
-
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
同一个变量不能捕捉两次
值捕捉的变量不能修改,因为值捕捉的变量相当于默认加了const
引用捕捉允许对变量进行修改
新的类功能
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载,C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载
- 没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
-
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝
-
自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
namespace liu
{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);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}//构造string(const string& s){cout << "string(const string& s) -- 拷⻉构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷⻉赋值 " <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){//cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}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';}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 =new char('\0');size_t _size = 0;size_t _capacity = 0;};
};class Person
{
public:Person(const char*name="张三", int age=1):_name(name),_age(age){ }~Person(){}private:liu::string _name;int _age;
};int main()
{//如果Person类中没有实现析构函数、拷贝构造、拷贝赋值重载,那么编译器会自己生成一个移动构造,由于Person类中包含了string类,string类中实现了移动构造,所以直接调用string类中的移动构造Person s1;Person s2=s1;Person s3=(move(s2));//因为Person类中实现了析构函数,所以编译器不会自己生成移动赋值,哪怕string类中实现了移动赋值,也不会调用。Person s4;s4 = move(s2);return 0;
}
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;
private:bit::string _name;int _age;};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
包装器
std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可以调用对象:
-
函数指针
-
仿函数
-
lambda
-
bind
存储的可调用对象称为:function的目标,若没有目标,则称为空,调⽤空 std::function 的目标导致抛出std::bad_function_call异常
- 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的参数,实现字符串和可调用对象的映射表功能。
int f(int a,int b)
{return a + b;
}struct Functor
{
public:int operator()(int a, int b){return a + b;}
};int main()
{//包装函数function<int(int, int)> f1 = f;//包装仿函数function<int(int, int)> f2 = Functor();//包装lamdbafunction<int(int, int)> f3 = [](int a, int b) {return a + b; };cout << f1(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<int(Plus*,double, double) > f5 = &Plus::plusd;Plus pl;cout << f5(&pl, 1, 1) << endl;//包装非静态成员函数也可以像如下这样,因为this本质是不允许显示传递的。//传入Plus*其实是用指针调用成员函数,传入Plus是用对象调用成员函数//调用成员函数其实是使用".*"操作符进行调用function<double(Plus, double, double) > f6 = &Plus::plusd;cout << f6(pl, 1.1, 1.1) << endl;//包装非静态成员函数也可以像如下这样。也可也使用左值引用,只不过使用左值引用就不能传入Plus()匿名对象了function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pl), 1.1, 1.1) << endl;cout << f7(Plus(), 1.1, 1.1) << endl;return 0;
}
bind
- 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的一个命名空间中。