【C++】引用(上)

简介: 【C++】引用(上)

👉引用👈

引用概念


引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空

间,它和它引用的变量共用同一块内存空间


   我们通过下面的代码来初步了解一下引用。


#include <iostream>
using namespace std;
int main()
{
  int a = 10;
  int& ra = a;//<====定义引用类型
  printf("%p\n", &a);
  printf("%p\n", &ra);
  return 0;
}

7fc0d187c1e64be186881859f805998f.png注意:引用类型必须和引用实体是同种类型的


引用特性


  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体(引用无法完全替代指针的原因)


#include <iostream>
using namespace std;
void TestRef()
{
  int a = 10;
  // int& ra; // 该条语句编译时会出错
  int& ra = a;
  int& rra = ra;
  // ra++或者rra++,都是a++
  ra++;
  rra++;
  printf("%d %d %d\n", a, ra, rra);
  printf("%p %p %p\n", &a, &ra, &rra);
}
int main()
{
  TestRef();
  return 0;
}

64f988a2e15d44449ff68862707ece10.png


使用场景


引用和指针的关系

cea73c625c5d4e8ca9ba4ba79ca92351.png


1.做参数


因为引用是一个变量的别名,所以对引用进行操作就是对变量进行操作。因此引用能够做到指针能做到的事情。比如:交换两个变量的值和修改头指针等等。


ddeeeff1a25549048f003efd6943fcac.png

8389b07c568446b68494fc7583e2c146.png


有了引用,链表的尾插和头插函数都不再需要传结构体的二级指针了,只需要将头插和尾插的函数的形参设置为一级指针的引用就可以了,这样就可以修改到头指针了。引用做参数的一个作用就是作为输出型参数,函数中修改别名的值,实参的值也就修改了。


引用做参数还有另一个作用就是减少拷贝,提升效率。见下面的代码:


#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
  A a;
  // 以值作为函数参数
  size_t begin1 = clock();
  for (size_t i = 0; i < 100000; ++i)
    TestFunc1(a);
  size_t end1 = clock();
  // 以引用作为函数参数
  size_t begin2 = clock();
  for (size_t i = 0; i < 100000; ++i)
    TestFunc2(a);
  size_t end2 = clock();
  // 分别计算两个函数运行结束后的时间
  cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
  cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
  TestRefAndValue();
  return 0;
}

ecacc07609334c24a5d5347b8c52bdf8.png


2.做返回值


学习引用做做返回值之前,我们先来分析一下一下的代码。


#include <iostream>
using namespace std;
int Count()
{
  int n = 0;
  n++;
  // ...
  return n;
}
int main()
{
  int ret = Count();
  return 0;
}

997c1ac1c9c24f5194299e827b5860e8.png


了解上一段代码,我们再来看一下这一段代码。


#include <iostream>
using namespace std;
// 引用返回
int& Count()
{
  static int n = 0;
  n++;
  // ...
  return n;
}
int main()
{
  int ret = Count();
  return 0;
}


注意,这时候Count函数的返回值是int&,所以Count函数返回的是n的别名。因为n是关键字static修饰的变量,所以n不会随着Count函数的函数栈帧销毁而销毁。也就是说,我们可以通过该返回值n的别名来访问n。

a3620c99d4a743678ebabb4320b8a146.png

现在我们已经知道了,如果一个函数的返回值为某个变量引用,那么该变量不能是在栈区上申请的,可以是在栈区或者静态区申请。如果返回一个局部变量的引用且再去访问这块空间,那么访问的结果是不可知的。那为什么会这样呢?见下图:


01e403fa39944addbd88beff9e3a0846.png


为了说明返回局部变量的引用是不可取的,我们来看下面几个例子。


032bd457da6f42a091fb8825ba2231bd.png


在上面的例子中,我们用ret做Count函数返回值的别名,相当于访问ret就是访问局部变量n的空间,但这个空间已经被销毁了。从上面的打印结果可以看出,第二和第三次打印的结果都是随机值。这就是用局部变量的引用做返回值带来的后果。


我们再来看一个例子。

c45e9d65ac00432caa0228380e814ca9.png


可以看到,三次打印的结果分别是 1、随机值和 100。那为什么会是这样的结果呢?第一次打印的时候,虽然局部变量n的空间被销毁了,但是系统没有使用这块空间,数据也没有清理掉,所以第一次打印的结果是 1。而第二次的结果是一个随机值,就更好理解了,就是系统已经用了这块空间并将其存储的数据置成了随机值(cout也是一次函数调用,需要建立函数栈帧,建立栈帧时刚好用到了这块空间)。而第三次打印呢,就是建立Func函数的函数栈帧时,x的地址刚好是之前n的地址,那么该地址存储的数据就变成了 100。并且Func函数的函数栈帧销毁后,存储x的空间还没有被系统使用,该空间存储的数据还是 100。所以打印ret时,ret访问的空间刚好就是存储x的空间,所以就打印出了 100。是不是真的就这样呢?我们将它们的地址都打印出来看一下,如下图所示:


7bd9eac646a042f4b08b3f765b8ecbc3.png


结论

  • 出了函数作用域,返回变量不存在了,不能用引用返回,因为引用返回的结果是未定义的。
  • 出了函数作用域,返回变量存在,才能使用引用返回。


b7b4d2553f134cb18ebc56c5f23335a0.png


知道了传引用返回需要注意的问题后,我们再来看一个程序。我们给之前写的顺序表增加两个函数接口SeqListSize和SeqListAt就可以替换掉打印顺序表和修改pos位置的值的函数接口了。


size_t SeqListSize(SL* psl)
{
  assert(psl);
  return psl->size;
}
SLDataType& SeqListAt(SL* psl, size_t pos)
{
  assert(psl);
  assert(pos < psl->size);
  return psl->a[pos];
}

85076c5055ff44998d403978c36f6543.png


相关文章
|
存储 前端开发 rax
【C++】C++引用(上)
【C++】C++引用(上)
|
5月前
|
C++
C++引用
C++引用
|
6月前
|
存储 程序员 C语言
|
6月前
|
存储 C++
c++引用
c++引用
51 1
|
6月前
|
设计模式 JavaScript 前端开发
不正确的引用 this
不正确的引用 this
24 0
|
6月前
|
存储 安全 编译器
【c++】引用
【c++】引用
【c++】引用
|
存储 编译器 C语言
C++引用下
C++引用下
108 1
|
人工智能 安全 编译器
[C++: 引用】(二)
[C++: 引用】(二)
93 0
|
存储 安全 编译器
C++之引用(下)
C++之引用(下)
83 0
|
C++
C++之引用(中)
C++之引用(中)
78 0