C++入门:引用

news2024/9/25 17:12:21

目录

一. 什么是引用

1.1 引用的概念

1.2 引用的定义

二. 引用的性质和用途

2.1 引用的三大主要性质

2.2 引用的主要应用 

三. 引用的效率测试

3.1 传值调用和传引用调用的效率对比

3.2 值返回和引用返回的效率对比

四. 常引用 

4.1 权限放大和权限缩小问题

4.2 跨数据类型的引用问题

五. 引用和指针的区别


一. 什么是引用

1.1 引用的概念

引用,通俗的讲,就是给已经存在的变量取一个别名,而不是创建一个新的变量。引用和被引用对象共同使用一块内存空间。

引用就好比一个人的大名和小名,大名和小名都是一个人。再比如,李逵外号黑旋风,叫李逵和黑旋风表示同一个人。

1.2 引用的定义

引用定义的语法格式:类型& 引用的名称 = 被引用实体

如,定义int a = 10,希望再定义一个引用b,来表示整形变量a的别名,语法为:int& b = a。演示代码1.1展示了引用的定义过程,对原变量a和引用b的其中任意一个赋值,都会使a和b的值均发生改变,这是因为a和b共用一块内存空间。

演示代码1.1:

int main()
{
	int a = 10;
	int& b = a;  //b是a的引用(别名)
	printf("a = %d, b = %d\n", a, b);  //10,10

	a = 20;  //对a赋值,同时改变a和b
	printf("a = %d, b = %d\n", a, b);  //20,20

	b = 30;  //对b赋值,同时改变a和b
	printf("a = %d, b = %d\n", a, b);  //30,30

	return 0;
}
图1.1 演示代码1.1的运行结果

二. 引用的性质和用途

2.1 引用的三大主要性质

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

定义引用时必须给出这个引用的被引用实体是谁,如:int &b; -- 是非法的。

演示代码2.1:

int main()
{
	int a = 10;
	int& b;  //报错
	int& c = a;  //初始化引用
	return 0;
}
图2.1  演示代码2.1的报错信息

 2、一个变量可以有多个引用

我们可以为一个变量取多个别名。如演示代码2.2所示,给a变量取b、c、d三个别名是可行的。对a、b、c、d中的任意一个赋值,都会使a、b、c、d的值均发生改变。a、b、c、d共用一块内存空间。

演示代码2.2:

int main()
{
	int a = 10;
	int& b = a;
	int& c = a;
	int& d = a;  //为a取b、c、d三个别名
	printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);  //10,10,10,10

	c = 20;
	printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);  //20,20,20,20

	return 0;
}
图2.2  演示代码2.2的运行结果

 3、一个引用一旦引用了某个实体,就不能再引用其他实体

演示代码2.3中的b = c并不是将b变为变量c的引用,而是将变量c的值赋给b,通过打印b和c的地址,我们可以发现b和c并不共用一块内存空间,而赋值之后,a和b的值都变为了20。

演示代码2.3:

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

	b = c;   //将c的值赋给b,而不是让b变为c的引用
	printf("&b = %p, &c = %p\n", &b, &c);   //b和c的地址不一致
	printf("a = %d, b = %d\n", a, b);  //a、b都变为了c的值20

	return 0;
}
图2.3  演示代码2.3的运行结果

正是因为引用一旦引用了某个实体之后就不能再引用其他实体,所以引用无法替代指针来实现链表数据结构。否则就无法实现链表的增、删等操作,链表的增删操作需要改变指针的指向。

2.2 引用的主要应用 

1、引用做函数参数

要写一个swap函数,实现两个整形数据的交换,如果用C语言来写这个函数,就必须使用指针来作为函数的参数,即:void swap(int* px, int* py)。但是,如果使用C++来写,则可以用引用传参来替代指针传参,因为引用和被引用实体共用一块内存空间,引用传参使得函数内部可以控制实参所占用的内存空间,这是,swap可以声明为:void swap(int& rx, int& ry)。

演示代码2.4:

void swap(int& rx, int& ry)
{
	int tmp = rx;
	rx = ry;
	ry = tmp;
}

int main()
{
	int x = 10, y = 20;
	printf("交换前:x = %d,y = %d\n", x, y);  //10,20
	swap(x, y);
	printf("交换后:x = %d,y = %d\n", x, y);  //20,10
	return 0;
}
图2.4  演示代码2.4的运行结果

至此,可以总结出函数的三种调用方法: 

  1. 传值调用。
  2. 传地址调用。
  3. 传引用调用。

 问题:void swap(int x, int y)和void swap(int& x, int& y)能否构成函数重载?

答案是可以的。因为其满足构成函数重载的条件之一 :参数的类型不同。

但是,在调用这两个swap函数时,会存在歧义。通过语句swap(x,y)调用,无法确定是调用swap(int x, int y)还是swap(int&x, int& y)。

2、引用做函数的返回值

在演示代码2.5中,定义函数int& Add(int x, int y),函数返回z的别名。我们希望这个函数能够对x+y进行计算。但是显然,这段代码是有潜在问题的,因为在add函数调用结束后,为add函数创建的栈帧会被销毁,这块栈空间会还给操作系统。此时再使用add函数的返回值,就会造成对内存空间的非法访问,而大部分情况下,编译器不会对非法访问内存报错。

演示代码2.5:

int& add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int& ret = add(1, 2);
	printf("ret = %d\n", ret);
	return 0;
}

对于演示代码2.5的运行结果,可以分为两种情况讨论:

  • 函数栈帧销毁后,编译器不对被销毁的栈空间进行清理,打印函数的返回值,结果依旧为x + y的值。
  • 函数栈帧销毁后,编译器对被销毁的栈空间进行清理,函数的返回值为随机值。

在VS2019 编译环境下,演示代码2.5的运行结果为3,说明VS编译器不会清理被销毁的函数栈帧空间中内容。

图2.5  演示代码2.5的运行结果

既然VS编译器不会对被销毁的函数栈帧进行清理,那么是否在VS编译环境下,可以正常使用演示代码2.5中的add函数呢?答案显然是否定的,这可以从以下两个方面解释:

  • 如果在其他编译环境下进行编译,则被销毁的函数空间可能会被清理,这样会降低代码的可移植性。
  • 即使函数栈帧空间不被清理,但这块空间已经换给了操作系统,如果调用完add函数后再调用其他函数,那么原本为z开辟的空间可能会被覆盖,从而改变ret的值。

如演示代码2.6所示,第一次调用add函数使用ret来接收返回值,第二次调用add函数不接收返回值。但是第二次调用add函数之后,ret的值却变为了30,这是因为第二次调用add函数覆盖了第一次调用时创建的函数栈帧,原来第一次调用存放变量z的内存空间的内容由3变为了30,因此,程序运行的结果为30。这段代码在运行过程中栈帧的创建和销毁情况见图2.7。

演示代码2.6:

int& add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int& ret = add(1, 2);
	cout << ret << endl;

	add(10, 20);
	cout << ret << endl;

	return 0;
}
图2.6 演示代码2.6的运行结果
图2.7  两次调用add函数栈帧的开辟和被覆盖情况

总结(什么时候可以用引用返回,什么时候不可以):

  • 如果出了函数作用域,函数返回的对象被销毁了,则不能使用引用类型作为返回值。
  • 如果出了函数作用域,函数的返回对象还没有被销毁(存储返回对象的内存还没有还给操作系统),则可以使用引用作为返回值。

演示代码2.7给出了两种可以使用引用作为返回的情况,一种是以静态变量作为返回对象,另一种是返回对象为调用函数中开辟的一块内存空间中的内容(调用函数中开辟的数组)。

演示代码2.7:

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

char& func2(char* str, int i)
{
	return str[i];
}

int main()
{
	cout << func1() << endl;  //1
	cout << func1() << endl;  //2

	char ch[] = "abcdef";
	for (int i = 0; i < strlen(ch); ++i)
	{
		func2(ch, i) = '0' + i;
	}
	cout << ch << endl;  //012345

	return 0;
}
图2.8  演示代码2.7的运行结果

思考问题:既然函数完成调用时才会返回,而调用完成时函数栈帧又会被销毁。那么,以值作为函数返回类型时,时如何从函数中接收返回值的呢?

就比如演示代码2.8中的add函数,函数返回值时add函数中的临时变量z的值,在主函数中的ret如何从add函数中接收z值。

演示代码2.8: 

int add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int ret = add(2, 3);
	return 0;
}

答案其实很简单,ret并不是直接从add函数栈帧的空间中接收返回值,而是在add函数完成调用、函数栈帧销毁之前,存储一个临时变量用于接收函数的返回值,然后在将临时变量的值赋给ret。

那么,这个临时变量存储在什么位置呢?分两种情况讨论:

  • 如果返回值比较小,则使用寄存器充当临时变量。
  • 如果返回值比较大,则将临时变量放在调用add函数的函数内部,在调用add函数之前在调用add的函数的栈帧中预先开辟一块空间用于存储临时变量。
图2.9  值返回情况下函数返回值被接收的过程

三. 引用的效率测试

3.1 传值调用和传引用调用的效率对比

演示代码3.1分别执行100000次传值调用和100000次传引用调用,每次传值调用传给函数的形参的大小为40000bytes,记录传值调用和传引用调用消耗的时间。

程序运行结果显示,10000次传值调用耗时71ms,100000次传引用调用耗时2ms,传引用调用的效率远高于传值调用。这是因为传引用调用不用再为形参开辟一块内存空间,而为形参开辟空间存在一定的时间消耗。

演示代码3.1:

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

//大小为40000bytes的结构体
typedef struct A
{
	int arr[10000];
}A;

void Testvaluefunc(A a) { };   //传值调用测试函数
void TestReffunc(A& a) { };  //传引用调用测试函数

void TestRefAndValue1()
{
	A a;
	int i = 0;

	size_t begin1 = clock();  //记录开始传值调用的时间(传值调用100000次)
	for (i = 0; i < 100000; ++i)
	{
		Testvaluefunc(a);
	}
	size_t end1 = clock();  //记录结束传值调用的时间

	size_t begin2 = clock();  //记录开始传引用调用的时间(调用100000次)
	for (i = 0; i < 100000; ++i)
	{
		TestReffunc(a);
	}
	size_t end2 = clock();

	cout << "传值调用10000次耗费时间:" << end1 - begin1 << endl;
	cout << "传引用调用10000次耗费时间:" << end2 - begin2 << endl;
}
图3.1  演示代码3.1的运行结果

3.2 值返回和引用返回的效率对比

演示代码3.2分别执行100000次值返回函数和100000次引用返回函数,记录调用值返回函数和调用引用返回函数消耗的时间。程序运行结果表明,调用100000次值返回函数耗时136ms,调用100000次引用返回函数耗时2ms,引用返回的效率远高于值返回。

演示代码3.2:

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

typedef struct A
{
	int arr[10000];
}A;

A a;

A TestValuefunc2()
{
	return a;
}

A& TestReffunc2()
{
	return a;
}

void TestRefAndValue2()
{
		int i = 0;

		size_t begin1 = clock();  //记录开始时间(调用100000次)
		for (i = 0; i < 100000; ++i)
		{
			TestValuefunc2();
		}
		size_t end1 = clock();  //记录结束时间

		size_t begin2 = clock();  //记录开始的时间(调用100000次)
		for (i = 0; i < 100000; ++i)
		{
			TestReffunc2();
		}
		size_t end2 = clock();  //记录结束时间

		cout << "以值作为返回:" << end1 - begin1 << "ms" << endl;
		cout << "以引用作为返回:" << end2 - begin2 << "ms" << endl;
}

int main()
{
	TestRefAndValue2();  //引用作为返回和值作为返回的效率测试
	return 0;
}
图3.2  演示代码3.2的运行结果

四. 常引用 

4.1 权限放大和权限缩小问题

如果int& b = a,而a是整形常量,被const关键字修饰,那么b就不能作为a的别,因为a变量是只读的,而将b定义为int&类型,则表明b是可读可写的类型,b对a存在权限放大问题。

对于int a = 10,使用const int& b = a来表示a的别名是可以编译通过的。因为a为读写类型,而b为只读类型,b相对于a权限缩小,C++允许权限缩小。

总结:C++允许权限缩小,不允许权限放大。

演示代码4.1:

int main()
{
	//权限放大问题
	const int a = 10;
	//int& b = a;   //报错
	const int& b = a;  //编译通过

	//权限缩小
	int c = 10;
	const int& d = c;   //能够编译通过

	return 0;
}

4.2 跨数据类型的引用问题

看一个很诡异的问题。在演示代码4.2中,定义一个双精度浮点型数据double d = 1.1,编译程序,出现下面的现象:

  • 将d赋给int型数据i1,编译通过。
  • 用int& i2 = d来作为d的引用(别名),编译报错。
  • 但是,使用const int& i3 = d来作为d的引言,编译通过。

演示代码4.2:

int main()
{
	double d = 11.11;

	int i1 = d;  //强转,编译通过
	//int& i2 = d;   //编译报错
	const int& i3 = d;  //编译通过

	printf("&d = %p\n", &d);
	printf("&i3 = %p\n", &i3);

	return 0;
}

那么,为什么const int& i3类型的可以作为d的引用,而int& i2却不行?问题出在强制类型转换上。要理解这个问题,首先要清楚强制类型转换的过程,强制类型转换(int i1 = d),并不是将d强转后的数据直接赋给i1,而是先将d强转为int类型数据的值存储在一个临时变量中,然后再将临时变量的值传给i1,详见图4.1。

图4.1 数据强制类型转换的过程

临时变量具有常性,只可读不可改。因此,int& i2 = d就存在权限放大的问题,编译无法通过,而const int& i3 = d不会存在权限放大的问题,可以编译通过。但是,这里的i3就不再是d的别名,而是存储d强转为int类型数据值的临时变量的别名,因此i3和d的地址也就不同。演示代码4.2打印了i3和d的地址,表面他们不同,i3其实并不是d的别名。

提示:一定要弄清楚强转类型转换时临时变量做中间值的问题!

图4.2  演示代码4.2的运行结果

五. 引用和指针的区别

  1. 引用是定义一个变量的别名,而指针存储一个地址。
  2. 引用不占用额外的内存空间,而指针要占用4bytes或8bytes的内存空间。
  3. 引用在定义时必须初始化,而指针可以不初始化。(建议指针在定义时避免不初始化)。
  4. 引用一旦引用了某个实体,便不能更改被引用实体,而指针可以更改指向。
  5. 对引用自加,即对被引用的实体+1,指针自加,向后偏移一个指针类型的大小(bytes)。
  6. 没有多级引用,有多级指针。
  7. 访问实体时,引用直接由编译器处理即可,指针需要解应用。
  8. 没有空引用,但有空指针NULL。
  9. 引用相对于指针更加安全。

因为指针存在野指针、空指针等问题,造成指针过于灵活,所以指针的安全性不如引用。

引用的底层是通过指针来实现的。

引用最大的局限性在于不能更改引用实体,因此虽然引用的底层是通过指针实现的,但引用不能替代指针来实现链表数据结构。因为链表的操作需要更改指针的指向。

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

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

相关文章

【超好用】自定义的mybatis-plus代码生成器

BACKGROUND你是否也有这样的烦恼&#xff1a;每次写代码都需要创建很多包很多层很多类很多接口&#xff1f;耗时且费力姑且不谈&#xff0c;有时可能还大意了没有闪&#xff0c;搞出一堆bug这谁顶得住啊都3202年了&#xff0c;让程序自力更生吧&#xff01;&#xff01;教程 le…

原创|关于一次产品需求程序设计及优化的经历

文章目录一、流程梳理二、设计梳理三、技术方案3.1、下单接口扩展3.3.1、Request类新增deviceType3.3.2、申请单新增字段产品策略(productStrategy)3.3.3、下单产品策略的处理逻辑3.2、询价模块的设计3.2.1、Context设计3.2.2、ProductStrategy类设计3.2.2.1、AbstractProductS…

k8s篇之概念介绍

文章目录时光回溯什么是K8SK8S不是什么一、K8S构成组件控制平面组件&#xff08;Control Plane Components&#xff09;kube-apiserveretcdkube-schedulerkube-controller-managercloud-controller-managerNode 组件kubeletkube-proxy容器运行时&#xff08;Container Runtime&…

Spring Cloud Nacos实战(七)- Nacos之Linux版本安装

Nacos之Linux版本安装 Linux版NacosMySql生产环境配置 ​ 已经给大家讲解过了Nacos生产环境下需要搭建集群配置&#xff0c;那么这里我们预计需要&#xff1a;1个Nginx3个Nacos注册中心1个MySql 具体配置&#xff1a; 在官网上下载NacosLinux版本&#xff1a;https://github…

基于SSM框架的CMS内容管理系统的设计与实现

基于SSM框架的CMS内容管理系统的设计与实现 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目…

并查集(高级数据结构)-蓝桥杯

一、并查集并查集(Disioint Set)&#xff1a;一种非常精巧而实用的数据结构用于处理不相交集合的合并问题。用于处理不相交集合的合并问题。经典应用&#xff1a;连通子图。最小生成树Kruskal算法。最近公共祖先。二、应用场景有n个人&#xff0c;他们属于不同的帮派。 已知这些…

Kafka漏洞修复之CVE-2023-25194修复措施验证

Kafka漏洞修复之CVE-2023-25194修复措施验证前言风险分析解决方案AdoptOpenJDK Zookeeper Kafka多版本OpenJDK安装切换Zookeeper安装Kafka安装与使用其他Kafka消息发送流程Linux配置加载顺序参考链接前言 场景介绍 Kafka最近爆出高危漏洞CNNVD-202302-515&#xff0c;导致Apa…

LeetCode刷题复盘笔记—一文搞懂贪心算法之56. 合并区间(贪心算法系列第十四篇)

今日主要总结一下可以使用贪心算法解决的一道题目&#xff0c;56. 合并区间 题目&#xff1a;56. 合并区间 Leetcode题目地址 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间…

QXlsx(访问Excel)

再Qt中已经有了QAxObject来访问Excel&#xff0c;但访问的是微软的com&#xff0c;只能访问正版的Excl中的 .xls//xlsx ,而且使用起来及其不方便&#xff08;本人亲测&#xff09;。 在这里使用QXlsx,能更简单的访问Excel数据&#xff0c;但QXlsx这个类并没有在Qt Creator中&a…

《MySQL学习》 MySQL优化器选择如何选择索引

一.优化器的选择逻辑 建表语句 CREATE TABLE t (id int(11) NOT NULL AUTO_INCREMENT,a int(11) DEFAULT NULL,b int(11) DEFAULT NULL,PRIMARY KEY (id),KEY a (a),KEY b (b) ) ENGINEInnoDB;往表中插入10W条数据 delimiter ;; create procedure idata() begindeclare i in…

目标检测三大数据格式VOC,YOLO,COCO的详细介绍

注&#xff1a;本文仅供学习&#xff0c;未经同意请勿转载 说明&#xff1a;该博客来源于xiaobai_Ry:2020年3月笔记 对应的PDF下载链接在&#xff1a;待上传 目录 目标检测常见数据集总结 V0C数据集(Annotation的格式是xmI) A. 数据集包含种类: B. V0C2007和V0C2012的区别…

QT学习记录散件

fromLocal8Bit() qt中fromLocal8Bit()函数可以设置编码。 因为QT默认的编码是unicode&#xff0c;不能显示中文的 而windows默认使用&#xff08;GBK/GB2312/GB18030&#xff09; 所以使用fromLocal8Bit()函数&#xff0c;可以实现从本地字符集GB到Unicode的转换&#xff0c;从…

32-Golang中的map

Golang中的map基本介绍基本语法map声明的举例map使用的方式map的增删改查操作map的增加和更新map的删除map的查找map的遍历map切片基本介绍map排序map的使用细节基本介绍 map是key-value数据结构&#xff0c;又称为字段或者关联数组。类似其它编程语言的集合&#xff0c;在编程…

2023美赛ABCDEF思路汇总

注&#xff1a;以下每个题思路仅是个人所想所做&#xff0c;不代表他人。由于时间仓促完成这么多&#xff0c;难免有不足之处&#xff0c;还请谅解。 文章目录A题第一大问第二大问B题第一问第二问第三问C题第一问第二问第三问第四问D题第一问第二问第三问第四问第五问E题第一问…

#Paper Reading# Language Models are Unsupervised Multitask Learners

论文题目: Language Models are Unsupervised Multitask Learners 论文地址: https://life-extension.github.io/2020/05/27/GPT技术初探/language-models.pdf 论文发表于: OpenAI 2019 论文所属单位: OpenAI 论文大体内容&#xff1a; 本文主要提出了GPT-2&#xff08;Gener…

Visual Studio 2022: 增加对虚幻引擎的支持

自 Visual Studio 2022 发布以来&#xff0c;我们一直专注于为游戏和大型项目开发人员提供一系列生产力和性能改进。今天&#xff0c;我们很高兴与大家分享下一组专门用来提高虚幻引擎开发效率的功能。我们听到并看到了来自你&#xff08;我们的游戏开发人员&#xff09;的大量…

Spring MVC之 一次请求响应的过程

Spring MVC 会创建两个容器&#xff0c;其中创建Root WebApplicationContext 后&#xff0c;调用其refresh()方法会触发刷新事件&#xff0c;完成 Spring IOC 初始化相关工作&#xff0c;会初始化各种 Spring Bean 到当前容器中我们先来了解一个请求是如何被 Spring MVC 处理的…

2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享 很多时候&#xff0c;我们都想将一些文件或文本传送给别人&#xff0c;或者跨端传递一些信息&#xff0c;但是我们又不…

自抗扰控制ADRC之三种微分跟踪器TD仿真分析

目录 前言 1 全程快速微分器 1.1仿真分析 1.2仿真模型 1.3仿真结果 1.4结论 2 Levant微分器 2.1仿真分析 2.2仿真模型 2.3仿真结果 3.非线性跟踪微分器——韩教授 3.1仿真分析 3.2小结 4.总结 前言 工程上信号的微分是难以得到的&#xff0c;所以本文采用微分器…

重磅 | 小O软件新品【鲸鱼地图】发布

千呼万唤始出来.......&#xff0c;小O系列软件又添新品【鲸鱼地图】&#xff01;&#xff01;&#xff01; 2023年新年伊始&#xff0c;小O就投入到新品研发工作中&#xff0c;秉承“发现地理价值”理念&#xff0c;为用户提供更加好用、易用的地图软件产品&#xff0c;经过春…