C++入门——“C++11-右值引用和移动语义”

ops/2024/11/2 6:14:48/

        C++11相比于C++98增加以许多新特性,让C++语言更加灵活好用,但是貌似也增加了许多学习的难度,现在先看第一部分。

        一、右值引用和移动语义

        1.右值引用和左值引用

        在C++中,值可以大致分为右值和左值,左值大概是哪些已经被定义的变量或者对象,它一般具有持久性,它可以出现在赋值符号的左边,也可以出现在赋值符号的右边;右值一般指的是临时变量、字面常量等等。一般来说,区分左右值的方法可以用是否可以取地址来判断:左值可以取地址,右值不可以被取地址。

        下面让我们简单看一段代码吧:

void Func(int& a)
{cout << "左值引用" << endl;
}void Func(int&& a)
{cout << "右值引用" << endl;
}int main()
{int a = 1;Func(1);Func(a);return 0;
}

        其中,a是一个普通变量,为左值,而1是一个字面常量,为右值,那么最后的输出结果因该是“右值引用”、“左值引用”。让我们看一看结果:

       2.右值引用引用左值和左值引用引用右值

        右值可以被左值引用引用,因为右值一般具有常性,所以可以通过在左值引用前加上“const”来使得左值引用可以引用右值;而右值引用引用左值则需要使用“move”函数来改变左值的属性。

int main()
{int a = 2;const int& L = 1;int&& R = move(a);cout << L << endl << R << endl;return 0;
}

3.右值引用存在的意义

a.延长临时对象的生命周期

        右值引用可以延长临时对象的生命周期比如:

int main()
{string("123456789");return 0;
}

        此代码中,string的生命周期仅限于这一行,倘若使用右值引用来引用,那么就可以延长它的生命周期:

int main()
{string&& S = string("123456789");cout << S << endl;return 0;
}

        同样的,表达式相加的临时对象,函数返回时的临时对象,都可以使用右值引用来延长它的生命周期。

b.移动语义

        当然了,右值引用存在的意义可不是为了简单的延长生命周期,而是为了转移临时对象的数据,这使得数据的转移更加高效和安全,这也正是移动语义的的机制。在此之前还需要了解的是虽然右值是不可以被改变,但是右值引用是具有左值属性的,也就是说,被右值引用引用的右值是可以被修改的。

        拿拷贝构造来说,我们会将它的它的参数写为const类型的,在保证不修改实参的情况下,还能够接收右值。但是无论是左值还是右值,在有资源的情况下需要进行大量的复制行为,特别是在右值的情况下(因为右值的生命周期即将结束,还得进行一次复制,这样会造成效率的低下)。为了解决这个问题,这个时候就需要介绍一下移动构造和移动赋值了。

        移动构造和移动赋值旨在将临时对象(右值)的资源转移到我们的类中,由于右值的生命周期即将结束,秉承着趁你病要你命的原则,我是可以在虚弱的时候掠夺你的资源。这个时候我就可以直接把我的没用的东西和你的资源进行交换,这样做的效率可不是一般的高,因为避免了大量的数据拷贝。

        移动构造和移动赋值同普通的构造和赋值函数一样,只是参数变为了右值,并且函数内部进行资源的互换。

        就用我们的自己写的string类为例。(此处在linux环境下测试,因为VS的编译器会进行优化)

#pragma once
#include<iostream>using namespace std;namespace Mynamespace
{class string{friend ostream& operator<<(ostream& _cout, const Mynamespace::string& s);friend istream& operator>>(istream& _cin, Mynamespace::string& s);public:typedef char* iterator;public:string(const char* str = ""){_str = new char[strlen(str)+1];char* der = _str;const char* sour = str;while (*sour != '\0'){*(der++) = *(sour++);}*der = '\0';_size = strlen(str);_capacity = _size;}string(const string& s){_str = new char[s._capacity];char* der = _str;const char* sour = s._str;while (*sour != '\0'){*(der++) = *(sour++);}*der = '\0';_size = s._size;_capacity = s._capacity;}string(string&&  s){cout << "string(string&& s) 移动构造" << endl;swap(s);}~string(){delete[] _str;_capacity = 0;_size = 0;}string& operator=(const string& s){delete[] _str;_str = new char[strlen(s._str) + 1];char* der = _str;const char* sour = s._str;while (*sour != '\0'){*(der++) = *(sour++);}*der = '\0';_size = s._size;_capacity = s._capacity;return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}//// iteratortypedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}/// modifyvoid push_back(char c);string& operator+=(char c);void append(const char* str);string& operator+=(const char* str);void clear();void swap(string& s);const char* c_str()const;/// capacitysize_t size()const;size_t capacity()const;bool empty()const;void resize(size_t n, char c = '\0');void reserve(size_t n);/// accesschar& operator[](size_t index);const char& operator[](size_t index)const;///relational operatorsbool operator<(const string& s){return strcmp(_str, s._str) < 0;}bool operator<=(const string& s){return *this < s || *this == s;}bool operator>(const string& s){return !(*this <= s);}bool operator>=(const string& s){return !(*this < s);}bool operator==(const string& s){return strcmp(_str, s._str) == 0;}bool operator!=(const string& s){return !(*this == s);}// 返回c在string中第一次出现的位置size_t find(char c, size_t pos = 0) const;// 返回子串s在string中第一次出现的位置size_t find(const char* s, size_t pos = 0) const;// 在pos位置上插入字符c/字符串str,并返回该字符的位置string& insert(size_t pos, char c);string& insert(size_t pos, const char* str);// 删除pos位置上的元素,并返回该元素的下一个位置string& erase(size_t pos, size_t len);private:char* _str;size_t _capacity;size_t _size;static const size_t npos = -1;};
}
int main()
{Mynamespace::string mystring1 = Mynamespace::string("123456789");return 0;
}

        可能是编译器的版本的问题,在我的linux环境中,以上代码即使关闭了优化,也仍然进行直接构造而不是构造+移动构造:

cx@Ubuntu-Linux:~/study$ g++ test.cpp -fno-elide-constructors
cx@Ubuntu-Linux:~/study$ ./a.outstring(const char* str )

        有条件的小伙伴可以自己试一试,其中“-fno-elide-constructors”或者“-O0”可以关闭优化。

所以这里先测试一下移动赋值:

int main()
{Mynamespace::string mystring1;mystring1  = Mynamespace::string("123456789");return 0;
}
cx@Ubuntu-Linux:~/study$ g++ test.cpp -fno-elide-constructors
cx@Ubuntu-Linux:~/study$ ./a.outstring(const char* str )string(const char* str )
string& operator=(string&& s)

          可以看到最后调用了一次移动赋值。

二、引用折叠

        在C++11中,新增加了引用折叠的特性,引用折叠针对的是引用的引用,但是不可以显式的写出来引用的引用,比如:

int main()
{int a;int&& & b = a;return 0;
}

        但是可以隐式的引用(typedef后的引用):

int main()
{typedef int& L;typedef int&& R;int a;L& b = a;R& c = a;R&& d = 1;L&& e = a;return 0;
}

        引用折叠理解起来有点像与门,在这里,左值引用代表着0,右值引用代表着1,当左值引用右值引用同时存在的时候就是左值引用,只有当两个引用都为右值的时候才为右值引用。

        就拿以上代码为例    L& b = a; 里,b的类型为int&,    R& c = a; 里,c的类型为int&,R&& d = 1;  里,d 的类型为int&&,    L&& e = a; 里 e为左值。

        1.万能引用

        引用折叠的用途在哪里呢?实际上,它可以用来在函数模板中实现万能引用。请看以下函数模板:

template <class T>
void func(T&& n)
{cout << "void func(int&& n)" << endl;
}
int main()
{int a = 1;func(1);func(a);return 0;
}

        已知字面常量1是一个右值,变量a是一个左值,由于引用折叠,第一次调用的是右值引用版本的func,第二次是左值引用的func。最后的运行结果如下:

        这样可以用一个模板实现左值引用和右值引用的两个版本。

        2.完美转发

        先看以下代码:

void F(int& a)
{cout << "左值引用" << endl;}
void F(int&& a)
{cout << "右值引用" << endl;}
void F(const int& a)
{cout << "左值引用" << endl;
}template <class T>
void func(T&& n)
{F(n);
}int main()
{int a = 1;func(a);func(2);return 0;
}

        我们想要的输出结果是“左值引用”、“右值引用”,那么先看一下运行结果:

        哎。为什么是两个左值引用呢?

        由于右值引用的属性为左值,当我们想连续传递一个右值的时候,在第一次传递后,这个值就已经变为左值属性了,为了解决这个问题,C++11引入了完美转发,它的目得是为了在右值连续传递的过程中不改变右值的属性。

        完美转发的本质是一个函数模板forward,它的底层是一个强转(左值引用和右值引用的本质还是指针,只是在语义上不同)。它的用法是在传递参数的地方加上模板的类型,后边紧接着参数是传递的值:

void F(int& a)
{cout << "左值引用" << endl;}
void F(int&& a)
{cout << "右值引用" << endl;}
void F(const int& a)
{cout << "左值引用" << endl;
}template <class T>
void func(T&& n)
{F(forward<T>(n));
}int main()
{int a = 1;func(a);func(2);return 0;
}

        运行结果:

        我们可以分析一下:

        a.传递的参数为a的时候,a为左值,func发生了引用折叠,编译器推导出T为int&,那么forward强转n为左值并返回。

        b.传递的参数为字面常量1的时候,1为右值,func没有发生引用折叠,编译器推导出T为int&&,那么forward强转n为右值并返回。

        总的来说,完美转发就是根据T的类型来推导最后返回的类型,如果T是右值,那么最后返回的就是右值属性的对象,如果T为左值,那么最后返回的就是左值属性的对象。


http://www.ppmy.cn/ops/130347.html

相关文章

DirectShow过滤器开发-写MP3音频文件过滤器(再写 写MP3)

下载本过滤器DLL 本过滤器将MP3音频流写到MP3音频文件。 过滤器信息 过滤器名称&#xff1a;写MP3_2 过滤器GUID&#xff1a;{AE46BC15-71E5-471C-8540-3B73094111EC} DLL注册函数名&#xff1a;DllRegisterServer 删除注册函数名&#xff1a;DllUnregisterServer 过滤器有1个…

C语言中函数的实参和形参

本文主要叙述C语言中函数的实参和形参的概念和区别。 实参&#xff08;实际参数&#xff09; 实参是在函数调用时提供的具体值或变量&#xff0c;它们被传递给函数以供函数内部使用。实参可以是常量、变量、表达式或其他函数的返回值。实参在函数调用时被传递给形参&#xff…

嵌入式常用功能之通讯协议1--IIC

嵌入式常用功能之通讯协议1--串口 嵌入式常用功能之通讯协议1--IIC&#xff08;本文&#xff09; 嵌入式常用功能之通讯协议1--SPI 一、IIC总线协议介绍 Inter-Integrated Circuit(集成电路总线&#xff09;&#xff0c;是由 Philips 半导体公司&#xff08;现在的 NXP 半导体…

第七章 selinux

1、selinux的说明 SELinux是Security-Enhanced Linux的缩写&#xff0c;意思是安全强化的linux。 SELinux 主要由美国国家安全局&#xff08;NSA&#xff09;开发&#xff0c;当初开发的目的是为了避免资源的误用。 系统资源都是通过程序进行访问的&#xff0c;如果将 /var/ww…

SpringBoot【实用篇】- 测试

文章目录 目标&#xff1a;1.加载测试专用属性3.Web环境模拟测试2.加载测试专用配置4.数据层测试回滚5.测试用例数据设定 目标&#xff1a; 加载测试专用属性加载测试专用配置Web环境模拟测试数据层测试回滚测试用例数据设定 1.加载测试专用属性 我们在前面讲配置高级的时候…

Reactor模型

Reactor模型 引言 ​ ​ 概念&#xff1a; Reactor模型&#xff1a;又称为反应堆模型&#xff0c;它是一种基于事件驱动和I/O多路复用的设计模式&#xff0c;常用于处理大量并发I/O事件&#xff0c;是一个高性能模型&#xff0c;它通过事件驱动的方式&#xff0c;高效地管…

parted 磁盘分区

目录 磁盘格式磁盘分区文件系统挂载使用扩展 - parted、fdisk、gdisk 区别 磁盘格式 parted /dev/vdcmklabel gpt # 设置磁盘格式为GPT p # 打印磁盘信息此时磁盘格式设置完成&#xff01; 磁盘分区 开始分区&#xff1a; mkpart data_mysql # 分区名&…

django图书管理系统-计算机毕业设计源码00648

摘要 图书管理系统在数字化阅读趋势、图书馆自动化管理、用户体验需求和信息技术应用等方面具有重要的研究意义。图书馆自动化管理系统的引入和应用提高了图书借阅过程的效率和准确性&#xff0c;减少了对手工操作和纸质记录的需求。用户对系统的易用性、查询速度、借还流程有更…