C++ 引用详解

我与影子孤独终老i 提交于 2020-02-11 12:00:07

引用是C++引入的新类型,是对一块内存空间起的一个别名,主要分为左值引用常量左值引用右值引用三种。C++语言标准规定,一个引用不是左值引用就是右值引用。其中,函数引用是一种特殊的左值常量引用;万能引用(universal reference)是一种特殊的引用类型,既可以表示左值引用,也可以表示右值引用,具体的引用类型最终会由编译器决定,判断依据是引用折叠(reference collasping)

一、左值引用

一句话总结:左值引用是一级指针的语法糖。只有左值才能绑定到左值引用上。

int &a = 0; // a是int*的语法糖。
int *b = nullptr; 
int *&b_ref = b; // b_ref是int**的语法糖。

大量的资料表示,编译器中的引用是以指针实现的。然而,左值引用必须要初始化后才能使用,否则会引发编译错误(这与指针不同,野指针或者空指针即便不初始化也可以通过编译),所以可以这样理解:如果代码通过了编译,那么引用的对象一定是可用的。然而,凡事有利必有弊,这样的特性也会导致问题。最典型的问题是:引用无法表示空值。例如,有些对象的成员并不是必需的,在复制文件时并不一定需要提供进度通知,应该由用户自行决定,而不是强制要求提供:

class file_copier
{
    progress& _progress; 
    //...
public:
    file_copier(progress &progress, /*....*/) : _progress(progress), /*...*/ {} 
};

此时,为了构造file_copier对象,必须提供一个非空的progress对象来进行进度通知,但是当用户不需要进度通知功能时怎么办呢?只好指定一个特殊的progress对象,表示空值;与其如此,为什么不使用指针呢:

class file_copier
{
    progress* _progress; 
    //...
public:
    file_copier(progress *progress, /*....*/) : _progress(progress), /*...*/ {} 
};

int main(int argc, char *argv[])
{
    file_copier fp(nullptr, /*...*/); // 使用空指针,表示用户不需要进度提示。
    return 0;
}

所以,左值引用这颗糖是否甜,取决于实际情况。在这里也总结一下左值引用和指针的区别:

  • 指针有自己独立的内存空间,而引用没有。
  • sizeof(...)运算的结果不同:指针的大小平台相关,而引用则是被引用对象的大小。
  • 指针可以被初始化为nullptr,而引用必须被初始化,且不为空。
  • 指针可以改变指向,但是引用不能。
  • 可以存在多级指针,但是不存在多级引用,也不存在引用的数组
  • 指针和引用的++运算含义不同:指针表示步进,而引用表示调用对象的operator++运算符。
  • 只能使用指针进行动态内存分配,也无法对引用使用delete。

二、常量左值引用

左值和右值都可以绑定到常量左值引用上,这是因为常量左值引用可以保证部分右值的不可修改属性。一句话总结:常量左值引用是具有底层const的一级指针的语法糖,同时也可以绑定到右值上

void test_bind_to_left_ref(const int &cref)
{
}

void test_bind_to_right_ref(const int &cref)
{
}

int main(int argc, char *argv[])
{
    int a = 0;
    const int &cref = a; // 常量左值引用绑定到左值上。
    const int &cref = std::move(a); // 常量左值引用绑定到右值上。
                                    // std::move(...)将参数转为右值。
    
    test_bind_to_left_ref(a); // 常量左值引用绑定到左值上。 
    test_bind_to_right_ref(std::move(a)); // 常量左值引用绑定到右值上。
    return 0;
}

函数引用是一种特殊的常量左值引用,它没有使用const修饰,但同样具有常量语义:

double add(double x, double y) { return x + y; }
double sub(double x, double y) { return x - y; }

int main(int argc, char *argv[])
{
    double(&func_ref)(double, double) = add; // 将函数引用绑定到函数上。
    func_ref(10, 20); // add(10, 20);
    
    // 然而,func_ref的值不能改变。例如:
    func_ref = sub; // 编译错误,因为func_ref是常量左值引用。
    return 0;
}

其实函数引用需要与函数类型声明、函数指针区别一下的,因为这三种类型都很常见:

double add(double x, double y) { return x + y; }
double sub(double x, double y) { return x - y; }

// 函数声明(c++11):
using func_decl = double(double, double);
func_decl mutiply; // 等价于:double mutiply(double x, double y);

double mutiply(double x, double y) { return x * y; } // 函数实现。

int main(int argc, char *argv[])
{
    double(&func_ref)(double, double) = add; // 将函数引用绑定到函数上。
    func_ref(10, 20); // add(10, 20);
    func_ref = sub; // 编译错误,因为func_ref是常量左值引用。
    
    double(*func_ptr)(double, double) = add; // 将函数指针指向add函数。
    func_ptr(10, 20); // add(10, 20);
    (*func_ptr)(10, 20); // add(10, 20);
    func_ptr = sub; // 正确,函数指针可以改变指向。 
    return 0;
}

三、右值引用

右值引用只能绑定到右值上,主要目的是:

  • 为了延长临时变量的生命周期,从而节约性能。
  • 为了方便代码书写。

例如下面的例子:

#include <vector>
#include <initializer_list>

void func1(std::vector<int> &vec)
{
    // ...
}

void func2(std::vector<int> vec)
{
    // ...
}

void func3(const std::vector<int> &vec)
{
    // ...
}

void func4(std::vector<int> &&vec)
{
    // ...
}

int main(int argc, char *argv[])
{
    func1({1, 2, 3, 4}); // 错误,因为字面量{1, 2, 3, 4}是右值,不能绑定到左值引用上。
    func2({1, 2, 3, 4}); // 正确,按值传递,但是会导致容器的复制,浪费资源。
    func3({1, 2, 3 ,4}); // 正确,右值可以绑定到常量左值引用上,但是无法修改容器中的值。
    {
        std::initializer_list<int> lst = {1, 2, 3, 4};
        func1(lst); // 正确,左值lst可以绑定到左值引用上,可以修改容器中的值,但是书写麻烦。
    }
    func4({1, 2, 3 ,4}); // 完美,右值可以绑定到右值引用上,书写简单,
                         // 不会导致容器复制,且可以修改容器中的值。
    return 0;
}

四、万能引用

万能引用既可以绑定到左值,也可以绑定到右值,它出现在自动类型推断的场合,包括模板、auto等:

template<typename T>
void func(T&& t) {} // t既可以绑定到左值,也可以绑定到右值。

int main(int argc, char *argv[])
{
    auto&& a = /* ... */; // a既可以绑定到左值,也可以绑定到右值。
    return 0;
}

如何判断右值引用究竟是左值还是右值呢?这需要使用引用折叠的概念:当模板或者自动类型推断实例化时,可能会推导出三个(或四个)引用符号,编译器会自动将这三个(或四个)引用符号合并为一个(或两个):

template<typename T>
void func(T&& t) {}

int main(int argc, char *argv[])
{
    int a = 0;
    int &ra = a;
    
    func(ra); // 此时,由于T是&&,并且ra是&,最终会推导出int&&&的结果,自动合并为&(左值引用)。
    func(std::move(ra)); // 此时,由于T是&&,并且ra是&&,
                         // 最终会推导出int&&&&的结果,自动合并为&&(右值引用)。
    auto &&b = ra; // 此时,由于b是&&,并且ra是&,最终会推导出int&&&的结果,自动合并为&(左值引用)。
    auto &&c = std::move(ra); // 此时,由于T是&&,并且ra是&&,
                              // 最终会推导出int&&&&的结果,自动合并为&&(右值引用)。
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!