超级详细的C++教程3:引用详解

上一期C++教程的博客链接:超级详细的C++教程2:缺省参数,函数重载


1.前言

C++对C语言改进最大其实就是引用,引用不仅难懂,同时它还非常的重要。刚开始学的时候可能会觉得指针能解决的东西为什么还出一个引用,这不显得C++有些杂乱吗?其实这是因为我们接触的少,后面代码写多了之后就会发现引用还是挺香的。


2.引用

2.1引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

我们可以把引用简单的理解为给变量起外号,比如孙悟空,在猪八戒眼里弼马温,天上的神仙称呼它为大圣,弼马温和大圣都是孙悟空。


2.2引用的定义

类型& 引用变量名(对象名) = 引用实体;


注意:上面&这个符号表示它是一个变量的引用不是取地址操作符。

我们可以给任何类型的变量取别名,例如:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int& c = a;
    return 0;
}

上面代码中,c就是a别名,因为编译器不会为引用变量开辟内存空间,所以c和a共用同一块空间,c的改变也会影响a,同时a的改变也会影响c,例如:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int& c = a;

    a += 20;
    cout << a << " " << c << endl;
    c-= 20;
    cout << a << " " << c << endl;
    return 0;
}

我们将a和c的地址打印出来,就会看到它们两个的地址是一样的,例如:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int& c = a;

    cout << &a << endl;
    cout << &c << endl;
    return 0;
}

既然它们的地址都是一样的,我们就可以简单的理解为a和c是同一个变量。

一个变量可以有多个别名,也可以给别名取别名,例如:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    int& b = a;
    int& c = a;
    int& d = c;
    
    return 0;
}

它们都是同一个变量,也就是最初的a变量。


2.3引用的使用场景

1.做参数

1.交换两个变量的值

还记得在C语言中所学的swap函数吗?它的功能是交换两个变量的值,因为形参是实参的临时拷贝,形参的改变不会影响到实参,如果要改变实参,只能传地址过去,所以swap函数的参数必须是指针,例如:

#include <iostream>
using namespace std;

void Swap(int* left, int* right)
{
    int temp = *left;
    *left = *right;
    *right = temp;
}

int main()
{
    int a = 3;
    int b = 5;
    Swap(&a,&b);
    return 0;
}

指针虽然好用,但是每次调用传参的时候都要取地址,有些麻烦,有了引用之后,我们就可以不用指针了,例如:

#include <iostream>
using namespace std;

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

int main()
{
    int a = 3;
    int b = 5;
    Swap(a,b);
    return 0;
}

因为left是a的别名,right是b的别名,left和right的改变会影响到a和b。所以a和b的值就被交换了,使用引用的swap函数,不用写*(解引用操作符),还有传参的时候不用写&(取地址操作符),所以引用使用上还是很爽的。

2.解决二级指针所带来的麻烦

C语言中要改变实参的值需要使用一级指针,那如果要改变实参为一级指针所指向的地址呢?

C语言要使用二级指针才行,例如:

#include <iostream>
#include <stdlib.h>
using namespace std;

void test(int** ppa)
{
    *ppa = (int*)malloc(sizeof(int));
    **ppa = 10;
}

int main()
{
    int a = 20;
    int *pa = &a;
    cout << *pa << endl;
    test(&pa);
    cout << *pa << endl;
    free(pa);
}

C++可以不用二级指针,直接使用引用,例如:

#include <iostream>
#include <stdlib.h>
using namespace std;

void test(int* &ra)
{
    ra = (int*)malloc(sizeof(int));
    *ra = 10;
}

int main()
{
    int a = 20;
    int *pa = &a;
    cout << *pa << endl;
    test(pa);
    cout << *pa << endl;
    free(pa);
}

上面写到我们可以给任何类型的变量取别名;所以我们可以给指针取别名,ra是pa的别名,ra的改变也会影响到pa。虽然看上去只少写了一个*(解引用操作符)和&(取地址操作符),等到了后面我写了C++数据结构:AVL树类和对象后你就知道使用引用的好处了。

3.减少传参时间消耗

如果函数参数是一个很大的对象,除了使用指针外,使用引用也能提高效率指针的大小也就4/8个字节引用传的对象的别名,这样能减少不必要的时间消耗,提高程序的运行效率,例如:

struct test
{
    int a[1000000];
};

void func(struct test* t)
{
    //......
}

void func(struct test& t)
{
    //......
}

2.做返回值

引用做返回值候需要注意了,不能返回局部变量局部变量一但离开它所在的作用域,它的生命周期就结束了,例如:

#include <iostream>
using namespace std;
//错误代码演示:
int& func()
{
    int n = 0;
    n++;
    return n;
}

int main()
{ 
    int& a = func();
    return 0;
}

上面代码中,main函数中func函数调用完毕,它的栈帧就会销毁,局部变量n也就不在了,func函数最后返回了n的引用,又用引用来接收,如果我们去访问a就是在访问野指针,Linux平台上使用G++编译运行后直接报错(在VS上它会做一次保留)。所以引用做返回值的时候,我们需要注意一下返回对象的生命周期!!!

我们需要将上面代码中func函数里的局部变量n修改为静态的局部变量或者全局变量,所以引用做返回值正确的写法,例如:

#include <iostream>
using namespace std;

int& func()
{
    static int n = 0;
    n++;
    return n;
}

int main()
{ 
    int& a = func();
    return 0;
}

1.提高效率

如果函数最后返回的是一个很大的对象,除了使用指针,用引用做返回也能提高效率,例如:

struct test
{
    int a[1000000];
};

struct test& func()
{
    static struct test t;
    return t;
}

int main()
{
    struct test& a = func();
    return 0;
}

后面我们学到类和对象后,就会经常使用引用做返回值。

2.修改返回对象

在C语言中,如果我们想修改返回对象,必须返回的是指针,如下面代码,我们要修改的是func函数里的静态局部数组arr下标位置为pos的元素,所以我们返回了该元素的指针后就能在外部进行修改。

#include <iostream>
using namespace std;

int* func(int pos)
{
    static int arr[10] = {1};
    return &arr[pos];
}


int main()
{
    *(func(9)) = 180230;
    cout << *(func(9)) << endl;
    // int *pa = func(9);
    // cout << *pa << endl;
    return 0;
}

C++中使用引用也能做到,例如:

#include <iostream>
using namespace std;

int& func(int pos)
{
    static int arr[10] = {1};
    return arr[pos];
}


int main()
{
    func(9) = 180230;
    cout << func(9) << endl;
    return 0;
}

指针能修改返回对象是因为返回的是对象地址,通过地址间接修改了返回对象的值,引用能修改返回对象是因为返回的是对象的引用(别名),通过别名进行修改返回对象的值。

这一部分理论知识不扎实的同学会难以理解,需要不断的去练习才能加深印象,因为后面的类和对象会遇到类似于上面部分的代码。


2.4常引用

1.权限部分

前面我们都是给变量取别名,我们能不能给常变量取别名呢?答案是可以的,但是引用的前面必须加上const才行,例如:

int main()
{
    const int a = 10;
    //int& r = a;//错误代码,因为a是常变量,所以引用前面必须加上const
    const int& r = a;
    return 0;
}

上面代码中,如果引用的前面不加上const编译器会报错,因为出现了权限的放大。a加上了const后就应该不能被修改了,但是我们给它取了个别名后不加const后面可能会间接或直接的修改了a,这样做显然不合理。

所以使用引用的原则是:权限不能被放大,权限可以平移,权限可以缩小,例如:

int main()
{
    //权限的平移
    const int a = 10;
    const int& ra = a;

    //权限的缩小
    int b = 20;
    const int& rb = b;
    
    return 0;
}

只要不涉及权限的放大,我们还可以给常量取别名(一般只会出现在传参),例如:

int main()
{
    const int& e = 10;
    return 0;
}

2.隐式类型转换部分

引用可不可以引用不是同类型的对象(如int类型的引用,去引用double类型)?答案是不可以,例如:

int main()
{
    double a = 3.14;
    int& re = a;//错误代码,编译器直接报错
    return 0;
}

但是如果给上面代码中的引用re加上const以后编译就能通过了,例如:

int main()
{
    double a = 3.14;
    const int& re = a;
    return 0;
}

原因是因为re引用的不是adouble类型的a转成int类型,并不是直接拿a去转成int类型,而是中间会生成一个临时变量(隐式类型转换/显示的强制类型转换中间都会生成一个临时变量),所以re引用的其实是中间生成的临时变量临时变量具有常属性,如果不加上const会造成权限的放大,这就是加上const后编译通过的原因,如下图所示:


2.5引用需要注意的点(重点)

1.引用必须初始化,例如:

int main()
{
    int& a;//错误代码,引用必须初始化
    int& b = NULL;//错误代码,NULL是一个常量标识符,必须要常引用才行
    return 0;
}

2.引用初始化不能改变指向,例如:

int main()
{
    int a = 10;
    int b = 20;
    int& ra = a;
    ra = b; 
    return 0;
}

上面代码中,ra = b的意思是将b的值赋值给ra,不是将ra的指向修改为b。

3.引用不能完全替换掉指针,因为引用不能改变指向,所以引用是无法替换掉指针,我们可以去思考链表这一部分,如果将指针替换成引用会导致节点丢失。


2.6引用的底层(了解)

引用的底层其实就是指针,我们可以通过VS上的调式里转到反汇编中就可以看出来,例如:

可以看见,指针的汇编代码和引用的汇编代码是一样的。

引用在语法上不开空间(我们可以用sizeof来计算(计算出来的结果是引用的对象的大小),最好不要用int类型),引用在底层开了4/8字节的空间,因为引用的底层是指针。


2.7引用和指针的区别

1. 引用概念上定义一个变量的别名,指针存储一个变量地址;
2. 引用在定义时必须初始化,指针没有要求;
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
4. 没有NULL引用,但有NULL指针;
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节);
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
7. 有多级指针,但是没有多级引用;
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
9. 引用比指针使用起来相对更安全;

前情回顾:

看到这里同学,如果感兴趣还可以去看一下我所写的数据结构的博客,手把手带你手撕代码,可以关注一下我的C++和C数据结构,它们都还在持续更新:

超级详细的C++教程1:命名空间,输入输出

C语言数据结构:顺序表

C语言数据结构:链表

C语言数据结构:带头双向循环链表

C语言数据结构:栈

C语言数据结构:队列

C语言数据结构:树和二叉树的基本概念

C语言数据结构:堆

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值