引用

引用可以对别名进行引用!

#include<iostream>
using namespace std;

int main()
{
	int a = 0;    // 李逵
	int& b = a;   // 铁牛
	int& c = b;	  // 在铁牛的基础上取名为黑旋风
	return 0;
}

引用的特性:

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

java当中没有指针! 因此每个节点中存放的是下一个节点的引用!

C++不能这样实现,因为C++中的引用是不能被修改的!

同一个域的引用不能同名;不同域的引用可以同名!

引用的使用场景

  • 做参数(输出型参数)

void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}
  • 输出型参数:此时形参的改变会影响实参(引用)
  • 输入型参数 :形参的改变不会影响实参!

struct + 结构体得名字才是结构体的类型!(C语言中)

但是C++可以直接使用结构体的名字!

引用做参数还可以提高效率(后面详解)(大对象/深拷贝对象)

#include<iostream>
using namespace std;

void swap(int& p, int& q)
{
	int tmp = p;
	p = q;
	q = tmp;
}

int main()
{
	int aa = 10;    // 李逵
	int bb = 20;
	int& a = aa;   // 铁牛
	// int& c = b;	  // 在铁牛的基础上取名为黑旋风
	int& b = bb;
	cout << aa <<' '<<bb<< endl;
	//cout << bb << endl;
	swap(aa, bb);
	cout << aa << ' ' << bb << endl;


	return 0;
}

做返回值

分析下面两个代码:

int Count1()
{
	int n = 0;
	n++;
	// ...
	return n;
}

(n拷贝给临时变量,临时变量再拷贝给ret!) 

 不能直接返回n的值,因为n在栈区,出作用域直接销毁,因此会创建临时变量!(详细解释如下:)

        中间会生成临时变量,临时变量可能由寄存器代替,会把临时变量放到寄存器里面,作为表达式的返回值,再给ret!

        但是寄存器一般只有4/8个字节,数据量大就放不下!

        在C/C++中,当你从一个函数返回一个局部变量(如n)的值时,编译器会创建一个临时变量来存储这个值。这个临时变量通常存放在栈区(stack)中。
        具体来说,当函数Count1执行完毕后,局部变量n会被销毁,但在返回时,编译器会将n的值复制到一个临时变量中。这个临时变量的生命周期会持续到它被使用完为止,通常是在调用函数的上下文中。
        因此,虽然n在函数结束时会被销毁,但返回的值(即临时变量的值)仍然可以在调用该函数的地方使用。这个临时变量的存储位置仍然是在栈区,直到它不再被需要。

int Count2()
{
	static int n = 0;
	n++;
	// ...
	return n;
}

此时n位于静态区,出作用域不会被销毁! 

那么此时n会创建临时变量吗?

会!编译器不看变量在栈区还是静态区,只要是传值返回,都会创建临时变量!(傻瓜处理)

此时,用引用做返回值就不会生成临时变量!

  • 减少拷贝,提高效率(大对象影响很大)

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

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

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

struct A { int a[10000]; }; // 定义结构体A,包含一个大小为10000的整数数组  
A a; // 创建一个全局变量a,类型为A  

// 值返回  
A TestFunc1() { return a; } // 定义一个函数TestFunc1,返回全局变量a的副本  

// 引用返回  
A& TestFunc2() { return a; } // 定义一个函数TestFunc2,返回全局变量a的引用  

void TestReturnByRefOrValue() {  
    // 以值作为函数的返回值类型  
    size_t begin1 = clock(); // 记录开始时间  
    for (size_t i = 0; i < 100000; ++i) // 循环调用TestFunc1 100,000次  
        TestFunc1();  
    size_t end1 = clock(); // 记录结束时间  

    // 以引用作为函数的返回值类型  
    size_t begin2 = clock(); // 记录开始时间  
    for (size_t i = 0; i < 100000; ++i) // 循环调用TestFunc2 100,000次  
        TestFunc2();  
    size_t end2 = clock(); // 记录结束时间  

    // 计算两个函数运算完成之后的时间  
    cout << "TestFunc1 time: " << end1 - begin1 << " ms" << endl; // 输出TestFunc1的执行时间  
    cout << "TestFunc2 time: " << end2 - begin2 << " ms" << endl; // 输出TestFunc2的执行时间  
}  

int main() {  
    TestReturnByRefOrValue(); // 调用性能测试函数  
    return 0; // 返回0表示程序正常结束  
}

分析下面用引用做返回值的代码 :

这里ret与n的关系是拷贝! (栈帧的销毁不会影响ret)

这个代码的输出结果为随机值!

n所在的空间已经被销毁!(归还它的使用权) 此时访问的数据的结果是不确定的!访问的是随机值!

  • 如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的结果是侥幸正确的!
  • 如果Count函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值!

相当于访问的是野指针!

int& Count2()
{
	int n = 0;
	n++;
	// ...
	return n;
}

int main()
{
	//int ret2 = Count2();
	int& ret2 = Count2();
	cout << ret2 << endl;
	return 0;
}

这种情况下:count()等于n的别名,ret2是count2的别名,相当于给别名取别名!

此时ret2也是n的别名!

  • ret是n的别名,当函数调用结束后,栈帧销毁,如果没有清理栈帧,此时两次打印ret的值为11和21;(访问的是一个已经被销毁的空间的变量);
  • 第二次调用函数所占的空间一样大,将原来的10覆盖为20,
  • 如果栈帧被清理,此时打印的就是随机值!

但是如果此时调用任何一个其他的函数,ret的值就为一个随机值!

因为调用下一个函数的时候,需要建立函数栈帧,正好将上面的空间覆盖,覆盖后为什么样子不确定,此时再访问这块空间就为一个随机值!(再次调用count()是在同一块空间的同一块位置将其值从10覆盖为20)

因此!上面的做法不可取!返回局部作用的变量的引用非常危险!

我们采用以下做法:

int& Count()
{
   static int n = 0;
   n++;
   // ...
   return n;
}

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

此时栈帧销毁,n位于静态区,n一直存在,不会影响返回!

 总结:

  1. 基本任何场景都可以使用引用传参;
  2. 谨慎用引用做返回值,出了函数作用域,对象不在了,就不能引用返回,还在就可以用引用返回!

可以使用引用返回的:static修饰的,全局变量、malloc开辟的,常量值.

常引用

第一种

const int a = 0;
int& b = a;

这种写法是错误的!权限不能扩大!a的值为常量不能改变 ;

第二种

const int c = 0;
int d = c;

这种写法可以!因为c拷贝给d,此时d的改变不影响c;

第三种

int main()
{
	int a = 10;
	int& b = a;   // 平移
	const int& z = a;    // 缩小
	a++;
	z++;   // 这种写法是错误的,z不能修改!
	cout << a << endl;
	cout << z << endl;

	return 0;
}

引用过程中,权限可以平移或者缩小,但是不能放大!

这里a++的时候z也能++,但是不能使用z++!

第四种:

const int& m = 10;

权限进行了平移,两边都不能修改!

第五种:

	double a = 1.11;
	int b = a;
	int& c = a;  // 这种行不通!!!
	const int& d = a;
    double& e = a;

b实际上是强制类型转换,发生类型转换(类型提升,截断等)的时候会产生临时变量,类似于传值返回!

实际上是把dd给临时变量,再将临时变量给ri,因此,中间会产生一个int的临时变量!

对应的,下面的实际上是将dd给临时变量,再把临时变量给rii!

临时变量具有常性!相当于被const修饰!

因此这样子不满足不是因为类型不行,而是因为权限放大!

第六种

上面这种写法不可取!因为func1()返回的是一个临时变量,临时变量具有常属性!此时这样子做会造成权限的方法!

解决方法:前面加上const即可!

当函数返回一个基本数据类型的值时,返回的是该值的副本。这个副本是一个临时变量,不能被修改,所以我们一般用一个变量来接受这个函数的返回值!(传值返回)

第七种:

上面这两种都可以!(没有产生临时对象)

分析下面这种特殊情况:

当j和i进行比较的时候,实际上i的值没有发生改变,但是会产生一个临时变量,将i转化为double类型的临时变量,此时再用这个临时变量和j进行比较!

指针和引用的区别

语法层面上:

int main()
{
	int a = 10;

	// 语法层面上,不开空间,是对a取别名
	int& b = a;
	b = 20;
	// 语法层面上,开空间,存储a的地址
	int* ps = &a;
	*ps = 30;
	return 0;
}

先看指针:

  • lea是取地址的意思:取a的地址放到eax中(eax是一个寄存器);
  • 然后把eax的值放入pa中,pa存放的是地址;(内存的速度太慢了,一般都会借助寄存器,缓存当中转!)
  • 解引用的时候将pa的值放到eax中;再对eax解引用将30放进去!

可以发现:从底层汇编指令实现的角度来看:引用是类似指针的方法实现的!

语法层面上我们认为引用不开辟空间,但是从底层来看引用还是会开辟空间!(底层和上层可能是不一样的!)

引用和指针的区别:

1. 引用概念上定义一个变量的别名,指针存储一个变量地址;
2. 引用在定义时必须初始化,指针没有要求;
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体;
4. 没有NULL引用,但有NULL指针;
(引用不能指向 NULL 是因为引用必须始终绑定到一个有效的对象,以确保安全性和简洁性。引用的设计旨在避免指针所带来的复杂性和潜在错误,因此不允许引用指向 NULL。如果需要表示“无对象”的状态,应该使用指针。)
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节);
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
7. 有多级指针,但是没有多级引用 ;
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理 ;
9. 引用比指针使用起来相对更安全 ;

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部