C++ 引用 参数传递 机制

本文主要分析C++引用赋值和引用参数传递的案例。


关于多开GDB,手懒把所有程序都编译成a.out的注意了,gdb中,不确定已经读取文件正在执行过程中会不会产生干扰。至少一次运行结束后,原来断点什么的就不存在了,文件找不到了。

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
`/home/huqw/test/cpp/a.out' has changed; re-reading symbols.
Error in re-setting breakpoint 1: No source file named refParamStack.cpp.
Error in re-setting breakpoint 2: No source file named refParamStack.cpp.
Error in re-setting breakpoint 3: No source file named refParamStack.cpp.
Error in re-setting breakpoint 4: No source file named refParamStack.cpp.

两个源文件如下,一个swap()函数,分别传入普通参数和引用参数:

//为何引用能改变原变量,看一下函数的栈情况
#include<iostream>
#include<stdio.h>
void swap(int &a,int &b)
{
        int temp = a;
        a = b;
        b = temp;
}

int main(){
        int i = 100;
        int j = 200;
        printf("i:%d,j:%d.\n",i,j);
        swap(i,j);
        printf("i:%d,j:%d.\n",i,j);
}

//为何引用能改变原变量,看一下函数的栈情况
//对比试验,看看正常函数参数的堆栈情况
#include<iostream>
#include<stdio.h>
void swap(int a,int b)
{
        int temp = a;
        a = b;
        b = temp;
}

int main(){
        int i = 100;
        int j = 200;
        printf("i:%d,j:%d.\n",i,j);
        swap(i,j);
        printf("i:%d,j:%d.\n",i,j);
}

编译好两个目标文件,打开两个gdb,分别读取两个目标文件,打好断点,同时运行,


如上图,感觉有点串,一个文件的i和另一个文件的j一样,一个文件的j和另一个文件的i一样。。。。两个“例程”的地址已经混淆了?其实这倒也没什么,这只是逻辑地址,而且是静态的,很多文件编译好了变量的地址就不变了,“屡试不爽”了,但是装载到内存,各是各的,不影响。

继续向下运行,顺便也来验证一下传递引用和传递普通参数的区别。




引用方面,函数内外地址一致是肯定的,a、b和i、j对应(至于info frame,目前还看不太好):


其实即使可执行的目标文件相同,调试打断点和显示代码还是要看源文件,所以最好用两个独立的源文件和两个独立的目标文件,免得产生干扰。


=========================================================================================================================

@符号的详细解释?不管能不能解释@符号,最起码可以找找相似,既然是@加地址,又因为传指针按理说也是传地址,所以用传指针的情况作对比。

代码如下:

//对照组:传递指针
#include<iostream>
#include<stdio.h>
void swap(int *a,int *b)
{
        int temp = *a;
        *a = *b;
        *b = temp;
}

int main(){
        int i = 100;
        int j = 200;
        printf("i:%d,j:%d.\n",i,j);
        swap(&i,&j);
        printf("i:%d,j:%d.\n",i,j);
}
gdb调试(函数内部堆栈):
(gdb) bt
#0  swap (a=0xbffff60c, b=0xbffff608) at refParamStack3.cpp:7
#1  0x080485df in main () at refParamStack3.cpp:15

但是指针和引用毕竟不同:层级不同,一个是直接当变量用,一个是指针,需要提取内容。

把原引用用例再加上普通引用(非函数参数)作为对比:

(gdb) n
14		int &refI = i;
(gdb) n
15		int &refJ = j;
(gdb) bt
#0  main () at refParamStack.cpp:15
(gdb) info locals
i = 100
j = 200
refI = @0xbffff604
refJ = @0x55fff4

另一个问题是,同一次运行中,出现了不一样的地址,在调用swap()前地址如下

(gdb) info locals
i = 100
j = 200
refI = @0xbffff604
refJ = @0x55fff4

调用中:

(gdb) print a
$1 = (int &) @0xbffff604: 100
(gdb) print b
$2 = (int &) @0xbffff600: 200

调用后如下:

(gdb) info locals
i = 200
j = 100
refI = @0xbffff604
refJ = @0xbffff600
原因应该是refJ 还未定义完成,下一步应该就行了(可见也是分了声明和定义赋值两步)

可见,引用的格式就是@地址,不过具体反映到操作上,@的机制是什么?


=========================================================================================================================


通过汇编指令:

(gdb)layout asm

(gdb)si

查看详细操作

引用初始化的汇编过程示例:

int &refI = i;

int &refj = j;

b+ x0x80485af <main()+25>   lea    0x14(%esp),%eax                  x
   x0x80485b3 <main()+29>   mov    %eax,0x18(%esp)                  x
b+ x0x80485b7 <main()+33>   lea    0x10(%esp),%eax                  x
   x0x80485bb <main()+37>   mov    %eax,0x1c(%esp) 


引用赋值测试:

引用refI指向i,进行如下操作:

refI = 300;

0x804859f <main()+9>    movl   $0x64,0x14(%esp)                 x
B+>x0x80485b7 <main()+33>   mov    0x18(%esp),%eax                  x
   x0x80485bb <main()+37>   movl   $0x12c,(%eax)                    x
   x0x80485c1 <main()+43>   lea    0x10(%esp),%eax                  x
   x0x80485c5 <main()+47>   mov    %eax,0x1c(%esp)                  x
   x0x80485c9 <main()+51>   mov    0x10(%esp),%edx                  x
   x0x80485cd <main()+55>   mov    0x14(%esp),%eax                  x
   x0x80485d1 <main()+59>   mov    %edx,0x8(%esp)                   x
   x0x80485d5 <main()+63>   mov    %eax,0x4(%esp)                   x
   x0x80485d9 <main()+67>   movl   $0x8048764,(%esp)                x
   x0x80485e0 <main()+74>   call   0x8048498 <printf@plt>           x
   x0x80485e5 <main()+79>   mov    0x1c(%esp),%eax                  x
   x0x80485e9 <main()+83>   mov    (%eax),%edx                      x
   x0x80485eb <main()+85>   mov    0x18(%esp),%eax                  x
   x0x80485ef <main()+89>   mov    (%eax),%eax    
可以看到

$0x12c存入eax指向的内存,而eax指向的内存

(gdb) print refI
$3 = (int &) @0xbffff604: 100
(gdb) print $eax
$4 = -1073744380
(gdb) print *($eax)
$5 = 100
(gdb) print &i
$6 = (int *) 0xbffff604
使用计算器换算一下十六进制和十进制,i的地址604和eax的值-1073744380是一样的,eax存的是i的地址, 进行赋值操作也是直接给i所在的内存赋值。
所以,引用本质上是指针( 32位也需要占用4字节空间)。


至于

和指针的操作层次不一样(指针还要再加*操作才能得到值);

只能初始化,不能再赋值

这都是C++和编译器的规则了!也加上他毕竟和指针的层不一样,int &refI = i;这种特定操作只有声明时候有,注定不能像指针一样用refI = j;这种形式再绑定其他变量,用*操作又等于提取内存的值。

其他的话,想不到什么原理,目的估计就是用着方便(不用写提取符号*)和实现特定绑定(不能更改的特性)。


做一个char类型的例子,char &refC = c;可以看到refC实际上存了一个c的地址,而不是副本,所以这个实际需要的存储空间是一个指针的大小。


  x0x8048566 <main()>                                                      push   %ebp                                                     x
   x0x8048567 <main()+1>                                                    mov    %esp,%ebp                                                x
   x0x8048569 <main()+3>                                                    sub    $0x10,%esp                                               x
   x0x804856c <main()+6>                                                    movb   $0x63,-0x5(%ebp)                                         x
B+ x0x8048570 <main()+10>                                                   lea    -0x5(%ebp),%eax                                          x
  >x0x8048573 <main()+13>                                                   mov    %eax,-0x4(%ebp)                                          x
   x0x8048576 <main()+16>                                                   mov    -0x4(%ebp),%eax                                          x
   x0x8048579 <main()+19>                                                   movb   $0x64,(%eax)                                             x
   x0x804857c <main()+22>                                                   mov    $0x0,%eax  
对于引用, sizeof()不能准确反映内部情况,也许是在sizeof()调用时通过地址找到原变量c,传递的时候就和普通变量正常调用一样。

实测,已经被编译器优化成这样了:

x0x80485b3 <main()+29>                                                   movl   $0x1,0x18(%esp)
差点忘了,sizeof是操作符,并不是函数!!!!这个操作肯定是提前约定好了!!!因为在程序运行前,还有大篇幅的初始化之类的操作,看不太懂了。


至于给引用的赋值是直接在原变量的地址上赋值,还是把变量读出来?

(refI = 300)这是取出地址到eax,直接从eax取出内存地址,给内存赋值的。

B+>x0x80485df <main()+73>                                                   mov    0x2c(%esp),%eax                                          x
   x0x80485e3 <main()+77>                                                   movl   $0x12c,(%eax)                                            x
(refC = 'D'),同上

0x80485b4 <main()+30>                                                   mov    0x28(%esp),%eax                                          x
   x0x80485b8 <main()+34>                                                   movb   $0x64,(%eax)  
也是,只是赋值,不加减,从内存读到eax,再从eax写回去,没什么必要!

光顾着gdb调试看原理了,忘了总结一条重点了,函数参数为引用时,函数的栈不需要额外存储引用,实际上什么也不传,而是直接就用ebp和偏移去找。估计要靠编译器优化和记忆。见下图:局部变量temp是ebp负向偏移0x4,也就是在swap()当前栈;而a和b,实际上也就是i和j,靠ebp正向偏移0x8和0xc去找,也就是上一层的栈。(PS:栈是负向增长,压栈等于指针减,又因为ebp是栈底,所以ebp正向偏移就是上一层的栈空间了)


引用传递的优势终于找到了:

引用传递比较省栈空间,少申请就少开支,就更有效率吧?!

至于指针,还没试,但是根据指针的特性,空间开支是免不了的,借助指针能改原变量只是一种借助地址操作的巧妙用法,更改指针本身却不会对原变量产生任何影响。指针形参和其他形参本质上没有区别,都要分配空间,都是不影响原变量的副本


那么引用的多层传递呢?按理说是一样的,这样多层传递的时候节省的开支就很可观了。(要和指针对比)

测试:

#include<iostream>
#include<stdio.h>
void func(int &c,int &d)
{
        c = 1000;
        d = 2000;
}
void swap(int &a,int &b)
{
        func(a,b);
}

int main(){
        int i = 100;
        int j = 200;
        printf("i:%d,j:%d.\n",i,j);            
        swap(i,j);
        printf("i:%d,j:%d.\n",i,j);
}

调试

x0x8048591 <func1(int&, int&)+6>         mov    0x8(%ebp),%eax   x
   x0x8048594 <func1(int&, int&)+9>         movl   $0x5,(%eax)      x
   x0x804859a <func1(int&, int&)+15>        mov    0xc(%ebp),%eax   x
   x0x804859d <func1(int&, int&)+18>        movl   $0x6,(%eax)      x
   x0x80485a3 <func1(int&, int&)+24>        mov    0xc(%ebp),%eax   x
   x0x80485a6 <func1(int&, int&)+27>        mov    %eax,0x4(%esp)   x
   x0x80485aa <func1(int&, int&)+31>        mov    0x8(%ebp),%eax   x
   x0x80485ad <func1(int&, int&)+34>        mov    %eax,(%esp)      x
   x0x80485b0 <func1(int&, int&)+37>        call   0x8048574 <func2(x
   x
第一层func1(),用ebp正向偏移0x8和0xc去找地址进行操作。
(入栈sub 0x8,比较省,没复制那么多汇编语句进来看不到。)

在call func2之前,进行了两个变量地址的压栈操作,这样保证了下一个frame,也就是func2,的栈帧偏移仍然是0x8和0xc,但是这貌似还是要占用空间,并不能节省空间?或者说都免不了这一步(下边有说明,入栈传参,必须的步骤),总的来说还是要省?

   x0x8048570 <frame_dummy+32>              call   *%eax            x
   x0x8048572 <frame_dummy+34>              leave                   x
   x0x8048573 <frame_dummy+35>              ret                     x
   x0x8048574 <func2(int&, int&)>           push   %ebp             x
   x0x8048575 <func2(int&, int&)+1>         mov    %esp,%ebp        x
B+>x0x8048577 <func2(int&, int&)+3>         mov    0x8(%ebp),%eax   x
   x0x804857a <func2(int&, int&)+6>         movl   $0x3e8,(%eax)    x
   x0x8048580 <func2(int&, int&)+12>        mov    0xc(%ebp),%eax   x
   x0x8048583 <func2(int&, int&)+15>        movl   $0x7d0,(%eax)    x
   x0x8048589 <func2(int&, int&)+21>        pop    %ebp             x
   x0x804858a <func2(int&, int&)+22>        ret       


让func1和func2都传普通变量:

#include<iostream>
#include<stdio.h>
void func2(int c,int d)
{
        c = 1000;
        d = 2000;
}
void func1(int a,int b)
{
        a = 5;
        b = 6;
        func2(a,b);
}

int main(){
        int i = 100;
        int j = 200;
        printf("i:%d,j:%d.\n",i,j);
        func1(i,j);
        printf("i:%d,j:%d.\n",i,j);
}


其实不管是传递引用还是普通参数,调用函数前的压栈操作都一样的,都是压入到esp偏移0和偏移4(本例两个int变量),这两个栈空间应该就是为了向下传参用的(如果下边不调用func2(),会不会少开辟点栈空间?)

形参都要有,只不过传递引用是给地址,传递变量是给值。下图是形参传递数值的。


下图是引用传递地址的(负数十进制换算一下十六进制就好了):




另外,到最后一层,func2()的时候,esp都没有自减操作了,是不需要么(另外,哪些栈空间是存返回值,以及如何返回,还没留意。。)




一个指针的例子,进行对比:

#include<iostream>
#include<stdio.h>
int func2(int *c,int *d)
{
        *c = 1000;
        *d = 2000;
}
int func1(int *a,int *b)
{
        *a = 5;
        *b = 6;
        func2(a,b);
}

int main(){
        int i = 100;
        int j = 200;
        printf("i:%d,j:%d.\n",i,j);
        func1(&i,&j);
        printf("i:%d,j:%d.\n",i,j);
}


如下图,调用函数时的传参,和引用是一样的


赋值操作也是一样的(提取地址到eax,再通过eax找到内存地址,对内存直接操作):

mov 0xc(%ebp),%eax

movl $0x6,(%eax)



额外加一句指针赋值:

int func1(int *a,int *b)
{
        a = b;

局部栈变量a(副本)发生改变。

B+ x0x8048591 <func1(int*, int*)+6>         mov    0xc(%ebp),%eax                                                                           x
   x0x8048594 <func1(int*, int*)+9>         mov    %eax,0x8(%ebp) 



可见,局部的更改都只在局部栈空间,局部的指针变量的值。而指针对内容的修改,是因为对内存进行了提取, 再对内存中的原变量进行操作

而引用,首先要和指针一样传递地址,其次,修改时等于自动带了地址提取功能,改的是原变量的内存。



PS:仔细留意可以发现函数调用时,参数入栈顺序是反的。

比如i和j,传给a和b,先把j入栈,再把i入栈。

据说C/C++这样做的好处是支持可变参数列表(个数)。


再加一例测试:

#include<iostream>
#include<stdio.h>
int func2(int *c,int *d)
{
        *c = 1000;
        *d = 2000;
}
int func1(int *a,int *b,...)
{
        a = b;
        *a = 5;
        *b = 6;

        func2(a,b);
}

int main(){
        int i = 100;
        int j = 200;
        int k = 300;
        int l = 400;
        printf("i:%d,j:%d.\n",i,j);
        func1(&i,&j,&k,&l);
        printf("i:%d,j:%d.\n",i,j);
}

调试,变量的声明:

   x0x80485c6 <main()+9>    movl   $0x64,0x1c(%esp)                 x
B+ x0x80485ce <main()+17>   movl   $0xc8,0x18(%esp)                 x
   x0x80485d6 <main()+25>   movl   $0x12c,0x14(%esp)                x
   x0x80485de <main()+33>   movl   $0x190,0x10(%esp) 
调用函数时入栈顺序刚好相反:

B+ x0x8048602 <main()+69>   lea    0x10(%esp),%eax                  x
   x0x8048606 <main()+73>   mov    %eax,0xc(%esp)                   x
  >x0x804860a <main()+77>   lea    0x14(%esp),%eax                  x
   x0x804860e <main()+81>   mov    %eax,0x8(%esp)                   x
   x0x8048612 <main()+85>   lea    0x18(%esp),%eax                  x
   x0x8048616 <main()+89>   mov    %eax,0x4(%esp)                   x
   x0x804861a <main()+93>   lea    0x1c(%esp),%eax                  x
   x0x804861e <main()+97>   mov    %eax,(%esp) 
仔细分析,一个帧(frame)的栈结构也出来了:帧栈的前半部分(因为地址递减,所以是高地址)是本地参数,帧栈的后半部分(低地址)是保留下来,给函数传参用的。( 大小随要调用函数的不同而不同。

无法画线,大概示意图如下:
$ebp

i 0x1c(%esp)--------------------->|

j 0x18(%esp)-------------->|      |

k 0x14(%esp)------->|      |      |

l 0x10(%esp)->|     |      |      |

param4 0xc(%esp) <-|      |      |      |

param3 0x8(%esp)<---------|      |      |

param2 0x4(%esp)<----------------|      |

param1 0x0(%esp)<-----------------------|

$esp


   x0x80485bd <main()>      push   %ebp                             x
   x0x80485be <main()+1>    mov    %esp,%ebp                        x
   x0x80485c0 <main()+3>    and    $0xfffffff0,%esp                 x
   x0x80485c3 <main()+6>    sub    $0x20,%esp  


不解的一点是,无论是两个参数还是4个参数,main压栈时都是同样的与操作和自减20。栈底都是esp偏移0x1c和0x18。但是这个栈空间只够4个参数的,五个怎么办?

下边是六个变量的声明和传参测试(代码就不贴了):

声明与定义:

0x80485bd <main()>                      push   %ebp             x
   x0x80485be <main()+1>                    mov    %esp,%ebp        x
   x0x80485c0 <main()+3>                    and    $0xfffffff0,%esp x
   x0x80485c3 <main()+6>                    sub    $0x40,%esp       x
   x0x80485c6 <main()+9>                    movl   $0x1,0x3c(%esp)  x
   x0x80485ce <main()+17>                   movl   $0x2,0x38(%esp)  x
   x0x80485d6 <main()+25>                   movl   $0x3,0x34(%esp)  x
   x0x80485de <main()+33>                   movl   $0x4,0x30(%esp)  x
   x0x80485e6 <main()+41>                   movl   $0x5,0x2c(%esp)  x
   x0x80485ee <main()+49>                   movl   $0x6,0x28(%esp)  x
传参与调用:
B+>x0x8048612 <main()+85>   lea    0x28(%esp),%eax                  x
   x0x8048616 <main()+89>   mov    %eax,0x14(%esp)                  x
   x0x804861a <main()+93>   lea    0x2c(%esp),%eax                  x
   x0x804861e <main()+97>   mov    %eax,0x10(%esp)                  x
   x0x8048622 <main()+101>  lea    0x30(%esp),%eax                  x
   x0x8048626 <main()+105>  mov    %eax,0xc(%esp)                   x
   x0x804862a <main()+109>  lea    0x34(%esp),%eax                  x
   x0x804862e <main()+113>  mov    %eax,0x8(%esp)                   x
   x0x8048632 <main()+117>  lea    0x38(%esp),%eax                  x
   x0x8048636 <main()+121>  mov    %eax,0x4(%esp)                   x
   x0x804863a <main()+125>  lea    0x3c(%esp),%eax                  x
   x0x804863e <main()+129>  mov    %eax,(%esp)                      x
   x0x8048641 <main()+132>  call   0x804858b <func1(int*, int*, ...)x
   x
总结或猜测,终于,main压栈,esp变成自减40了,估计是栈的一个取整的模式,够多少个变量和参数,main函数加0x10的栈空间。

PS:此例esp自减虽然是0x20,如果传参只传两个,esp总的自减是0x10,所以单位是0x10。下边的参数也有以0x10为单位的。
帧栈的顶格前(不是半)部分是参数,帧栈的顶格后(不是半)部分是要传递的参数,中间是预留。

示意图如下:
$ebp

i 0x1c(%esp)--------------------->|

j 0x18(%esp)-------------->|      |

k 0x14(%esp)------->|      |      |

l 0x10(%esp)->|     |      |      |

m

n

.........

........

param6

param5

param4 0xc(%esp) <-|      |      |      |

param3 0x8(%esp)<---------|      |      |

param2 0x4(%esp)<----------------|      |

param1 0x0(%esp)<-----------------------|

$esp


这样就直观了,帧栈的空间是要预先分配好的,多大就是多大,假设变量一百个,但是给函数传的参数未必有一百个,但总不能就用100+2的大小啊,所以要预留出来一定大小的空间,也就是中间的一片空白。

具体来说,这个大小应该是编译阶段根据实际代码已经计算出来决定了,不然怎么分配合适的大小呢。

当然变量和参数也不是对应关系,只是本例这样用,比如声明6个变量,传2个变量,栈空间是0x30。




最后,可变参数列表这多出来的参数要怎么使用呢?又没有形参。

用argv[]之类的?是默认的还是要手动写的?

前边的a和b也可以用argv[]吗?

这应该是另一个基础,我给忘了。

猜你喜欢

转载自blog.csdn.net/huqinweI987/article/details/50769096