看此文,务必须要先了解本文讨论的背景,很少说,给出连接:html
探讨C++ 变量生命周期、栈分配方式、类内存布局、Debug和Release程序的区别(一)函数
本文会以此问题做为讨论的实例,来具体讨论如下四个问题:布局
(1) C++变量生命周期优化
(2) C++变量在栈中分配方式spa
(3) C++类的内存布局指针
(4) Debug和Release程序的区别code
一、Debug版本输出现象解析htm
先来讲说Debug版本的输出,前5次输出,交替输出,后5次输出,交替输出,可是,前5次和后5次的地址是不同的。对象
咱们来看看反汇编:blog
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仍然只有分配一个地址供这些变量使用。
理由很简单,变量的生命周期结束了,它的地址天然就能够给其余变量用了,更况且这样变量的大小仍是同样的呢!
二、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次循环的交替输出的地址是同样的。
三、再提一点:最后的十次输出现象解析:
for(int i=0;i<10;i++)
{
p[i]->showNum();
}
实际上是没有意义的,由于这10个指针指向的对象的生命周期早就结束了。
那么为何还能输出正确的值呢?由于,这些对象的生命周期虽然结束了,可是这些对象的内存没有遭到破坏,仍还存在,而且数据未被改写。
若是此程序后续还增长代码,这些地址的内容是否会被其余对象占用都是不可知的,因此,请不要使用生命周期已经结束了的对象。
四、总结:
给你们建议,C++语言的对象生命周期的概念很重要,要重视,另外,使用指针要注意空指针的问题。
有时候,能够直接使用对象的方式,就不要使用太多指针,都是坑!
后记:
忽然以为本身好无聊,之后仍是少分析这些问题,多作些实事!不过,偶尔分析一下,仍是能够的。