网站建设类课题的研究方法,网站建设 蜀美网络,建设银行天津招聘网站,公司网站制作的方法文章目录左值引用与右值引用1、左值与右值2、纯右值、将亡值3、左值引用与右值引用4、右值引用和 std::move 使用场景引用限定符移动语义—std::move()完美转发emplace_back 减少内存拷贝和移动总结c11中引用了右值引用和移动语义#xff0c;可以避免无谓的复制#xff0c;提…
文章目录左值引用与右值引用1、左值与右值2、纯右值、将亡值3、左值引用与右值引用4、右值引用和 std::move 使用场景引用限定符移动语义—std::move()完美转发emplace_back 减少内存拷贝和移动总结c11中引用了右值引用和移动语义可以避免无谓的复制提高了程序性能。左值引用与右值引用
1、左值与右值
概念1 左值可以放到等号左边的东西叫左值。右值不可以放到等号左边的东西就叫右值。 概念2 左值可以取地址并且有名字的东西就是左值。右值不能取地址的没有名字的东西就是右值。 概念3 左值是指那些在表达式执行结束后依然存在的数据也就是持久性的数据。右值是指那些在表达式执行结束后不再存在的数据也就是临时性的数据。
有一种很简单的方法来区分左值和右值对表达式取地址如果编译器不报错就为左值否则为右值。例如int a b c;a 是左值有变量名可以取地址也可以放到等号左边表达式 bc 的返回值是右值没有名字且不能取地址(bc) 不能通过编译而且也不能放到等号左边。
左值一般有
变量名和函数名注意是函数名不是函数调用返回左值引用的函数调用前置自增自减表达式i、–i由赋值表达式或赋值运算符连接的表达式ab, a b等解引用表达式 *p字符串字面值 “abcd”
2、纯右值、将亡值
纯右值和将亡值都属于右值。
纯右值运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda 表达式等都是纯右值。举例
除字符串字面值外的字面值返回非引用类型的函数调用后置自增自减表达式 i、i–算术表达式 aba*babab 等取地址表达式等a
将亡值 将亡值是指 c11 新增的和右值引用相关的表达式通常指将要被移动的对象、T 函数的返回值、std::move函数的返回值、转换为 T 类型转换函数的返回值将亡值可以理解为即将要销毁的值通过“盗取”其它变量内存空间方式获取的值在确保其它变量不再被使用或者即将被销毁时可以避免内存空间的释放和分配延长变量值的生命周期常用来完成移动构造或者移动赋值的特殊任务。举例
class A {xxx;
};A a;
auto c std::move(a); // c是将亡值
auto d static_castA(a); // d是将亡值3、左值引用与右值引用
左值引用就是对左值进行引用的类型右值引用就是对右值进行引用的类型他们都是引用都是对象的一个别名并不拥有所绑定对象的堆存所以都必须立即初始化。引用可以通过引用修改变量的值传参时传引用可以避免拷贝。
type name exp; // 左值引用
type name exp; // 右值引用左值引用 左值引用能指向左值不能指向右值的就是左值引用
int a 5;
int b a; // b是左值引用
b 4;int c 10; // error10无法取地址无法进行引用
const int d 10; // ok因为是常引用引用常量数字这个常量数字会存储在内存中可以取地址。引用是变量的别名由于右值没有地址没法被修改所以左值引用无法指向右值等号右边的值必须可以取地址如果不能取地址则会编译失败。
但是const 左值引用常量引用是可以指向右值的const 左值引用不会修改指向值因此可以指向右值这也是为什么要使用 const 作为函数参数的原因之一。
右值引用 c11 标准新引入了另一种引用方式称为右值引用用 “” 表示。如果使用右值引用那表达式等号右边的值需要是右值不能是左值可以使用 std::move 函数强制把左值转换为右值。
int a 4;
int b a; // error, a 是左值
int c std::move(a); // okint num 10;
int a num; //error, 右值引用不能初始化为左值
int a 10; // ok【注意】和声明左值引用一样右值引用也必须立即进行初始化操作。
左值引用与右值引用本质
1右值引用指向左值
int a 5; // a是个左值
int ref_a_left a; // 左值引用指向左值
int ref_a_right std::move(a); // 通过std::move将左值转化为右值可以被右值引用指向
cout a; // 打印结果5前面讲过可以使用 std::move 函数强制把左值转换为右值实现右值引用指向左值。std::move 是一个非常有迷惑性的函数
不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量比如在上边的代码里看上去是左值 a 通过 std::move 移动到了右值 ref_a_right 中那是不是a里边就没有值了并不是打印出a的值仍然是5。事实上 std::move 移动不了什么唯一的功能是把左值强制转化为右值让右值引用可以指向左值。其实现等同于一个类型转换 static_castT(lvalue)。 所以单纯的 std::move(xxx) 不会有性能提升。
同样的右值引用能指向右值本质上也是把右值提升为一个左值并定义一个右值引用通过 std::move
int ref_a 5;
ref_a 6;// 等同于以下代码
int temp 5;
int ref_a std::move(temp);
ref_a 6;2左值引用、右值引用本身是左值还是右值
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的也位于等号左边。仔细看下边代码
// 形参是个右值引用
void change(int right_value) { right_value 8; }
int main() {int a 5; // a是个左值int ref_a_left a; // ref_a_left是个左值引用int ref_a_right std::move(a); // ref_a_right是个右值引用change(a); // 编译不过a是左值change参数要求右值change(ref_a_left); // 编译不过左值引用ref_a_left本身也是个左值change(ref_a_right); // 编译不过右值引用ref_a_right本身也是个左值change(std::move(a)); // 编译通过change(std::move(ref_a_right)); // 编译通过change(std::move(ref_a_left)); // 编译通过change(5); // 当然可以直接接右值编译通过cout a ;cout ref_a_left ;cout ref_a_right;// 打印这三个左值的地址都是一样的
}看完后你可能有个问题std::move 会返回一个右值引用 int 它是左值还是右值呢 从表达式 int ref std::move(a) 来看右值引用 ref 指向的必须是右值所以move返回的 int 是个右值。所以右值引用既可能是左值又可能是右值吗 确实如此右值引用既可以是左值也可以是右值如果有名称则为左值否则是右值。
或者说作为函数返回值的 是右值直接声明出来的 是左值。 这同样也符合前面章节对左值右值的判定方式其实引用和普通变量是一样的 int ref std::move(a) 和 int a 5 没有什么区别等号左边就是左值右边就是右值。
3无论是左值引用还是右值引用都是引用
int temp 5;
int ref_t temp;
int ref_a std::move(temp);
ref_a 6;
cout temp , ref_t , ref_aendl;
cout temp: temp endl;
// 输出结果
// 0x61fe84 0x61fe84 0x61fe84
// temp:6最后从上述分析中我们得到如下结论
从性能上讲左右值引用没有区别传参使用左右值引用都可以避免拷贝。右值引用可以直接指向右值也可以通过 std::move 指向左值而左值引用只能指向左值const左值引用也能指向右值。作为函数形参时右值引用更灵活。虽然 const 左值引用也可以做到左右值都接受但它无法修改有一定局限性。
void f(const int n) {n 1; // 编译失败const左值引用不能修改指向变量
}void f2(int n) {n 1; // ok
}int main() {f(5);f2(5);
}4、右值引用和 std::move 使用场景
std::move 只是类型转换工具不会对性能有好处右值引用在作为函数形参时更具灵活性。他们有什么实际应用场景吗
1、右值引用优化性能避免深拷贝 1浅拷贝重复释放对于含有堆内存的类我们需要提供深拷贝的拷贝构造函数如果使用默认构造函数会导致堆内存的重复删除比如下面的代码
class A {
public:A(int size) : size_(size) { data_ new int[size]; }A() {}A(const A a) {size_ a.size_;data_ a.data_;cout copy endl;}~A() { delete[] data_; }int* data_;int size_;
};int main() {A a(10);A b a;cout b b.data_ endl;cout a a.data_ endl;return 0;
}上面代码中两个输出的是相同的地址a 和 b 的 data_ 指针指向了同一块内存这就是浅拷贝只是数据的简单赋值那再析构时 data_ 内存会被释放两次导致程序出问题这里正常会出现 double free 导致程序崩溃的。
2深拷贝构造函数 在上面的代码中默认构造函数是浅拷贝在析构的时候会导致重复删除指针。正确的做法是提供深拷贝的拷贝构造函数比如下面的代码
#include iostream
using namespace std;
class A {
public:A() : m_ptr(new int(0)) { cout constructor A endl; }A(const A a) : m_ptr(new int(*a.m_ptr)) {cout copy constructor A endl;}~A() {cout destructor A, m_ptr: m_ptr endl;delete m_ptr;m_ptr nullptr;}
private:int* m_ptr;
};// 为了避免返回值优化此函数故意这样写
A Get(bool flag) {A a;A b;cout ready return endl;if (flag)return a;elsereturn b;
}int main() {{A a Get(false); // 正确运行}cout main finish endl;return 0;
}深拷贝就是在拷贝对象时如果被拷贝对象内部还有指针引用指向其它资源自己需要重新开辟一块新内存存储资源而不是简单的赋值。虽然深拷贝可以解决浅拷贝的问题但是存在效率问题。
3移动构造函数 深拷贝构造函数可以保证拷贝构造时的安全性但有时这种拷贝构造存在效率问题比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量然后通过这个临时变量拷贝构造了一个新的对象 b临时变量在拷贝构造完成之后就销毁了如果堆内存很大那么这个拷贝构造的代价会很大带来了额外的性能损耗。
有没有办法避免临时对象的拷贝构造呢答案是肯定的。看下面的代码
#include iostream
using namespace std;
class A {
public:A() : m_ptr(new int(0)) { cout constructor A endl; }A(const A a) : m_ptr(new int(*a.m_ptr)) {cout copy constructor A endl;}// 移动构造函数可以浅拷贝A(A a) : m_ptr(a.m_ptr) {a.m_ptr nullptr; // 为防止a析构时delete data提前置空其m_ptrcout move constructor A endl;}~A() {cout destructor A, m_ptr: m_ptr endl;if (m_ptr) delete m_ptr;}
private:int* m_ptr;
};// 为了避免返回值优化此函数故意这样写
A Get(bool flag) {A a;A b;cout ready return endl;if (flag)return a; elsereturn b;
}int main() {{A a Get(false); // 返回右值调用移动构造函数}cout main finish endl;return 0;
}上面的代码中实现了移动构造 Move Construct。从移动构造函数的实现中可以看到它的参数是一个右值引用类型的参数 A这里没有深拷贝只有浅拷贝这样就避免了对临时对象的深拷贝提高了性能。
在实际开发中通常在类中自定义移动构造函数的同时会再为其自定义一个适当的拷贝构造函数由此当用户利用右值初始化类对象时会调用移动构造函数使用左值非右值初始化类对象时会调用拷贝构造函数。 这里的 A 用来根据参数是左值还是右值来建立分支如果是临时值则会选择移动构造函数。
移动构造函数只是将临时对象的资源做了浅拷贝不需要对其进行深拷贝从而避免了额外的拷贝提高性能。这也就是所谓的移动语义 move 语义右值引用的一个重要目的是用来支持移动语义的移动语义的分析详细见下文。
引用限定符
将左值的类对象称为左值对象将右值的类对象称为右值对象。默认情况下对于类中用 public 修饰的成员函数既可以被左值对象调用也可以被右值对象调用举个例子
#include iostream
using namespace std;class demo {
public:demo(int num) : num(num) {}int get_num() { return this-num; }private:int num;
};int main() {demo a(10);cout a.get_num() endl;cout move(a).get_num() endl;return 0;
}可以看到demo 类中的 get_num() 成员函数既可以被 a 左值对象调用也可以被 move(a) 生成的右值 demo 对象调用运行程序会输出两个 10。
某些场景中我们可能需要限制调用成员函数的对象的类型左值还是右值为此 c11 新添加了引用限定符。所谓引用限定符就是在成员函数的后面添加 “” 或者 “”从而限制调用者的类型左值还是右值。【注意】引用限定符不适用于静态成员函数和友元函数。
// 代码修改
class demo {
public:demo(int num) : num(num) {}int get_num() { return this-num; } // 添加了 限定调用该函数的对象必须是左值对象private:int num;
};int main() {demo a(10);cout a.get_num() endl; // 正确// cout move(a).get_num() endl; // 错误return 0;
}// 代码修改
class demo {
public:demo(int num) : num(num) {}int get_num() { return this-num; } // 添加了 限定调用该函数的对象必须是右值对象private:int num;
};int main() {demo a(10);//cout a.get_num() endl; // 错误cout move(a).get_num() endl; // 正确return 0;
}const 和引用限定符 const 也可以用于修饰类的成员函数习惯称为常成员函数。 const 和引用限定符修饰类的成员函数时都位于函数的末尾。C11 标准规定当引用限定符和 const 修饰同一个类的成员函数时const 必须位于引用限定符前面。如下
#include iostream
using namespace std;
class demo {
public:demo(int num, int num2) : num(num), num2(num2) {}//左值和右值对象都可以调用int get_num() const { return this-num; }//仅供右值对象调用int get_num2() const { return this-num2; }private:int num;int num2;
};【注意】当 const 修饰类的成员函数时调用它的对象只能是右值对象当 const 修饰类的成员函数时调用它的对象既可以是左值对象也可以是右值对象。无论是 const 还是 const 限定的成员函数内部都不允许对当前对象做修改操作。
移动语义—std::move()
所谓移动语义指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象之前的拷贝是对于别人的资源自己重新分配一块内存存储复制过来的资源而对于移动语义类似于转让或者资源窃取的意思对于那块资源转为自己所拥有别人不再拥有也不会再使用通过 c11 新增的移动语义可以省去很多拷贝负担怎么利用移动语义呢是通过移动构造函数。
移动语义可以将资源堆、系统对象等通过浅拷贝方式从一个对象转移到另一个对象这样能够减少不必要的临时对象的创建、拷贝以及销毁可以大幅度提高 c 应用程序的性能消除临时对象的维护创建和销毁对性能的影响。
class A {
public:A(int size) : size_(size) { data_ new int[size]; }A() {}A(const A a) {size_ a.size_;data_ new int[size_];cout copy endl;}A(A a) { // 移动构造函数this-data_ a.data_;a.data_ nullptr;cout move endl;}~A() {if (data_ ! nullptr) {delete[] data_;}}int* data_;int size_;
};int main() {A a(10);A b a;A c std::move(a); // 返回右值调用移动构造函数return 0;
}如果不使用 std::move()会有很大的拷贝代价使用移动语义可以避免很多无用的拷贝提供程序性能c 所有的 STL 都实现了移动语义方便我们使用。
【注意1】移动语义仅针对于那些实现了移动构造函数的类的对象对于那种基本类型 int、float 等没有任何优化作用还是会拷贝因为它们实现没有对应的移动构造函数。 【注意2】在实际开发中通常在类中自定义移动构造函数的同时会再为其自定义一个适当的拷贝构造函数由此当用户利用右值初始化类对象时会调用移动构造函数使用左值非右值初始化类对象时会调用拷贝构造函数。
完美转发
首先解释一下什么是完美转发它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美即不仅能准确地转发参数的值还能保证被转发参数的左、右值属性不变。例如
template typename T
void function(T t) {otherdef(t);
}如上所示function() 函数模板中调用了 otherdef() 函数。在此基础上完美转发指的是如果 function() 函数接收到的参数 t 为左值那么该函数传递给 otherdef() 的参数 t 也是左值反之如果 function() 函数接收到的参数 t 为右值那么传递给 otherdef() 函数的参数 t 也必须为右值。
显然 function() 函数模板并没有实现完美转发。一方面参数 t 为非引用类型这意味着在调用 function() 函数时实参将值传递给形参的过程就需要额外进行一次拷贝操作另一方面无论调用 function() 函数模板时传递给参数 t 的是左值还是右值对于函数内部的参数 t 来说它有自己的名称也可以获取它的存储地址因此它永远都是左值也就是说传递给 otherdef() 函数的参数 t 永远都是左值。总之无论从那个角度看 function() 函数的定义都不“完美”。
接下来那如何实现完美转发呢答案是使用 std::forward()
首先在定义模板函数时采用右值引用的语法格式定义参数类型由此该函数既可以接收外界传入的左值也可以接收右值其次还需要使用 c11 标准库提供的 std::forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。 由此即可轻松实现函数模板中参数的完美转发如下所示
void PrintV(int t) { cout lvalue endl;
}
void PrintV(int t) { cout rvalue endl;
}template typename T
void Test(T t) { // 1、采用右值引用的语法格式定义参数类型PrintV(t);PrintV(std::forwardT(t));PrintV(std::move(t));
}int main() {Test(1); // lvalue rvalue rvalueint a 1;Test(a); // lvalue lvalue rvalue// 2、使用 std::forword() 模板函数修饰被调用函数Test(std::forwardint(a)); // lvalue rvalue rvalueTest(std::forwardint(a)); // lvalue lvalue rvalueTest(std::forwardint(a)); // lvalue rvalue rvaluereturn 0;
}Test(1)1是右值模板中 T t 这种为万能引用右值 1 传到 Test 函数中变成了右值引用但是调用 PrintV() 时候t 变成了左值因为它变成了一个拥有名字的变量所以打印 lvalue而 PrintV(std::forwardT(t)) 时候会进行完美转发按照原来的类型转发所以打印 rvaluePrintV(std::move(t)) 毫无疑问会打印 rvalue。Test(a)a 是左值模板中 T 这种为万能引用左值 a 传到 Test 函数中变成了左值引用所以有代码中打印。Test(std::forwardT(a))转发为左值还是右值依赖于 TT 是左值那就转发为左值T 是右值那就转发为右值。
#include iostream
using namespace std;//重载被调用函数查看完美转发的效果
void otherdef(int t) {cout lvalue\n;
}void otherdef(const int t) {cout rvalue\n;
}//实现完美转发的函数模板
template typename T
void function(T t) {otherdef(forwardT(t));
}int main()
{function(5); // rvalueint x 1;function(x); // lvaluereturn 0;
}
// 打印结果
// rvalue
// lvalueemplace_back 减少内存拷贝和移动
对于STL容器c11 后引入了 emplace_back 接口。emplace_back 是就地构造不用构造后再次复制到容器中因此效率更高。考虑这样的语句
vectorstring testVec;
testVec.push_back(string(16, a));上述语句足够简单易懂将一个 string 对象添加到 testVec 中。底层实现
首先string(16, ‘a’) 会创建一个 strin g类型的临时对象这涉及到一次string 构造过程。其次vector 内会创建一个新的 string 对象这是第二次构造。最后在 push_back 结束时最开始的临时对象会被析构。加在一起这两行代码会涉及到两次 string 构造和一次析构。
c11 可以用 emplace_back 代替 push_backemplace_back 可以直接在vector中构建一个对象而非创建一个临时对象再放进vector再销毁。emplace_back可以省略一次构建和一次析构从而达到优化的目的。
emplace_back 内部没有使用拷贝构造函数也没有使用移动构造函数而是直接调用构造函数因此更加高效。
总结
c11 在性能上做了很大的改进最大程度减少了内存移动和复制通过右值引用、 forward、emplace 和一些无序容器我们可以大幅度改进程序性能。
右值引用仅仅是通过改变资源的所有者剪切方式而不是拷贝方式来避免内存的拷贝能大幅度提高性能。forward 能根据参数的实际类型转发给正确的函数参数用 的方式。emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。