1

认识引用

Declares a named variable as a reference, that is, an alias to an already-existing object or function. 来自: C++参考手册

先从一个简单的例子开始: 交换函数 swap()

// 代码片段01
void swap01(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

// 代码片段02
void swap02(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, const char * argv[]) {

    int a= 10, b = 20;
    
    swap01(a, b); // 不会交换
    
    swap02(&a, &b); // 会交换

    return 0;
}

上述代码似乎不用解释都知道它所要说明的问题。似乎与今天的主题无关,但是不着急,看下 代码片段02 。内部充斥着 *a*b 这样的操作,代码似乎不够简洁。有没有什么方式可以使其更简洁一些呢?有! 引用!!

有了引用以后,代码是这样的:

void swap03(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int main(int argc, const char * argv[]) {

    int a= 10, b = 20;
    
    swap03(a, b); // 会交换
    
    return 0;
}

代码看起来和上面没有什么大的差别。&a&b 似乎是!在取地址。实际上不是的。它表示的 a 是某个 int 类型变量的引用(或者说某个 int 类型变量的别名)。

那么如何创建一个 引用 呢?

int main(int argc, const char * argv[]) {

    int num = 10;
    
    int & ref = num; // 创建一个引用。
    
    int & refref = ref; // 创建一个引用

    return 0;
}

如上,创建的是 num 的引用,名字叫 ret既然是引用,就是说,使用 numret 没有任何区别。

没有任何区别是什么意思?意思就是说:numret 都是同一个内存地址的名称。举个小例子,如图:
image-20190526182118680

num: 周樟寿
ref: 周树人
refref: 鲁迅

只是名字变了,但它都是同一个内存!!

说到这里,有没有发现其实引用是什么?像不像下面这样:

int &ref = num;

// 等价于
int *ref = #

另外有一个需要注意的地方是: 引用一旦创建,便不能更改(只读)!!既然不能修改,那就说明必须在创建时就要给引用初始化!!!。

int one = 10;
int two = 20;

// 错误01(ref创建时就要被初始化)
int &ref;
ref = one;
// 正确写法:int &ref = one;

// 异常 02(引用一旦创建,便不能修改)
int &ref = one;
ref = two; // 这句话不会报错,但是它表达的意思不是让ref变成two的引用。而是将 two 的值,赋值给 ref。即,ref = 20;

至此,对引用有了一个基本的认识。总结一下:

  1. 引用是某个变量的别名。
  2. 引用一旦创建,便不可以修改。
  3. 引用创建时,必须要初始化。

基于以上的观点。我们可以对引用有一个本质上的认识,即指针常量

int & ref = num;

// 等价于
int * const ref = #

引用的作用

引用难道只是为了简化 *a 这样的代码,使代码更简洁么? 不是的。引用来源于指针,但不同于指针。

引用最主要的用途是:用作函数的参数(尤其是结构体或对象参数)!!

  • 作为函数参数

    struct Student {
        std::string id;
        std::string name;
        int age;
    };
    
    void func01(Student student) { 
        // do something 
    }
    
    void func02(Student &student) { 
        // do something 
    }
    • func01 中会执行创建临时的Student对象,并执行Student的数据拷贝(拷贝构造函数)。这是耗时和耗空间的。
    • func02 中使用的是Student的引用,因此操作的是同一块内存区域,不会创建临时对象,不耗时,也不占用空间。
    • 【注意】 使用引用作为参数时,常加上 const 修饰。理由如下:

      1. 避免无意中修改被引用的数据,如:

        int i = 5;
        
        int cube(int &a) { 
            a *= a * a; 
            return a; 
        }
        
        int sum(int &a) {
            a = a + 5;
            return a;
        }
        
        int tmp1 = cube(i); // 输入时i= 5,输出时i = 125
        int tmp2 = sum(i); // 输入时i = 125,输出时i = 130
      2. 能够处理 const非const 实参,否则只能接受 非const 数据,如:

        const int NUM = 100;
        
        int sum01(int &a) {
          // do something
          return 0;
        }
        
        int sum02(const int &a) {
            // do something
            return 0;
        }
        
        sum01(NUM);// ERROR!!!!
        sum02(NUM);// OK
      3. 使函数能够正确生成并使用临时变量。(这种在早期C++较宽松的规则下才会出现)
  • 作为函数返回值

    返回值是引用的函数,实际上返回的是被引用变量的“别名”

    有一个需要注意的地方是:不要返回临时变量。如下:

    Student &clone(const Student &s) {
        Student student; // 局部变量,函数执行完成时会被销毁。
        student.id = s.id;
        student.name = s.name;
        student.age = s.age;
        return student;
    }

    如上,会返回 student。但是 student 是局部的临时变量,在 clone 函数执行完成后,将被销毁,导致程序出现异常。避免这种问题有一些做法:

    1. 返回一个作为参数传递给函数的引用。如:

      Student &clone(const Student &s) {
          return s;
      }
    2. new 来分配新的内存空间, 如:

      Student &clone(const Student &s) {
          Student *student = new Student();
          student->id = s.id;
          student->name = s.name;
          student->age = s.age;
          return *student;
      }

引用的使用建议

使用引用参数的主要原因有两个:

  • 程序员能够修改调用函数中的数据对象。
  • (最主要)通过传递引用而不是整个数据对象(如:结构体和类对象这种数据对象较大的对象),可以提高程序的运行速度。

正如前面所说,引用来源于指针,但不同于指针。甚至引用像是指针的 "语法糖" 。那么问题来了,什么时候使用引用,什么时候使用指针,什么时候使用按值传递呢???

对于使用传递的值而不作修改的函数

  • 如果数据对象比较小(如:基本数据类型或者小型的结构),选择按值传递
  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向 const 的指针
  • 如果数据对象是较大的结构,则使用指针或者引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间
  • 如果数据对象是类对象,则使用 const 修饰的引用。类设计的语义常常要求使用语义,这是C++新增这项特性的主要原因。因此传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数

  • 如果数据对象是基本数据类型,则使用指针。
  • 如果数据对象是数组,则只能使用指针。
  • 如果数据对象是结构,则使用指针或者引用。
  • 如果数据对象是类对象,则使用引用。

oogh
222 声望5 粉丝

绝知此事要躬行!