【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


相关文章
|
8月前
|
机器学习/深度学习 编解码 vr&ar
NeurIPS 2024最佳论文,扩散模型的创新替代:基于多尺度预测的视觉自回归架构
本文详细解读NeurIPS 2024最佳论文《视觉自回归建模:基于下一尺度预测的可扩展图像生成》。该研究提出VAR模型,通过多尺度token图和VAR Transformer结构,实现高效、高质量的图像生成,解决了传统自回归模型在二维结构信息、泛化能力和计算效率上的局限。实验表明,VAR在图像质量和速度上超越现有扩散模型,并展示出良好的扩展性和零样本泛化能力。未来研究将聚焦于文本引导生成和视频生成等方向。
865 8
NeurIPS 2024最佳论文,扩散模型的创新替代:基于多尺度预测的视觉自回归架构
|
Rust 安全 编译器
初探 Rust 语言与环境搭建
Rust 是一门始于2006年的系统编程语言,由Mozilla研究员Graydon Hoare发起,旨在确保内存安全而不牺牲性能。通过所有权、借用和生命周期机制,Rust避免了空指针和数据竞争等问题,简化了并发编程。相较于C/C++,Rust在编译时预防内存错误,提供类似C++的语法和更高的安全性。Rust适用于系统编程、WebAssembly、嵌入式系统和工具开发等领域。其生态系统包括Cargo包管理器和活跃社区。学习资源如&quot;The Book&quot;和&quot;Rust by Example&quot;帮助新手入门。安装Rust可通过Rustup进行,支持跨平台操作。
325 2
初探 Rust 语言与环境搭建
|
存储 人工智能 物联网
端侧设备AI代理优化框架问世,领域内准确率可达97%
【7月更文挑战第30天】新框架Octo-planner提升端侧AI代理效率与准确性至97%。此框架由Nexa AI等机构合作研发,采用&quot;Planner-Action&quot;模式,将AI代理任务划分为规划与执行两部分,利用&quot;Octopus&quot;及&quot;Phi-3 Mini&quot;模型分别处理。通过fine-tuning技术及GPT-4辅助,实现在资源受限设备上的高性能。更多细节见论文: https://arxiv.org/pdf/2406.18082
236 1
|
安全 关系型数据库 分布式数据库
PolarDB-PG 安全体系全解,如何给客户7*24的放心
随着企业业务全面向数字化、在线化、智能化演进,企业面临着呈指数级递增的海量存储需求和挑战,传统的商业数据库已经难以满足和响应快速变化持续增长的业务诉求。云数据库凭借着成本、性能、业务连续性以及在线业务扩展等优势成为企业更优的选择。随着企业数据逐步上云,云数据库安全变得至关重要。云数据库安全不仅可以防止未授权访问和数据泄露问题,保护数据的机密性和完整性,还可以保护企业的声誉和客户信任,保障企业遵守法律法规的要求。只有通过确保云数据库的安全性,企业才能够在数字化时代中安心地利用云服务。
|
存储 网络协议 Ubuntu
Linux环境下SVN服务器的搭建与公网访问:使用cpolar端口映射的实现方法
由于文档资料越来越多,将所有资料都存放在自己的电脑上容易混淆,并且也不利于分享。这种情况下,考虑将资料上传SVN统一管理,这样一来其他人也能很方便的查略各种资料。
|
前端开发
CSS实现鼠标悬停图片向上浮动,放大,翻转
CSS实现鼠标悬停图片向上浮动,放大,翻转
460 0
|
前端开发 Java 网络安全
ssh(Spring+Spring mvc+hibernate)简单增删改查案例
ssh(Spring+Spring mvc+hibernate)简单增删改查案例
|
前端开发 JavaScript
从0搭建Vue3组件库(五): 如何使用Vite打包组件库
从0搭建Vue3组件库(五): 如何使用Vite打包组件库
1005 0
|
XML JSON 前端开发
SpringBoot2.x系列教程15--SpringBoot中整合HttpMessageConverters实现JSON格式化
前言 在之前的章节中,壹哥 带着各位学习了如何在Spring Boot中进行SSM整合。那么接下来,我们会继续深入研究SpringBoot对SpringMVC框架的支持,学习SpringBoot如何进行更深度的定制化Web开发。 前面我讲过,SpringBoot严格的来说,应该是一种负责把其他已有框架整合在一起的工具,SpringBoot主要是把各种框架都整合集中在一起,简化我们的Web开发。所以很多的功能,其实都不是SpringBoot完成的,而是由SpringBoot中整合的其他框架来完成的。比如Web开发,更多的是由SpringMVC来完成,只是SpringBoot很好的把Spring
1317 0
|
机器学习/深度学习 自然语言处理 前端开发
CSS3动画属性之Transition
CSS3动画属性之Transition
193 0

热门文章

最新文章