今天,莫名其妙地“奋奋”。
看着一本C++的时候,突然想到,是不是可以用数组越界来探讨一下,C++的存储方式呢?由于书页上的内容太多。就简单地写写吧。
int main()
{
const int SIZE = 4;
float a[SIZE] = {33.3, 44.4, 55.5, 66.6};
for (int i=0; i<7; i++)
{
cout << "\ta[" << i << "] = " << a[i] << endl;
}
system("pause");
return 0;
}
//这里不报错吧?很犀利。就是要用这个不报错的bug(C++的硬伤)来深入一下这个强大的东西。想知道他是怎么存储的。
有一则例子,应该是很早以前的编译器写的。那个时候C++的确实会像我写那样输出。
int main()
{
float a[] = {22.2, 44.4, 66.6};
float x = 11.1;
cout << "x = " << x << endl;
a[3] = 88.8;
cout << "x = " << x << endl;
system("puase");
return 0;
}
输出是什么呢?
x = 11.1
x = 88.8
此时很惊讶。a[3]的非法使用,本身是不报错的,可是它修改了另一个变量的数据x!
要是在大型程序,几千万行代码中,怎么办?这种把头皮擦破都想不出来。因为它能通过编译。
不过,有经验的人都会主动去查野指针、数组越界。这个是很好的经验,确实能够修复bug,也能够让测试人员顺利地验证通过。
原因就会表明:数组越界、野指针。具体操作和内存的使用并不太清楚。这个也是我为了想知道,并且在知道后兴奋地写博客的原因。
我相信,现在兴奋的时候,会主动地写,以后没了这个激情,见怪不怪就不会写了。为了保存这一刻青涩的激动。并且抛弃上面的老古董。
来看看新的代码。和上面的类似。不过上面的代码在当前很多规范的编译器已经克服掉了。那么就看看它怎么克服的。
上面有一条规律总结:a[3]的内存地址就是x的内存地址。即a[3]就是x。
#include <iostream>
using namespace std;
int main()
{
float x = 11.1;
float a[] = {22.2, 44.4, 66.6};
cout << "x = " << x << endl;
a[7] = 88.8;
cout << "x = " << x << endl;
cout << "a的地址 " << &a << endl;
cout << "x的地址 " << &x << endl;
cout << "float的大小 " << sizeof(float) << endl;
system("pause");
return 0;
}
//这里a[7]就是x!
输出:
x = 11.1
x = 88.8
a的地址 0x22ff50
x的地址 0x22ff6c
float的大小 4
这个时候,应该很激动地来计算&a与&x的公式了: &x = &a + 2*3*sizeof(float) + 1。
这个公式经典了!
也就是说,当前的编译器是怎么在防止数组越界的。在开辟的首地址做始点,然后2倍的数组空间长,这就是数组的内存空间。
那么下一个变量是在哪里呢?很明显,是这个数组内存空间后的第一位。
这个问题清楚了。激动了。那么就来不激动的了:
我的概念里,有静态存储方式,这个要区别static。因为这个是内存角度,或者说类似读计算机组成原理来的概念,被我自己的语言总结成的。
这一点先说明好了。贴下面的体会就好理解了。
1 这里揭示了一个很重要的信息:C++是如何保存数值的。从这里测试例子来看很明显,可以很直观地解释:固定的变量是可以确定空间大小的,包括各种内置类型、类类型等的对象,他们按作用域被封装在一块内存区域,这个内存区域中,各个变量的空间是连续挨着的,这里通过数组可以越界来测试这种存储方式。这里的变量都是静态存储。
2 这个测试要注意:只能测试静态存储方式。因为这种方式的类型是确定的。容易把握后边的数据怎么翻译的问题。如果方式不确定,不能确定翻译需要的空间定长,或者不匹配,都会读出不同结果的值。这个值对自己的测试目的和把握来说,是不可控的。
3 通过数组可以越界这一特点,来测试C++的内存管理。可以确定静态存储的有两种变量:局部变量和全局变量。static和auto。static是另外一片连续的存储空间,按声明顺序来分配空间。这篇区域叫静态存储区。这个静态和存储方式的静态在这里是必须要区别的(不做解释,理解提升了就懂了我的意思了,这里强调是方式,是跟物理内存有关的实现,而不是上层的概念)。另外关于动态的,是用堆来实现。程序中无法在编译阶段确定大小的,需要等待运行时数据,在运行过程中开辟空间。而普通的能在编译阶段确定的也是按作用域来分段,按声明顺序来定义的静态存储方式。
4 经测试,我发现当前的做法是数据以2倍+1的空间分界,下一个变量以此界开始分配。
//这一段是我读的过程中写的笔记,虽然总结得不太清楚,也不大好理解。但是确实编译器是这样子的。要梳理总结比较麻烦,就贴这里吧。
至于那个所谓的堆,其实是数据结构里,我记得那段malloc的算法代码。严蔚敏那本书很枯涩,但是治学就是这么严谨了。对她来说,这已经她尽了很大努力把概念通俗了。
我说的堆,其实也就是那段代码的实现方式(毕竟我弄不出C++编译器的真正源代码)。