Skip to content

引用(reference)

回顾变量

  • 变量名实质上是一段连续内存空间的别名,是一个标号(门牌号)
  • 程序中通过变量来申请并命名内存空间
  • 通过变量的名字可以使用存储空间

C++中新增了引用的概念,引用可以作为一个已定义变量的别名。

引用基本用法

引用是C++对C的重要扩充。在C/C++中指针的作用基本都是一样的,但是C++增加了另外一种给函数传递地址的途径,这就是按引用传递(pass-by-reference),它也存在于其他一些编程语言中,并不是C++的发明。

基本语法:

cpp
Type& ref = val;

我们可以简单的把引用理解为给变量起了一个外号,一般用&在外号前面显式标记;起完外号后,变量本身和外号指向的是同一个变量值。

对一段连续的内存空间只能取一个别名吗?

答案是否定的,可以有多个别名,这些别名指向同一段连续的内存空间。

引用示例代码
cpp
#include <iostream>
using namespace std;

//1. 认识引用
void test01() {
    int a = 10;
    //给变量a取一个别名b
    int& b = a;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "------------" << endl;
    //操作b就相当于操作a本身
    b = 100;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "------------" << endl;
    //一个变量可以有n个别名
    int& c = a;
    c = 200;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "c:" << c << endl;
    cout << "------------" << endl;
    //a,b,c的地址都是相同的
    cout << "a:" << &a << endl;
    cout << "b:" << &b << endl;
    cout << "c:" << &c << endl;
}

//2. 使用引用注意事项
void test02() {
    //1) 引用必须初始化
    //int& ref; //报错:必须初始化引用

    //2) 引用一旦初始化,不能改变引用
    int a = 10;
    int b = 20;
    int& ref = a;
    ref = b; //不能改变引用

    //3) 不能对数组建立引用
    int arr[10];
    //int& ref3[10] = arr;
}
int main(int argc, char *argv[]) {
    test01();
    test02();

    return 0;
}

程序输出:

shell
a:10
b:10
------------
a:100
b:100
------------
a:200
b:200
c:200
------------
a:0x7ffca10aa6a4
b:0x7ffca10aa6a4
c:0x7ffca10aa6a4

cpp
#include <iostream>
using namespace std;

void test01() {
    //1. 建立数组引用方法一
    typedef int ArrRef[10];
    int arr[10];
    ArrRef &aRef = arr;
    for (int i = 0; i < 10; i++) {
        aRef[i] = i + 1;
    }
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    //2. 建立数组引用方法二
    int(&f)[10] = arr;
    for (int i = 0; i < 10; i++) {
        f[i] = i + 10;
    }
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

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

    return 0;
}

程序输出:

shell
1 2 3 4 5 6 7 8 9 10 
10 11 12 13 14 15 16 17 18 19

注意事项

  • 类型标识符是指目标变量的类型。
  • 不能有NULL引用,也不能将字面值常量进行引用;必须确保引用是和一块合法的存储单元关联。
  • 可以建立对数组的引用。

最常见看见引用的地方是在函数参数返回值中。当引用被用作函数参数的时,在函数内对任何引用的修改,将对还函数外的参数产生改变。当然,可以通过传递一个指针来做相同的事情,但引用具有更清晰的语法。

如果从函数中返回一个引用,必须像从函数中返回一个指针一样对待。当函数返回值时,引用关联的内存一定要存在。

示例代码
cpp
#include <iostream>
using namespace std;

//值传递
void ValueSwap(int m, int n) {
    int temp = m;
    m = n;
    n = temp;
}

//地址传递
void PointerSwap(int* m, int* n) {
    int temp = *m;
    *m = *n;
    *n = temp;
}

//引用传递
void ReferenceSwap(int& m, int& n) {
    int temp = m;
    m = n;
    n = temp;
}

void test() {
    int a = 10;
    int b = 20;
    //值传递
    ValueSwap(a, b);
    cout << "a:" << a << " b:" << b << endl;
    //地址传递
    PointerSwap(&a, &b);
    cout << "a:" << a << " b:" << b << endl;
    //引用传递
    ReferenceSwap(a, b);
    cout << "a:" << a << " b:" << b << endl;
}


int main(int argc, char *argv[]) {
    test();
    
    return 0;
}

程序输出:

shell
a:10 b:20
a:20 b:10
a:10 b:20

通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单:

  1. 函数调用时传递的实参不必加&
  2. 在被调函数中不必在参数前加*

引用作为其它变量的别名而存在,因此在一些场合可以代替指针。C++主张用引用传递取代地址传递的方式,因为引用语法容易且不易出错。

示例代码
cpp
#include <iostream>
using namespace std;

//返回局部变量引用
int& TestFun01() {
    int a = 10; //局部变量
    return a;
}
//返回静态变量引用
int& TestFunc02() {
    static int a = 20;
    cout << "static int a : " << a << endl;
    return a;
}
int main(int argc, char *argv[]) {
    //不能返回局部变量的引用
    int& ret01 = TestFun01();
    //如果函数做左值,那么必须返回引用
    TestFunc02();
    TestFunc02() = 100;
    TestFunc02();
    
    return 0;
}

程序输出:

shell
static int a : 20
static int a : 20
static int a : 100
  • 不能返回局部变量的引用。
  • 函数当左值,必须返回引用。

引用的本质

cpp
Type& ref = val; // Type* const ref = &val;

C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同(),只是这个过程是编译器内部实现,用户不可见。

cpp
#include <iostream>
using namespace std;

//发现是引用,转换为 int* const ref = &a;
void testFunc(int &ref) {
    ref = 100; // ref是引用,转换为*ref = 100
}

int main(int argc, char *argv[]) {
    int a = 10;
    int& aRef = a; //自动转换为 int* const aRef = &a;这也能说明引用为什么必须初始化
    aRef = 20; //内部发现aRef是引用,自动帮我们转换为: *aRef = 20;
    cout << "a:" << a << endl;
    cout << "aRef:" << aRef << endl;
    testFunc(a);

    return 0;
}

程序输出:

shell
a:20
aRef:20

常量引用

常量引用的定义格式:

cpp
const Type& ref = val;

注意

  • 字面量不能赋给引用,但是可以赋给const引用
  • const修饰的引用,不能修改。
示例代码
cpp
#include <iostream>
using namespace std;

void test01() {
    int a = 100;
    const int& aRef = a; //此时aRef就是a
    //aRef = 200; 不能通过aRef的值
    a = 100; //OK
    cout << "a:" << a << endl;
    cout << "aRef:" << aRef << endl;
}

void test02() {
    //不能把一个字面量赋给引用
    //int& ref = 100;
    //但是可以把一个字面量赋给常引用
    const int& ref = 100; //int temp = 200; const int& ret = temp;
}

int main(int argc, char *argv[]) {
    test01();
    test02();

    return 0;
}

程序输出:

shell
a:100
aRef:100

常量引用使用场景

常量引用主要用在函数的形参,尤其是类的拷贝/复制构造函数。

cpp
//const int& param防止函数中意外修改数据
void ShowVal(const int& param) {
    cout << "param:" << param << endl;
}

如果希望实参随着形参的改变而改变,那么使用一般的引用,如果不希望实参随着形参改变,那么使用常引用。

将函数的形参定义为常量引用的好处

  • 引用不产生新的变量,减少形参与实参传递时的开销。
  • 由于引用可能导致实参随形参改变而改变,将其定义为常量引用可以消除这种副作用。

指针引用

在C语言中如果想改变一个指针的指向而不是它所指向的内容,函数声明可能这样:

cpp
void fun(int**);

给指针变量取一个别名。

cpp
Type* pointer = NULL;
Type*&x = pointer;
示例代码
cpp
#include <iostream>
using namespace std;

struct Teacher {
    int mAge;
};

//指针间接修改teacher的年龄
void AllocateAndInitByPointer(Teacher** teacher) {
    *teacher = (Teacher*)malloc(sizeof(Teacher));
    (*teacher)->mAge = 200;
}

//引用修改teacher年龄
void AllocateAndInitByReference(Teacher*& teacher) {
    teacher->mAge = 300;
}

void test() {
    //创建Teacher
    Teacher* teacher = NULL;
    //指针间接赋值
    AllocateAndInitByPointer(&teacher);
    cout << "AllocateAndInitByPointer:" << teacher->mAge << endl;
    //引用赋值,将teacher本身传到ChangeAgeByReference函数中
    AllocateAndInitByReference(teacher);
    cout << "AllocateAndInitByReference:" << teacher->mAge << endl;
    free(teacher);
}

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

    return 0;
}

程序输出:

shell
AllocateAndInitByPointer:200
AllocateAndInitByReference:300

对于C中的定义那个,语法清晰多了。函数参数变成指针的引用,用不着取得指针的地址。

图解 轻松看懂「指针的引用*&」

写这篇文章是因为指针的引用在数据结构中的树和图的算法中应用广泛。如指针一样,指针的引用容易使人困惑。

我们注意到类似下面这种语法

cpp
void func(int *&x){
  ++x;
}

我猜你可能对int *&x有点疑惑。

这叫做指针的引用

cpp
int *&x

不要觉得看着复杂,其实一点也不复杂。

我帮你拆开来看:

按照C++程序员的习惯,指针“*”号是和类型放在一起的。

C++中&是引用符号。

我们需要注意的是“引用”不产生副本,而是给原变量起别名。

对引用操作就是对原变量操作

所以只需要这样:

cpp
int* &x

一目了然!

对指针变量本身的修改无法作用到原指针变量,

所以需要通过引用来实现修改指针变量。

我用两张图来告诉你指针的引用为什么有用:

举个栗子,我用代码来给你解释解释什么叫局部修改:

cpp
#include <stdio.h>

void swap(int* p1,int* p2){
    int* temp=p1;
    p1=p2;
    p2=temp;
    printf("交换中:a=%d,b=%d \n",*p1,*p2);
    printf("交换中(地址):p1=%d \n",p1);
    printf("交换中(地址):p2=%d \n",p2);
}

int main(int argc, char *argv[]){
    int a=1,b=3;
    int *p1=&a,*p2=&b;

    // 交换前
    printf("交换前:a=%d,b=%d \n",*p1,*p2);
    printf("交换前(地址):p1=%d \n",p1);
    printf("交换前(地址):p2=%d \n",p2);

    // 交换中
        swap(p1,p2);

    // 交换后
        printf("交换后:a=%d,b=%d \n",*p1,*p2);
    printf("交换后(地址):p1=%d \n",p1);
    printf("交换后(地址):p2=%d \n",p2);

    return 0;
    }

猜一猜结果。

输出的结果:

cpp
交换前:a=1,b=3
交换前(地址):p1=6422028
交换前(地址):p2=6422024
交换交换中:a=3,b=1
交换中(地址):p1=6422024
交换中(地址):p2=6422028
交换后:a=1,b=3
交换后(地址):p1=6422028
交换后(地址):p2=6422024

运行截图

在执行swap()函数的时候就是执行中。

可以发现在执行swap()函数的时候确实修改了地址,也交换了a、b的值。

但是,当我们在main()函数中输出a、b的时候,完全没有交换。

同样的代码,我只改一个地方。

在形参中添加引用

来康康会发生什么改变。

运行截图

cpp
交换前:a=1,b=3 
交换前(地址):p1=6422044 
交换前(地址):p2=6422040 
交换中:a=3,b=1 
交换中(地址):p1=6422040 
交换中(地址):p2=6422044 
交换后:a=3,b=1 
交换后(地址):p1=6422040 
交换后(地址):p2=6422044

我就简简单单添加了“&”,竟然如此神奇!

所以我们可以发现:

指针的引用能够全局修改指针变量!

引用是C++中很强大的语法,在编程中极其实用。

明白这个语法很关键,因为这个在树和图的算法中应用广泛

引用和指针的区别

  • 引用在定义时必须初始化 不能为空,指针可以不初始化 也可以为空
  • 引用在初始化后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  • 引用和指针的汇编实现可能相同​(如mov指令直接操作地址),但符号表处理不同。引用在符号表中记录的是目标对象的地址,而指针记录的是自身地址
  • 引用不支持多级引用,指针支持
  • sizeof时,引用返回的是目标对象的大小,而指针返回的是自身的大小
  • 指针更贴近机器:适合底层操作(如动态内存分配、硬件地址访问)
  • ​引用更贴近需求:提高代码可读性(如函数参数传递、链式调用)

右值引用

我们直接只用引用来引用一个字面量是错误的。要想引用一个字面量,必须加上const。

cpp
int &a = 10; // 错误
const int &a = 10; // 正确

但是,C++11中新增了右值引用的概念,右值引用就是必须绑定到右值的引用。

我们通过&&而不是&来获得右值引用。

cpp
int a = 10;
int &b = a; // 左值引用
// int &c = 10; // 错误,10是右值
int &&r = 10; // 右值引用

左值和右值

左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。

通俗来说:

  • 左值:它有内存,有名字,值可以被修改
  • 右值:它没有内存,没有名字,值不可以被修改

一个右值引用变量本身是一个左值,它可以被赋值,也可以被引用。

cpp
int &&r = 10;
int &r1 = r; // 右值引用变量r本身是一个左值

练习作业

  1. 设计一个类,求圆的周长。
示例代码
cpp
#include <iostream>
using namespace std;

class Circle {
public:
    Circle() {
        this->m_r = 0;
    }
    Circle(int r) { this->m_r = r; }

    float getCircumference() {
        float circumference = 0;
        circumference = this->m_r * this->m_r * 3.14159;
        return circumference;
    }

private:
    int m_r;
};

int main(int argc, char *argv[]) {
    Circle mycircle(3);
    float ret = mycircle.getCircumference();
    printf("%.2f\n", ret);

    return 0;
}

程序输出:

shell
28.27
  1. 设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
示例代码
cpp
#include <iostream>
using namespace std;
#include <string>

class Students {
public:
    Students(int no, string name) {
        this->m_num = no;
        this->m_name = name;
    }
    void setNo(int no) { this->m_num = no; }
    void setName(string name) { this->m_name = name; }
    int getNo(void) { return this->m_num; }
    string getName(void) { return this->m_name; }

private:
    int m_num;
    string m_name;
};

int main(int argc, char *argv[]) {
    Students st(100001, "张楚岚");
    cout << "Name:" << st.getName() << "\tNo:" << st.getNo() << endl;
    
    st.setName("张楚");
    st.setNo(10001);
    cout << "Name:" << st.getName() << "\tNo:" << st.getNo() << endl;

    return 0;
}

程序输出:

shell
Name:张楚岚     No:100001
Name:张楚       No:10001