cpp_1_2_左值引用 Lvalue reference

Overview

1.Lvalue reference 左值引用
2.Lvalue reference variables 左值引用变量
3.Lvalue reference to const const值的左值引用
4.initializing an lvalue reference to const with a rvalue 用右值完成 const 引用初始化
5.Pass by value v.s. Pass by lvalue reference 传值 v.s. 传引用
6.Pass by const lvalue reference 传递 const 左值引用

Lvalue reference 左值引用

a reference is an alias for an existing object. Once a reference has been defined, any operation on the reference is applied to the object being referenced.

An lvalue reference (commonly just called a reference since prior to C++11 there was only one type of reference) acts as an alias for an existing lvalue (such as a variable).

intuitively,
1.引用就是对一个已有的对象起一个”别名”, 我们一旦对一个对象建立了引用, 任何在引用上的操作, 都会施加到被引用的对象上; 这好比每个阿里员工进入阿里之前都用自己的真名字, 进入阿里之后会产生一个”花名”, 老板和同事之间都会叫新的花名而不会叫真名, 同事老板宣布最佳新人奖或者双11优秀员工的时候会用花名来提名, 并且略微多给一点年终奖或者不给, 这些影响都会施加在绑定在具体个人的花名之上进行操作;
2.左值引用, 通常简称为引用 (因为c11之前只有一种类型的引用), 充当现有左值 (例如变量) 的别名;
3.==引用的本质是建立一种变量的绑定关系==, 如果能理解引用是建立绑定关系, 那么其实很多问题凭借朴素的直觉也能想明白为什么有些操作编译器不让做或者可允许;

int       // 正常的int类型
int&      // 一个int类型ojbect的左值引用
double&   // 一个double类型object的左值引用

Lvalue reference variables 左值引用变量

A reference defines an alternative name for an object. a reference type “refers to” another type. A reference is not an object, a reference is just another name a alias for an exsiting object.

When we initialize a variable, the value of the initializer is copied into the object we are creating; when we defined a reference, instaed of copying the initializer’value, we bind the reference to its initializer. Once initialized, a reference remains bound to its initial object. There is no way to rebind a reference to refer to a different object.

Because there is no way to rebind a reference, references must be initialized.

intuitively,
1.我们可以使用左值引用做的事情就是创建左值引用变量; 左值引用变量本质是给另一个已有的变量起了个”别名”, 引用的核心是建立”绑定”关系, 也就是说定义一个引用类型变量时也就绑定到了原始变量上, 所以左值引用本质就是看绑定在谁身上; 如果一个引用类型的变量修改了值, 那么原来的变量也同时修改了, 因为已经形成了一种绑定关系.
2.对编译器来说, 写法1:int& r和写法2:int &ref没有任何区别, 是个代码风格问题; 现代Cpp程序员喜欢把&和类型int写在一起, 而不是把&和名称写在一起, 也就是写成int&, 这种写法更能表示清楚引用是类型信息的一部分. 在这种情况下, &符号本身代表的意思不是”address of xx” (xx的地址) 而是”lvalue reference to xx”(xx的左值引用)
3.定义左值引用类型必须初始化, 因为引用是绑定在已有的变量上的;
4.左值引用可以用来修改值, 我们通常使用引用就是要更灵活地修改值; 但如果绑定到一个不能更改的变量(const修饰的)怎么办? 如果想绑定的变量不能修改, 那么就没法完成绑定, 编译器会直接报错;
5.左值引用无法重新绑定, 也就是一旦一个引用完成了绑定, 无法更改引用另一个对象;
6.引用变量和被引用变量具有独立的生命周期, 当引用变量被销毁之后, 被引用的变量不受到同期的影响;

#include <iostream>
using namespace std;

int main() {
  int x = 5;
  int& ref = x;         // ref 是一个左值引用变量, 就相当于给x起了个别名叫ref然后用它;
  int &ref2 = x;        // int& ref和 int &2对编译器没有任何区别
  int& invalidRef;      // 报错, 引用必须被初始化, 说明绑定在谁身上

  cout << x << endl;    // 输出5
  cout << ref << endl;  // 输出5

  x = 3;
  cout << ref << endl;  // 输出3
  cout << ref2 << endl; // 输出3

  ref = 9;
  cout << x << endl;    // 输出9
  cout << ref2 << endl; // 输出9

  const int y = 5;
  int& ref3 = y;        // 报错, 引用只能绑定到可修改的值上面;
  return 0;
}

引用无法重新绑定, 也就是无法更改为引用另一个对象; 如果对引用变量发生了赋值, 也只是发生赋值, 绑定关系不会改变; 例如

#include <iostream>

int main() {
  int x = 5;
  int y = 6;

  int& ref = x; 

  std::cout << "ref: " << ref << '\n'; 

  ref = y; 
  // 没有将绑定关系改变为绑定y, ref始终是绑定x
  // assigns 6 (the value of y) to x (the object being referenced by ref)
  // 只是将值6赋值给了ref 绑定的变量y上面

  std::cout << "after x: " << x << std::endl;
  std::cout << "after y: " << y << std::endl;
  std::cout << "after ref: " << ref << std::endl; 
  return 0;
}

引用变量和被引用变量具有独立的生命周期, 如果引用变量销毁了, 原有的变量其实还在

#include <iostream>

int main() {
  int x = 5;
  {
    int& ref = x;
    std::cout << ref << '\n'; // prints value of ref (5)
  } // ref 在这时候销毁-- x is unaware of this
  std::cout << x << '\n'; // prints value of x (5)
  return 0;
} // x 在这时候销毁

Lvalue reference to const const值的左值引用

因为左值引用只能绑定可修改的变量, 所以如果原始变量是const修饰的不可修改, 那么就没办法通过编译;

#include <iostream>

int main() {
  const int x = 5;
  int& ref = x;  // 编译报错, 左值引用不能绑定到不能修改的值
  return 0;
}

所以我们想对const变量对它创建一个引用变量怎么做? 上述用常规的引用变量是不行的;
我们的解决方法是把引用声明称const类型的, 这种叫做lvalue reference to a const value (const值的左值引用), 或者叫做 reference to const (对const值的引用/常量引用) 或者叫做 const reference (const引用/常引用);
声明const引用之后, 引用的值是不能修改了, 因此声明成const引用的话引用就失去了修改值的功能, 但是仍然保留了可以传递引用的功能.

#include <iostream>

int main() {
  const int x = 5;
  const int& ref = x;   // 正确, 这是const值的引用, 也叫const引用

  std::cout << ref << std::endl;
  ref = 6;              // 报错, 我们不能通引用对值进行修改
  return 0;
}

除此之外, 常引用可以绑定可修改的左值, 如果常引用绑了个左值, 引用本身不可以修改值, 但是原值还是一个左值, 所以原值还是可以修改的;

#include <iostream>

int main() {
  int x = 5;
  const int& ref = x;   // 正确, 这是常量的左值引用, 也叫常引用

  std::cout << ref << std::endl;
  ref = 7;              // 编译报错, 我们不能通过常引用对值进行修改
  x = 6;                // 正确, 常引用虽然不能修改值, 但是原值还是可以修改的
  return 0;
}

通常情况下, 我们只考虑使用const值的左值引用, 而不是对非const值声明const的引用, 除非非要修改值;

initializing an lvalue reference to const with a rvalue 用右值完成 const 引用初始化

1.const引用可以用右值来完成初始化, 发生这种情况时候, 会创建一个临时对象 (temporary object, 有时又叫做anonymous object 匿名变量) 并使用右值进行初始化, 并对const的引用绑定到该临时对象;
2.temporary 对象 (anonymous对象) 是单个表达式中创建的为了临时使用的对象, 临时用完马上销毁. 临时变量完全没有作用域 (作用域是 identifier 的一种熟悉, 临时变量没有identifier), 临时变量只在创建的那一个时刻使用, 因此不可能在创建之后还引用它.
3.const reference 延长了临时对象的生命周期; 临时变量本应该在表达式结束之后就被销毁掉, 但是在下面的例子里面, 右值5创建临时对象, 在初始化ref的表达末尾没有被销毁, 它的生命周期得以延长;

#include <iostream>

int main() {
  const int& ref = 5;   // 正确, const引用可以用右值完成初始化
  std::cout << ref << std::endl;
  return 0;
}

Pass by lvalue reference 函数参数传递左值引用

只有上面的介绍, 我们还是没体会到引用的使用场景, 为什么要对一个变量要多创建一个别名呢? 接下来介绍左值引用核心用处: 函数参数传递左值引用;
我们回顾下函数传值这件事情, 当我们调用printValue这个函数的时候, 我们首先创建了变量x的一个copy, 把变量x拷贝到参数y里面, 然后在函数printValue对于y进行一通操作, 当函数调用结束之后y会被销毁, 结束变量y的生命周期;

#include <iostream>
void printValue(int y) {
  std::cout << y << '\n';
} // y is destroyed here

int main() {
  int x{2};
  printValue(x); // x is passed by value (copied) into parameter y (inexpensive)
  return 0;
}

对所有的函数调用来说, 都会实现创建参数的copy=>使用=>销毁这么个过程, 对于基础类型比如这种int类型或char类型都没有显著的代价; 但对标注库中的很多类型, 属于class types (典型的常用类就是std::string), 通常copy的代价是非常高的, 因此任何情况下, 我们都要避免这种高开销的copy操作;

这时候我们修改函数的参数, 用pass by reference 取代 pass by value: 当函数调用的时候, 不再产生任何的参数copy操作;

#include <iostream>
#include <string>

void printValue(std::string& y) {// type changed to std::string&
  std::cout << y << '\n';
} // y is destroyed here

int main() {
  std::string x{"Hello, world!"};
  printValue(x); // x is now passed by reference into reference parameter y (inexpensive)
  return 0;
}

当然, 我们如果采用了 pass by reference 的这种函数参数类型, 在函数中修改值会自然而然地修改原有的变量的值, 使用的时候就要小心了;

#include <iostream>
void addOne(int& y) {// y is bound to the actual object x
  ++y; // this modifies the actual object x
}

int main() {
  int x={5};
  std::cout << "value = " << x << '\n';
  addOne(x);
  std::cout << "value = " << x << '\n'; // x has been modified
  return 0;
}

输出结果为

value = 5
value = 6

同时, 如果函数参数传递的是左值引用, 那我们只能绑定一个可修改的左值; 不能传递一个不可修改的值, 比如 const 值

#include <iostream>

void printValue(int& y) { // y only accepts modifiable lvalues
  std::cout << y << '\n';
}

int main() {
  int x{5};
  printValue(x); // ok: x is a modifiable lvalue
  const int z{5};
  printValue(z); // error: z is a non-modifiable lvalue
  printValue(5); // error: 5 is an rvalue
  return 0;
}

Pass by value v.s. Pass by lvalue reference 传值 v.s. 传引用

when we invoke a function, a special area of memory is set up on what is called program stack. Within is special area of memory there is space to hold the value of each function parameter. It is also holds the memory associated with each object defined within the function - we call these local objects.

when the function completes, this area of memory is discarded. We say that it is popped from the program stack.

by default, when we pass an object to a function, such as vec[i], its value is copied to the local definition of the parameter. This is called pass by value sematics. There is no connection between the objects manipulated with swap() and the objects passed to it within bubble_sort().

we must bind the swap() parameters to the actual objects being passed in, this is called pass by reference semantics. This simplest way of doing this is to declare the parameter as references.

one reason to declare a parameter as a reference is to ==allow us to modify directly the actual object being passed to the function==. This is important because, as we’ve seen, our program otherwise may behave incorrectly.

the second reason to declare a parameter as a reference is to ==eliminate the overhead of copying a large object==. This is a less important reason. Our program is correct; it is simply less efficient

intuitively,
1.调用函数的时候, 内存会建立一块区域”程序堆栈”, “程序堆栈”提供了函数定义的每个对象的内存空间, 这些对象称为”局部对象”.
2.一个函数一旦函数调用完成, 内存就被释放掉, 准确说是从程序堆栈中被pop出来.
3.我们通过”传值”的方式, 将参数复制到局部变量里面, 比如下面[bubble_sort()传给swap()的对象]和[swap()内操作的对象]是没有任何关系的两组对象.
4.采用引用作为函数参数的优势在于能够减少copy复杂对象的开销, 通常情况下, class类型的copy的代价是非常高昂的, 因此我们尽量不要去避免这种因为copy带来的开销;

#include <iostream>
#include <string>
#include <vector>

using namespace std;

void display(const vector<int>& vec) {
  for (int i = 0; i < vec.size(); ++i)
    cout << vec[i] << " ";
  cout << endl;
  return;
}

void swapPassByValue(int a, int b) {
  int tmp = a;    
  a = b;
  b = tmp;
  return; 
}

void swapPassByReference(int& a, int& b) {
  int tmp = a;    
  a = b;
  b = tmp;
  return; 
}

void bubbleSort(vector<int>& vec, bool passReference) {
  for (int i = 0; i < vec.size(); ++i) {
    for (int j = i + 1; j < vec.size(); ++j) {
      if (vec[i] > vec[j]) {
        if (passReference)
          swapPassByReference(vec[i], vec[j]);
        else
          swapPassByValue(vec[i], vec[j]);
      }
    }
  }
  return;
}

int main () {
  vector<int> v1 = {17, 2, 7, 13, 11, 5, 31, 27};
  vector<int> v2 = {17, 2, 7, 13, 11, 5, 31, 27};
  cout << "v1 before sort: ";
  display(v1);
  bubbleSort(v1, false);
  cout << "v1 after sort: ";
  display(v1);
  cout << "v2 before sort: ";
  display(v2);
  bubbleSort(v2, true);
  cout << "v2 after sort: ";
  display(v2);
  return 0;
}

Pass by const lvalue reference 传递const左值引用

上面我们理解了 pass by value 能产生很强的性能优势, 但是上述 reference to non-const 只能接受左值参数, 比如我们想传递一个参数为5, 函数参数为引用的都不行, 还得必须定义一个左值x = 5, 然后再传递x, 也就太尴尬了, 这显然不是我们想要的;

#include <iostream>
#include <string>

void printValue(int& y) { // y 只能接受一个左值引用参数
  std::cout << y << std::endl;
}

int main() {
  int x = 5;
  printValue(x); // 正确, x是个左值
  printValue(5); // 错误, 5是一个右值

  const int z = 5;
  printValue(z); // 错误, z是一个const值
  return 0;
}

所以我们通常采用 const reference 作为函数的参数, 这样的优势在于:
1.避免了因采用传值的方式包含的参数copy的额外开销;
2.允许传递实参的时候可以直接用右值;
3.能够保证参数值在函数过程中不被修改;

#include <iostream>
#include <string>

void printValue(const int& y) { // y is now a const reference
  std::cout << y << '\n';
}

int main() {
  int x = 5;
  printValue(x); // 正确 
  printValue(5); // 正确, 参数传递的是const引用

  const int z = 5;
  printValue(z); // 正确, 参数传递的是const引用
  return 0;
}

这里我们会想, 如果我非要想在函数里面修改值而且也不想思考我传入的实参是左值or右值呢? 最简单的方法就是函数输入做成多个不同引用类型的输入参数, 想改值的我们可以 pass by rvalue reference, 不想改值的我们 pass by const rvalue reference

#include <string>
void foo(int a, int& b, const std::string& c) {
}

int main() {
  int x{5};
  const std::string s{"Hello, world!"};
  foo(5, x, s);
  return 0;
}

Reference

1.https://www.learncpp.com/cpp-tutorial/lvalue-references-to-const/
2.https://www.learncpp.com/cpp-tutorial/pass-by-const-lvalue-reference/


转载请注明来源, from goldandrabbit.github.io

💰

×

Help us with donation