众所周知, 由c/c++编译的程序占用的内存有四个分区,分别是堆区、栈区(又称堆栈)、静态区和只读区。
其中堆区存放新出炉的数据;栈区存放局部变量、形参等;静态区存放全局变量和静态变量;只读区存放常量(其中包括const修饰的变量)。
而我们下面要讨论的则是局部变量在栈区中的存储。
需要注意,不同编译器下的存储方式是不同的。这里主要用VS2019分析。
先看两段代码:
第一段代码: int main() { int a = 10; int b = 20; int* p = &a; printf("%d", *(p - 3)); return 0; }
第二段代码: int main() { int i = 0; int arr[10] = { 0 }; for (i = 0; i <= 12; i++) { arr[i] = 0; printf("第%02d次打印\n", i); } return 0; }
让我们看看在VS2019中它们的运行结果:
第一段:
第二段:
可以看到,第一段代码对(p - 3)解引用打印出来的是b的值;而第二段代码虽然越界访问了,但竟然陷入了死循环!这是为什么呢?
为了解释上面两个问题,我们首先要了解栈区的存放规则:
我们把栈区分成一个个小空间,当VS2019编译器在栈区存放局部变量的时候,先存放的变量放在上面,后声明的变量依次向下存放。但是,两块空间之间有没有空内存呢?
我们可以通过调试来进行观察:
可以看到,a的地址和b的地址之间差了两个字节的空间:
两个变量之间是每次都空两个字节还是随机空的呢?在VS2019中,两个变量是固定空两个子节的。在其他编译器中则会有不同,后面会对gcc编译器进行举例。
了解了这些,我们就能分析上面两段代码是怎么执行的了:
对于第一段代码,p指向的是a的地址,根据上面的图我们可以直观判断出p - 3指向的就是b的地址,所以对p - 3解引用找到的实际上是b。
对于第二段代码,执行逻辑同上,arr[0]是数组第一个元素的地址,以此类推,arr[9]就是数组最后一个元素的地址,那越界访问的arr[10]呢?是随机的一个地址还是arr[9]后面的一块字节空间?我们通过调试看一下:
可以看到arr[10]确实是arr[9]后面一个字节的空间。数组的存储是根据下标的增加依次向上开辟空间的,那么i和arr[9]之间就差了两个字节的空间,所以...存放arr[12]的地址不就是存放i的地址了嘛!如果编译器敢越界访问操作,等循环到i=12的时候,修改arr[12]的值不就是修改i的值了嘛!嘿!还真是这样!所以当循环到第12次的时候,i就被赋成0了,所以我们观察第2段代码的运行结果会发现,打印完“第11次打印”的字样后接着打印的就是“第0次打印”。
现在我们就可以总结一下下:局部变量在栈区中根据声明顺序从上至下分配空间,在VS2019中两个变量之间空2个字节的空间。
但是,在别的编译器下也是空2个字节吗?下面在Linux下的gcc编译器下测试一下上面的代码:
可以看到,p存放的是a的地址,而p+1存放的则是b的地址,与VS2019中的存放规则完全不同!
另外,我们再看下边一行代码:
通过上面两段代码我们很容易发现:单变量的存储是按声明顺序从低地址到高地址依次存储的!