1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 1#include "stdafx.h"
2#include<iostream>
3#include<windows.h>
4using namespace std;
5
6class T
7{
8protected:
9 int t;
10public:
11 T(int r=0):t(r){}
12 void showNum(){cout<<t<<endl;}
13};
14
15class T1:public T
16{
17private:
18 int x;
19public:
20 T1(int r):x(r),T(r){}
21 void show(){cout<<"x="<<x<<endl;}
22};
23
24class T2:public T
25{
26private:
27 int x;
28public:
29 T2(int r):x(r*r),T(r){}
30 void show(){cout<<"x="<<x<<endl;}
31};
32
33void main()
34{
35 T* p[10];
36 for(int i=0;i<5;i++)
37 {
38 if(i%2==0)
39 {
40 T1 r(2);
41 p[i]=&r;
42 cout<<&r<<endl;
43 }
44 else
45 {
46 T2 r(3);
47 p[i]=&r;
48 cout<<&r<<endl;
49 }
50 }
51 for(int i=5;i<10;i++)
52 {
53 if(i%2==0)
54 {
55 T1 r(4);
56 p[i]=&r;
57 cout<<&r<<endl;
58 }
59 else
60 {
61 T2 r(5);
62 p[i]=&r;
63 cout<<&r<<endl;
64 }
65 }
66 for(int i=0;i<10;i++)
67 {
68 p[i]->showNum();
69 }
70 system("pause");
71}
72
Debug版本:
当0 <= i < 5时,输出5个地址,是一种交替状输出,分别是001FF750和001FF740。
当5 <= i < 10时,又输出5个地址,也是交替状输出,分别是001FF714和001FF724。
最后循环调用p[i]->showNum()这个方法10次,输出的结果是正确的,似乎那些栈中的局部对象未失效。
Release版本:
当0 <= i < 5时,输出5个地址,是一种交替状输出,分别是001CFB00和001CFB08。
当5 <= i < 10时,又输出5个地址,也是交替状输出,奇怪的是地址的值也是001CFB00和001CFB08。
最后循环调用p[i]->showNum()这个方法10次,输出的结果全是5和4,似乎前5次输出失效了,后5次没有失效。
1、Debug版本输出现象解析
先来说说Debug版本的输出,前5次输出,交替输出,后5次输出,交替输出,但是,前5次和后5次的地址是不一样的。
我们来看看反汇编:
1
2
3
4
5
6
7
8
9 1T1 r(2);
201363A0D push 2
301363A0F lea ecx,[r]
401363A12 call T1::T1 (1361172h)
5 p[i]=&r;
601363A17 mov eax,dword ptr [i]
701363A1A lea ecx,[r]
801363A1D mov dword ptr p[eax*4],ecx
9
关键是看对象r的地址是如何分配的,但是,反汇编中似乎没有明显的信息,只有一句:lea ecx,[r],这条语句是什么意思呢?将对象r的地址取到通用寄存器ecx中。
我们知道,程序在编译链接的时候,变量相对于栈顶的位置就确定了,称为相对地址确定。所以,此时程序在运行了,根据所在环境,变量的绝对地址也就确定了。
通过lea指令取得对象地址,调用对象的构造函数来进行构造,即语句call T1::T1 (1361172h). 构造完之后,对象所在地址的值才被正确填充。
好了,我们知道了这些局部变量相对于栈的相对地址,其实在编译链接的时候就确定了,那么,这个策略是什么样的呢?就是说,编译器是如何来决定这些局部变量的地址的呢?
一般来说,对于不同的变量,编译器都会分配不同的地址,一般是按照顺序分配的。但是,对于那些局部变量,而且非常明显的生命周期已经结束了,同一个地址,也会分配给不同的变量。
举个例子,地址0X00001110,被编译器用来存放变量A,同时也可能被编译器用来存放变量B,如果A和B的大小相等,并且肯定不会同时存在。
编译器在判断一个地址是否能够被多个变量同时使用的时候,这个判断策略取决于编译器本身,不同的编译器判断策略不同。
微软的编译器,就是根据代码的自身逻辑来判断的。当编译器检测到以下代码的时候:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1for(int i=0;i<5;i++)
2 {
3 if(i%2==0)
4 {
5 T1 r(2);
6 p[i]=&r;
7 cout<<&r<<endl;
8 }
9 else
10 {
11 T2 r(3);
12 p[i]=&r;
13 cout<<&r<<endl;
14 }
15 }
16
微软的编译器认为,只需要分配两个地址则可,分别用来保存两个对象,循环执行的话,因为前一次生成对象的生命周期已经结束,直接使用原来的地址则可。
因此,我们在用VS编译这段程序时,就出现了地址交替输出的情况。
当微软的编译器接着又看到以下代码的时候,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1for(int i=5;i<10;i++)
2 {
3 if(i%2==0)
4 {
5 T1 r(4);
6 p[i]=&r;
7 cout<<&r<<endl;
8 }
9 else
10 {
11 T2 r(5);
12 p[i]=&r;
13 cout<<&r<<endl;
14 }
15 }
16
微软的编译器认为,需要再分配两个地址,分别用来保存这两个新的对象,
于是,我们再次看到了地址交替输出的情况,只是这一次交替输出的地址与前一次交替输出的地址不同。
延伸1:稍微修改代码再试试
我们已经能够理解VS下Debug版本为什么会输出这样的结果了,再延伸一下,我们把代码进行修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1修改前的代码:
2
3 for(int i=0;i<5;i++)
4 {
5 if(i%2==0)
6 {
7 T1 r(2);
8 p[i]=&r;
9 cout<<&r<<endl;
10 }
11 else
12 {
13 T2 r(3);
14 p[i]=&r;
15 cout<<&r<<endl;
16 }
17 }
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 1修改后的代码为:
2 if (0 == i)
3 {
4 T1 r(2);
5 p[i]=&r;
6 cout << &r << endl;
7 }
8 else if (1 == i)
9 {
10 T2 r(3);
11 p[i]=&r;
12 cout << &r << endl;
13 }
14 else if (2 == i)
15 {
16 T1 r(2);
17 p[i]=&r;
18 cout << &r << endl;
19 }
20 else if (3 == i)
21 {
22 T2 r(3);
23 p[i]=&r;
24 cout << &r << endl;
25 }
26 else if (4 == i)
27 {
28 T1 r(2);
29 p[i]=&r;
30 cout << &r << endl;
31 }
32)
33
代码修改之后,功能完全一样,那么前五次循环的输出会有什么不同吗?
也许你猜到了,修改完代码之后,前5次地址输出,是5个不同的地址,按规律递增或者递减。
很明显,代码的改动,编译器的认知也改变了,分配了5个地址来给这5个对象使用。
延伸2:GCC编译器是如何编译这段代码的呢?
我们再延伸一下,不同的编译器,对代码的编译是不同的,GCC编译器是如何编译这段代码的呢?默认编译之后,运行结果如下:
不用我说,大家也知道了,GCC编译器检测到这些变量生命周期结束了,尽管有十次循环,尽管代码有改动,但是GCC仍然只有分配一个地址供这些变量使用。
理由很简单,变量的生命周期结束了,它的地址自然就可以给其他变量用了,更何况这样变量的大小还是一样的呢!
2、VS下Release版本输出现象解析:
不再延伸,回到正题,VS下Release版本的表现为什么和Debug版本不一样呢?
同样,我们来看原始代码的反汇编:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 1if(i%2==0)
2 {
3 T1 r(2);
4 p[i]=&r;
5 cout<<&r<<endl;
600C11020 mov ecx,dword ptr [__imp_std::endl (0C12044h)]
700C11026 push ecx
800C11027 mov ecx,dword ptr [__imp_std::cout (0C12048h)]
900C1102D test bl,1
1000C11030 jne main+42h (0C11042h)
1100C11032 lea eax,[esp+14h]
1200C11036 mov dword ptr [esp+14h],ebp
1300C1103A mov dword ptr [esp+18h],ebp
1400C1103E mov edx,eax
15 }
16 else
1700C11040 jmp main+50h (0C11050h)
18 {
19 T2 r(3);
20 p[i]=&r;
2100C11042 lea eax,[esp+1Ch]
2200C11046 mov dword ptr [esp+1Ch],edi
2300C1104A mov dword ptr [esp+20h],esi
24 cout<<&r<<endl;
25 }
26
Release版本做了进一步的优化,esp内的值在本程序运行的过程中未曾改变,因此,尽管有十次循环,也只分配了两个对象的空间,即两个地址。
最后,我们看到,前5次循环和后5次循环的交替输出的地址是一样的。
3、再提一点:最后的十次输出现象解析:
for(int i=0;i<10;i++)
{
p[i]->showNum();
}
其实是没有意义的,因为这10个指针指向的对象的生命周期早就结束了。
那么为什么还能输出正确的值呢?因为,这些对象的生命周期虽然结束了,但是这些对象的内存没有遭到破坏,仍还存在,并且数据未被改写。
如果此程序后续还增加代码,这些地址的内容是否会被其他对象占用都是不可知的,所以,请不要使用生命周期已经结束了的对象。
4、总结:
给大家建议,C++语言的对象生命周期的概念很重要,要重视,另外,使用指针要注意空指针的问题。
有时候,可以直接使用对象的方式,就不要使用太多指针,都是坑!