跳至主要內容

引用

张威大约 12 分钟c/c++c++基础

引用

什么是引用?

在理解引用概念前,先回顾一下变量名。 变量名实质就是一段连续内存空间的别名。那一段连续的内存空间只能取一个别名吗? 显然不是,引用的概念油然而生。在C++中,引用是一个已定义变量的别名。其语法是:

类型 &引用名 = 目标变量名;

void test0()
{
    int a = 1;
    int &ref1 = a;
    int &ref2;
}

在使用引用的过程中,要注意以下几点:

  1. &在这里不再是取地址符号,而是引用符号,相当于&有了第二种用法
  2. 引用的类型必须和其绑定的变量的类型相同
  3. 声明引用的同时,必须对引用进行初始化;否则编译时报错
  4. 一旦绑定到某个变量之后,就不会再改变其指向 (类型 * const)

引用和取地址的区别

的为引用,不带的是取地址
int &b = a;
&c;

引用的本质

C++中的引用本质上是一种被限制的指针(类型 * const)。类似于线性表和栈的关系,栈是被限制的线性表,底层实现相同,只不过逻辑上的用法不同而已(获取地址,然后赋值)。

#include <iostream>
//#include <typeinfo.h>	编译提示typeinfo.h找不到
#include <typeinfo>

int main()
{
    int a = 10;
    int * p = &a;
    int &b = a;
 
    *p = 20;
    b= 30;

    return 0;
}
  • 编译提示typeinfo.h找不到 解决方法:typeinfo.h 改为typeinfo
$g++ -g main.cc
$objdump -M intel -S a.out 
image-20240202155647969
image-20240202155647969

由于引用是被限制的指针,所以引用是占据内存的,占据的大小就是一个指针的大小。有很多的说法,都说引用不会占据存储空间,其只是一个变量的别名,但这种说法并不准确。引用变量会占据存储空间,存放的是一个地址,但是编译器阻止对它本身的任何访问,从一而终总是指向初始的目标单元。在汇编里, 引用的本质就是“间接寻址”。在后面学习了类之后,我们也能看到相关的用法。

引用作为函数参数

在没有引用之前,如果我们想通过形参改变实参的值,只有使用指针才能到达目的。但使用指针的过程中,不好操作,很容易犯错。 而引用既然可以作为其他变量的别名而存在,那在很多场合下就可以用引用代替指针,因而也具有更好的可读性和实用性。这就是引用存在的意义。 一个经典的例子就是交换两个变量的值。

//用指针作为参数
void swap(int *pa, int *pb) 
{
    int temp = *pa;
    *pa = *pb;
    *pb = temp;
}
//引用作为参数
void swap(int &x, int &y)
{
    int temp = x;
    x = y;
    y = temp;
}

参数传递的方式除了上面的指针传递引用传递两种外,还有值传递采用值传递时,系统会在内存中开辟空间用来存储形参变量,并将实参变量的值拷贝给形参变量,即形参变量只是实参变量的副本而已;如果函数传递的是类对象,系统还会调用类中的拷贝构造函数来构造形参对象,假如对象占据的存储空间比较大,那就很不划算了。这种情况下,强烈建议使用引用作为函数的形参,这样会大大提高函数的时空效率。 当用引用作为函数的参数时,其效果和用指针作为函数参数的效果相当。当调用函数时,函数中的形参就会被当成实参变量或对象的一个别名来使用,也就是说此时函数中对形参的各种操作实际上是对实参本身进行操作,而非简单的将实参变量或对象的值拷贝给形参。 使用指针作为函数的形参虽然达到的效果和使用引用一样,但,而引用则不需要这样,故在C++中推荐使用引用而非指针作为函数的参数

引用作为函数的返回值

//语法: 
类型 &函数名(形参列表)
{ 
    //函数体 
}

当以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,

int gNumber;//全局变量

int func1() // 当函数返回时,会对temp进行复制
{
    temp = 100;
    return temp;//此处会进行复制操作
}

int &func2()//当函数返回时,不会对temp进行复制,因为返回的是引用
{
    temp = 1000;
    return temp;
}

当引用作为函数的返回值时,必须遵守以下规则:

  1. 不能返回局部变量的引用。主要原因是
  2. 不能在函数内部返回new分配的堆空间变量的引用
int &func3() 
{
    int number = 1;
    return number;
}

int &func4()
{
    int *pInt = new int(1);
    return *pInt;
}

void test()
{
    int a = 3, b = 4;
    int c = a + func4() + b;//内存泄漏
}

引用总结:

  1. 在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
  2. 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性
  3. 引用与指针的区别是,指针后,对它所指向的变量。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作()

课堂代码

#include <iostream>

using std::cout;
using std::endl;

//指针与引用的异同点?
void test()
{
    int number = 10;
    int &ref = number;//引用是变量的别名,引用的提出就是为了减少指针的使用
    &ref;
    cout << "number = " << number << endl;
    cout << "ref = " << ref << endl;
    printf("number = %p\n", &number);	//打印地址常用printf %p更方便 
    printf("ref = %p\n", &ref);

    cout << endl;
    int number2 = 200;
    ref = number2;//操作引用与操作变量本身是一样的
    cout << "number2 = " << number2 << endl;
    cout << "number = " << number << endl;
    cout << "ref = " << ref << endl;
    printf("number2 = %p\n", &number2);
    printf("number = %p\n", &number);
    printf("ref = %p\n", &ref);

    cout << endl;
    //引用的实质:指针常量 * const
    /* int &ref2;//引用不能独立存在,在定义的时候必须要进行初始化,在定义的 */
              //的时候绑定到变量上面,跟变量绑定到一起,不会改变引用的指向
}

//1、引用作为函数参数
#if 0
//值传递====副本
//没有触及a b本身
void swap(int x, int y)//int x = a, int y = b
{
    int temp = x;
    x = y;
    y = temp;
}
#endif
#if 0
//值传递====地址值
void swap(int *px, int *py)//int *px = &a, int *py = &b;
{
    int temp = *px;
    *px = *py;
    *py = temp;
}
#endif
//引用传递====变量本身
void swap(int &x, int &y)//int &x = a, int &y = b
{
    int temp = x;
    x = y;
    y = temp;
}

void test2()
{
    int a = 3, b = 4;
    cout << "在交换之前 a = " << a << ", b = " << b << endl; 
    swap(a, b);
    cout << "在交换之后 a = " << a << ", b = " << b << endl; 
}

//2、引用作为函数返回类型

int func1()
{
    int number = 10;
    return number;//执行一个拷贝操作
}

int &func2()
{
    int number = 10;//局部变量
    return number;//不能返回一个局部变量的引用
}

//不要去返回堆空间的引用,必须要有内存回收的机制
int &getHeapData()
{
    int *pInt = new int(100);

    return *pInt;
}

void test4()
{
    int a = 3, b = 5;
    int temp = a + getHeapData() + b;
    cout << "temp = " << temp << endl;

    int &ref = getHeapData();
    delete &ref;	//虽然没问题,但这种写法很怪,而且别人不知道分装了new
}

//函数返回类型是引用的前提:实体的生命周期一定要大于函数的生命周期
int arr[10] = {1, 3, 5, 7, 9, 10};
int &getIndex(int idx)
{
    return arr[idx];//先不去考虑越界
}

void test3()
{
    cout << "getIndex(0) = " << getIndex(0) << endl;
    getIndex(0) = 200;
    cout << "getIndex(0) = " << getIndex(0) << endl;
    cout << "arr[0] = " << arr[0] << endl;
    
    /* func1() = 200; */
}
int main(int argc, char **argv)
{
    test4();
    return 0;
}

引用和指针的区别

  1. 引用是变量的别名,是对目标对象的直接操作;指针是通过某个指针变量指向一个对象,对所指的对象进行间接操作
  2. 引用必须初始化,不能初始化空对象,初始化后不能改变;指针可以不初始化,但最好初始化(野指针)
  3. 引用的本质是指针常量,在汇编上引用和指针生成的指令一样,通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的
  4. 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针
  5. 在参数传递时,引用不会产生副本,指针会将实参地址拷贝给形参

左值引用

左值:有内存地址,有名字,值可以修改;

int a = 10;	//左值:有内存地址,有名字,值可以修改;
int &b =a;

int &c =10;//错误 20 是右值,20=40是错误的,其值不能修改,没内存,没名字,是一个立即数;

上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:

const int &var = 10;

使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:

const int temp = 10; 
const int &var = temp;

根据上述分析,得出如下结论:

  • ; 但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了

那么C++11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。

右值引用

C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:

  • 可以取地址的,有名字的,非临时的就是左值;
  • 不能取地址的,没有名字的,临时的就是右值;

可见立即数函数返回的值等都是右值非匿名对象(包括变量),函数返回的引用const对象等都是左值。

从本质上理解,

  • 创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);
  • 用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
#include <iostream>
#include <typeinfo>
int main()
{
    int a = 10;
    int &b = a;
    int &&c = 20;	//C++11提供了右值引用     
    
    std::cout << typeid(b).name() << std::endl; //i	
    std::cout << typeid(c).name() << std::endl; //i
    
    const int *d = &a;
    std::cout << typeid(d).name() << std::endl; //PKi
    return 0;
}
$g++ -g -std = c++11 main.cc
$objdump -M intel -S a.out
image-20240202162644415
image-20240202162644415

const int &var = 10;
//var = 20;	//error,被const修饰不可以修改

int &&c = 20;	//右值引用  
c = 30;	//OK,没有const修饰,右值引用可以修改值

int &e = c;	//一个右值引用变量本身是一个左值(c本身有内存有名字)
//int &&e = c;	//error,右值引用只能引用右值,因为右值引用会产生一个临时量,而左值本身就有内存,所有代码有冲突

总结:

  1. 右值引用只能用来引用右值类型int &&c = 20;指令上,可以自动产生临时量,然后直接引用临时量;可以修改临时量的内存c = 30

  2. 右值引用本身是一个左值,只能用左值引用来引用它

  3. 不能用一个右值引用变量来引用左值

例题

写一句代码在内存的0x0018ff44处写一个4字节的10

int* p = (int*)0x0018ff44;	//0x0018ff44是一个整数需要强转
*p = 10;
int *&&p = (int*)0x0018ff44;	//右值引用,引用对象是int * 类型
int *const &p = (int*)0x0018ff44;	//引用对象是个常量

🍗🍗🍗

// 例1
int a = 10;
int *p = &a;
const int *&q = p; // 等价于const int** <= int**,error
//不要被表象迷住了,看成const int* <= int*,要转成指针看
//-> const int **q = &p
//-> const int ** = int**


// 例2
int a = 10;
int *const p = &a;
int *&q = p; //因为p是一个指针常量,即指针的值(即地址)是不可变的,但是q却是一个指向指针的引用,它允许改变指针的值
// 等价于int **q = &p; 
//即 int** <= int* const *
//-> int* <= const int *,error