目录
同一列表初始化
{} 初始化
std::initializer_list
auto
decltype
nullptr
范围for
STL中的变化
右值引用和移动语义
左值引用和右值引用
左值引用和右值引用比较
右值引用使用场景和意义
完美转发
万能引用
新的类功能
强制生成默认函数的关键字default
禁止生成默认函数的关键字delete
可变参数模板
lambda表达式
lambda表达式语法
捕捉列表
包装器
bind
C++11中的特性我们在之前的文章中也介绍了一点,这一片文章就来好好的介绍一下,但是C++11发布了很多特性,我们不需要记那么多,需要记住好用和常用的就可以了,关于为何叫做C++11,大家又想去可以自己搜一下,这里就不过多赘述了。
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。例如:
struct A { int _a; }; int main() { int arr[] = { 0, 1, 2, 3, 4 }; A a = { 0 }; return 0; }
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。int main() { int x1 = 1; int x2 = { 2 }; int x3{ 3 }; return 0; }
第一种是常用的,下面两种也可以初始化,但是还是用第一种就可以了。我们自己定义的自定义类型也可以使用花括号来初始化。
class A { public: A(int x1, int x2, int x3) :_x1(x1) ,_x2(x2) ,_x3(x3) { cout << "A(int x1, int x2, int x3)" << endl; } private: int _x1; int _x2; int _x3; }; int main() { // 他们是在调用构造函数 A a1(1, 2, 3); // C++11所支持的 A a2 = { 1, 2, 3 }; A a3{ 1, 2, 3 }; return 0; }
但是还是那句话,看懂就可以了,不建议使用。那既然C++11支持了这种功能,但却都不好用,不要急,接下来继续看。
int main() { vector
v = { 1, 2, 3 }; list l = { 1, 2, 3 }; auto e = { 1, 2, 3 }; cout << typeid(e).name() << endl; return 0; } 花括号也支持了这样的功能,在C++11中也增加了一个initializer_list,打印出的e的类型就是它,他就是一个容器,可以像迭代器一样去使用,之后vector构造函数支持了一个initializer_list版本,不仅是vector,很多容器都增加了这种方法,都支持类似这样的构造函数。
class A { public: A(int x1, int x2, int x3) :_x1(x1) ,_x2(x2) ,_x3(x3) { cout << "A(int x1, int x2, int x3)" << endl; } private: int _x1; int _x2; int _x3; }; int main() { A a1(1, 2, 3); A a2(4, 5, 6); vector v1 = { a1, a2 }; // 这种写法也一样,隐式类型转化为自定义类型对象,优化为直接构造,再放入vector vector v2 = { {1, 2, 3}, {4, 5, 6} }; return 0; }
有了这样的支持就可以实现这种写法,这种相当于一种隐式类型的转换。单参数支持隐式类型转换,用"xxxxxxx"构造string,string支持char*,本来是构造一个匿名对象再去拷贝构造,编译器优化为直接构造。
C++11以后一切对象都可以用列表初始化,但是普通对象还是使用以前的方式初始化,容器想用也可以用列表初始化。
这个关键字在之前也讲过,所以就不过多说明了,简单说,当一个类型过长,使用auto就会很方便,但是他会降低代码的可读性。
关键字decltype将变量的类型声明为表达式指定的类型。int main() { int x = 0; decltype(&x) p; // p的类型就是int* return 0; }
用的也很少。
由于C++中NULL被定义成字面量0,实际上这个0的类型是int,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。之前也说了,在C++中就使用nullptr当做空指针就行了。
这个也不说了,用过很多次了,底层就是直接替换成迭代器,很简单,也很好用。
在C++11中添加了array、forward_list以及unordered_map和unordered_set,其中最重要的两个就是后两个,前面两个也不好用。一个一个说。
首先说array,array的初衷是为了替换C语言中的数组,C++觉得数组非常的不好。
int main() { const size_t N = 100; array
a; // 一个模板参数,一个非类型模板参数 return 0; } C语言中数组越界检查时会出问题,越界读检查不出来,越界写也是抽查;常量区和代码段的写可以被检查出来,那一段是被保护的,是不允许写的;而array不仅读还是写都可以被检查出来,因为他就是一个静态数组的容器,它在越界的时候调用的operator[]是会检查的。 array是定义在栈上的,如果空间太大是会栈溢出的,而vector是在堆上申请空间,一句话就是array不如vector。
下一个是forward_list就是一个单链表,也是不如使用list。
原来在这之前所说的引用都是左值引用,而C++11中新增了的右值引用和语法特性,接下来就看看什么是右值引用,无论是左值引用还是右值引用都是给对象取别名。
在这之前先来了解一下什么是左值,什么是左值引用。
左值是一个表示数据的表达式(如变量名或指针),我们可以获取它的地址并且可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main() { // 以下的a,p,c都是左值 // 左值可以取地址 int a = 0; int* p = &a; const int c = 10; // 以下的是他们的引用 int& ra = a; int*& rp = p; const int& rc = c; int& pvalue = *p; return 0; }
那什么是右值,什么是右值引用。
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main() { double a = 1.1, b = 2.2; // 以下几个是常见的右值,对这些右值赋值是会报错的 // 右值也不能取地址 10; // 常量 a + b; // 表达式 fmin(a, b); // 传值返回,返回一个临时变量 // 以下几个都是对右值的引用,&&这就是右值引用的符号 int&& rr1 = 10; double&& rr2 = a + b; double&& rr3 = fmin(a, b); return 0; }
简单理解一下右值引用,就是给了右值一个地址,并且可以修改这个地址的值,如果这个值不想被修改,在前面再加一个const就可以了。
对于左值引用:
- 左值引用只能引用左值,不能引用右值。
- const左值既可以引用左值,也可以引用右值。
// 原来写拷贝构造函数的时候会把参数写成const左值引用的形式 // 就是为了传参的时候既可以传左值也可以传右值 template
void Func(const T& x) { // ... } 对于右值引用:
- 右值引用只能引用右值,不能引用左值。
- 但是右值引用可以move以后的左值。
C++11中,std::move()函数位于头文件中,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。int main() { int a = 0; int&& rra = move(a); return 0; }
对于引用的价值原来也总结过,它就是为了减少拷贝,有的容器拷贝起来消耗很大。
左值引用解决的问题:
- 做参数。可以减少拷贝,提高效率;做输出型参数。
- 做返回值。也是减少拷贝,提供参数;引用返回,可以修改返回对象。
左值引用可以做参数也可以做返回值,这样都可以提高效率。但是左值引用并不是万能的,当一个函数返回对象是一个局部变量的时候,出了函数作用域就不存在了,这个时候只能传值返回,传值返回的时候会产生一个临时对象,返回值要拷贝一次,临时对象再给接受函数返回值的对象拷贝一次。现在的编译器优化之后至少有一次拷贝,直接把返回的对象拷贝给接受返回值的对象,如果旧一点的编译器就可能是两次拷贝。
string func() { string str; // ... return str; } int main() { // 假如这个string是我们自己实现的,返回时就会产生一个临时对象,这样就会导致两次拷贝 // 编译器优化为:把str直接拷贝给ret // 这个函数的返回值就是一个右值 string ret = func(); return 0; }
C++11把右值进行了一个划分:1. 内置类型的右值是纯右值;2. 自定义类型的右值是将亡值。
// 拷贝构造 string (const string& s) :_str(nullptr) { // 如果是一个左值,一定要去深拷贝 string tmp(s._str) swap(tmp); } // 移动构造 string (string&& s) :_str(nullptr) { // C++11就有了移动构造 // 参数就是一个右值引用,他是一个将亡值 // 既然他都要消失了,在他消失之前把资源转换一下 swap(s); }
int main() { string s1("aaaa"); // 构造 string s2(s1); // 拷贝构造 srting s3(move(s1)); // 使用move把s1变成右值,把s1的资源移到了s3里 // 如果没有移动构造,这里只能是深拷贝,并把旧资源释放 // 移动构造把资源直接转移,代价小一些 return 0; }
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。移动语义还是对深拷贝还是有价值的。
C++11中有很多容器也增加了右值引用的版本的接口。
template
void PerfectForward(T&& t) { Func(t); }
- 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。
- 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。
但是想要在传递过程中保持它的左值或者右值的属性, 就需要用完美转发。
template
void PerfectForward(T&& t) { Func(std::forward (t)); } 如果使用了模板的万能引用就要考虑要不要使用完美转发,万能引用会把引用的参数变成左值,如果还想要使用右值就必须要对变量完美转发。完美转发就是为了保持引用对象的属性。
原来C++的类中会有6个默认成员函数,C++11中新增了两个就是上面讲到的移动构造和移动赋值运算符重载。
对于哪些类需要写这两个函数呢?
- 拷贝对象需要深拷贝时,需要自己写这两个函数。
- 如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,也就是值拷贝;自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
默认移动赋值跟上面移动构造规则是一样的。
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
如果想要实现一个类,这个类只能在堆上创建,可以把构造函数或者析构函数私有就可以做到,还可以使用delete关键字实现,需要禁止析构函数生成。
class OnlyHeap { public: ~OnlyHeap() = delete; }; int main() { OnlyHeap p; return 0; }
这样对象就不会在栈中创建,因为对象出了作用域会调用析构函数,没有析构函数就不能创建了,这时候就可以在堆上new一块空间,使用指针指向它,因为指针不会调用析构函数。
class OnlyHeap { public: ~OnlyHeap() = delete; }; int main() { OnlyHeap* p = new OnlyHeap; return 0; }
如果类中还有其他的成员,这个成员申请了空间,没有析构函数,就会导致资源泄露,delete这个指针也会调用析构函数,想要解决这个问题就要再写一个释放的函数(Destroy),再用指针调用这个函数,最后还有释放指针指向的堆上的这块空间,使用operator delete() 或者 free()。
class OnlyHeap { public: void Destroy() { delete[] s; operator delete(this); // free(this); } ~OnlyHeap() = delete; private: char* s; }; int main() { OnlyHeap* p = new OnlyHeap; p->Destroy(); // 放到这里也可以 // operator delete(p); // free(p); return 0; }
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template
void func(Args... args) { cout << sizeof...(args) << endl; // 这样可以知道参数包中的参数个数 } int main() { string s("xxxxxxxxx"); func(); func(1); func(1, 'a'); func(1, 'a', s); return 0; } 想要获取参数包中的参数可以使用递归的方法。
// 递归终止函数 void func() { cout << endl; } // 没有了才到这里 // 展开函数 template
void func(const T& val, Args... args) { cout << val << " "; // 拿到第一个值 cout << endl; func(args...); // 递归后面的 } int main() { string s("xxxxxxxxx"); func(); func(1); func(1, 'a'); func(1, 'a', s); return 0; } 这里了解一下,STL的容器中,除了push_back还会有一个emplace函数,使用可变参数有的时候会更高效一些。
lambda表达式也叫做匿名函数,到了这里我们已经见过很多像函数一样使用的对象或类型,比如函数指针,C语言使用的qsort函数就是使用的函数指针,写一个比较函数进行回调;C++中sort不想要使用函数指针,使用的是仿函数,也叫做函数对象;还有一个就是这个lambda表达式。它是一个局部的匿名函数对象。
书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
- [capture-list]:捕捉列表,编译器根据 [] 来判断是否为lambda表达式,捕捉列表能够捕捉上下文的变量。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同 () 一起省略。 mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即参数不能为空)。 ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。 {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
简单实现一个相加的lambda表达式
int main() { // 两数相加 auto add = [](int a, int b) { return a + b; }; cout << add(1, 2) << endl; return 0; }
通过上面的例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[val]:表示值传递方式捕捉变量val [=]:表示值传递方式捕获所有作用域中的变量(包括this) [&val]:表示引用传递捕捉变量val [&]:表示引用传递捕捉所有作用域中的变量(包括this) [this]:表示值传递方式捕捉当前的this指针
【注意事项】:
从语法上捕捉列表可有多个捕捉项组成,并以逗号分割,比如:
- [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递的方式捕捉其他所有变量。
- [&, a, this]:值传递的方式捕捉a和this,引用传递的方式捕捉其他变量。
捕捉列表不允许变量重复传递,否则就会导致编译报错,比如:
- [=, a]:已经以值传递的方式捕捉了所有变量,又重复捕捉了a。
在作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。要注意,全局变量也是可以捕捉到的,如果lambda要捕捉的变量在其他函数栈帧中,这样就捕捉不到了。
其实,从使用方式上来看,仿函数与lambda表达式完全一样。 实际在底层编译器对于lambda表达式的处理方式,完全就是按照仿函数的方式处理的,如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
这就可以解释,lambda表达式之间不能相互赋值,即使看起来类型相同,但其实是不同的类型。
function包装器
function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
其实函数指针、函数对象(仿函数)、lambda表达式底层实现是差不多的,但是却是不同的类型,如果他们同时被一个函数的参数用模板类型接受,这样就会实例化出多份,导致模板的效率低下,想要解决这个问题就需要包装器了。
std::function在头文件
// 类模板原型如下 template
function; template class function ; // 模板参数说明: // Ret: 被调用函数的返回类型 // Args…:被调用函数的形参 #include
int add1(int left, int right) //函数 { return left + right; } class add2 // 仿函数 { public: int operator()(int left, int right) { return left + right; } }; class A { public: static int add4(int left, int right) // 静态成员函数 { return left + right; } int add5(int left, int right) // 非静态成员函数 { return left + right; } }; int main() { auto add3 = [](int left, int right) { return left + right; }; // lambda表达式 function func1 = add1; function func2 = add2(); function func3 = add3; function func4 = A::add4; // 静态成员函数需要指定类域 function func5 = &A::add5; // 非静态成员函数需要取地址符号,并使用对象去调用 cout << func1(1, 2) << endl; cout << func2(1, 2) << endl; cout << func3(1, 2) << endl; cout << func4(1, 2) << endl; cout << func5(A(), 1, 2) << endl; return 0; }
上述的最后一个func5中,包装器对于静态的成员函数和普通的函数一样处理,但是非静态成员函数就必须在参数中多一个对象,但是我想多这个对象,那么使用bind就可以解决这个问题。
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接收一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。我们用它可以把一个原本接收N个参数的函数,通过绑定一些参数,返回一个接收M个参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
这个参数列表文档里也有说明,他有一个placeholder,这是占位符,每个参数都可以绑定到一个值,也可以是一个占位符。
参数的_1代表第一个参数,_2代表第二个参数。
using namespace placeholders; int main() { int x = 10, y = 5; cout << Div(x, y) << endl; // 可以调整参数的顺序 // _1, _2代表定义在placeholder命名空间中绑定函数对象的形参 // 如果不想些placeholder::这么长需要添加using namespace placeholders; auto bindDiv = bind(Div, placeholders::_2, placeholders::_1); cout << bindDiv(x, y) << endl; return 0; }
调整参数顺序其实是没有什么用的,它的用处是可以绑定参数,解决上面的问题。
class A { public: int add5(int left, int right) { return left + right; } int sumwith3(int left, int mid, int right) { return left + mid + right; } }; int main() { function
func5 = &A::add5; // 非静态成员函数需要取地址符号,并使用对象去调用 // 通过bind把这个函数的一个参数绑定为A() auto bindfunc5 = bind(&A::add5, A(), placeholders::_1, placeholders::_2); cout << func5(A(), 1, 2) << endl; cout << bindfunc5(1, 2) << endl; // 三数之和 function func6 = &A::sumwith3; cout << func6(A(), 1, 2, 3) << endl; // bind也可以绑定多个参数,绑定前三个参数 auto bindfunc6 = bind(&A::sumwith3, A(), 1, 2, placeholders::_1); cout << bindfunc6(1) << endl; return 0; }