Loading

C++ 基础系列——引用机制

1. 引用入门

引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。

引用必须在定义的同时初始化,并且初始化后不能再更改引用对象。

int a = 99;
int &r = a;
cout << a << ", " << r << endl;
cout << &a << ", " << &r << endl;

r = 47;
cout << a ", " << r << endl;

运行结果:

99,99
0x28ff44,0x28ff44
47,47

引用在定义时需要添加 &,使用时不能添加 &,使用时 & 表示取地址(与指针相反)。

当将函数的形参指定为引用的形式时,实参和形参会绑定在一起,函数中对形参的修改会直接作用于实参的数据。

当引用作为函数返回值时,不能返回局部变量的引用。例如下面例子:

int &plus10(int &r) 
{
    int m = r + 10;
    return m; //返回局部数据的引用
}

int main() 
{
    int num1 = 10; 
    int num2 = plus10(num1); 
    cout << num2 << endl;   // 20
    int &num3 = plus10(num1);   // 不同版本编译器上会有不同的结果
    int &num4 = plus10(num3);   // 不同版本编译器上会有不同的结果
    cout << num3 << " " << num4 << endl;  // 不同版本编译器上会有不同的结果
    return 0;
}

2. 引用的本质

引用只是对指针进行简单的封装,它的底层是通过指针实现的,引用占用的内存和指针占用的内存长度一样,在32位环境下是4个字节,64位环境下是8个字节,之所以不能获取引用的地址,是因为编码器进行了内部转换。

int a = 99; 
int &r = a; 
r = 18;
cout << &r << endl;

编译时会被转换成如下形式:

int a = 99; 
int *r = &a;
*r = 18;
cout << r << endl;

当使用 &r 取地址时,编译器会进行隐式的转换,使得输出的是 r 的内容(a 的地址),而不是 r 的地址,这就是为什么获取不到引用变量的地址的原因。也就是说,不是变量 r 不占用内存,而是编译器不让获取它的地址。

当引用作为函数参数时,也会有类似的转换。以下面的代码为例:

//定义函数
void swap(int &r1, int &r2){
    int temp = r1; 
    r1 = r2;
    r2 = temp;
}

// 调用函数
int num1 = 10, num2 = 20;
swap(num1, num2);

会被转换成如下形式:

//定义函数
void swap(int *r1, int *r2){
    int temp = *r1; 
    *r1 = *r2; 
    *r2 = temp;
}

//调用函数 
int num1 = 10, num2 = 20;
swap(&num1, &num2);

C++ 中引入引用的直接目的是为了让代码的书写更加漂亮, 尤其是在运算符重载中,不借助引用有时候会使得运算符的使用很麻烦。

3. 引用和指针的区别

  1. 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,可以在之后指向任意数据。
  2. 可以有 const 指针,但是没有 const 引用。即无法定义成这种形式:int & const r = a;
  3. 指针可以有多级,但是引用只能有一级,例如,int **p 是合法的,而 int &&r 是不合法的。如果希望定义 一个引用变量来指代另外一个引用变量,那么也只需要加一个&。
  4. 指针和引用的自增(++)自减(--)运算意义不一样。对指针使用 ++ 表示指向下一份数据,对引用使用 ++ 表示它所指代的数据本身加 1;自减(--)也是类似的道理。

4. 引用不能绑定的情况

4.1 放在寄存器的临时数据

引用不能绑定到放在寄存器的临时数据。下面代码表达式产生的都是临时结果,会放到寄存器中,使用 & 取地址是错误的。

int n = 100, m = 200;
// & 取地址错误,放在寄存器的临时数据,不能取址
int *p1 = &(m + n);     // m + n 的结果为 300
int *p2 = &(n + 100);   // n + 100 的结果为 200 
bool *p4 = &(m < n);    // m < n 的结果为 false

int func()
{
    int n = 100;
    return n;
}
// func 函数返回放在寄存器的临时数据,不能取址
int *p = &(func());

int、double、bool、char 等基本类型的数据往往不超过 8 个字节,这些类型的临时数据通常会放到寄存器中;而对象、结构体变量是自定义类型的数据,大小不可预测,这些类型的临时数据通常会放到内存中。

4.2 常量表达式

对于常量表达式,如 100、200+34、34.5*23、3+7/3 等,会在编译阶段就求值,相当于一个立即数,不能寻址。

总起来说,常量表达式的值虽然在内存中,但是没有办法寻址,所以也不能使用&来获取它的地址,更不能用 指针指向它。

// 错误情况
int *p1 = &(100);
int *p2 = &(23 + 45 * 2);

4.3 函数形参情况

当引用作为函数参数时,有时候很容易给它传递临时数据。

// 参数是引用类型,不能接受常量或者表达式
bool isOdd(int &n)
{
    if( n%2 == 0 )
        return false;
    else
        return true;
}

bool isOdd2(int n)
{
    if( n%2 == 0 )
        return false;
    else
        return true;
}

// 改为常引用,正确
bool isOdd3(const int &n)
{ 
    if(n%2 == 0)
        return false;
    else
        return true;
}

int main()
{
    int a = 100;
    isOdd(a); //正确 
    isOdd(a + 9); // 错误,a+9是常量表达式,不能取址
    isOdd(27); // 错误,常量表达式,不能取址
    isOdd(23 + 55); // 错误
}

5. const 引用绑定临时数据

引用不能绑定到临时数据,大多数情况是正确的,但当使用 const 关键字对引用加以限定后,引用就可以绑定到临时数据了。

// 以下代码正确
int m = 100, n = 36; 
const int &r1 = m + n; // 编译器会为 m + n 创建临时变量,再取址
const int &r2 = m + 28; 
const int &r3 = 12 * 3;
const int &r4 = 50;

这块代码在 GCC 和 Visual C++ 下都能通过。(不同编译器下对临时变量引用的容忍程度不一样)。编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。临时变量也是变量,也会被分配内存。

为什么编译器为常引用创建临时变量是合理的,而为普通引用创建临时变量就不合理呢?

  1. 临时数据往往无法寻址,不能写入,即使为临时数据创建了一个临时变量,修改的也是临时变量里的数据,不会影响原来的数据,这样就使得引用所绑定到的数据和原来的数据不能同步更新,最终产生了两份不同的数据,失去了引用的意义。即为普通引用创建临时变量没有任何意义。
  2. const 引用只能读不能写,不用考虑同步更新的问题,也不会产生两份不同的数据,为 const 引用创建临时变量反而会使得引用更加灵活和通用。

6. const 引用与类型转换

编译器禁止指针指向不同类型的数据,对于引用也是一样。(可以强转)

int n = 100;
int *p1 = &n;   //正确 
float *p2 = &n; //错误 
char c = '@'; 
char *p3 = &c;  //正确
int *p4 = &c;   //错误

对于不同类型,程序对它们的处理方式不同:

  • 对于 int,最高 1 位为符号位,剩下 31 位为数值位;
  • 对于 float,最高 1 位为符号位,最低 23 位为尾数位,中间 8 位为指数位。

但对于 const 引用,其处理方式与将 const 引用绑定到临时数据时采用的方案一样,编译器会创建一个临时变量,再将引用绑定到临时变量上。而将数值赋值给临时变量时,会发生自动类型转换。

float f = 12.45;
const int &r = f;   // r:12

当自动类型转换不遵循数据类型的自动转换规则时,编译器会报错。

给引用添加 const 限定后,不但可以将引用绑定到临时数据,还可以将引用绑定到类型相近的数据,这使得引用更加灵活和通用,它们背后的机制都是临时变量。

当引用作为函数参数时,如果不会修改数据,尽量为引用添加 const 限制。

概括起来说,将引用类型的形参添加 const 限制的理由有三个:

  • 使用 const 可以避免无意中修改数据的编程错误;
  • 使用 const 能让函数接收 const 和非 const 类型的实参,否则将只能接收非 const 类型的实参;
  • 使用 const 引用能够让函数正确生成并使用临时变量。

7. 总结

  • 引用可以看作是数据的别名,需要在定义时初始化,且此后不能更改引用对象。
  • 引用本质上是对指针的封装,编译器不让获取存储该引用(指针)的地址,只能获取绑定对象的地址(引用的值)。
  • 引用和指针区别:初始化、const、多级、自增自减运算符。
  • 引用不能绑定临时数据、常量表达式,除非该引用使用 const 加以限制。
  • const 引用能绑定临时数据是因为编译器为临时数据创建了临时变量,临时数据无法寻址无法写入同步,增加const 限制后不需要修改就不会面临同步问题。
  • 引用和指针都不能指向不同类型的数据,但 const 引用将通过对临时数据创建临时变量的方法,在能进行自动类型转换的前提下,能绑定不同类型的数据。
posted @ 2021-09-06 20:25  锦瑟,无端  阅读(292)  评论(0编辑  收藏  举报