问题描述
C++代码如下(基于Windows7系统使用VS2015进行编译):#include <iostream>
using namespace std;
void func1()
{
int a;
int b;
cout << "&a=" << &a << endl
<< "&b=" << &b << endl << endl;
}
int main()
{
func1();
func1();
func1();
func1();
func1();
return 0;
}
运行结果如下所示:
&a=002AFB48
&b=002AFB44
&a=002AFB44
&b=002AFB48
&a=002AFB44
&b=002AFB48
&a=002AFB44
&b=002AFB48
&a=002AFB44
&b=002AFB48所奇怪的是为什么第一次调用func1() 函数时,变量a在高地址、变量b在低地址,而后面四次调用 func1() 函数时,变量a在低地址、变量b在高地址,这是为什么呢?
具体的原因请参考
@Thomson大大的回答:
函数的局部变量在栈中是如何分布的? - Thomson 的回答 - 知乎<- 帮忙顶起来~
这事情很明了:题主用Visual Studio 2015中的C++编译器(“CL”),以Release模式编译了问题描述中的测试程序。
所以呢?Release模式的配置下默认是会打开优化的。而此例中 main() 对 func1() 的5次调用都会被完全内联到 main() 中,内联后的样子大概是这样的:
int main()
{
// func1(); // 1
{
int a;
int b;
cout << "&a=" << &a << endl
<< "&b=" << &b << endl
<< endl;
}
// func1(); // 2
{
int a;
int b;
cout << "&a=" << &a << endl
<< "&b=" << &b << endl
<< endl;
}
// func1(); // 3
{
int a;
int b;
cout << "&a=" << &a << endl
<< "&b=" << &b << endl
<< endl;
}
// func1(); // 4
{
int a;
int b;
cout << "&a=" << &a << endl
<< "&b=" << &b << endl
<< endl;
}
// func1(); // 5
{
int a;
int b;
cout << "&a=" << &a << endl
<< "&b=" << &b << endl
<< endl;
}
return 0;
}
然后CL编译器决定对这5对a、b局部变量总给只分配2个stack slot给它们,并在作用域不重叠的情况下复用空间。
但是!不知道CL编译器具体为啥有点抽风(但就此测试的结果来说绝对是正确符合C++语义要求的),这5对a、b局部变量的分配,分别是:
_b$1 = -8 ; size = 4
_b$2 = -8 ; size = 4
_b$3 = -8 ; size = 4
_b$4 = -8 ; size = 4
_a$5 = -8 ; size = 4
_a$6 = -4 ; size = 4
_a$7 = -4 ; size = 4
_a$8 = -4 ; size = 4
_a$9 = -4 ; size = 4
_b$10 = -4 ; size = 4
这是用CL 19.x系列在x86上/O2优化级别编译出来的结果,-4、-8分别代表分配到的stack slot相对frame pointer的偏移量,即便最终生成的代码省略了frame pointer这里的语义也还是一样。
对应到上面示意的内联后代码,分别是:
// group 1
_a$5 = -8 ; size = 4
_b$10 = -4 ; size = 4
// group 2
_a$9 = -4 ; size = 4
_b$4 = -8 ; size = 4
// group 3
_a$8 = -4 ; size = 4
_b$3 = -8 ; size = 4
// group 4
_a$7 = -4 ; size = 4
_b$2 = -8 ; size = 4
// group 5
_a$6 = -4 ; size = 4
_b$1 = -8 ; size = 4
所以是的,CL编译器就是选择了给第一对a、b局部变量反着来分配它们的stack slot。这是规范完全允许的实现,是编译器的自由。咱看不到CL编译器源码也就无谓深究它是如何抽风了…
附录:
编译器版本:Microsoft (R) C/C++ Optimizing Compiler Version 19.10.24629 for x86
编译参数:/O2
以下是我以上述配置做实验所看到的 main() 的汇编:
_b$1 = -8 ; size = 4
_b$2 = -8 ; size = 4
_b$3 = -8 ; size = 4
_b$4 = -8 ; size = 4
_a$5 = -8 ; size = 4
_a$6 = -4 ; size = 4
_a$7 = -4 ; size = 4
_a$8 = -4 ; size = 4
_a$9 = -4 ; size = 4
_b$10 = -4 ; size = 4
_main PROC ; COMDAT
sub esp, 8
lea eax, DWORD PTR _a$5[esp+8]
push eax
push OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
push OFFSET std::cout
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
add esp, 4
lea ecx, DWORD PTR _b$10[esp+8]
push ecx
push OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
push eax
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
push eax
call std::endl<char,std::char_traits<char> >
add esp, 8
lea eax, DWORD PTR _a$9[esp+8]
push eax
push OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
push OFFSET std::cout
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
add esp, 4
lea ecx, DWORD PTR _b$4[esp+8]
push ecx
push OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
push eax
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
push eax
call std::endl<char,std::char_traits<char> >
add esp, 8
lea eax, DWORD PTR _a$8[esp+8]
push eax
push OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
push OFFSET std::cout
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
add esp, 4
lea ecx, DWORD PTR _b$3[esp+8]
push ecx
push OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
push eax
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
push eax
call std::endl<char,std::char_traits<char> >
add esp, 8
lea eax, DWORD PTR _a$7[esp+8]
push eax
push OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
push OFFSET std::cout
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
add esp, 4
lea ecx, DWORD PTR _b$2[esp+8]
push ecx
push OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
push eax
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
push eax
call std::endl<char,std::char_traits<char> >
add esp, 8
lea eax, DWORD PTR _a$6[esp+8]
push eax
push OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
push OFFSET std::cout
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
add esp, 4
lea ecx, DWORD PTR _b$1[esp+8]
push ecx
push OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
push eax
call std::operator<<<std::char_traits<char> >
add esp, 8
mov ecx, eax
call std::basic_ostream<char,std::char_traits<char> >::operator<<
push eax
call std::endl<char,std::char_traits<char> >
push eax
call std::endl<char,std::char_traits<char> >
xor eax, eax
add esp, 16 ; 00000010H
ret 0
_main ENDP
留意一下,以 _a$5[esp+8] 为例,这个意思是以esp+8为stack frame base的话,我们要访问的变量位于 _a$5 也就是 -8 的偏移量上,所以结合起来看这里实际的代码是 [esp] ([esp + 8 + _a$5] => [esp + 8 + -8] => [esp])
就这样嗯。