设为首页 加入收藏

TOP

[C++]右值引用和转移语义(一)
2016-04-30 15:25:10 】 浏览:737
Tags:引用 转移 语义

右值引用和转移语义

本文尝试着解释何为右值引用和转移语义以及使用它们具有优势,并提供相关案例分析。

定义

左值和右值

首先我们先来理解一下什么是左值和右值

C/C++语言中可以放在赋值符号左边的变量,左值表示存储在计算机内存的对象,左值相当于地址值。右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值,右值相当于数据值。

C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。简单来说就是,左值相当于地址值,右值相当于数据值。右值指的是引用了一个存储在某个内存地址里的数据。

左值右值翻译:

L-value中的L指的是Location,表示可寻址。A value (computer science)that has an address.

R-value中的R指的是Read,表示可读。in computer science, a value that does not have an address in a computer language.

左值和右值是相对于赋值表达式而言的。左值是能出现在赋值表达式左边的表达式。左值表达式可以分为可读写的左值和只读左值。右值是可以出现在赋值表达式右边的表达式,他可以是不占据内存空间的临时量或字面量,可以是不具有写入权的空间实体。如

int a=3;
const int b=5;
a=b+2; //a是左值,b+2是右值
b=a+2; //错!b是只读的左值但无写入权,不能出现在赋值符号左边
(a=4)+=28; //a=4是左值表达式,28是右值,+=为赋值操作符
34=a+2; //错!34是字面量不能做左值

(from 百度百科)

左值引用

左值引用根据其修饰符的不同,可以区分为常量左值引用和非常量左值引用。左值引用实际上就是指针。

非常量左值引用只能绑定到非常量左值,不能绑定到常量左值和常量右值,(因为非常左值可以改变其值,但常量不可改变,性质相矛盾),非常量右值。而如果绑定到非常量右值,就有可能指向一个已经被销毁的对象。

常量左值引用能绑定到非常量左值,常量左值,非常量右值,常量右值。

右值引用

从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升…

右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:

消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

能够更简洁明确地定义泛型函数。

左值引用和右值引用的语法

为了区别,C++把&作为左值引用的声明符,把&&作为右值引用的声明符。

void process_value(int& i) {
    std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
    std::cout << "RValue processed: " << i << std::endl;
}

int main() {
    int a = 0;
    process_value(a);
    process_value(1);  //  1对于编译器而言就是临时对象。
}

output:

LValue processed: 0
RValue processed: 1
Program ended with exit code: 0

但是如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象

void process_value(int& i) {
    std::cout << "LValue processed: " << i << std::endl;
}

void process_value(int&& i) {
    std::cout << "RValue processed: " << i << std::endl;
}

void forward_value(int&& i) {
    //  在函数传递中i被认为是命名对象。
    process_value(i);
}

int main() {
    int a = 0;
    process_value(a);
    process_value(1);
    forward_value(2);
}

output:

LValue processed: 0
RValue processed: 1
LValue processed: 2
Program ended with exit code: 0

转移语义

右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。

转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。

通过转移语义,临时对象中的资源能够转移其它的对象里。(注意是临时对象中的“资源”而不是临时对象本身!这里所谓的使用资源是指指针的指向问题,通过改变指针的指向可以直接使用临时对象中的资源。所以如果,临时对象中并不涉及动态分配内存的问题时,转移语义并不能起到作用,也不必起作用。)

在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。

普通的函数和操作符也可以利用右值引用操作符实现转移语义。

实例:实现转移构造函数和转移赋值函数

以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。
示例1:没有转移构造函数和转移copying函数

 class MyString { 
 private: 
  char* _data; 
  size_t   _len; 
  void _init_data(const char *s) { 
    _data = new char[_len+1]; 
    memcpy(_data, s, _len); 
    _data[_len] = '\0'; 
  } 
 public: 
  MyString() { 
    _data = NULL; 
    _len = 0; 
  } 

  MyString(const char* p) { 
    _len = strlen (p); 
    _init_data(p); 
  } 

  MyString(const MyString& str) { 
    _len = str._len; 
    _init_data(str._data); 
    std::cout << "Copy Constructor is called! source: " << str._data << std::endl; 
  } 

  MyString& operator=(const MyString& str) { 
    if (this != &str) { 
      _len = str._len; 
      _init_data(str._data); 
    } 
    std::cout << "Copy Assignment is called! source: " << str._data << std::endl; 
    return *this; 
  } 

  virtual ~MyString() { 
    if (_data) free(_data); 
  } 
 }; 

 int main() { 
  MyString a; 
  a = MyString("Hello"); 
  std::vector
   
     vec; vec.push_back(MyString("World")); }
   

output:

 Copy Assignment is called! source: Hello 
 Copy Constructor is called! source: World

这个类基本满足我们的需求。但是实际上他的效率很低。因为每一次赋值操作符的调用都会先析构原有的对象的内存,临时对象构造,复制,析构等一系列操作。拷贝构造函数也是如此。非常地低效。于是有人想到,如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

示例2:实现转移构造函数和转移copying函数

class MyString {
private:
    char* _data;
    size_t   _len;
    void _init_data(const char *s) {
        _data = new char[_len+1];
        memcpy(_data, s, _len);
        _dat
首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇【LeetCode】LeetCode――第15题.. 下一篇数据结构与算法――有向无环图的..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目