自动类型推导
右值引用
auto
auto用于copy一份集合中的数据,对于这种情况,在循环体中不论怎么修改copy的数据,是不会影响到原有集合中的数据的。
举个简单的例子
int arr[5] = {0, 1, 2, 3, 4};
cout << "使用auto" << endl;
for (auto a: arr) {
a += 1;
cout << a << "\t";
}
cout << endl;
cout << "原数组" << endl;
for (int i = 0; i < 5; i++) {
cout << arr[i] << "\t";
}
cout << endl;
结果可以看到,原数组没有发生改变
使用auto
1 2 3 4 5
原数组
0 1 2 3 4
auto &相当于获取一份集合中数据的引用,那么对于数据的修改,就会直接影响到集合数据本身。
同样简单的例子
int arr[5] = {0, 1, 2, 3, 4};
cout << "使用auto &" << endl;
for (auto &a: arr) {
a += 1;
cout << a << "\t";
}
cout << endl;
cout << "原数组" << endl;
for (int i = 0; i < 5; i++) {
cout << arr[i] << "\t";
}
cout << endl;
使用auto &
1 2 3 4 5
原数组
1 2 3 4 5
在auto &的前面加上了const修饰,意味着它会获取一份集合中数据的引用,但是只可以被读取,不能被修改。
int arr[5] = {0, 1, 2, 3, 4};
cout << "使用const auto &" << endl;
for (const auto &a: arr) {
a += 1; // // error 不能修改,报错
cout << a << "\t";
}
常数据引用 可以接受 常数据,非常数据,临时右值,同时也不会调用拷贝构造函数,避免拷贝构造开销。但是由于返回是const类型,常对象以后将无法调用任何非const的函数
所以c++11提出了auto&& 用于绑定任何值,
#include <iostream>
#include <array>
using namespace std;
array<int, 10> aa{11, 22, 33, 44, 55, 66, 77, 88, 99, 10};
void test1()
{
int total = 0;
for (auto&& value : aa)//也可以 for (auto value : aa) 或者for (const auto& value : aa)
total += value;
//每次都都将容器内遍历后的元素返回给total
cout << total<<endl;
};
int main()
{
test1();
for (auto i : aa)
{
cout<<i<<" ";
}
cout << endl;
}
505
11 22 33 44 55 66 77 88 99 10
右值引用在移动语义里也有,在讲移动构造函数之前,先看一下右值引用的作用
#include <iostream>
using namespace std;
class buffer
{
public:
buffer()
{
cout << "构造函数:" << endl;
}
buffer(const buffer &new_buffer)
{
cout << "拷贝构造" << endl;
}
~buffer()
{
cout << "析构函数" << endl;
}
};
buffer getbuffer()
{
buffer buf;
return buf;
}
void setBuffer(buffer &buf)
{
}
int main()
{
//使用getbuffer生成一个对象
buffer buf = getbuffer();
setBuffer(buf);
}
程序结果
构造函数:
析构函数
实际上这是编译器优化的结果, 在linux环境下的实际的执行结果
执行getbuffer体:
buffer buf ;//调用构造函数执行 getbuffer() 函数内部的 demo() 语句,即调用 buffer类的默认构造函数生成一个匿名对象;
return buf ;//会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_buffer() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);
buffer buf = getbuffer();//再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_buffer() 函数返回的对象会被析构);程序执行结束前,会自行调用 buffer 类的析构函数销毁 a。
你可能会说这里getbuffer 不是返回了临时的对象吗,没错但是这里 buffer buf = getbuffer(); getbuffer 函数的 return 语句还在执行这句,执行完此句后,函数体执行完毕。
我们可以简化代码,省去创建main()函数中 buf对象的创建,直接使用下面的句子
setBuffer(getbuffer());
但是getbuffer()内部的对象早已销毁,所以不可行;
//语法:
//让一个常引用指向一个将亡值,那么这个将亡值的生命周期就会被延长到和这个常引用的生命周期相同的长度
所以我们将mian函数替换为
int main()
{
const buffer & new_buf= getbuffer();//常引用指向将亡值
//不过 serBuffer(buffer&buf)需要换成 serBuffer( const buffer&buf)
//因为非const可以转化成const 但const 只能转化成const
//然后getbuffer()的生命周期就和new_buf一样长了
setBuffer(new_buf); //非右值作参数
setBuffer(getbuffer()); // 延长生命周期的右值
}
//
void setBuffer(const buffer&buf)
{
}
此时有两个问题
第一 这个参数的引用是一个const 引用,我们不能修改被引用的部分
第二 我们无法做到 每次都人为判断 传入的值是否是一个将亡值,也就无从使用const 引用指向将亡值,提升他的存活周期
所以c++11提出了 右值引用
我们可以将setBuffer的参数修改成如下的样子,(保留二者)
void setBuffer(buffer &&buf){};
改完之后完整的代码为:
#include <iostream>
using namespace std;
class buffer
{
public:
buffer()
{
cout << "构造函数:" << endl;
}
buffer(const buffer &new_buffer)
{
cout << "拷贝构造" << endl;
}
~buffer()
{
cout << "析构函数" << endl;
}
};
buffer getbuffer()
{
buffer buf;
return buf;
}
void setBuffer(buffer &&buf)
{
cout<<"buffer &&buf"<<endl;
}
void setBuffer(const buffer&buf)
{
cout<<"const buffer&buf"<<endl;
}
int main()
{
//使用getbuffer生成一个对象
buffer buf = getbuffer();
setBuffer(buf);//传非右值
setBuffer(getbuffer());//传右值
}
运行结果:
构造函数:
const buffer&buf //setBuffer(buf);调用的
构造函数:
buffer &&buf // setBuffer(getbuffer());调用的
析构函数
析构函数
const buffer&buf 是在传入参数为非右值的时候传入的
buffer &&buf 是在传入参数为右值的时候调用的
可见 通过右值引用,我们可以有效地区分不同的调用方式,进而进一步的进行优化。
- 右值引用可以接收到将亡值,
- 右值引用可以接收大部分的数据类型
我们可以把这两个特点用在构造函数中
有时候我们难以避免将右值初始化对象,或者在运算过程中存在右值传递(比如连续的相加运算)
所以我们会使用 常引用作为拷贝构造函数的参数,但常引用有他的缺点,比如引用对象不可修改。
而且 使用普通的构造函数为了解决浅拷贝的问题,会额外new 一次,创建对象, 但是当 我们对临时对象进行拷贝时,我们明知道临时对象很快就会销毁,所以临时对象的空间也将很快消失,所以对临时对象的拷贝就不用采取深拷贝,因为不存在析构多个对象共用的一块空间(也就是后面发生的多次析构同一块内存)。
所以我们对于这种情况,常会使用移动拷贝构造函数 和普通普通构造函数共存
移动语义:所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了/home/ding-jin-xing/文档/vscode代码文件夹子cpp/.vscode/launch.json /home/ding-jin-xing/文档/vscode代码文件夹子cpp/.vscode/右值引用.cpp初始化的执行效率。
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
//添加移动构造函数
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num; //指针类型需要深拷贝
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo(); //临时对象赋值
return 0;
}
可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。
在 Linux 系统中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors
命令执行此程序,输出结果为:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
这里并未调用到下面这个函数,因为优先调用移动语义函数
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。