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的地址复制到寄存器。而内容复制一般复制量都比地址复制要大,这也就造成了效率上的差距。且传值修改的是复制的内容,所以实参不会受影响;但后两者修改的是传入指针里的内容,这两个指针(传参和实参指针)指向的内容是一致的,所以实参会收到影响。

  • 所以总结下,函数传参的流程如下:
  1. 执行leamov指令将内容或指针拷贝到寄存器上。
  2. 执行push指令把寄存器里的内容push进栈。
  3. 执行call指令调用函数。
  4. 执行add指令确保堆栈平衡,相当于执行pop操作把前面push的内容弹出。而add的值跟参数个数有关(之前push的值)。

三、运行时间对比实践

  一直好奇三者之间运行时间的差异,正好借着这次实践测试一下。

1. 传参类型偏小

  首先是测试传参类型偏小的情况吧。这里选择的传参类型是int,在32位环境下,intint*大小是一致的4字节。根据上面的汇编源码来看,我个人认为mov4字节和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++学习记录:深入理解三种传参方式的主要内容,如果未能解决你的问题,请参考以下文章

ajax的三种传参方式

三种传参方式

vue三种传参方法

vue2 route包含的信息和router使用的详细介绍 vue3 useRouter和useRoute 使用以及三种传参方式

Vue 之 路由常用的几种传参方式

JS有哪几种传参方式