php论坛网站源码下载,小程序开发制作流程,点击链接即可进入,简述什么是seo及seo的作用文章目录 一、左值与左值引用二、右值与右值引用三、 左值引用与右值引用比较四、右值引用使用场景和意义1.左值引用的短板2.移动构造和移动赋值3.STL中右值引用的使用 五、万能引用与完美转发1.万能引用2.完美转发 一、左值与左值引用
在C11之前#xff0c;我们把数据分为常… 文章目录 一、左值与左值引用二、右值与右值引用三、 左值引用与右值引用比较四、右值引用使用场景和意义1.左值引用的短板2.移动构造和移动赋值3.STL中右值引用的使用 五、万能引用与完美转发1.万能引用2.完美转发 一、左值与左值引用
在C11之前我们把数据分为常量和变量在C11之后我们将数据分为左值和右值
此外传统的C语法中就有引用的语法而C11中新增了的右值引用语法特性所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用都是给对象取别名
左值是一个表示数据的表达式(如变量名或解引用的指针)我们可以获取它的地址还可以对它赋值左值可以出现赋值符号的左边也可以出现在赋值符号的右边定义时const修饰符后的左值不能给他赋值但是可以取它的地址如下
// 以下的p、b、c、*p都是左值
int* p new int(0);
int b 1;
const int c 2;左值引用就是给左值的引用给左值取别名
// 以下几个是对上面左值的左值引用
int* rp p;
int rb b;
const int rc c;
int pvalue *p;二、右值与右值引用
右值也是一个表示数据的表达式如字面常量、表达式返回值函数返回值(这个不能是左值引用返回)等等右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边右值不能取地址,如下
double x 1.1, y 2.2;
// 以下几个都是常见的右值
10;
x y;
fmin(x, y);
// 这里编译会报错error C2106: “”: 左操作数必须为左值
10 1;
x y 1;
fmin(x, y) 1;右值引用就是对右值的引用给右值取别名
// 以下几个都是对右值的右值引用
int rr1 10;
double rr2 x y;
double rr3 fmin(x, y);注意事项
1.为什么函数返回值是右值当函数返回的是一个局部变量的时候因为局部变量出了函数作用域生命周期就会结束所以返回时会将该变量拷贝到寄存器中然后返回这个寄存器中的内容而寄存器中的变量是临时变量临时变量具有常量属于右值。其实在函数建立栈帧的时候不仅会有参数的压栈还会有返回值的压栈即在两个函数的栈帧之后的一个空间在存贮函数的返回值。通过这个中间值将函数的返回值进行返回。
2.为什么右值不能取地址在C中右值则是一个临时使用的不可寻址的内存空间右值没有独立的内存空间它只是存储在寄存器或者其他临时内存中的一个值我们也不能将右值放入内存因为右值没有确定的内存位置所以右值不能取地址
需要注意的是右值是不能取地址的但是给右值取别名后会导致右值被存储到特定位置且可以取到该位置的地址也就是说例如不能取字面量10的地址但是rr1引用后可以对rr1取地址也可以修改rr1。如果不想rr1被修改可以用const int rr1 去引用是不是感觉很神奇这个了解一下实际中右值引用的使用场景并不在于此这个特性也不重要。
int main()
{int x 1, y 2;int rr1 10;const int rr2 x y;rr1;cout rr1 endl;cout rr1 endl;cout rr2 endl;return 0;
}rr2 5; // 报错所以如果我们不希望改变右值引用我们就需要将右值引用定义为const右值引用
三、 左值引用与右值引用比较
左值引用只能引用左值不能引用右值。但是const左值引用既可引用左值也可引用右值,因为 const左值引用也是只读的而权限可以平移
int main()
{// 左值引用只能引用左值不能引用右值。int a 10;int ra1 a; // ra为a的别名//int ra2 10; // 编译失败因为10是右值// const左值引用既可引用左值也可引用右值。const int ra3 10;const int ra4 a;return 0;
}右值引用只能右值不能引用左值但是右值引用可以move以后的左值。
int main()
{// 右值引用只能右值不能引用左值。int r1 10;// error C2440: “初始化”: 无法从“int”转换为“int ”// message : 无法将左值绑定到右值引用//int a 10;//int r2 a;// 右值引用可以引用move以后的左值int r3 std::move(a);return 0;
}四、右值引用使用场景和意义
1.左值引用的短板
前面我们可以看到左值引用既可以引用左值和又可以引用右值那为什么C11还要提出右值引用呢
我们先看看左值引用的两个作用
1.修改实参的值
2.引用做参数/函数返回值可以减少拷贝
我们可以把函数形参定义为实参的引用这样函数在传参时就不用拷贝构造形参了从而提高程序的效率特别是对于需要深拷贝的自定义类型来说。左值引用作为返回值的效果也一样当返回的对象出了作用域还存在时直接使用引用返回可以减少一次拷贝构造
void func1(string s)
{}
void func2(const string s)
{}
int main()
{string s1(hello world);// func1和func2的调用我们可以看到左值引用做参数减少了拷贝提高效率的使用场景和价值func1(s1);func2(s1);// string operator(char ch) 传值返回存在深拷贝// string operator(char ch) 传左值引用没有拷贝提高了效率s1 !;return 0;
}但是当函数返回对象是一个局部变量出了函数作用域就不存在了就不能使用左值引用返回只能传值返回。因为局部变量出了函数作用域就不存在了此时引用就是一个野指针
template class T
T func(const T x)
{T tmp;//...return tmp;
}这种情况下编译器会使用这个临时对象拷贝构造一个临时对象然后再返回这个临时对象也就是说这样会比引用返回多一次拷贝构造当局部对象是一个需要进行深拷贝的自定义类型的时候比如vectorvector,拷贝构造的代价就会很大所以右值引用的提出就是为了补足左值引用存在的这些短板
2.移动构造和移动赋值
为了更好的演示左值引用和右值引用对拷贝构造的优化我们自己实现一个string类在拷贝构造/赋值重载函数中进行打印相关的信息便于观察
namespace hdp
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str _size;}string(const char* str ):_size(strlen(str)), _capacity(_size){cout string(char* str) -- 构造 endl;_str new char[_capacity 1];strcpy(_str, str);}// s1.swap(s2)void swap(string s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string s):_str(nullptr){cout string(const string s) -- 深拷贝 endl;string tmp(s._str);swap(tmp);}// 赋值重载string operator(const string s){cout string operator(string s) -- 深拷贝 endl;string tmp(s);swap(tmp);return *this;} 移动构造//string(string s)// :_str(nullptr)// , _size(0)// , _capacity(0)//{// cout string(string s) -- 移动语义 endl;// swap(s);//} 移动赋值//string operator(string s)//{// cout string operator(string s) -- 移动语义 endl;// swap(s);// return *this;//}~string(){delete[] _str;_str nullptr;}char operator[](size_t pos){assert(pos _size);return _str[pos];}void reserve(size_t n){if (n _capacity){char* tmp new char[n 1];strcpy(tmp, _str);delete[] _str;_str tmp;_capacity n;}}void push_back(char ch){if (_size _capacity){size_t newcapacity _capacity 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] ch;_size;_str[_size] \0;}//string operator(char ch)string operator(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}现在假设我们要实现一个to_string函数代码如下
namespace hdp
{hdp::string to_string(int value){bool flag true;if (value 0){flag false;value 0 - value;}hdp::string str;while (value 0){int x value % 10;value / 10;str (0 x);}if (flag false){str -;}std::reverse(str.begin(), str.end());return str;}
}我们可以看到由于to_string函数的返回值是一个局部的对象所以我们这里只能使用传值返回而传值返回对于string来说需要进行深拷贝 其实这里程序的执行结果和我们预想的并不一样正常的情况应该是str先拷贝构造一个临时对象然后再由这个临时对象来拷贝构造ret所以应该是两个拷贝构造()上面的结果第一次构造是to_string函数内部构造str,第二次是函数返回时调用拷贝构造而我们实现的拷贝构造函数中又调用了一次构造函数所以打印了两个构造。但是编译器的优化只能适用于部分场景对于很多场景还是会拷贝构造产生临时对象 尽管编译器进行了优化这里还是会有一次拷贝构造那么我们能不能想办法将str的资源直接赋值给s中间不产生拷贝构造呢此时我们就需要用到右值引用 了
C11中的右值可以分为两种
1.纯右值:内置类型表达式的值
2.将亡值自定义类型表达式的值所谓的将亡值就是指声明周期马上就要结束 的值一般来说匿名对象临时对象move后的自定义类型都可以看做将亡值
按照语法右值引用只能引用右值但右值引用一定不能引用左值吗因为有些场景下可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时可以通过move函数将左值转化为右值。C11中std::move()函数位于 头文件中该函数名字具有迷惑性它并不搬移任何东西唯一的功能就是将一个左值强制转化为右值引用然后实现移动语义
我们需要注意的是上面我们说右值不能取地址其实是右值的严格定义但其实将亡值也是可以被当做右值看待的而将亡值有独立的内存空间可以取地址。既然将亡值的声明周期马上就要结束了那么在拷贝构造中我么可以直接将将亡值的资源拿过来给我们自己使用这样就需要需要进行深拷贝了。
void swap(string s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
// 移动构造
string(string s):_str(nullptr), _size(0), _capacity(0)
{cout string(string s) -- 移动语义 endl;swap(s);
}这样我们就重载了一个右值引用版本的构造函数–移动构造这样当参数为右值的对象需要进行拷贝构造的时候就会调用此函数在函数中我们直接交换两个对象的资源从而使得深拷贝变成了浅拷贝提供了程序的效率
移动构造本质是将参数右值的资源窃取过来占位已有那么就不用做深拷贝了所以它叫做移动构造就是窃取别人的资源来构造自己
我们使用移动构造之后我们之前的程序就会减少深拷贝的次数 由此我们通过移动构造将深拷贝变成了浅拷贝
但是我们需要注意的是只有当参数为右值时才会调用移动构造当实参为左值的时候还是会调用拷贝构造函数因为编译器不知道我们是否还会对左值进行操作所以它不敢拿走左值的资源来构造新的对象 移动赋值和移动构造同理只是移动赋值中将亡值还需要释放之前的资源不过这个过程是自动的
// 移动赋值
string operator(string s)
{cout string operator(string s) -- 移动语义 endl;swap(s);return *this;
}有人提出这样的一个观点右值引用延长了变量的声明周期这种说法是不准确的因为右值引用只是将变量的资源转移给了另一个变量让它的资源能够不随着变量的销毁而释放而该变量本身的生命周期是没有变化的
【总结】
1.左值引用让形参称为实参的别名直接减少拷贝
2.右值引用是通过实现移动构造和移动赋值将将亡值的资源进行转移间接的减少拷贝(浅拷贝的类不需要进行资源的转移所以也没有移动赋值和移动拷贝)
3.STL中右值引用的使用
C11在设计出右值引用之后为STL所有容器都提供了移动构造和移动赋值包括容器适配器 此外还提供了右值版本的插入接口 所以以后如果我们要向容器中插入需要深拷贝的自定义类型的数据时我们尽量使用匿名构造对象进行插入这样调用的就是右值插入接口元素会调用移动拷贝函数完成拷贝从而提高程序的效率
我们可以将我们自己实现的list类支持右值版本的插入接口部分代码如下
#pragma once
#include assert.h
#include algorithmnamespace hdp
{// 定义节点结构templateclass Tstruct list_node{list_nodeT* _prev;list_nodeT* _next;T _data;// 构造list_node(const T x):_prev(nullptr), _next(nullptr), _data(x){}// 移动构造list_node(T x):_prev(nullptr), _next(nullptr), _data(move(x)){}};// 封装迭代器// 同一个类模板实例化出的两个类型// typedef __list_iteratorT, T, T* iterator;// typedef __list_iteratorT, const T, const T* const_iterator;templateclass T, class Ref, class Ptr// 迭代器类struct __list_iterator{typedef list_nodeT node; // 重命名为list节点typedef __list_iteratorT, Ref, Ptr Self;// 成员变量node* _pnode;// 构造__list_iterator(node* p):_pnode(p){}// 重载箭头Ptr operator-(){return _pnode-_data;}// 重载*Ref operator*(){return _pnode-_data;}// 重载前置Self operator(){_pnode _pnode-_next;return *this;}// 重载后置Self operator(int){Self tmp(*this);_pnode _pnode-next;return tmp;}//重载前置--Self operator--(){_pnode _pnode-_prev;return *this;}// 重载后置--Self operator--(int){Self tmp(*this);_pnode _pnode-_prev;return tmp;}// 重载不等于bool operator!(const Self it) const{return it._pnode ! _pnode;}// 重载等于bool operator(const Self it) const{return _pnode it._pnode;}};// 定义list类templateclass Tclass list{// list 节点typedef list_nodeT node;public:typedef __list_iteratorT, T, T* iterator; //迭代器typedef __list_iteratorT, const T, const T* const_iterator; //const迭代器// 迭代器iterator begin(){return iterator(_head-_next);}iterator end(){// iterator it(_head);// return it;// 匿名对象构造return iterator(_head);}// const 迭代器const_iterator begin() const{return const_iterator(_head-_next);}const_iterator end() const{return const_iterator(_head);}// 创建哨兵节点void empty_initialize(){_head new node(T());_head-_next _head;_head-_prev _head;_size 0;}// 构造 不是listT的原因构造函数名和类名相同而listT是类型list(){empty_initialize();}// 迭代器构造templateclass InputIteratorlist(InputIterator first, InputIterator last){empty_initialize();while (first ! last){push_back(*first);first;}}// 拷贝构造// lt2(lt1)list(const listT lt){empty_initialize();for (const auto e : lt){push_back(e);}}// 拷贝构造现代写法list(listT lt){empty_initialize();listT tmp(lt.begin(), lt.end());swap(tmp);}// 赋值重载listT operator(const listT lt){if (this ! lt){clear();for (const auto e : lt){push_back(e);}}return *this;}// 赋值重载现代写法listT operator(listT lt){swap(lt);return *this;}// 交换void swap(listT lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}size_t size() const{return _size;}bool empty() const{// return _head-_next _head;// return _head-_prev _head;return _size 0;}// 析构~list(){clear();delete _head;_head nullptr;}// 清理void clear(){iterator it begin();while (it ! end()){it erase(it);}_size 0;}// 尾插void push_back(const T x){//node* newnode new node(x);//node* tail _head-_prev; _head tail newnode//tail-_next newnode;//newnode-_prev tail;//newnode-_next _head;//_head-_prev newnode;insert(end(), x);}// 尾插--右值版本void push_back(T x){insert(end(), move(x));}// 头插void push_front(T x){insert(begin(), move(x));}// 头插 -- 右值版本void push_front(const T x){insert(begin(), x);}// 尾删void pop_back(){//earse(end()-prev);erase(--end());}// 头删void pop_front(){erase(begin());}// 在pos之前插入数据iterator insert(iterator pos, const T x){node* newnode new node(x);node* cur pos._pnode;node* prev cur-_prev;// prev newnode curprev-_next newnode;newnode-_prev prev;newnode-_next cur;cur-_prev newnode;_size;return iterator(newnode);}// 在pos之前插入数据 -- 右值版本iterator insert(iterator pos, T x){node* newnode new node(move(x));node* cur pos._pnode;node* prev cur-_prev;// prev newnode curprev-_next newnode;newnode-_prev prev;newnode-_next cur;cur-_prev newnode;_size;return iterator(newnode);}// 删除pos位置的数据iterator erase(iterator pos){assert(pos ! end());node* prev pos._pnode-_prev;node* next pos._pnode-_next;prev-_next next;next-_prev prev;delete pos._pnode;--_size;return iterator(next);}private:node* _head;size_t _size 0; // 保存节点个数};
}list的函数调用逻辑如下 我们需要注意的是右值函数的形参的类型都是T x,而不是const T x这是因为最终在hdp::string类中我们需要将x的资源转移给别人这就要求x必须是可以修改的能够交换_str,_size,_capacity三个指针此外右值引用x之所以能够被修改是因为给右值取别名之后右值会被存储起来右值引用虽然引用的是右值但是右值引用本身是左值所以当我们继续往下一层进行传递参数的时候我们需要将x重新move为右值否则下一层调用时就会调用参数为左值的函数
五、万能引用与完美转发
1.万能引用
我们上面都是单独定义一个参数为右值引用的函数然后让编译器根据实参的类型来判断调用左值引用还是右值引用的函数我们能不能让函数能够根据实参的类型自动实例化出对应的不同函数呢万能引用就实现了这个功能
万能引用是一个函数模板且函数的形参类型为右值引用对于这样的函数模板模板编译器能够自动根据实参的类型–左值/const 左值/右值/const右值自动推演实例化出不同参数形参分别为左值引用/const左值引用/右值引用/const右值引用的函数
templatetypename T
void PerfectForward(T t)
{Fun(t);
}
int main()
{PerfectForward(10); // 右值int a;const int b 8;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}我们可以看到无论实参是什么类型模板函数都能正确的接收并实例化出对应的引用类型所以我们把形参为右值引用的函数模板你叫做万能引用其中当实参为左值或者const左值的时候T会被实例化为T 或者const T我们称其为引用折叠即将折叠为
2.完美转发
尽管完整引用能够接收任何类型的参数但是这里还是存在一个很大的问题万能引用实例化后函数的形参的属性全都是左值如果实参为左值/const左值那么实例化函数的形参是左值/const左值如果实参是右值/const右值虽然实例化函数的形参是右值引用/const右值引用但是右值引用本身是左值所以就会出现下面的情况
void Fun(int x) { cout 左值引用 endl; }
void Fun(const int x) { cout const 左值引用 endl; }
void Fun(int x) { cout 右值引用 endl; }
void Fun(const int x) { cout const 右值引用 endl; }
templatetypename T
void PerfectForward(T t)
{Fun(t);
}
int main()
{int a;const int b 8;PerfectForward(a); // 左值PerfectForward(b); // const 左值PerfectForward(10); // 右值PerfectForward(std::move(b)); // const 右值return 0;
}此外我们这里也不能简单的将t move后传递给Fun函数因为这样会让t全部变为右值又满足不了实参为左值的情况 为了在传参的时候能够保留对象的原始类型C设计了 完美转发–forward 【总结】
1.为了弥补左值引用局部对象返回会发生拷贝构造问题C11设计出了右值引用右值引用可以通过移动构造和移动赋值来实现资源转移将深拷贝转化为浅拷贝从而提高了效率此外还为STL容器提供了右值版本的插入接口由于右值引用本身是左值所以函数参数往下一层传递时不能保证参数仍为右值所以提供了move可以将左值变为右值
2.为了使得函数模板能够同时接收const左值和const右值并正确实例化为对应的引用类型C11设计了万能引用但是无论是左值引用还是右值引用其本身是左值所以往下一层传递时不能保证其类型了此时move也不能够解决问题了所以C11设计了完美转发来保证传参过程中原生类型属性能够保持不变