【C++】C++ 入门(二)(引用)

news2024/11/15 20:36:17

目录

一、前言

二、引用

1、引用的概念

2、引用特性

3、使用场景

3.1、做参数

3.2、做返回值 

4、传值、传引用效率比较

值和引用作为参数的性能比较

值和引用作为返回值类型的性能比较

5、常引用

6、引用和指针的区别


一、前言

上一篇文章我们讲解了 C++ 的命名空间、缺省参数、函数重载等内容,接下来我们继续讲解引用相关的知识。

二、引用

1、引用的概念

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

需要注意的是:引用类型必须和引用实体是同种类型的。 

2、引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体

3、使用场景

3.1、做参数

目的是让形参的改变影响实参。

例一、

我们在学习C语言函数时,一定写过交换函数,用来实现两个数值的交换:

void Swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

交换函数中参数一定是以指针形式存在的。因为如果不是指针,那么形参的改变不会影响实参,自然也就无法实现实参之间的交换。

学了C++之后,我们又多了一种方法可以实现两值交换,那便是引用:

void Swap(int& x, int& y)
{
	int temp = x;
	x = y;
	y = temp;
}

 此时函数调用所传递的形参是实参的别名,所以使用实参的别名来交换数值,可以影响到实参。


例二、

在学习链表过程中,因为在进行插入删除操作时有可能需要改变头节点的指针,所以我们之前都是采用传递二级指针的方法实现的。

typedef struct Node
{
    struct Node* next;
    int val;
}Node, *PNode;

void PushBack(Node** phead, int x)
{
    Node* newnode = (Node*)malloc(sizeof(Node));
    if(*phead == NULL)
    {
        *phead = newnode;
    }
    //.............
}

int main()
{
    Node* head = NULL;
    PushBack(&head, 1);
    PushBack(&head, 2);
    PushBack(&head, 3);
    return 0;
}

使用引用的话就不需要传递二级指针了,因为传递过去的形参其实是实参的别名,改变形参可以改变实参。 

typedef struct Node
{
    struct Node* next;
    int val;
}Node, *PNode;

void PushBack(Node*& phead, int x)
{
    Node* newnode = (Node*)malloc(sizeof(Node));
    if(phead == NULL)
    {
        phead = newnode;
    }
    //.............
}

int main()
{
    Node* head = NULL;
    PushBack(head, 1);
    PushBack(head, 2);
    PushBack(head, 3);
    return 0;
}

3.2、做返回值 

目的是:

  1. 减少拷贝
  2. 让调用者可以修改返回对象

  在C语言中,我们调用函数获取返回值是通过临时变量来传递的。因为函数栈帧在函数调用结束后会被销毁,栈帧中的值无法直接被传递,具体相关知识可见这篇文章:

int Count()
{
	int n = 0;
	n++;

	return n;
}

int main()
{
	int ret = Count();

	return 0;
}

 如果返回值占据空间较小,那么通常由寄存器来充当临时变量。如果返回值占据空间较大,那么这个临时变量会提前在main函数栈帧中开辟好。


现在我们把 n 创建为一个静态变量。 

int Count()
{
	static int n = 0;
	n++;

	return n;
}

int main()
{
	int ret = Count();

	return 0;
}

 此时变量 n 就不在 Count 函数的函数栈帧中了,而是在静态区里,也就是说在 Count 函数栈帧销毁后,变量 n 仍然保留。

这是不是说明返回 n 的值时不需要借助临时变量,而是直接返回 n 呢?

其实不会,编译器不会擅自做出这样聪明的改动,返回 n 的值时仍然会借用临时变量,尽管没有必要。


编译器不会主动做出这样的改动,但是人为可以。我们使用引用来作为返回值,直接返回变量 n 的引用:

int& Count()
{
	static int n = 0;
	n++;

	return n;
}

int main()
{
	int ret = Count();

	return 0;
}

不再需要借助临时变量,直接返回 n 的引用。 可以理解为直接返回了 n


 再来举个例子理解一下:

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}

 大家认为输出的结果是什么样的呢?

如果没有理解可以看下面这张图中的讲解:

  注意:当函数返回时,如果出了函数作用域,返回对象还在(还没还给系统),则可以使用
引用返回。如果已经还给系统了,则必须使用传值返回,如果使用引用返回,结果是未定义的。

比如返回对象在静态区、堆区,或者被创建在上一层栈帧中时都可以使用引用返回。

4、传值、传引用效率比较

 以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。使用引用返回则可以提升效率。

这里提供一份测试性能的模板,大家有兴趣可以自己测试一下:

值和引用作为参数的性能比较

#include <iostream>
#include <time.h>
using namespace std;
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 < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++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;
}

值和引用作为返回值类型的性能比较

#include <iostream>
#include <time.h>
using namespace std;
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 < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
	
	return 0;
}

5、常引用

变量的权限可以缩小,但是不能放大。

比如我们定义一个常变量,常变量是只读的,所以我们不能直接给常变量起别名:

int main()
{
    const int a = 10;
    int& b = a;//这种写法是报错的

    const int* p1 = NULL;
    int* p2 = p1;//这种写法也是错的
}

因为常变量 a 本身不能修改,但是给他取了别名 b 之后, b 却是能修改的了,这属于权限放大,很不合理。

所以应该这样写:

int main()
{
    const int a = 10;
    const int& b = a;

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

这就叫做常引用

当然我们缩小变量权限也是可以的:

int main()
{
    int a = 10;
    const int& b = a;

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

当我们使用引用来接收函数返回值时,也不能进行权限放大:

int Count()
{
	static int n = 0;
	n++;

	return n;
}

int main()
{
	int& ret = Count();//这样写是错误的

	return 0;
}

 因为函数返回值是通过临时变量返回的,为传值返回。临时变量具有 常性 ,不可修改,如果直接引用属于权限放大,所以依然需要使用 const 来修饰:

int Count()
{
	static int n = 0;
	n++;

	return n;
}

int main()
{
	const int& ret = Count();

	return 0;
}

换个例子:

int main()
{
    int i = 0;
    double& rd = i;//这种写法是错误的
}

这种错误是因为 i 的类型是 int ,但是 rd 的类型是 double ,不匹配。

但是这样写为什么就可以了呢?

int main()
{
    int i = 0;
    const double& rd = i;
}

这是因为我们在进行类型转换时会产生临时变量

int i = 10;
double d = i;

这里的赋值并不是把 i 的值直接赋给 d ,而是在中间产生一个临时变量,这个临时变量是 double 类型的。 把 i 的值先进行类型转换后传给临时变量,再把临时变量的值传给 d

 所以代码 double& rd = i 中, rd 是临时变量的别名,而不是 i 的别名。而临时变量具有 常性,所以为了不会权限放大,要写成: const double& rd = i 

6、引用和指针的区别

在 语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。而指针有自己的独立空间。

但是在 底层实现上 引用实际是有空间的,因为引用是按照指针方式来实现的。

int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;

	int* pa = &a;
	*pa = 20;

	return 0;
}

我们把这段代码反汇编观察一下:

 lea :取地址

可以发现不管是引用还是指针,汇编语言基本相同,都会访问变量的地址。在汇编语言的角度,引用依然是开辟了空间的。

引用和指针的不同点总结:

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

关于引用的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/193080.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IDEA快速生成实体类(加注释)

步骤&#xff1a; 1、点击右侧的datesource图标&#xff0c;要是没有该图标&#xff0c;请去自行百度 2、点击 号 3、选择 datasource 4、选择 mysql 1、填写一个连接名&#xff0c;随便填什么都行 2、不用选择&#xff0c;默认就行 3、填写数据库连接的 IP地址&#xff0c;比…

Android 时间工具类

最近总结了一下时间相关的用法&#xff0c;如下。 1、日期转换为字符串 默认"yyyy-MM-dd HH:mm:ss" 2、任意类型日期字符串转时间 3、获取当前对应格式的日期 4、获取当前对应格式的日期 默认"yyyyMMddHHmmssSSS" 5、计算该天是星期几 6、获取星期几…

XSS - 进阶篇(蓝莲花的基本使用)

数据来源 本文仅用于信息安全的学习&#xff0c;请遵守相关法律法规&#xff0c;严禁用于非法途径。若观众因此作出任何危害网络安全的行为&#xff0c;后果自负&#xff0c;与本人无关。 xss漏洞接收平台-蓝莲花&#xff1a; 1&#xff09;下载并安装Phpstudy&#xff08;安…

分享157个ASP源码,总有一款适合您

ASP源码 分享157个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 157个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1_IF9pFQX4NM-kmJyIAGBQQ?pwdcb55 提取码&#x…

RBAC简介

RBAC BAC基于角色的访问控制&#xff0c;RBAC认为权限授权的过程可以抽象地概括为&#xff1a;Who是否可以对What进行How的访问操作 RBAC简介 基于角色的权限访问控制模型 在RBAC模型里面&#xff0c;有3个基础组成部分&#xff0c;分别是&#xff1a;用户、角色和权限。RB…

微信公众号小程序怎么做?

​微信公众号小程序在当下已经成为人们日常生活中不可或缺的工具&#xff0c;在用户体验方面也做得很好&#xff0c;不仅可以实现沟通和交流&#xff0c;还可以通过微信公众号进行在线预约服务。那么关于微信公众号小程序怎么做&#xff0c;下面就给大家说说。 1、注册微信公众…

Cadence PCB仿真 使用 Allegro PCB SI 元器件端口设置的PDN分析功能介绍图文教程

🏡《总目录》   🏡《分目录》 目录 1,概述2,启动方法3,功能介绍3.1,元器件设置列表(Device)3.2,端口设置列表(Ports)4,总结1,概述 在进行PDN分析时需要对电源网络涉及到的所有元器件的指定端口的参数进行配置。本文介绍PDN网络元器件端口设置的功能。 2,启动…

【寒假day3】leetcode刷题

&#x1f308;一、选择题 ❤第1题&#xff1a;关于重载函数,&#xff08; &#xff09;说明是正确的。 A: 函数名相同&#xff0c;参数类型或个数不同 B: 函数名相同&#xff0c;返回值类型不同 C: 函数名相同&#xff0c;函数内部实现不同 D: 函数名称不同答案&#xff1a…

数据挖掘-特征选择方法:方差过滤,相关性过滤

目录特征选择1、Filter过滤法方差过滤1&#xff0c;消除方差为0的特征2&#xff0c;只留下一半的特征3&#xff0c;特征是二分类时2、相关性过滤法2.1 卡方过滤2.2 F检验2.3 互信息法3、 Embedded嵌入法4、Wrapper包装法5、总结特征选择 数据预处理完成后&#xff0c;就进入特…

常见网络报文数据包格式

当我们应用程序用TCP传输数据的时候&#xff0c;数据被送入协议栈中&#xff0c;然后逐个通过每一层&#xff0c;知道最后到物理层数据转换成比特流&#xff0c;送入网络。而再这个过程中&#xff0c;每一层都会对要发送的数据加一些首部信息。整个过程如下图。以太网帧格式以太…

Mybatis框架(二)再遇Mybatis之Mybatis配置文件与映射文件

本文是本人专栏【Java开发后端系列框架】里的文章&#xff0c;文章根据各框架官网与网上资料加上本人工作经验&#xff0c;进行修改总结发布在这个专栏&#xff0c;主要目的是用于自我提升&#xff0c;不用于获取利益。如果系列文章能到帮到您本人将感到荣幸&#xff0c;如果有…

2023年3月5日DAMA-CDGA/CDGP数据治理认证考试报名入口

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

【每日阅读】前端进阶知识点(一)

如何更改网页中语言属性值 声明当前语言类 html标签更改属性值 lang属性中不区分大小写 en-us en-US 一致 具体可使用 window,document.querySelector(“html”)?.setAttribute(“lang”,newValue); qs库 qs是一个流行的查询参数序列化和解析库。可以将一个普通的object序列…

OpenPPL PPQ量化(2):离线静态量化源码剖析

目录 模型支持 量化onnx原生模型&#xff1a;quantize_onnx_model 输入输出 执行流程 ONNX格式解析 后记 模型支持 openppl支持了三种模型&#xff1a;onnx、caffe、pytorch&#xff0c;其中pytorch和caffe是通过quantize_torch_model和quantize_caffe_model&#xff0c…

Elasticsearch:Terms set 查询

什么是 terms set 查询&#xff1f; Terms set 查询根据匹配给定字段的精确术语的最少数量返回文档。 terms set 查询与 term 查询有何不同&#xff1f; Terms set query 和 Terms query 之间的唯一区别是你可以提供必须匹配的最少数量的术语才能检索特定文档。 什么是 minim…

【Ansys Meshing】Fluent Meshing和Ansys Meshing在划分边界层网格能力上的对比

一、几何模型展示 如下图所示&#xff0c;一端的圆柱是流体入口&#xff0c;另一端的圆柱是流体出口&#xff0c;中间都是导热管。 二、在spaceclaim中进行切割实体 2.1 切割手段 切割平面的位置如图两根线所示&#xff0c;最终得到左右两边两个有圆柱的大块&#xff0c;以…

【SAP Abap】X档案:SAP 快速提供基础数据给第三方系统访问的几种方法(附常用基础数据)

SAP 快速提供基础数据给第三方系统访问的几种方法1、数据封装2、开放RFC访问3、开放接口服务4、开放DB访问5、常用基础数据1、数据封装 在企业信息系统建设过程中&#xff0c;少不了的就是系统集成数据对接。 尤其是SAP系统中大量的基础数据集成&#xff0c;如各种字段值域&am…

C语言:操作符详解

往期文章 C语言&#xff1a;初识C语言C语言&#xff1a;分支语句和循环语句C语言&#xff1a;函数C语言&#xff1a;数组 目录往期文章前言1. 操作符分类2. 算术操作符3. 移位操作符4. 位操作符5. 赋值操作符6. 符合赋值符7. 单目操作符8. 关系操作符9. 逻辑操作符10. 条件操作…

Java 中的基本设计模式

设计模式是针对常见软件设计问题的可重用解决方案。它们提供了一种以一致且高效的方式组织和构建代码的方法。一些常见的设计模式包括&#xff1a;工厂模式是一种创建型设计模式&#xff0c;它提供用于在超类中创建对象的接口&#xff0c;但允许子类更改将要创建的对象的类型。…

【数据结构趣味多】优先级队列——堆

1. 优先级队列 概念&#xff1a; 队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然不合适&#xff0c;比…