• 探讨C++ 变量生命周期、栈分配方式、类内存布局、Debug和Release程序的区别2


    探讨C++ 变量生命周期、栈分配方式、类内存布局、Debug和Release程序的区别(二)

    看此文,务必需要先了解本文讨论的背景,不多说,给出链接:

     

    探讨C++ 变量生命周期、栈分配方式、类内存布局、Debug和Release程序的区别(一)

     

     

    本文会以此问题作为讨论的实例,来具体讨论以下四个问题:

     

    (1)       C++变量生命周期

     

    (2)       C++变量在栈中分配方式

     

    (3)       C++类的内存布局

     

    (4)       Debug和Release程序的区别

     

     

    1、Debug版本输出现象解析

     

    先来说说Debug版本的输出,前5次输出,交替输出,后5次输出,交替输出,但是,前5次和后5次的地址是不一样的。

     

    我们来看看反汇编:

     

    复制代码
                  T1 r(2);
    01363A0D  push        2   
    01363A0F  lea         ecx,[r]
    01363A12  call        T1::T1 (1361172h)
                  p[i]=&r;
    01363A17  mov         eax,dword ptr [i]
    01363A1A  lea         ecx,[r]
    01363A1D  mov         dword ptr p[eax*4],ecx
    复制代码

     

     

    关键是看对象r的地址是如何分配的,但是,反汇编中似乎没有明显的信息,只有一句:lea  ecx,[r],这条语句是什么意思呢?将对象r的地址取到通用寄存器ecx中。

     

    我们知道,程序在编译链接的时候,变量相对于栈顶的位置就确定了,称为相对地址确定。所以,此时程序在运行了,根据所在环境,变量的绝对地址也就确定了。

     

    通过lea指令取得对象地址,调用对象的构造函数来进行构造,即语句call  T1::T1 (1361172h). 构造完之后,对象所在地址的值才被正确填充。

     

     

    好了,我们知道了这些局部变量相对于栈的相对地址,其实在编译链接的时候就确定了,那么,这个策略是什么样的呢?就是说,编译器是如何来决定这些局部变量的地址的呢?

     

    一般来说,对于不同的变量,编译器都会分配不同的地址,一般是按照顺序分配的。但是,对于那些局部变量,而且非常明显的生命周期已经结束了,同一个地址,也会分配给不同的变量。

     

    举个例子,地址0X00001110,被编译器用来存放变量A,同时也可能被编译器用来存放变量B,如果A和B的大小相等,并且肯定不会同时存在。

     

     

    编译器在判断一个地址是否能够被多个变量同时使用的时候,这个判断策略取决于编译器本身,不同的编译器判断策略不同。

     

    微软的编译器,就是根据代码的自身逻辑来判断的。当编译器检测到以下代码的时候:

     

    复制代码
      for(int i=0;i<5;i++)
        {
            if(i%2==0)
            {
                T1 r(2);
                p[i]=&r;
                cout<<&r<<endl;
            }
            else
            {
                T2 r(3);
                p[i]=&r;
                cout<<&r<<endl;
            }
        }
    复制代码

     

    微软的编译器认为,只需要分配两个地址则可,分别用来保存两个对象,循环执行的话,因为前一次生成对象的生命周期已经结束,直接使用原来的地址则可。

     

    因此,我们在用VS编译这段程序时,就出现了地址交替输出的情况。

     

     

     

    当微软的编译器接着又看到以下代码的时候,

     

    复制代码
     for(int i=5;i<10;i++)
        {
            if(i%2==0)
            {
                T1 r(4);
                p[i]=&r;
                cout<<&r<<endl;
            }
            else
            {
                T2 r(5);
                p[i]=&r;
                cout<<&r<<endl;
            }
        }
    复制代码

     

    微软的编译器认为,需要再分配两个地址,分别用来保存这两个新的对象,

     

    于是,我们再次看到了地址交替输出的情况,只是这一次交替输出的地址与前一次交替输出的地址不同。

     

     

     

    延伸1:稍微修改代码再试试

     

    我们已经能够理解VS下Debug版本为什么会输出这样的结果了,再延伸一下,我们把代码进行修改:

     

    复制代码
      修改前的代码:
    
        for(int i=0;i<5;i++)
        {
            if(i%2==0)
            {
                T1 r(2);
                p[i]=&r;
                cout<<&r<<endl;
            }
            else
            {
                T2 r(3);
                p[i]=&r;
                cout<<&r<<endl;
            }
        }
    复制代码

     

    复制代码
    修改后的代码为:
      if (0 == i)
          {
                T1 r(2);
                p[i]=&r;
                cout << &r << endl;
            }
            else if (1 == i)
            {
                T2 r(3);
                p[i]=&r;
                cout << &r << endl;
            }
            else if (2 == i)
            {
                T1 r(2);
                p[i]=&r;
                cout << &r << endl;
            }
            else if (3 == i)
            {
                T2 r(3);
                p[i]=&r;
                cout << &r << endl;
            }
            else if (4 == i)
            {
                T1 r(2);
                p[i]=&r;
                cout << &r << endl;
            }
    )
    复制代码

     

    代码修改之后,功能完全一样,那么前五次循环的输出会有什么不同吗?

     

    也许你猜到了,修改完代码之后,前5次地址输出,是5个不同的地址,按规律递增或者递减。

     

    很明显,代码的改动,编译器的认知也改变了,分配了5个地址来给这5个对象使用。

     

     

     

    延伸2:GCC编译器是如何编译这段代码的呢?

     

    我们再延伸一下,不同的编译器,对代码的编译是不同的,GCC编译器是如何编译这段代码的呢?默认编译之后,运行结果如下:

     

     

     

    不用我说,大家也知道了,GCC编译器检测到这些变量生命周期结束了,尽管有十次循环,尽管代码有改动,但是GCC仍然只有分配一个地址供这些变量使用。

     

    理由很简单,变量的生命周期结束了,它的地址自然就可以给其他变量用了,更何况这样变量的大小还是一样的呢!

     

     

     2、VS下Release版本输出现象解析:

     

    不再延伸,回到正题,VS下Release版本的表现为什么和Debug版本不一样呢?

     

    同样,我们来看原始代码的反汇编:

     

    复制代码
            if(i%2==0)
             {
                  T1 r(2);
                  p[i]=&r;
                  cout<<&r<<endl;
    00C11020  mov         ecx,dword ptr [__imp_std::endl (0C12044h)]
    00C11026  push        ecx 
    00C11027  mov         ecx,dword ptr [__imp_std::cout (0C12048h)]
    00C1102D  test        bl,1
    00C11030  jne         main+42h (0C11042h)
    00C11032  lea         eax,[esp+14h]
    00C11036  mov         dword ptr [esp+14h],ebp
    00C1103A  mov         dword ptr [esp+18h],ebp
    00C1103E  mov         edx,eax
             }
             else
    00C11040  jmp         main+50h (0C11050h)
             {
                  T2 r(3);
                  p[i]=&r;
    00C11042  lea         eax,[esp+1Ch]
    00C11046  mov         dword ptr [esp+1Ch],edi
    00C1104A  mov         dword ptr [esp+20h],esi
                  cout<<&r<<endl;
             }
    复制代码

     

    Release版本做了进一步的优化,esp内的值在本程序运行的过程中未曾改变,因此,尽管有十次循环,也只分配了两个对象的空间,即两个地址。

     

    最后,我们看到,前5次循环和后5次循环的交替输出的地址是一样的。

     

     

    3、再提一点:最后的十次输出现象解析:

     

        for(int i=0;i<10;i++)

     

        {

     

            p[i]->showNum();

     

        }

     

    其实是没有意义的,因为这10个指针指向的对象的生命周期早就结束了。

     

    那么为什么还能输出正确的值呢?因为,这些对象的生命周期虽然结束了,但是这些对象的内存没有遭到破坏,仍还存在,并且数据未被改写。

     

    如果此程序后续还增加代码,这些地址的内容是否会被其他对象占用都是不可知的,所以,请不要使用生命周期已经结束了的对象。

     

     

    4、总结:

     

    给大家建议,C++语言的对象生命周期的概念很重要,要重视,另外,使用指针要注意空指针的问题。

     

    有时候,可以直接使用对象的方式,就不要使用太多指针,都是坑!

     

     

    后记:

     

    突然觉得自己好无聊,以后还是少分析这些问题,多做些实事!不过,偶尔分析一下,还是可以的。

     

     

     

     

     

    分类: 编程语言
    标签: C++

     
     
  • 相关阅读:
    C#调用WebService
    在asp.net中Bind和Eval的区别详解
    详细说明WebService特性
    Remoting技术简介
    Web Service是如何工作的
    C#面试题
    创建一个简单的Web Service
    innerHTML属性导致未知的运行时错误ie bug
    一些想法:关于备份
    数据库考试中常见题分析:关系代数中的除法运算
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3149136.html
Copyright © 2020-2023  润新知