zl程序教程

您现在的位置是:首页 >  其他

当前栏目

【C++】引用与指针

2023-03-07 09:46:44 时间

专栏放在【C++知识总结】,会持续更新,期待支持?


引用

引用的概念

在C++中,引用的本质其实就是给一个已经存在的变量”起别名“。也就是说,引用与它所引用的对象共用一块空间。(同一块空间的多个名字)

就比如说,李逵又叫黑旋风,而黑旋风就是指李逵本人,只是名字换了而已。

引用的特性

1. 引用在定义时必须初始化

2. 一个变量可以有多个引用,但一个引用只能有一个实体对象

#include<iostream>
using namespace std;
int main()
{
    int a = 0;
    //一个变量可以有多个引用,但一个引用只能有一个实体
    int& b = a;//不可以写成int& b;  引用必须在定义时初始化
    int& c = b;
    int& d = c;

    cout << &a << endl;//012FFBD0
    cout << &b << endl;//012FFBD0
    cout << &c << endl;//012FFBD0
    cout << &d << endl;//012FFBD0
    //地址相同,abcd共用同一块空间
    // 另外,引用类型与引用实体的类型必须一致,这里不能写为char & d = a(error)
    return 0;
}

扩展(函数栈帧的创建与销毁)

这里我们进行复习一下关于函数栈帧的一些知识。我们知道,在调用一个函数时, 首先会在内存占用一块空间,用来创建该函数的函数栈帧,当调用结束后,该函数栈帧会被销毁,这里需要注意的是,当 栈帧被销毁后,这里的空间实际上在内存中还是存在的,只不过空间的使用权不再归我们使用并且函数栈帧的销毁,可能会对原有空间进行清理。

这里可以举个例子来理解一下,就好比说我们在酒店开了一个房间,并且在退房时把我们的电脑放在了房间里,在这里,酒店就相当于内存的存在,而我们退房的那一刻,就好比 函数栈帧销毁的那一刻,但是虽然我们退房了,该房间还是实际存在的,并没有说随着我们的退房而消失,只不过不再归我们使用。并且房间里的东西也 可能会被清理(也可能依然还在),加入此时我们再进行使用该房间,用是可以用,只不过肯定是不合法的,这种行为就好比 空间的非法访问。

引用的使用场景

做参数进行引用(输出型参数)

所谓输出型参数,实际上就是可以影响实参的参数,就比如我们经常写的交换两个变量的值,在以前我们会使用指针来完成传址调用,从而实现形参的改变影响实参,但现在我们可以用引用来实现,如下:

//做参数来使用,由于共用同一块空间,所以这里的c实际上就是a,d实际就是b
void Swap(int& c, int& d)
{
    int tmp = c;
    c = d;
    d = tmp;
}
int main()
{
    int a = 1, b = 2;
    Swap(a, b);
    cout << a << " " << b << endl;//2 1
    return 0;
}

可以做返回值使用

我们先来看这样一段代码:

这里注意的是:这里的a是局部变量,生命周期会随着栈区的销毁而结束,所以这里返回的实际上并不是a,我们通过查看反汇编发现实际上是借助了一个临时变量来实现的。

那么不禁会有个疑问,假如这里的a不随着栈帧的结束而销毁,那么会不会直接返回a呢?可以试验一下:

对于这种现象,我们可以把引用作为返回值来使用,从而实现优化,写成如下格式:

//返回值
int& Test()
{
    static int a = 10;
    a++;
    return a;//也会产生临时变量,但是临时变量的类型是int& 也就是a的别名,即临时变量就是返回的a,减少了拷贝操作
}
int main()
{
    int ret = Test();
    return 0;
}

这就是引用返回,即在返回类型前面加上&,虽然也需要借助临时变量的存在,但是由于临时变量的类型为int& ,即临时变量就是a,所以就减少了临时变量的拷贝工作,会使效率得到提升。我们可以来验证一下:

传值返回 vs 传引用返回效率对比

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
    // 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 1000000; ++i)
        TestFunc1();
    size_t end1 = clock();
    // 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 1000000; ++i)
        TestFunc2();
    size_t end2 = clock();
    // 计算两个函数运算完成之后的时间
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

int main()
{
    TestReturnByRefOrValue();
}

运行结果如下:

我们发现传引用返回对比传值返回,效率会有显著提高(作为参数使用时,传引用参数的效率也会高于传值作为参数的效率)

当然,传引用作为返回值的使用是有一定的限制的,我们发现上面的代码能使用传引用返回的原因在于,返回的变量不会随着作用域的销毁而销毁假如说返回的对象出了作用域后已经销毁,则必须使用传值返回,否则返回的结果是不确定的!

并且引用用作返回时,还可以修改返回对象(后面的学习会用到很多,这里简单介绍)

如下:

#define N 10
typedef struct ARR
{
    int arr[N];
}ARR;

int& PosARR(ARR& arr,int i)
{
    return arr.arr[i];//这里的arr就是main函数里的arr,不会随着PosARR函数的结束而销毁,所以可以用引用返回
}
int main()
{
    ARR arr;
    for (int i = 0; i < N; i++)
    {
        PosARR(arr, i) = i * 10;//引用返回可以修改返回对象,这里的返回对象为arr.arr[i],对此进行修改
    }
    for (int i = 0; i < N; i++)
    {
        cout << arr.arr[i] << " ";//0 10 20 30 40 50 60 70 80 90
    }
    return 0;
}

总结

引用可以用作参数来使用(输出型参数),也可以用作返回使用,用作返回使用时返回的对象必须是出了所在函数作用域后不会销毁的(比如static修饰的变量,全局变量,malloc......),并且引用返回时,返回的对象可以被修改。同时还可以减少拷贝提高效率。

常引用

我们要记住这样一句话:指针和引用在赋值或者初始化时,权限可以被缩小或者保持,但不可进行修改。

这是什么意思呢?通过以下代码进行了解:

    // 权限放大(error)
    //const int c = 2;//const 修饰的常量不可以进行修改,可以理解只具有读的属性,不具有写的属性,而d可以修改,所以权限被放大
    //int& d = c;//这里正确写法应为const int& d=c;

    //const int* p1 = NULL;
    //int* p2 = p1;//同上,前面加个const即可,const int* p2=p1; (√)

    // 权限保持
    const int c = 2;
    const int& d = c;

    const int* p1 =NULL;
    const int* p2 = p1;

    // 权限缩小
    int x = 1;//x可以进行修改,可以理解为具有读和写的属性,而x是const修饰的,只具有读的属性,权限缩小了
    const int& y = x;

    int* p3 = NULL;
    const int* p4 = p3;//同上

不仅如此,由于所谓临时变量具有常性(即不可被修改)的原因,也会出现以下的情况:

    int i = 0;
    //double& p = i;//error
    //由于int到double类型发生类型转换,而类型转换会产生临时变量,临时变量又具有常性(只可读)
    //因此在前面加上const即可
    const double& p=i;//(√)

这也就解释了上文说到的引用类型与引用实体的类型必须一致,同样,在函数中也一样适用:

int add(int x, int y)
{
    int c = x + y;
    return c;//实际上是借助临时变量,将c拷贝给临时变量,再将临时变量拷贝给p
}
int main()
{
    int a = 1, b = 2;
    //int& p = add(a, b);//由于临时变量具有常性的特点,所以不可以这样写
    //前面加上const 即可
    const int& p = add(a, b);

    return 0;
}

引用与指针

&是一个很熟悉的符号,与指针有关,用在变量前面就是取地址符号,用在类型后面则为引用符号,那么指针与引用之间是否有着什么关系呢?

int a=0;
int* p=&a;//&:取地址符
int& b=a;//&:引用

指针与引用的相同点

实际上,引用与指针,两者之间在底层实现上其实是一样的,我们可以来进行验证

当然,两者之间也存在着很大的区别。

指针与引用的不同点

首先就是在语法概念上的区别, 引用只是同一个实体的不同名称, 不会单独开辟空间,但是指针会在内存开辟一块4/8byte大小的空间。 引用在定义时 必须初始化,指针没有要求 引用在初始化时引用一个实体后,就 不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体(这一点也就意味着 引用并不能实现完全替代指针,就比如在链表这里,用来指向下一个节点的变量类型,只能是指针) 有多级指针,但是没有多级引用 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 引用比指针使用起来相对更安全(野指针)

...

看法:

因此对于指针与引用,我们只能是说引用相较于指针来说,更加容易理解使用,并且也不会存在空引用的问题,但是在一些场景下,引用自身的特点(不能改变指向)也存在着使用限制,此时就得用指针来实现


end

生活原本沉闷,但跑起来就会有风!?