【C++·峰顶计划】引用操作及底层原理深析

Hi!我是Duoni!

d3801154c39743509738312e129cbba2.jpeg

目录

🍊引用

🍊引用与取地址符的区分

🍊引用的特性

🍊引用的应用场景

🍊传值返回与传引用返回

🍊传值返回

🍊传值返回的实现

🍊传引用返回

🍊传引用返回的实现

🍊传值返回与传引用返回的优缺点

🍊常引用

🍊权限规则

🍊隐式类型转换

🍊指针与引用的区别

🍊引用与指针底层刨析

🍊底层刨析

🍊汇编解读

🍊引用实现部分

🍊指针实现部分


🍊引用

引用概念:引用并不是一个独立定义的变量或其他值,而是一个对已存在的变量或其他值取了一个别名。这一期间编译器不会为他开辟空间(语法角度),它与其引用对象共同使用内存空间,对别名进行操作会影响引用对象。

引用用法:类型& 引用变量名 = 引用对象名

int num_a = 6;
int& num_b = num_a;//num_b是num_a的别名

为了便于理解,我们将取别名再理解为取外号。

tips:”小狗“被取了别名叫”修勾“。


🍊引用与取地址符的区分

&符在类型名后的是引用

int num_a = 6;
int& num_b = num_a;//给变量num_a定义了一个名为num_b的别名

&符在变量名前是取地址

int num_a = 6;
int* num_b = &num_a;//取num_a的地址给num_b

附:定义别名真的不会另开空间吗

int main()
{
       int num_a = 6;
       int& num_b = num_a;//num_b是num_a的别名
       std::cout << "变量num_a的地址:" << & num_a << std::endl;
       std::cout << "别名num_b的地址:" << & num_b << std::endl;
       return 0;
}

15fa449c143144b59900353e3e4c231d.png

小结:从语法角度上,定义别名,其别名是和引用对象共占一块空间的。

tips:底层上,引用实际是依靠指针实现的,具体后期深入。

附:对别名的修改会影响到引用对象吗?

int main()
{
       int num_a = 6;
       int& num_b = num_a;
       std::cout << "引用对象修改前:" << num_a << std::endl;
       std::cout << "别名修改前:" << num_b << std::endl;
       num_b--;
       std::cout << std::endl;
       std::cout << "引用对象修改后:" << num_a << std::endl;
       std::cout << "别名修改后:" << num_b << std::endl;
       return 0;
}

f8eb612aaf28466895c78d634b462917.png

小结:对别名的操作会直接对引用对象进行影响。


🍊引用的特性

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

int main()
{
       int& val;//错误,引用的定义(使用)必须指明引用对象
       return 0;
}

809c08786ef14efea1ff0543f6ea6753.png

2.一个变量可以有多个引用,而一个别名也可以有多个别名

int main()
{
       int num_a = 2;
       int& num_1 = num_a;//一个变量可以拥有多个别名
       int& num_2 = num_a;
       int& num_3 = num_a;
       int& num_cp1 = num_1;//一个别名可以拥有多个别名
       int& num_cp2 = num_1;
       int& num_cp3 = num_1;
       std::cout << "我们都是变量num_a的别名:" << num_1 << " " << num_2 << " " << num_3 << std::endl;
       std::cout << "我们都是别名num_1的别名:" << num_cp1 << " " << num_cp2 << " " << num_cp3 << std::endl;
       return 0;
}

0732a43f6f194855af24f7b2117c37a9.png

3.引用一旦引用某一实体,就不能够再引用其他实体。

int main()
{
       int val_a = 2;
       int val_b = 6;
       int& num_cp = val_a;
       num_cp = val_b;//num_cp不能够再改变实体,在这只能进行赋值动作
       std::cout << &val_a << std::endl;//观察num_cp十分还是val_a的别名
       std::cout << &num_cp << std::endl;
       return 0;
}

69ef9382e4624902be99e74ef08c021a.png


🍊引用的应用场景

1.可用于做参数(输出型参数)

void Change_Num(int& num_1, int& num_2)//用引用接收参数
{
       num_1 = 60;//是否会对原值进行改变?
       num_2 = 80;
}

int main()
{
       int val_1 = 6;
       int val_2 = 8;
       Change_Num(val_1, val_2);//传参
       std::cout << "val_1:" << val_1 << std::endl;
       std::cout << "val_2:" << val_2 << std::endl;
       return 0;
}

4936535437734be9a770268a2e1e6b28.png

优点:提高了传参的效率,相对于指针的传址解引用修改。引用做参数能更加直观、便利的完成操作。

tips:输入型参数与输出型参数是什么?

(1)、输入型参数是指:传参处传递的是普通变量,且在函数执行后,不会对外部的实体产生影响。

(2)、输出型参数是指:传参处传递的是地址,或者在接收参数时使用别名接收,运行后会对外部实体产生影响。

2.大型对象的传参,提高效率。(节省了参数拷贝的环节)

3、做返回值(输出型返回值,调用者可通过返回值修改引用对象。减少返回时的一次临时拷贝,提高效率)


🍊传值返回与传引用返回

🍊传值返回

传值返回的意义:函数结束后,通过临时拷贝带回所需要的值。

int test_return()
{
       int num = 6;
       return num;//第三步对值进行拷贝,产生值的临时拷贝//返回的只是num的一份临时拷贝
}

int main()//第一步栈空间开辟
{
       int val = test_return();//第二步调用函数//第三步接收返回值
       return 0;
}


🍊传值返回的实现

进入主函数,main函数在栈帧创建空间,再调用test_return函数,test_return函数在栈帧开辟空间,走到(return num值)的那一步,编译器做了两个动作:

局部变量的生命周期取决于所存储的物理空间,栈空间在函数执行完毕后就会销毁,这也说明函数内的一切局部变量将随着栈空间的销毁而被销毁。

而如果直接将值进行返回,那将是错误的,因为在函数结束后,所有的局部变量空间都被销毁,此时的返回值并不存在,最终形成了一次越界访问的错误,该值也会是一个随机值。

为了返回有效的值,编译器是这么处理的:第一步,当进行到(return num值)时,会对num进行一次临时拷贝,存储它的数据。第二步,再将这一份临时拷贝的数据进行返回拷贝。


附:若函数中所返回值不存放在栈区,那编译器会怎么做?

int test_return()
{
       static int num = 6;//我存放在静态区,栈帧销毁与我无关
       //int num = 6;
       return num;
}

确实,静态变量存放在静态区,栈区的销毁动作也并不能影响到它。但编译器同样还是会进行临时拷贝再返回的动作,因为这么做是最安全的。

小结:只要是传值返回,都会形成临时拷贝。


🍊传引用返回

int& test_return()//返回num的别名
{
       int num = 6;//局部变量在函数结束后会被销毁
       return num;
}

int main()
{
       int val = test_return();
       return 0;
}


🍊传引用返回的实现

num成为引用对象,别名被返回。但函数结束意味着栈帧中局部变量的销毁,而别名一旦被定义引用对象便无法再修改。此时别名被返回到主函数中,若对别名进行访问,其结果是未被定义且不确定的。(可能是原值,也可能是随机值)

小结;如果引用对象出了作用域一定被销毁,那么便一定不能使用引用返回,只能使用传值返回。若要使用传引用返回,那么前提一定要保证引用对象出作用域不被销毁

附:引用对象被销毁后,再次用别名访问,得到的一定是随机值。因为栈空间销毁后,所有地址都保存着一个随机值,虽然引用对象被销毁,但空间一直存在!

tips:引用对象是全局变量或静态变量或是存于堆空间,就可使用引用返回。


🍊传值返回与传引用返回的优缺点

传值返回的优缺点

优点:安全、稳定,应用范围广

缺点:需要进行一次临时拷贝,效率低、速度慢

传引用返回的优缺点

   

优点:不用临时拷贝,直接返回别名、对别名的修改可以直接影响实体、高效

缺点:要满足使用条件,才可使用(出作用域不被销毁)


🍊常引用

常引用的意义:const对引用进行修饰,限制权限。


🍊权限规则

1.权限无法被放大

int main()
{
       const int val = 6;//val由const修饰,只可读,不可写
       int& num = val;//num是变量val的别名,权限发生改变:可读可写。错误!权限被放大
       return 0;
}

9a0087a639cf4fefbbeda39439d7aed9.png

2.权限可以平移

int main()
{
       const int val = 6;//权限:可读,不可写
       const int& num = val;//权限:可读,不可写。正确,权限平移
       return 0;
}

3.权限可以被缩小

int main()
{
       int val = 6;//权限:可读,可写
       const int& num = val;//权限:可读,不可写。正确,权限缩小
       return 0;
}


🍊隐式类型转换

隐式类型转换的意义:不同类型间的转换,通常情况下,小类型会隐式转换成大类型

int main()
{
       int a = 6;
       double b = a;
       //整形变量会向双精度浮点值进行转换,形成一个临时变量进行提升,不会对本体进行改变
       return 0;
}

tips:不同类型间的运算或赋值,变量会进行隐式转换。小类型会向大类型进行提升、截断。

这一过程是在临时变量上进行实现,因为小类型本身的体量过小,没有办法在本体进行提升,所以本体不会发生改变,但也不会被使用,使用的是提升后的那份临时变量。

附:为什么没有发生权限问题?

因为在类型转化中将整形a提升为双精度浮点值后,虽然临时变量具有常性,但表达式本身也只是赋值功能,将临时变量赋值给b,并没有发生权限的改变。

因为权限规则只对指针和引用有效。

int main()
{
       int a = 6;
       double& b = a;//定义整形a为双精度浮点值b的引用对象,错误!
       //整形变量会向双精度浮点值进行转换,在临时变量中进行二进制的提升,所以此时临时变量具有常性,权限不能被扩大。错误!
       return 0;
}

35f21a6955db459ca223e100797b618f.png

tips:进行隐式类型转换后,临时变量具有常性,所以不能够成为b的引用对象,因为如此的话,具有常性的临时变量权限将受到放大,这是不符合规则的。

若要正确的对其进行引用,应该加上const修饰别名,起到权限平移的功能。

       const double& b = a;

附:引用的作用在于形成别名,并且对别名具有可访问、可操作的功能,所以会引发类型转化后的权限问题。所以今后在函数参数接收上或具有类型转换的表达式中,最好使用const修饰,提高接收度。


🍊指针与引用的区别

1.在定义时,引用必须初始化,而指针不要求必须初始化

int main()
{
       int& pre;//error:未初始化引用
       int* ret;
       return 0;
}

383c2d6c91b44e44a5ba2bfd2b8dfdf2.png

2.没有空引用,但有空指针

int main()
{
       int& pre = nullptr;//不存在对空的引用
       int* ret = nullptr;
       return 0;
}

38f7ef59137248888c7463f488b4a34e.png

3.引用在初始化阶段引用一个实体后,就不能再引用其他实体。而指针可以在任何情况下改变指向。(同类型实体)

引用举例

using namespace std;
int main()
{
       int num_1 = 5;
       int num_2 = 10;
       int& pre = num_1;
       pre = num_2;
       cout << "pre别名的值:" << pre << " " << "pre引用对象地址:" << &pre << endl;
       cout << "num_1的地址" << &num_1 << endl;
       cout << "num_2的地址" << &num_2 << endl;
       return 0;
}

3be9d4aea32e4de8a4e47ec644828592.png

tips:初始化时别名的引用对象为num_1,尽管后面又被“貌似”的引用num_2,但其只是赋值操作。究其根本就是,别名的赋值操作改变了num_1的值,但并不能改变别名的引用对象。

指针举例

int main()
{
       int num_1 = 10;
       int num_2 = 20;
       cout << "num_1:" << &num_1 << "  " << "num_2:" << &num_2 << endl;
       int* pre = &num_1;
       cout << "pre初始化指向的实体地址:" << " " << pre << endl;
       pre = &num_2;
       cout << "pre改变指向的实体地址:" << "   " << pre << endl;
       return 0;
}

d40332b01b934639a51c414bf59174c4.png

tips:指针确实可以任意修改指向,而引用则不可以随意改变实体。

4.在sizeof操作符中的含义不同,引用的大小取决于引用对象的类型大小,指针的大小取决于系统环境,32位平台下指针大小为:4字节,64位平台下指针大小为:8字节。

int main()
{
       short num = 2;
       short& pre = num;//引用的大小取决于引用对象的类型大小
       short* p = &num;//指针的大小取决于环境
       cout << "pre:" << sizeof(pre) << "  " << "p:" << sizeof(p) << endl;
       return 0;
}

afa9e2bc017c4150a2c94077e6370ae7.png

5.引用进行自加的效果会让引用对象的值增加1,指针的自加会让指针向后偏移一个类型大小。

int main()
{
       int num = 5;
       int& pre = num;
       int* p = &num;
       cout << "pre:" << ++pre << "  " << "p:" << ++p << endl;
       //pre++表示引用对象值自加1,指针自加则表示向后访问一个自身类型大小的地址
       return 0;
}

68a69bd28f1143849e51b85194f82958.png

6.有多级指针,但没有多级引用。

int main()
{
       int num = 5;
       int* p = &num;
       int** pp = &p;//有多级指针
       int& pre = num;
       int& ppre = pre;//不能形成多级引用,在这只表示:给别名取一个别名
       return 0;
}

7.访问方式不同:指针需要显式的解引用访问,引用则可以直接使用别名访问。(编译器自己处理)

int main()
{
       int num = 2;
       int& pre = num;
       int* p = &num;
       cout << "pre:" << pre << "  " << "p:" << *p << endl;
       //引用可直接使用别名访问实体对象,指针必须使用解引用才可访问实体对象
       return 0;
}

121366004d484b2a88302c425247649f.png

8.引用的使用比指针要安全。


🍊引用与指针底层刨析

从语法角度来看,引用自身是不开辟空间,是与引用对象共用一块空间。但在底层,引用的实现却并不是如此。

先说结论:引用的实现需要开辟空间,并且底层是依靠指针实现,之所以使用方法不同,在于等于引用进行了封装。


🍊底层刨析

代码:

int main()
{
       int num = 20;
       int& pre = num;
       pre = 30;
       int* p = &num;
       *p = 40;
       return 0;
}

汇编:

       int num = 20;
005F1FDF  mov         dword ptr [num],14h 
       int& pre = num;
005F1FE6  lea         eax,[num] 
005F1FE9  mov         dword ptr [pre],eax 
       pre = 30;
005F1FEC  mov         eax,dword ptr [pre] 
005F1FEF  mov         dword ptr [eax],1Eh 
       int* p = &num;
005F1FF5  lea         eax,[num] 
005F1FF8  mov         dword ptr [p],eax 
       *p = 40;
005F1FFB  mov         eax,dword ptr [p] 
005F1FFE  mov         dword ptr [eax],28h


🍊汇编解读

       int num = 20;
005F1FDF  mov         dword ptr [num],14h 

首先创建一个四个字节的空间用于存储整形。dword表示:d指的是double(双倍),word表示两个字节,共四个字节,num是变量名,将八进制的20存进变量中。(mov)代表移动。


🍊引用实现部分

       int& pre = num;
005F1FE6  lea         eax,[num] 

将num的地址存入到寄存器eax中。

lea表示:装入有效地址,操作数必须为地址。

005F1FE9  mov         dword ptr [pre],eax 

将寄存器eax中的值移动到pre中。

       pre = 30;
005F1FEC  mov         eax,dword ptr [pre] 

将pre的值移动至寄存器eax中。

005F1FEF  mov         dword ptr [eax],1Eh 

将八进制的30移动到存放pre值的寄存器eax中,相当于赋值操作。

小结:从汇编代码中,可以证实使用引用是必须开辟空间的。


🍊指针实现部分

       int* p = &num;
005F1FF5  lea         eax,[num] 

将变量num的地址存进寄存器eax中。

005F1FF8  mov         dword ptr [p],eax 

开辟一个四个字节的指针,将寄存器eax的值存进整形指针p中。

       *p = 40;
005F1FFB  mov         eax,dword ptr [p] 

将p的值移动到寄存器eax中。

005F1FFE  mov         dword ptr [eax],28h

将八进制的40赋值给eax。

小结:指针与引用的底层实现相同!


附:指针与引用的相似处

都可以用作函数参数或返回值(输出型参数、输出型返回值)。

tips:引用的不可多级引用是其的缺点,体现在单链表的实现,不可以使用引用,指针更有优势。

小结:指针更为复杂,功能更为强大,可适用的场景更广,但也更为的危险。引用更加的便捷与安全,但适用性还是比指针要狭小一些。


文章到这就结束啦!如果喜欢就关注Duoni叭!

评论 50
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值