右值引用
C++11中,右值分为纯右值和将亡值。
- 临时变量,常量、一些运算表达式(1+3)等叫纯右值
- 声明周期将要结束的对象叫将亡值
右值引用只能指向右值,使用&&声明,定义时必须初始化
左值引用可以引用左值,也可以引用右值
// 纯右值
int&& a = 3 + 1;
int&& b = 3.14 + 3;
cout << a << endl; // 4
cout << b << endl; // 6
// 演示将亡值
String GetString(char* pStr) {
String strTemp(pStr);
return strTemp;
}
void test() {
String s1(GetString("hehe"));
}
编译器用调用GetString,创造一个匿名对象strTemp,返回的临时对像(可能会优化成直接返回)
若String类中未写右值引用版本拷贝构造,则默认使用左值引用版本重新拷贝一份资源给s1,然后释放临时对象。这样就造成不必要的拷贝。
在string类中加入以下函数即可优化上述步骤:
String(String&& s)
: _str(s._str)
, _size(s._size)
, _capacity(s._capacity) {
s._str = nullptr;
s._size = 0;
s._capacity = 0;
}
默认移动构造/赋值
编译器隐式生成一个(如果没有用到则不会生成)移动构造函数,如果声明了移动构造或拷贝构造或移动赋值,则编译器不会生成默认版本。
编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是浅拷贝。
同理,编译器默认生成移动赋值,如果声明了移动赋值或赋值运算符重载或移动构造,则编译器不会生成默认版本。
注意:在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义。
move()
将一个左值强制转化为右值引用,搭配移动构造使用,可以达到类似对象数据转移的效果。
完美转发
函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。
C++11通过forward函数实现:
void Fun(int& x) { cout << "lvalue ref" << endl; }
void Fun(int&& x) { cout << "rvalue ref" << endl; }
void Fun(const int& x) { cout << "const lvalue ref" << endl; }
void Fun(const int&& x) { cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T&& t) { Fun(std::forward<T>(t)); }
int main() {
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 1;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
auto
C++11中,auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
- 使用auto时必须进行初始化,编译阶段需根据初始化表达式来推导auto的实际类型
- auto是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- auto同一行声明多个变量时,变量必须类型相同,否则会报错,编译器只对第一个进行推导,然后用推导出来的类型定义其他变量
- auto不能作为函数的参数,不能直接用来声明数组
- 不能定义类的非静态成员变量
- 实例化模板时不能使用auto作为模板参数
decltype
int Test(size_t size) {
cout << "Test()" << endl;
return 0;
}
int main() {
// 推导函数类型 RTTI
cout << typeid(decltype(Test)).name() << endl;
// 推导的是函数返回值的类型,只推演,不会执行函数 RTTI
decltype(Test(0)) a = 3;
return 0;
}
RTTI缺点:会降低运行的效率
基于范围的for循环
- for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end方法,begin和end就是for循环迭代的范围
void Test(int arr[]) {
for(auto& e : arr) // arr为指针,编译器并不知道数组的范围所以报错
cout << e << endl;
}
- 迭代的对象要实现++和==的操作
nullptr
NULL实际是一个宏,在传统的C头文件(stddef.h) NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下
将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0
- C++11提供了nullptr代表一个指针空值常量。其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
- 使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
lambda表达式
如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法
#include <algorithm>
但在想要自定义比较规则时必须提供仿函数对象
C++11 中lambda表达式可以简化这个过程:
class AAA {
public:
AAA(int a, int b) {
_a = a;
_b = b;
}
int _a;
int _b;
};
class Greater {
public:
bool operator()(const AAA& a, const AAA& b) {
return a._a > b._a;
}
};
int main() {
vector<AAA> v{ {1, 3}, {6, 7} , {4, 1} };
// 必须提供仿函数的比较方法
sort(v.begin(), v.end(), Greater());
sort(v.begin(), v.end(),
[](AAA& a, AAA& b)->bool {
return a._a > b._a;
});
for (auto& e : v)
cout << e._a << " ";
return 0;
}
lambda表达式是一个匿名函数,可借助auto将其赋值给一个变量,然后调用。
表达式格式:
[捕捉列表] (参数列表) mutable -> 返回值类型 { 函数体 }
[]捕捉列表:
获取父作用域的变量,供函数使用,编译器根据[]来判断接下来
的代码是否为lambda函数。
[a, b]: 以值的形式获取父作用域的变量a,b
[=]: 以值的形式获取父作用域的所有变量, 变量的属性都为const(包括this)
[=, a]: 错误写法,捕捉a重复
[=, &a]: 正确写法,除了a,其他以值的形式获取父作用域变量, a以引用的形式获取
[&] : 以引用的形式获取父作用域的所有变量,(包括this)
[this]: 获取成员变量的this指针,只能在成员函数内部使用
()参数列表:
如果不需要参数传递,则可以连同()一起省略
mutable:
可选,删除捕捉列表中以值的形式获取的变量的const属性,使用该修饰符时,参数列表不可省略(即使参数为空)。
->返回值类型:
用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
{}函数体:
在函数体内,可以使用参数外,以及所有捕获到的变量。
使用注意事项:
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量
- lambda表达式之间不能相互赋值
- 可以将lambda表达式赋值给相同类型的函数指针
lambda底层:
通过汇编我们可以清楚的看到,编译器会自动生成一个类,在其中重载了operator()