一、右值引用与移动构造
1.1右值引用
左值与右值最大的区别就是,左值可以取地址,而右值不能取地址。例如:
1;//常量
10 + 2;//表达式
A();//匿名对象
还有将亡值,也就是超出作用域将要被销毁掉的值
以上这些都是右值,不能取地址。那么什么是右值引用呢?
与左值引用类似,右值引用用来引用右值。
int&& a = 1;//常量
int&& b = 10 + 2;//表达式
A&& c = A();//匿名对象
区别于左值引用只使用一个“&”,右值引用需要使用“&&”与左值引用进行区分。右值引用不能绑定左值,但是如果使用move可以将左值转化为右值被右值引用,但是这个时候右值引用绑定的是左值的拷贝,并不是左值本身。
另外,右值引用后的变量实际上是一个左值,也就是说例如:int&& a = 1; 这里的a实际上是左值,这是为了给移动构造做铺垫。
1.2移动构造
当函数返回值是函数内创建的临时变量时,接受返回值的过程实际上需要经历两次拷贝构造,如果构造是深拷贝就会浪费资源,而移动构造就是为解决这个问题产生的:
class MyString {
private:
char* data_;
size_t size_;
public:
// 移动构造函数
MyString(MyString&& other)
: data_(other.data_), // 直接"偷"指针
size_(other.size_) // 复制size
{
// 关键步骤:置空源对象,防止其析构时释放我们刚偷的内存
other.data_ = nullptr;
other.size_ = 0;
}
// 析构函数
~MyString() {
delete[] data_; // 安全:如果data_是nullptr,delete[]是安全的
}
// ... 拷贝构造函数、拷贝赋值、移动赋值、其他成员函数 ...
};
上面是移动构造的示例,通过右值引用接收将亡值,再将将亡值的数据直接转移走,如果正常拷贝构造需要new空间,那么使用移动构造就可以省去这个过程,节省大量资源。而移动赋值也是一样的,直接将数据转移,而不需要new新空间然后再赋值,从而将两次拷贝构造消耗的资源节省出来。
// 移动赋值运算符
MyString& operator=(MyString&& other) {
// 1. 防止自赋值 (虽然移动自赋值少见,但安全第一)
if (this != &other) {
// 2. 释放当前对象持有的资源
delete[] data_;
// 3. "偷"资源
data_ = other.data_;
size_ = other.size_;
// 4. 置空源对象
other.data_ = nullptr;
other.size_ = 0;
}
return *this; // 5. 返回 *this
}
而右值引用的变量是左值,也是因为移动构造需要修改这个变量,如果变量是右值不能修改,那么移动构造就失去了意义。
二、lambda表达式与std::function
2.1 lambda表达式
lambda表达式的格式为 [capture-list] (parameters) mutable -> return-type {function-body}
2.1.1 capture-list
capture-list就是捕捉列表,用来捕捉当前作用域中的变量:
[]
:不捕获任何变量[x]
:按值捕获变量x
[&x]
:按引用捕获变量x
[=]
:按值捕获所有外部变量[&]
:按引用捕获所有外部变量[this]
:捕获当前类的this
指针[x, &y]
:混合捕获(x 按值,y 按引用)[=, &x]
:默认按值捕获,但 x 按引用[&, x]
:默认按引用捕获,但 x 按值
捕捉列表不能省略,用来确定是lambda表达式。
2.1.2 parameters
类似于函数的参数,用来接收输入的参数,这个部分可以省略。
2.1.3 mutable
如果有这个关键字,那么就能修改捕获到的值,但是不会影响被捕获的值本身,因为如果不是引用捕捉,那么捕捉实际上是对外部变量进行了拷贝。这个关键字也可以省略。
2.1.4 -> return-type
用来标明返回的类型,通常也可以省略,因为会自动识别返回类型。
2.1.5 {function-body}
这个部分{}内部的内容可以不写,但是这个花括号必须写,是函数的主体。
lambda表达式使用时,类似于伪函数,通常使用auto f1 = [capture-list] (parameters) mutable -> return-type {function-body};接收lambda表达式,然后f1(parameters)即可。
int main(void) {
auto f1 = [](int a, int b)->bool { return a < b;};
cout << f1(1, 2);
return 0;
}
例如这样,就是一个简单的lambda表达式。
2.2 std::function
function是一个包装器,可以将函数指针,伪函数,lambda表达式包装成类,例如:
int main(void) {
function<bool(int, int)> f1 = [](int a, int b)->bool { return a < b;};
cout << f1(1, 2);
return 0;
}
此时,f1的类型就是class std::function<bool __cdecl(int,int)>,如果不进行包装,f1的类型就比较复杂:class `int __cdecl main(void)'::`2'::<lambda_1>,且不同编译器的命名方式也不同。因为lambda表达式的类型是隐藏的。
那么function有什么用呢?可以作为函数指针,伪函数,lambda表达式的类型去初始化类模板:
int main(void) {
map<string, function<int(int, int)>> mop;
mop.insert(make_pair("+", [](int a, int b) {return a + b;}));
mop.insert(make_pair("-", [](int a, int b) {return a - b;}));
mop.insert(make_pair("*", [](int a, int b) {return a * b;}));
mop.insert(make_pair("/", [](int a, int b) {return a / b;}));
cout << mop["+"](1, 2);
return 0;
}
这样就可以很简单的完成识别字符串中的运算类型并进行运算,而不需要使用switch写一大串,增加了可读性,且修改方便。
2.3 std::bind
int Add(int a, int b) {
return a + b;
}
int main(void) {
function<int(int)> f1 = bind(Add, placeholders::_1, 10);
cout << f1(2);
return 0;
}
当函数参数固定时,重复输入就会比较麻烦,这个时候,使用bind就可以固定住想要固定的参数,就如上面代码所示,将b固定为10。
placeholders::_1表示接收到的第一个参数_2则为第二个,例如:
int test(int a, int b, int c) {
return a - b + c;
}
int main(void) {
function<int(int, int)> f1 = bind(test, 10, placeholders::_2, placeholders::_1);
cout << f1(2, 3);
return 0;
}
这里就是将a固定为10,第一个参数给到c,第二个参数给到b,因此结果等于10-3+2=9。
bind通常与function一起使用。
三、统一初始化与初始化列表
统一初始化其实就是可以使用{}进行初始化,但是这样的初始化有不同的含义,例如:
int a{1};
double b{};//初始化为0.0
class A{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
A c{10};
在上面的例子中,初始化与直接使用()的方式类似,但是不能使用浮点数类型来初始化int类型,会报错因为涉及精度转换,类似的 float f1{1e40}; 这样也会报错。
在这种情况下的初始化,本质上类似于类型转换,因此如果精度不正确就会报错。
list<int> l1 = {1, 2, 3, 4, 5};
map<string, string> s1 = {{"banana", "香蕉"},
{"apple", "苹果"},
{"orange", "橘子"}};
而像上面这种情况下的初始化,则涉及到initializer_list这个在C++11中才有的容器。
在list的例子中,{1, 2, 3, 4, 5}被初始化成initializer_list<int>类型,然后传入list的初始化函数:
因此,如果要自定义的类型支持这种类似于C语言数组的初始化方式,就需要实现支持initializer_list的初始化方式。因为initializer_list支持迭代器,因此,只需要用范围for,然后调用insert或者push_back等即可。
四、其他
- auto类型推导,当类型过长或复杂时,使用auto直接推导类型。
- nullptr 用来区别于NULL,代表空指针。
- decltype 可以进行类型推导,尽管typeid().name也可以推导类型,但是decltype可以用来初始化类模板。
- override 用于指示派生类中的成员函数应该重写基类中的虚函数,没有重写就会报错,类似于一种强制重写的机制。
- finish 用于限制类的继承或成员函数的重写。用在类则不能被继承,用在成员函数则不能被重写。
对于智能指针和并发控制的介绍等后面再更新了。