C/C++学习记录:深入理解三种传参方式
Posted 河边小咸鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++学习记录:深入理解三种传参方式相关的知识,希望对你有一定的参考价值。
C/C++学习记录:深入理解三种传参方式
之前对传参这方面的东西一直是知其然不知所以然。概念用法怎么用都知道,但是其真正的内部操作流程确实是理解不足。这两天一直在总结shell脚本的笔记,写累了正好研究一下传参这方面的内容。
这篇笔记中记录了关于这方面我的理解过程和心得。关于本篇笔记的深度,也是到汇编为止不再深入,就我个人理解来看已经是足够了。
目录
一、关于三种传参方式
1. 值传参
1.1 简单总结
这是我在编程中最早接触的传参方式,也是一开始使用最多的传参方式。它的特点很明确就是简便,非常明了。当然缺点也是被说了很多次,就是慢+占用空间+不能修改实参。因为所谓的值传参是把实参的值复制了一遍,所以会有上面的特点。
1.2 我的疑问
总是说值传参的执行过程会复制实参的值,那么它的流程是怎么样的?
2. 引用传参
2.1 简单总结
这是C++里的概念,C里是没有的。它解决了值传参不能修改实参的问题,另外也比传值要快。就我目前接触到的C++代码中,里面均常常用到&
和const &
,例如stl的源码。
2.2 我的疑问
我看网上说传引用其实也是传的指针,所以一直对引用的流程很有兴趣。如果真的也是传指针,那么它的意义就是更简单明了的传指针吗?另外很多源码中都使用const &
,我一直很好奇传引用究竟能比传值快多少。
3. 指针传参
3.1 简单总结
第一次接触传指针,还是在当时学习链表的时候。在此之前,我对于指针作用的印象仅仅是文件指针和一丢丢字符串的内容,而对于学习中碰到的那些什么*p,&p
的完全没有实际应用中的感受,甚至产生了疑问,为何大伙都说指针牛p?
在接触到链表头结点的指针后,我首次发现原来传值是不能改变内容的(太菜了当时),得传指针,所以链表函数传参时,节点得取个地址传进去,由此我打开了新世界的大门,感受到了指针的牛p。以至于后面再接触java的时候感觉浑身难受,感受到了一种局限感,所以后面我决定以C/C++为方向。
对我而言,指针传参相当于是一种 “降维打击”,相当于“你收拾不了他就去找他爹收拾他”。总而言之,向下层操作性很大(提领指针的内容),可以修改实参并且速度也很快。但是,传指针相当于把传值的内容改为指针,所以指针层面也是不能被修改的(虽然我也没见过要修改最高层指针),由于指针的大小是固定的而且很小,传指针的速度也会很快。
3.2 我的疑问
底层流程是什么?是先获取地址,再走值传递那一套流程吗?
二、汇编层面剖析
1. 操作
我的理解方式是通过vs2019的反汇编功能查看低层汇编代码进行比对分析,而下面是我的操作过程。
首先是实验源码如下,可以看到我声明了三个函数,分别用了三种传参方法。
/*
* 三种传参方式测试
* 2021/8/22
*/
#include<cstdio>
//值传参
void func_value(int x)
x = 22;
//引用传参
void func_ref(int& x_ref)
x_ref = 2222;
//指针传参
void func_ptr(int* x_ptr)
*x_ptr = 22222;
int main()
int test_arg = 222;
//值传参
func_value(test_arg);
//引用传参
func_ref(test_arg);
//指针传参
func_ptr(&test_arg);
return 0;
接着,我开启调试反汇编,查看调用三个函数时的汇编源码,结果如下:
2. 总结
说实话,我没想到传引用和传指针的汇编源码竟然完全一样…而传值和另外两者的唯一区别就是第一条汇编指令。其中传值用的是汇编指令mov
,而传引用和传指针用的都是汇编指令lea
。
然后我搜了下,mov
是把内容复制到寄存器eax,而lea
是把地址复制到寄存器里。所以这里传值是把变量test_arg
的内容复制到寄存器,而后两者是把变量test_arg
的地址复制到寄存器。而内容复制一般复制量都比地址复制要大,这也就造成了效率上的差距。且传值修改的是复制的内容,所以实参不会受影响;但后两者修改的是传入指针里的内容,这两个指针(传参和实参指针)指向的内容是一致的,所以实参会收到影响。
- 所以总结下,函数传参的流程如下:
- 执行
lea
或mov
指令将内容或指针拷贝到寄存器上。 - 执行
push
指令把寄存器里的内容push进栈。 - 执行
call
指令调用函数。 - 执行
add
指令确保堆栈平衡,相当于执行pop操作把前面push的内容弹出。而add的值跟参数个数有关(之前push的值)。
三、运行时间对比实践
一直好奇三者之间运行时间的差异,正好借着这次实践测试一下。
1. 传参类型偏小
首先是测试传参类型偏小的情况吧。这里选择的传参类型是int,在32位环境下,int
和int*
大小是一致的4字节。根据上面的汇编源码来看,我个人认为mov
4字节和lea
一个地址时间消耗可能是五五开的,于是我进行了以下的测试。
测试代码如下,其中我使用到了一个自己实现的计时器,计时器内容在这篇博客里C++学习记录:基于chrono库的高精度计时器。
1.1 函数内单运算操作
这部分我在函数内均仅进行单运算操作,如下。
函数执行一定次数TIME
后的结果如下。果然在传参实际传入大小差不多的情况下,实际时间消耗也是差不多的。在我理解上,其实引用和指针传参可能也是算一种值传参吧,只不过它们传的值是指针。所以在传值大小相似的情况下时间消耗也相似。
1.2 函数内多运算操作
然后我想到,不同的传参方式,操作传参的时间消耗一致吗?于是在函数内新增了几条运算。既然该情况下传参速度相同,如果执行速度也相同,则说明操作传参的时间消耗一致。
函数内均修改为如下操作:
函数执行一定次数TIME
后的结果如下。果然在操作增多的情况下,实际时间消耗也是差不多的。这说明操作传参的时间消耗是一致的。
2. 传参类型偏大
“在我理解上,其实引用和指针传参可能也是算一种值传参吧,只不过它们传的值是指针。所以在传值大小相似的情况下时间消耗也相似。”
为了证明我的这个猜测,我对传参类型进行了改变,这次选择使用的数据类型是c++的数据结构std::string
。在32位环境下,std::string
的大小是28字节,std::string*
的大小还是4字节,即两者大小是七倍的关系。则如果时间消耗差距较大的话,则说明真正影响传参速度是传的大小,就说明我的猜测算是对的吧。
测试代码如下,还是用到了上文中提到的计时器。
这部分函数的操作如下,仅仅是简单的sizeof
操作。
运行结果如下,可以看到时间如下,果然时间差距是非常的大。说明传参时间根本上还是受传参大小影响。不过我好奇的是为何时间差距这么大,我猜测可能是内存分配时间不同或是调用了std::string
的构造参数吧。
四、体会
随着和C/C++打交道的时间越来越长,我探索的内容也越发深入、复杂。但是当真正理解了之前疑惑的内容,说实话还是很开心的。
另外吐槽下csdn上鱼龙混杂,发的大部分都是很基础没有营养的东西,或者不知道哪抄的错误百出的内容,当然也有很多大佬的内容让我受益匪浅(深表感谢orz),现在我搜个东西都得“发掘”半天。但是从某种意义上来讲我是有一点开心的,这说明我至少已经算入门了嘛XD
以上是关于C/C++学习记录:深入理解三种传参方式的主要内容,如果未能解决你的问题,请参考以下文章
vue2 route包含的信息和router使用的详细介绍 vue3 useRouter和useRoute 使用以及三种传参方式