C++引用雨指针的底层原理

来源:互联网 发布:mac五国问题 编辑:程序博客网 时间:2024/05/29 10:50

    【声明】本文无技术含量!在博客园上回复某个帖子,招来他的非议,我不想去细究这个人的治学态度,不想去问去管他到底有没有修改过自己的文章,对我来说没必要。我只能说不负责任,态度自大的,不严谨的人是令我失望的。但是对于一个问题,这里涉及到了“引用”,这是C++引入的一种新的形式,可以说是给程序员的一个语法上的好处,但是我翻看了BS的《The C++ Programming Lanuage》,并没有看到对引用的实现的解释。所以虽然我一直默认为引用是这样实现的,但在对别人提出自己的观点之前,我需要验证自己的“猜想”。这个问题很好去验证,所以我先给出一个最简单的试验:用一个 int 类型的引用来说明问题。

 

    输入下面的代码,我使用的是VC6.0:

复制代码
void ModifyNum(int& x){    x = x + 10;}int main(int argc, char* argv[]){    int a = 5;    ModifyNum(a);    printf("a=%d\n", a);    return 0;}
复制代码

 

 

    上面的代码将输出 a = 15,然后用 IDA 打开编译后的 exe 文件,查看函数 main 和 ModifyNum 的代码:

复制代码
.text:00401060 main            proc near               ; CODE XREF: j_mainj.text:00401060.text:00401060 var_44          = dword ptr -44h.text:00401060 var_4           = dword ptr -4.text:00401060.text:00401060                 push    ebp.text:00401061                 mov     ebp, esp.text:00401063                 sub     esp, 44h.text:00401066                 push    ebx.text:00401067                 push    esi.text:00401068                 push    edi.text:00401078                 mov     [ebp+var_4], 5.text:0040107F                 lea     eax, [ebp+var_4].text:00401082                 push    eax.text:00401083                 call    j_ModifyNum.text:00401088                 add     esp, 4.text:0040108B                 mov     ecx, [ebp+var_4].text:0040108E                 push    ecx.text:0040108F                 push    offset ??_C@_06DJNL@a?$DN?$CFd?$CB?6?$AA@ ; "a=%d!\n".text:00401094                 call    printf.text:00401099                 add     esp, 8//eax = 0 , return 0;.text:0040109C                 xor     eax, eax.text:0040109E                 pop     edi.text:0040109F                 pop     esi.text:004010A0                 pop     ebx.text:004010A1                 add     esp, 44h.text:004010A4                 cmp     ebp, esp.text:004010A6                 call    __chkesp.text:004010AB                 mov     esp, ebp.text:004010AD                 pop     ebp.text:004010AE                 retn.text:004010AE main            endp
复制代码

  

    注意上面的黄色背景的代码,显然函数的参数在底层上是把 int 变量的地址 (int*)作为参数传递的。那么 ModifyNum 的代码实际上不看也就能猜到了,它和 ModifyNum ( int* pX) 应该是一样的。这个代码很好找,在代码段(.text)的顶部,紧跟跳转表后面依次是 ModifyNum, main, printf, mainCRTStartup (即 PE 文件头中记录的入口点) 这几个函数。

 

复制代码
.text:00401020 ModifyNum       proc near               ; CODE XREF: j_ModifyNumj.text:00401020.text:00401020 var_40          = dword ptr -40h.text:00401020 arg_0           = dword ptr  8.text:00401020.text:00401020                 push    ebp.text:00401021                 mov     ebp, esp.text:00401023                 sub     esp, 40h.text:00401026                 push    ebx.text:00401027                 push    esi.text:00401028                 push    edi.text:00401038                 mov     eax, [ebp+arg_0].text:0040103B                 mov     ecx, [eax].text:0040103D                 add     ecx, 0Ah.text:00401040                 mov     edx, [ebp+arg_0].text:00401043                 mov     [edx], ecx.text:00401045                 pop     edi.text:00401046                 pop     esi.text:00401047                 pop     ebx.text:00401048                 mov     esp, ebp.text:0040104A                 pop     ebp.text:0040104B                 retn.text:0040104B ModifyNum       endp
复制代码

 

    上面的代码的实现显然就是针对指针操作,也就是说,ModifyNum 的实现相当于:

 

void ModifyNum(int* pX){    *pX = *pX + 10;}

 

    我也观察了下面的代码在汇编级别的实现(汇编代码就不贴了):

    int a =5;

    int& b = a;

    这里在汇编级别,b 相当于是一个 int* 类型的临时变量,和 int* b = &a 等效。当然在语言层面上我们可以理解成“b 是 a 的别名,b 就是 a”,只是看起来是这样,但它并不是实现,尤其是作为参数传递的时候编译器只能使用指针去实现。而且非常重要的是,b 作为 a 的引用,它是一个指向 a 的指针变量,它是需要在栈上额外占用存储空间的(如果理解成别名,有可能会误以为 b 不需要占用存储空间,这是不确切的)。

 

也就是说,C++中引用是编译器通过指针实现的,但这个实现在语言层面对程序员做了透明化处理。

 

    很显然,在C++里,如果一个函数需要使用(读)一个比较大的对象中的数据(而不是修改它),和在栈上构造出一个临时拷贝比起来,传递他的指针/引用是更高效的,《The C++ Programming Language》这本书中指出,这种情况,参数类型应该加 const 即 const T&,这在语义上明确表示,你仅仅是使用而不是修改它。相对的,如果不加 const,则意味着你想明确的在函数中修改对象。在规模越大的项目中,这种约定对代码可理解性起到的作用越大。

 

    现在很多上层表述有一些“按引用传递”,“按值传递”,这种表述,我想它是比较模糊一点的,他们实际上意味着前者是传递了对象的地址,在函数中因此可以修改对象,即为所谓的引用。后者,按值传递,意味着栈上是一个对象的拷贝,所以在函数中修改的是栈上的临时对象(当然临时对象是没必要修改的),而不能影响函数以外的那个对象。由于参数是通过栈通知给函数,所以只有“拷贝”(即 push)这个“传递”动作(所以你可以说底层上不存在前述的那些说法,那只是站在函数调用功能的上层角度来说的,而函数调用的底层实现只有按值传递一种,不管数据是从那里 push 到栈上的,栈上的数据都是从函数外传入,且之后对栈上参数的值的修改和传入的那个“源”无关),“按引用”和“按值”指的主要是参数意义(以及函数如何使用参数,这和参数意义是相关的)。例如,如果参数是一个地址,你可以通过这个地址读写它指向的对象即按引用方式,而你对这个地址的修改是无意义的,不会影响到函数以外的任何指针变量之类的东西。所以如果你想在函数里修改一个整数,传递它的地址,即整数的指针。如果修改一个指针,传递指针的地址,即指针的指针,修改一个对象,传递它的地址,。。。不论你想改的是什么(T),传递它的地址(参数类型是 T*),而不是它的值(拷贝),然后在函数里去解析引用(dereference)。

 

    顺着这个话题说下去,说的更精确一些。在 C# 里,假设一个对象 T,一个函数 void foo(T t); 存在下面的代码:

    T t = new T(); //或者 T t = null;

    foo(t);

    如果 T 是引用类型(class),它是按引用传递的,函数可以修改 T 的成员变量,但是不能修改 T 的指向。即函数foo调用后,t 的指向不会发生变化,依然是原来的对象,不能从 null 变为 其他对象,也不会被修改为 null。

    如果 T 是值类型(struct),它是按值传递的,函数对 T 的成员的修改只是针对栈上的临时拷贝,而不会影响外面的 t。例如如下 c# 代码:

 

复制代码
struct StructA{    public int num;    public int x;    public int y;    public StructA(int _n)    {        num = _n;        x = 0;        y = 0;    }    public StructA(int _n, int _x)    {        num = _n;        x = _x;        y = 0;    }}static void foo3(StructA a){    a.num = 300;  }static void foo4(ref StructA a){    //a = new StructA(50, 1000);    a.num = 400;}static void Main(string[] args){                StructA a = new StructA(10);    foo3(a);    Console.WriteLine("a.num = {0}", a.num); //a.num=10    foo4(ref a);    Console.WriteLine("a.x = {0}", a.x);    Console.WriteLine("a.num = {0}", a.num); //a.num=400}
复制代码

 

    由于 foo3 函数中 StructA 是“按值传递”,所以函数内对对象的修改并不能影响到函数之外的那个“源对象”。加了 ref 参数以后,它相当于是引用类型的“按引用传递”。对于引用类型的对象来说,ref 使函数中不仅可以修改对象的成员,还可以修改 a 的指向指向另一个对象,也就是可以修改“指向”和“被指向对象的内容”[1]。 

 

    out 参数的应用场景更加明确,要求函数必须明确的修改一个指针的指向或者值类型的值。而对 ref 来说,对被指向的变量(注意这个变量通常已经是一个对象的指针)的使用是自由的,即你可以不修改而仅仅使用它。相对于 out ,用处是,就是一个对象在传入函数时可能没有赋值过(可能是 null ),在函数里如果发现它是 null 就创建它(要求影响到外部变量),其他情况我们使用或修改它,这时候就应该加 ref 了。

 

    在C#中由于完全 OO 的需要,所以隐藏了指针,而代之以“引用类型”的对象,所以对于一个引用类型的对象 T,在C++里相当于对象T 的指针,在参数上加 ref 在 C++ 里相当于指针的指针,即二级指针 T**。所以如果参数类型加 ref 意味着要修改一个指针变量的指向,这也就意味着你想在函数里对函数以外的那个对象重新赋值,即让它指向其他对象或者 null。如果仅仅修改或读取对象 T 的成员,就无须加 ref,因为引用型对象本身已经是指针了!这是在函数参数前面是否加 ref 的应用场景,对于 C++ 因为必须有内存模型的概念,所以这非常自然,可以毫无歧义的理解清楚。但对于 .net 程序员可能很难搞清楚这里的原因和区别。

 

    希望每个人都能严谨的对待技术,而不是觉得自己非常了不起,听不进任何批评意见。PS:我尽可能去除了本文中的主观评论成分。

0 0
原创粉丝点击