C++的引用

目录

引用

常引用

指针与引用的关系

小拓展

引用的价值

做形参

 传值、传引用的效率比较

做返回值

函数传值返回

函数传引用返回(错误示范)

野引用(错误示范)

引用的正常应用

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

引用和指针的区别

语法上

底层(汇编)上


引用

基本概念:引用是给已存在变量取了一个别名

李逵,在家称为“铁牛”,江湖上人称“黑旋风”

特点:和原变量共用同一块内存空间(地址相同,且牵一发而动全身),同一变量可以有多个别名

格式:类型& 引用变量名(对象名) = 引用实体

注意事项:

1、C++中的&依然可以表示取地址运算符和按位与运算符

2、引用类型必须和引用实体是同种类型的

3、当引用实体和它的别名属于不同的域时,别名和引用实体的名字可以相同但是不建议这样做

#include <iostream>
using namespace std;

int main()
{
	int a = 10;
	int& b = a;
	
	cout << &a << endl;
	cout << &b << endl << endl;

	a++;
	b++;
	cout << a << endl;
	cout << b << endl << endl;
	return 0;
}

4、 引用必须在开始就初始化(说明自己是谁的别名)

int a = 0;

//wrong
int& b;
b = a;  

//right
int &b = a;

5、引用定义后,不能改变指向(别名与绑定后无法修改,对别名的任何操作就是对实体的操作)

int a = 0;
int& b = a;
int c = 1;
b = c;    // 这里不是让b指向c,而是将1赋值给了引用实体a

常引用

定义:使用 const 修饰的引用

特点:被const修饰的引用不能被修改

void printValue(const int& value) {
    // 这里无法通过value来修改原始值,如果输入value += 10;编译器就会报错
    cout << "Value: " << value << endl;
}

int main() {
    int num = 10;
    
    // 使用常量引⽤来传递num
    printValue(num);

    return 0;
}

指针与引用的关系

基本概念:指针和引用是类似的,指针找到并修改你,别名直接修改你(二者存在依赖关系)

        虽然C++的引用可以对使用指针后比较复杂的场景进行一些替换,让代码更简单易懂,但是因

为引用在定义后不能改变指向(删除双向链表的一个结点时, 需要用改变前去指针和后继指针,

用不能做到这一点),而指针可以,所以在C++中引用并不能替代指针:

struct Node
{
	struct Node* next;
	struct Node* prev;
	int val;
};

      之前我们在写单链表时,用二级指针pphead接收头指针的地址,而当我们有了引用的概念后,

可以以一种更好理解的方式实现下列单链表的(伪)代码:

#include <stdio.h> 
struct Node
{
	struct Node* next;
	struct Node* prev;
	int val;
};

//二级指针版本
void PushBack(struct Node** pphead,int x)
{
	*pphead = newnode;
}

//引用版本
void PushBack(struct Node*& phead, int x)//为指针取别名,plist指针的别名是phead
{
	phead = newnode;
}

int main()
{
	struct Node* plist = NULL;
	PushBack(plist,0);
	return 0;
}

        我们将原来的二级指针pphead换为struct Node*& phead,phead是plist的别名,对phead的操

作就是对plist的操作,这使得代码看起来更加简单。单链表使用二级指针的原因就是为了能够向

头指针中存入新节点的值(不用的话对于值得修改不能被带出尾插函数),这里直接通过引用就实

现了这一目的所以不需要再去考虑传值调用的问题

关于单链表的内容可以查看:单链表的实现(全注释promax版)

小拓展

#include <stdio.h> 

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

//引用版本
void PushBack(PNode& phead, int x)//为指针取别名,plist指针的别名是phead
{
	phead = newnode;
}

int main()
{
	PNode plist = NULL;
	PushBack(plist,0);
	return 0;
}
  • LNode: 是 struct Node 的别名(当你使用 LNode 时,就相当于使用了 struct Node)

  • PNode: 是结构体类型的指针struct Node*的别名

C++百分之八十的场景都在使用引用,剩下的才会用指针

引用的价值

做形参

        原来我们在交换两个变量的时候,向Swap函数中传递的是地址,形参是实参的拷贝。实参必

须传递的是地址,否则交换后的结果无法传递回去,有了别名后就不需要传递地址了,形参可以直

接写成实参的别名即可,对于别名的修改就相当于原来的实参的修改:

#include <iostream>
using namespace std;

//原版本,传递地址
void Swap(int* left, int* right)
{
	int tmp;
	tmp = *left;
	*left = *right;
	*right = tmp;
}

int main()
{
	int i = 10;
	int j = 20;

	Swap(&i, &j);

	cout << i << endl;
	cout << j << endl;
	return 0;
}

//现版本,引用
void Swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

int main()
{
	int i = 10;
	int j = 20;

	Swap(i, j);

	cout << i << endl;
	cout << j << endl;
	return 0;
}

从语法上来讲,left和right是i和j的拷贝,int& left和int& right是i和j的别名

🤡从底层上来讲,这里先不讲,我不会🤡 

 传值、传引用的效率比较

         以值作为参数和返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身
直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低:
#include <stdio.h>
#include <time.h>
#include <iostream>
using namespace std;

struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}

int main()
{
	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;

	return 0;
}

结论:引用做形参时是输出型参数,且当对象较大时,减少拷贝提高效率

这些效果指针也可以实现,但是没引用方便 

输出型参数:传递给函数的参数数据,函数可以使用且能修改这些数据

输入型参数:传递给函数的参数数据,函数可以使用但不能修改这些数据

做返回值

函数传值返回

        main函数开辟了一块栈帧,接着又调用了func函数,再开辟一块帧栈,然后func返回a的

值并结束,然后main函数又想接收func函数的返回值a,但是返回值a已经在func函数结束后被销

毁了,a空间中所存放的值已经不再被保证有效,所以此时ret得到是随机(也有可能是原来的

值,这相当于你鸡蛋碎在大街上了,你记着位置回去找,还能在地上找到蛋黄。如果等久一点,再

回去找,那就不一定能找到啥了,但是在大多数情况下,编译器在栈帧销毁时会将返回存储在

寄存器中(对象比较小时)或者其他适当位(除了函数返回值之外,编译器还可以选择使用寄存

器来保存一些重要的局部变量或者临时变量,以减少对内存的读写作)最后器中存放的原来返回值的值会交给ret

        在函数结束后硬要把局部变量搞成一个随机值是一件没有意义的事情。只能说函数结束后局部变量的值【不再保证有效】

        销毁是需要花点力气的,函数销毁后,原来函数的那一片空间变成空闲区域,随时会再次被使用。大部分情况下,你还能输出,是因为没人用到了那片空闲区域!然而机器也懒得去销毁,重置内存也得费电。

函数传引用返回(错误示范)

        func函数的返回值是一个int&类型的引用,即返回值是局部变量a的引用(别名),而当函数

返回该引用时,a的值已经不再保证有效了(随机值或者原值)

结论:函数传值返回的是返回变量的拷贝,函数传引用返回的是返回变量的引用(别名)

野引用(错误示范)

        func函数返回的是a的引用(别名),那么ret就是a的引用的引用,而a的那片空间在func函

数销毁时已经不再保证有效了,所以ret就是一个野引用(在程序运行过程中无法保证该内存空间

仍然有效或包含原始值(因为它可能已被其他数据覆盖),因此称之为野引用)

结论:返回变量(局部变量)出了函数作用域就被销毁时,不能用引用返回(薛定谔的🐱)

引用的正常应用

全局变量、静态变量、堆上分配对象等内容可以用引用返回:

  • 全局变量:全局变量在程序运行期间始终存在,因此可以安全地通过引用返回
  • 静态变量:静态变量也类似于全局常驻内存,在程序整个执行周期中都存在
  • 堆上分配对象:如果一个对象是通过 new 运算符在堆上动态分配内存创建的,则其生命周期由 new 和 delete 控制,并不受限于函数作用域
①int a = 10;
int& func()
{
    ②static int a = 0;
    return a;
}


int main()
{
    int& ret = func();
    cout << ret << endl;
    return 0;
}

堆上分配对象的例子我不会🤡

这是C语言(参杂了一点C++)实现顺序表的简化代码:

#include <iostream>
#include <assert.h>
#include <stdio.h>

using namespace std;

struct SeqList
{
	int* a;
	int size;
	int capacity;
};

//初始化
void SLInit(SeqList& sl)//利用引用了
{
	sl.a = (int*)malloc(sizeof(int) * 4);

	//... 
	
	sl.size = 0;
	sl.capacity = 4;
}

void SLPushBack(SeqList& sl, int x)
{
	//...(扩容)
	sl.a[sl.size++] = x;
}


//修改
void SLModity(SeqList& sl, int pos, int x)
{
	assert(pos >= 0);
	assert(pos <= sl.size);

	sl.a[pos] = x;
}

//获取pos位置的值
int SLGet(SeqList& sl, int pos)
{
	assert(pos >= 0);
	assert(pos <= sl.size);
	return sl.a[pos];
}

int main()
{
	SeqList s;//C++将stryct SeqList变为了类,所以可以直接用SeqList

	SLInit(s);

	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPushBack(s, 4);

	for (int i = 0; i < s.size; i++)
	{
		cout << SLGet(s,i) << " ";
	}
	cout << endl;

	for (int i = 0; i < s.size; i++)
	{
		int val = SLGet(s,i);
		if (val % 2 == 0)
		{
			SLModity(s, i, val * 2);
		}
	}
	cout << endl;

	for (int i = 0; i < s.size; i++)
	{
		cout << SLGet(s, i) << " ";
	}
	cout << endl;

	return 0;
}

这是完全使用C++语法写出的顺序表代码:

#include <iostream>
#include <assert.h>
#include <stdio.h>

using namespace std;

struct SeqList
{
	//成员变量
	int* a;
	int size;
	int capacity;

	//成员函数
	//C++的结构体(类)除了可以定义变量还可以定义函数
	void Init()//可以不写前缀
	{
		a = (int*)malloc(sizeof(int) * 4);

		//... 

		size = 0;
		capacity = 4;
	}

	void PushBack(int x)
	{
		//...(扩容)
		a[size++] = x;
	}

	int& Get(int pos)//加上一个引用就可以代替原来的SLModify和SLGet两个函数的作用
	{
		assert(pos >= 0);
		assert(pos <= size);

		return a[pos];//由于数组中pos位置的空间是由malloc开辟的,如果不主动释放它就一直都在,所以即使函数销毁,该空间也不会销毁,该空间中的值也会被保留 
	}
};

int main()
{
	SeqList s;//C++将stryct SeqList变为了类,所以可以直接用SeqList

	s.Init();
	
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	for (int i = 0; i < s.size; i++)
	{
		cout << s.Get(i) << " ";
	}
	cout << endl;

	//将满足条件的数组pos位置的值进行修改
	for (int i = 0; i < s.size; i++)
	{
		
		if (s.Get(i) % 2 == 0)
		{
			s.Get(i) *= 2;
		}
	}
	cout << endl;

	for (int i = 0; i < s.size; i++)
	{
		cout << SLGet(s, i) << " ";
	}
	cout << endl;

	return 0;
}
  • C语言:数据与函数分离,想要访问数据就要将数据作为参数传递给函数
  • C++:数据和函数不分离,都处于一个类中(实际上也传了编译器做的但是现在没学到)可以直接用(不需要再传一个结构体类型的指针将存储在结构体中的数据传递)

        在这段代码中我们不仅仅需要关注的是将原本放在外部的尾插、初始化等函数放在了结构体

,还需要注意在这里我们用Get一个函数就可以实现原来SLGet和SLModify两个函数的作用(读

写pos位置的数据)这是因为C++规定临时变量具有常性(特指存储在寄存器中的变量,虽然

pos位置的值除非主动释放否则不会销毁,但是可以将该值拷贝一份放入寄存器中作为函数的返回

值),临时变量默认被const修饰无法修改,如果在试图修改pos位置的值时就会出现不可修改的

左值的报错,但是即使C++没有做出这一规定,我们仍然不能做到对数组pos位置的值进行修改,

因为临时变量只是原来该位置数组的一个拷贝,对拷贝内容的修改无法影响到原来位置的数值,这

两种情况都只能对pos位置的值进行读取而不能进行修改,而当我们加上了一个引用,就可以实现

对pos位置的值的修改,此时Get函数返回的就不是一个临时变量(函数返回引用不会产生临时变

量),而是a[pos]的别名,至于谁的别名作者不知道就不做解释,只需要此时我们既可以读取到

pos位置的值也可以对该值进行修改 

        完成对数组pos位置的值的修改还有一个前提就是数组pos位置存放值的空间在函数结束后仍然可以保证值得有效,否则就会出现传引用返回(错误示范)中出现得值不保证有效的问题,对于这一点由于数组空间是由mallo开辟的,除非主动销毁否则不会释放,即使函数结束数组pos位置存放值得空间仍然存在,值仍然保证有效,所以可以放心引用该实体对象

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

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

struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }

int main()
{
	// 以值作为函数的返回值类型
	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()
{
    int a = 10;
    int& ra = a;//引用语法上不开空间
    ra = 20;

    int* pa = &a;//指针语法上开空间
    *pa = 20;

    return 0;
}

②、引用必须初始化,指针可以初始胡也可以不初始化(所以指针更容易出现野的情况)

③、引用不可以改变指向,指针可以改变指向

④、引用相对安全,没有空引用,但是有空指针,容易出现野指针,但是不容易出现野引用

⑤、在sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数

⑥、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

⑦、有多级指针,但是没有多级引用

⑧、访问实体方式不同,指针需要显式解引用,引用编译器自己处理

⑨、引用比指针使用起来相对更安全

底层(汇编)上

对于①中的引用,在底层(汇编)上的情况是这样的:

        “003F2026 lea eax,[a]”:取a的地址放在eax寄存器,故引用在底层层面需要开空间

结论:

  • 引用底层是用指针实现的
  • 语法含义和底层实现是背离的(鱼香肉丝没有🐟)

汇编层面上,没有引用,都是指针,引用编译后也转换成指针了

~over~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

人才测评系统的作用与应用场景有哪些?

人才测评有助于我们对于人才进行量化&#xff0c;例如&#xff1a;专业技能测评&#xff0c;性格的测评&#xff0c;以及智商和情商的测评&#xff0c;那么这些的集合就是一种人才的量化&#xff0c;通过这些属性参数&#xff0c;我们可以客观描述什么是“人才”。 那么人才测…

opencv VideoCapture

videocapture顾名思义视频捕捉&#xff0c;主要是从视频文件、摄像头或网络摄像头获取视频流数据&#xff0c;并将其作为一系列帧进行处理。 我们这里主要实现了获取项目文件夹下的1.mp4视频文件&#xff0c;然后经过灰度变化、均值滤波、边缘检测然后将视频显示出来 #include…

Xcode15与苹果ios17适配以及遇到的问题

大家好&#xff0c;我是你们的好朋友咕噜铁蛋&#xff01;最近&#xff0c;苹果发布了全新的iOS17系统&#xff0c;而作为开发者&#xff0c;我们需要确保我们的应用程序能够与这个新系统完美适配。因此&#xff0c;今天我将和大家分享一些关于Xcode15与苹果17系统适配的经验&a…

回溯例题(leetcode17/37)

文章目录 leetcode37leetcode17 回溯跟枚举差不多。要注意“回溯”&#xff0c;别忘记“回”之前把之前的改动都复原。 leetcode37 leetcode37是解数独问题。本题保证有且仅有唯一解。 思路&#xff1a;先把空格子的位置存下来&#xff0c;然后对每一个空位置挨个枚举1-9。枚…

【OpenGL的着色器03】内置变量(gl_Position等)

目录 一、说明 二、着色器的变量 2.1 着色器变量 2.2 着色器内置变量 三、最常见内置变量使用范例 3.1 常见着色器变量 3.2 示例1&#xff1a; gl_PointSize 3.3 示例2&#xff1a;gl_Position 3.4 gl_FragColor 3.5 渲染点片元坐标gl_PointCoord 3.6 gl_PointCoo…

Python中re模块的使用

正则表达式是一种强大的工具&#xff0c;用于处理字符串的匹配、搜索和替换操作。在Python中&#xff0c;我们可以使用内置的re模块来执行各种正则表达式操作。 1 基本用法 re.match(pattern, string): 从字符串的开头匹配一个模式。返回match对象或None。re.search(pattern,…

张俊将出席用磁悬浮技术改变生活演讲

演讲嘉宾&#xff1a;张俊 空压机销售总监 亿昇(天津)科技有限公司 演讲题目&#xff1a;用磁悬浮技术改变生活 会议简介 “十四五”规划中提出&#xff0c;提高工业、能源领城智能化与信息化融合&#xff0c;明确“低碳经济”新的战略目标&#xff0c;热能产业是能源产业和…

交友社交软件开发-php交友聊天系统-

为了开发一个高效的交友系统&#xff0c;需要一个完善的信息管理和筛选机制。这个系统应该能够根据用户的个人信息、兴趣爱好、价值观等标准进行筛选&#xff0c;并向用户提供符合他们要求心仪的人的信息。为了实现这个目标&#xff0c;系统可以利用人工智能技术&#xff0c;分…

100M服务器能同时容纳多少人访问

100M服务器的并发容纳人数会受到多种因素的影响&#xff0c;这些因素包括单个用户的平均访问流量大小、每个用户的平均访问页面数、并发用户比例、服务器和网络的流量利用率以及服务器自身的处理能力。 点击以下任一云产品链接&#xff0c;跳转后登录&#xff0c;自动享有所有…

Dell R730 2U服务器实践2:VMWare ESXi安装

缘起 刚到手边的一台Dell R730是三块硬盘raid0 &#xff0c;把我惊出一身冷汗&#xff0c;准备把它们改组成raid1 或者raid5 。 但是舍不得里面的ESXi 8 &#xff0c;寻找能否把raid0改成raid1 还不掉WSXi的方法&#xff0c;很遗憾没有找到。那样只能重装ESXi了。 ESXi软件下…

[unity] c# 扩展知识点其一 【个人复习笔记/有不足之处欢迎斧正/侵删】

.NET 微软的.Net既不是编程语言也不是框架,是类似于互联网时代、次时代、21世纪、信息时代之类的宣传口号,是一整套技术体系的统称&#xff0c;或者说是微软提供的技术平台的代号. 1.跨语言 只要是面向.NET平台的编程语言(C#、VB、 C、 F#等等)&#xff0c;用其中一种语言编写…

华为HCIP Datacom H12-821 卷4

1.单选题 下面哪些策略或工具不能够应用于 OSPF: A、access-list B、prefix-list C、route- Policy D、as-path filter 正确答案&#xff1a; D 解析&#xff1a; as-path-filter命令用来创建AS路径过滤器&#xff0c;OSPF属于IGP协议&#xff0c;不涉及到AS号。 2.单选题…

WinForm、Wpf自动升级 AutoUpdater.NET

Github AutoUpdater.NET 目录 一、IIS部署 更新站点 二、创建Winform 一、IIS部署 更新站点 IIS默认站点目录下创建 目录 Downloads、Updates Updates目录创建文件 UpdateLog.html、AutoUpdaterStarter.xml UpdateLog.html&#xff1a; <html><body><h1…

如何更好的引导大语言模型进行编程的高效开发流程?

这张图片展示了一种如何更好地引导大语言模型进行编程的方法。 首先&#xff0c;最简单也是最有效的方法是让大语言模型重复运行多次&#xff0c;每次增加一些额外的信息&#xff0c;直到获得想要的结果。这种方法虽然简单&#xff0c;但可能需要多次尝试才能得到满意的结果。…

Socket网络编程(四)——点对点传输场景方案

目录 场景如何去获取到TCP的IP和Port&#xff1f;UDP的搜索IP地址、端口号方案UDP搜索取消实现相关的流程&#xff1a;代码实现逻辑服务端实现客户端实现UDP搜索代码执行结果 TCP点对点传输实现代码实现步骤点对点传输测试结果 源码下载 场景 在一个局域网当中&#xff0c;不知…

使用Python改造一款外星人入侵小游戏

目录 引言 游戏概述 准备工作 游戏设计 1. 初始化设置 2. 创建飞船和外星人 3. 创建子弹 4. 游戏循环 5. 优化和扩展 总结 引言 当我们提及Python编程语言时&#xff0c;很多人首先想到的是数据分析、机器学习或网络爬虫等高级应用。然而&#xff0c;Python同样适用…

Windows系统误删文件恢复

最近很多用户反馈误删文件的场景比较多.下面华仔将讲解数据恢复的原理和过程.以及一些注意事项。 建议的数据恢复软件 1.EaseUS Data Recovery Wizard(易我数据恢复)需要断网使用 2.Wondershare Recoverit(万兴数据恢复)&#xff0c; Windows系统删除文件原理&#xff1a;如果是…

PageHelper开源框架解读

在使用springboot开发系统时&#xff0c;列表查询经常会用PageHelper来进行分页。使用起来很方便&#xff0c;但从未想过它的实现原理&#xff0c;所以对其进行解读。 Service public class ScUserServiceImpl extends ServiceImpl<ScUserMapper, ScUser> implements IS…

ABAP - SALV教程02 - 开篇:打开SALV的三种方式之二

全屏模式生成SALV的方式&#xff1a;http://t.csdnimg.cn/CzNLz本文讲解生成可控模式的SALV&#xff0c;该方式需要依赖自己创建屏幕的自定义控件区域&#xff08;Custom Control&#xff09;实现步骤&#xff1a;需要注意的点是SALV的实例对象和dispaly方法一定是在屏幕PBO事件…

CrossOver 24下载-CrossOver 24 for Mac下载 v24.0.0中文永久版

CrossOver 24是一款可以让mac用户能够自由运行和游戏windows游戏软件的虚拟机类应用&#xff0c;虽然能够虚拟windows但是却并不是一款虚拟机&#xff0c;也不需要重启系统或者启动虚拟机&#xff0c;类似于一种能够让mac系统直接运行windows软件的插件。它以其出色的跨平台兼容…