首页 > 编程语言 >C++引用 | 什么是引用

C++引用 | 什么是引用

时间:2024-01-23 12:12:07浏览次数:21  
标签:返回 变量 int 什么 别名 引用 指针 C++

引用

我们知道C语言以指针著名

C++大佬在发明C++的过程中,觉得指针有些难,就发明了引用

引用是什么?

引用并不是定义一个新的变量,而是给一个已存在的变量取一个别名.

编译器不会给引用变量开辟内存空间 , 这个别名 和它引用的的变量(原变量) 共用同一块内存空间

简单来说就是 : 引用变量和原变量的地址相同 就是同一块空间的不同名字

定义方法

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

int main()
{
    int a=10;
    int& ra=a;//这就是给a取别名 叫ra
    printf("%d\n",a);//输出10
    printf("%d\n",ra);// 也是输出10
    return 0;
}

image

可以观察他们的地址:
image

ra 和 a 的地址是一样的, 也就是说,他们共用一块内存空间,只是叫法不同而已

所以,用变量 或者变量的引用 都会引起内存空间值的变化

引用特性

  1. 引用必须在定义的时候初始化 (因为是给别的变量起别名,所以别名不会单独存在,首先需要有一个引用对象才可以)

  2. 一个变量可以有多个引用(一个人可以有多个外号),同样的一块空间也可以有多个名字

  3. 一个引用一旦引用一个实体,就不能再引用其他实体(也就是说,引用只能在定义的时候初始化,其他时候再赋值 其实就是对引用所指的那块内存空间进行赋值了)(java中的引用可以改变指向,C++不可以)
    image

引用的应用场景

1.做参数

  • 输出型参数: 参数传进去进行改变,外面可以拿到改变的值(也就是改变外部变量的值) -- 比如交换函数(交换两个变量的值)

    void Swap(int& a, int& b)
    {
    	//利用别名修改内存的值
    	int tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a = 10;
    	int b = -10;
    	cout << "交换前:" << " a= " << a << " b= " << b << endl;
    	Swap(a, b);//传递a和b的值 但是用a和b的引用来接收(也就是给a和b都起别名)
    	cout << "交换后:" << " a= " << a << " b= " << b << endl;
    
    	return 0;
    }
    

    形参利用引用接收,那么形参就是两个实参的别名, 那么对形参的修改也就会影响到内存空间的值,从而实现对变量的改变

    另一个例子: 我们知道链表需要传递二级指针,从而对头指针进行修改, 二级指针 就可以替换成引用

    • 形参作为头指针的别名
    //写法1:
    typedef struct SListNode{
        int val;
        struct ListNode* next;
    }SListNode, *PSListNode;
    /*后面这里把 SListNode* 类型typedef为 PSListNode*/
    
    //头插
    //写法1:
    void SListPushBack(ListNode*& phead,int val)
    {
        //形参为头指针的别名
        //形参的改变会影响实参
        
        /**
        *....
        */
    }
    //写法2:
    //PSListNode 其实就等价于方法1的 ListNode*
    void SListPushBack(PSListNode& phead,int val)
    {
        /****/
    }
    int main()
    {
        SListNode* list=NULL;//定义头指针
        SListPushBack(list,1);
        SListPushBack(list,2);
        SListPushBack(list,3);
        return 0;
    }
    
  • 大对象传参,提高效率

    因为值传递形参会拷贝一份实参,而引用传参并不会开辟空间

    只是起一个别名,所以提高效率

    测试:

    //大型参数传参
    #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 < 10000; ++i)
    		TestFunc1(a);//每次传参拷贝40000个字节
    	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;
    }
    
    /* 输出 */
    // 8 --值传参耗时(ms)
    // 0 --引用传参耗时(ms)
    可以看出 消耗是有差别的
    引用传参 基本无消耗,而值传参是有一定消耗的
    

2.做返回值

回顾一下传值返回

只要是传值返回,不管函数中的变量是在静态区还是在栈区(因为他不能智能去识别)

都是会生成一个对象的拷贝,作为函数调用的返回值

/** 传值返回 **/
int test1()
{
    int n=0;
    n++;
    return n;
}
//上面返回n的时候 其实是先把n放到寄存器中
// 然后利用寄存器把 值给带出来
// 此时函数已经销毁

/**
如果要返回的对象不是int类型 是很大的对象
那么就不会放在寄存器中,因为寄存器的容量很小 一般是4-8字节
此时就会在main函数的栈帧中提前开辟好一块临时空间保存返回值
*/

/* 如果要返回的值 保存在静态区 */
int test2()
{
    //此时n为静态变量 保存在静态区
    //函数调用完成 不被销毁
    // 此时返回的时候 也会创建临时空间去保存返回值
    // 因为编译器只会看 你是传值返回 就回去利用临时变量
    // 去保存返回值
    static n = 0;
    n++;
    return n;
}
int main()
{
    int ret=test1();//接收寄存器中的值
    return 0;
}
  • 传引用返回

    传引用返回的语法含义就是 返回 返回对象的别名

    int& func()
    {
        int n=0;
        n++;
     	return n;
    }
    //上面的函数 返回的是n的别名 (编译器给n的内存空间起了一个别名 并返回这个别名)
    int main()
    {
        //给编译器返回的别名又起了一个别名叫ret
        // 其实就是func中n的别名
        int& ret=func();
        //此时ret和上面func函数中的n表示的是同一块内存空间
        printf("%d\n",ret);//第一次调用
        printf("%d\n",ret);//第二次调用
        return 0;
    }
    //输出
    // -->  1
    // --> 5187647
    

    其实ret的结果是未定义的,(函数调用完成之后ret那块空间已经不属于我们了) 如果栈帧结束的时候,系统会清理栈帧置成随机值,那么ret 的结果就是随机值. 在VS下,这一次函数调用完并不会接着清理栈帧空间, 下一次调用函数才会清理栈帧重置为另一个函数的栈帧. 原来栈帧的内容被覆盖了,所以第一次调用printf 结果是 1

    而第二次就是随机值 了(第二次清理了栈帧)

image

所以上面这种情况使用引用返回是不对的 , 结果是没有保障的!

另一个例子

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main6()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	Add(9, 4);
	cout << "Add(1,2) is : " << ret << endl;
	return 0;
}
//输出
//-->13

	//会发现,ret的值总是最后一次调用Add()的结果
	// 这是因为 ret就是 Add()内部的c的引用,也就是每一次ret的值与那块内存的值有关
	// 每一次调用Add() 编译器把每一次c的位置都设置在那里(取决于编译器的特性)
	// 所以每一次c的值都会改变 所以ret是最后一次调用Add()之后的值

如果除了函数作用域,返回对象就销毁了.那么一定不能用引用返回,一定要用传值返回

引用的正确使用情况:

int& func()
{
    static int n = 0;
    n++;
    return n;
}
//n出了函数作用域 不会被销毁 
// 可以使用引用返回

3.引用返回的有意义场景

在顺序表中,如果要修改某个位置的元素

我们通常是传入修改的位置,还有修改成的元素

void SeqListModify(SeqList& s,int pos, int val)

但是可以利用引用做返回值实现更方便的修改方法

//如图 传入pos(位置)
// 返回pos位置处元素的引用(别名)
int& SeqlistAt(SeqList& slist, int pos)
{
	assert(pos >= 0 && pos < slist.size);
	return slist.a[pos];
}
//在main函数中 可以直接进行修改也可以打印
int main()
{
	SeqList st;
	SeqListInit(st);
	SeqListPushBack(st, 1);
	SeqListPushBack(st, 2);
	SeqListPushBack(st, 3);
    //可以利用SeqListAt函数直接输出 
	for (int i=0;i<st.size;i++)
	{
		cout << SeqlistAt(st,i) << " ";
	}
	cout << endl;
    //直接拿到0位置元素进行修改即可
	SeqlistAt(st, 0) = 10;
	for (int i = 0; i < st.size; i++)
	{
		cout << SeqlistAt(st, i) << " ";
	}
	return 0;
}

此时返回的是 返回对象的引用,所以不会进行拷贝,也提升了效率

引用总结

  • 做参数

1. 输出型参数  2. 大对象传参,提高效率

  • 2. 做返回值

1. 做输出型返回对象,调用者可以修改返回对象 2. 减少拷贝,提高效率

前提:出了函数作用域,返回对象不会被销毁的时候才能做引用返回

常引用

引用是给一个变量取别名

  • 一个int类型变量的别名,类型也是int类型

    但是如果是一个const int类型变量的别名, 类型也应该是const int类型(权限的平移)

  • 一个const int类型的变量 或者是一个常量 是不可以修改的 ,如果他的别名用int& 来接收

    相当于他的别名的类型就是 int类型 ,显然通过别名可以修改 内存空间的值 , 但是这块空间本来就是不可以修改的! 所以会报错 (权限放大不可以)

  • 但是一个int类型的变量 别名可以是 const int类型 (权限的缩小是可以的)

/**
 * 常引用
 */
int main7()
{
	const int a = 10;
	//int& ra = a;会报错,给一个常变量起别名,必须加const
	//给这个变量起一个别名,这个别名的类型和原类型必须相同
    // 否则就是(权限的放大)
    // int& ra = 100;//也是权限的放大

	const int& ra = a;//正确(权限的平移)
	cout << a << endl;
	cout << ra << endl;
	
	//权限的缩小
	int b = 20;
	const int& rb = b;
	//这时候 可以利用b对该内存的值进行修改
	// 但是不可以利用rb进行修改, 该别名的权限是只读(权限缩小了)
	cout << &b << endl;
	cout << &rb << endl;

	/* 另一个例子 */
	int aa = 10;
	double bb = 20;
	//int& raa = b;//b是一个double类型,赋值的时候需要隐式类型转换,此时会把b的值拿出来 创建一个临时变量,因为不会对原变量进行转换,所以只是把原变量的值拿出来转化后放入临时变量,临时变量再赋给raa .临时变量具有常性,所以 应该赋值给一个const
	const int& raa = bb;
	cout << raa << endl;
	return 0;

注意: 所有的隐式转换/算术提升/强转,都会产生一个临时变量 , 而不是对原变量进行修改

如果使用引用传参,并且函数内不改变实参的值,那么尽量使用const引用传参

就像指针中 使用const修饰的指针那样(一个效果)

引用和指针的区别

  1. 使用场景以及语法特性方面

    一般来说 指针能完成的事情,用引用都可替代

    除了一种情况: 链表的链式访问的地方,必须用指针

    因为引用一旦引用一个对象 就不能引用其他对象了(不能变指向), 并且引用定义的时候必须初始化

  2. 底层原理

    从语法的角度来看,引用并没有开辟空间,而指针会开辟 4 / 8 个字节

    但是在底层并不是这样的

    我们通过观察汇编语言来了解一下:

image

发现用引用 和 用指针 他们的汇编语言是一样的, 都是

  1. 把a的地址取出来放到eax寄存器
  2. 把eax中的内容放到 pa所在内存里(pa中存放a的地址)
  3. 把pa内存空间里的内容(a的地址) 放到eax里面
  4. 对eax里面的内容(a的地址)解引用 , 把14h(即十进制的10) 写进去( 也就是对a进行了赋值)

显然,引用的底层 就是指针实现的 , 也就是说 ,引用也会开辟空间

开辟空间的大小是4/8字节(地址的大小)

总结(指针的引用的区别)

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

标签:返回,变量,int,什么,别名,引用,指针,C++
From: https://www.cnblogs.com/iiianKor/p/17982033

相关文章

  • 微信小程序-wx:key的作用为什么不能使用index
    wxml中的代码为<viewclass="swiperContent"><swiper indicator-dots="true"autoplay="true"><swiper-itemwx:for="{{bannerList}}"wx:key="bannerId"><imagesrc="{{ite......
  • C++函数重载探究
    函数重载什么是函数重载简单来说,就是可以有多个相同函数名的函数,但是这些函数的参数个数 或者参数类型或者参数的类型顺序 是不一样的.通常来处理类似的功能,但是数据个数或者类型不同的情况如:计算器就是一个例子,加法可以是任何个数任何类型的数的加法但是都只......
  • 华企盾DSC日志审计在企业防泄密中有什么作用呢?
    在当今信息化时代,数据安全成为了企业的核心竞争力之一。一旦核心数据泄露,企业将面临巨大的经济损失甚至无法挽回的后果。因此,保护企业的数据安全成为了首要任务。在众多数据安全措施中,日志审计成为了企业防范数据泄露的重要手段。那么,日志审计在企业防泄密中有什么作用呢?日志审......
  • 计算机编程中的黑魔法编程是什么?如何求解一个浮点数的平方根倒数?计算机中的浮点数是如
    原视频:没有显卡的年代,这群程序员用4行代码优化游戏最原始的求解目标:(求一个浮点数的开方的导数)浮点数在计算机中的表示形式:对数的运算法则:A为a在计算机中的表示形式(二进制表示形式):求浮点数的平方根倒数的应用场景:这个情况,直白的说就......
  • C/C++可变参数函数
    C可变参数typedefchar*va_list;voidva_start(va_listap,prev_param);typeva_arg(va_listap,type);voidva_end(va_listap);//32位机器对int大小向上取整,64位机器对int64大小向上取整,因为参数在栈中传递都要对齐#define_INTSIZEOF(n)((sizeof(n)+si......
  • C++类和对象-对象特性(2)
    一.构造函数的分类及调用两种分类方式:按参数分为:有参构造和无参构造按类型分为:普通构造和拷贝构造三种调用方式:括号法显示法隐式转换法二.拷贝构造函数调用时机拷贝构造函数调用时机C++中拷贝构造函数调用时机三种情况使用一个已经创建完毕的对象来初始化一个新......
  • 3分钟带你了解,软件测试是做什么的
    软件测试是互联网技术中一门重要的学科,它是软件生命周期中不可或缺的一个环节,担负着把控、监督软件的质量的重任。目前,软件测试工程师缺口达30万,其中在我国大中型发达城市的人才需求就突破20万,并以每年20%的速度递增。人才稀缺自然带来待遇高涨。在某软件测试专场招聘会上,更有企......
  • C++学习笔记
    C++学习笔记(1)预编译、编译、链接预编译(Preprocessing)cppreference中:GPT这么说:C++预编译是指在编译阶段之前对代码进行的一系列预处理操作。预编译的目的是为了将代码中的预处理指令和宏展开,以及进行一些其他的预处理操作。预处理指令包括以井号(#)开头的指令,如#include、#......
  • C++日志记录库spdlog
    镜像库https://gitee.com/yctxkj/spdlog.gitspdlog是基于C++11实现的一款纯头文件的日志管理库(git地址:https://github.com/gabime/spdlog,API说明:https://spdlog.docsforge.com/v1.x/1.quickstart/):配置特别简单,仅包含头文件即可;写日志方式简单明了;可实现自动按日期创建日志文......
  • 什么是异步编程?
    异步编程在C#中通常使用async和await关键字来实现。这种模式允许方法异步地执行,这意味着方法可以在等待某些操作(如网络请求)完成时执行其他代码。async关键字async关键字标记一个方法为异步方法,这意味着该方法可能会包含一个或多个await表达式。async方法通常会返回一个Task或Task<......