我相信c++的初学者经常会对指针引用感到困惑。我想从底层的内存分配的角度,帮助大家理解我们的每一行代码都对内存做了什么事情,帮助大家更好的理解c++语言。
声明:我下面demo的运行环境是在linux 64位系统下的,所以一个指针所占用的空间是8个字节。如果是32位的操作系统,一个指针占用四个字节。
首先让我们看一段简单的main函数的代码(每行代码后面跟的注释是cout的输出的内容)
int main()
{
int i = 1;
int* iPonit = &i;
cout << "i = " << i << endl;//i = 1
cout << "iPonit = " << iPonit << endl;//iPonit = 0x7fff33ee981c
cout << "&i = " << &i << endl;//&i = 0x7fff33ee981c
cout << "&iPonit = " << &iPonit << endl;//iPonit = 0x7fff33ee981c
cout << "sizeof(i) = " << sizeof(i) << endl;//sizeof(i) = 4
cout << "sizeof(iPonit) = " << sizeof(iPonit) << endl; //sizeof(iPonit) = 8
cout << "sizeof(&i) = " << sizeof(&i) << endl;//sizeof(&i) = 8
}
从c++内存分配的角度来理解以下这段简单的代码,看看我们这段代码在内存中具体做了什么事情。
int i = 1;
这句代码,我们先在机器的栈区申请了四个字节的内存用来存储一个int类型的数据。(在函数内部申请的内存都是栈内存)。ok,我们申请了这么一块内存,内存中的数据都是01010101 01011111 00000000 00000000这种类型的二进制数据,(申请的内存内的数据是随机的,因为我们尚未进行初始化),我们如何访问内存区域的数据呢,c++给了我们一个机制,叫做别名(你也可以理解成索引),代码中的i 其实就是一个别名,用来代表这块内存块,而int 则是用来修饰i这个别名,表示i存储的是一个四个字节的内存数据。ok,这样我们可以更加方便的访问内存中的变更量。我们执行i = 10的赋值操作。c++通过别名帮助我们将10这个数字转化成二进制的数据,存入我们声请的内存区域。所以我们内存中的值实际编程这样了00000000 00000000 00000000 00000010。
int* iPonit = &i;这句代码又在内存中做了什么事情呢?
我们在栈内存中又申请一块8个字节的内存。我们给这个内存取了一个别名叫做iPonit。int*修饰这个别名,标识iPonit存放的是一个8字节的指针。什么是指针呢?指针就是一个内存中地址的值。也就是说这个8字节的内存区域存放的是内存中另外一个内存区域的地址。所以所有类型的指针都是占用8个字节的内存。我们得到了另外一块内存区域的地址,我们知道这块区域存的是int类型的数据,那么我们可以通过指针访问另外一块内存区域的地址了,这是不是很棒?我们申请了iPonit这个指针,但是如何给iPonit指针赋值呢?
c++提供了一个取地址的符号。我们知道一块内存地址的别名,那么我们可以通过&这个取地址符号得到这块内存的真正内存地址,然后赋值给iPonit。int* iPonit = &i中我们取得了i的地址,然后赋值给iPoint。那么我们如何取得iPonit的地址呢?同理,&iPonit可以取得iPonit的地址。
后面的cout是帮助大家理解,就不深入解释了。
int main()
{
int i = 1;
int i2 = 2;
int* iPonit = &i;
iPonit = &i2; //可以编译通过
iPonit = 0; //可以编译通过
&i2 = 0; //不可以编译通过
}
好现在我们再来理解以下,为什么iPonit = 0可以编译通过吧。&i2 = 0却不能编译通过
iPonit是我们申请的一个指针,iPonit = 0这个操作,我们将这块指针内存的数值赋值为0,当然是可以的。
但是看看&i2 = 0吧。他试图做一件可怕的事情,那就是想改变i2在内存中的实际存放的位置,c++是不提供直接修改内存位置的。就类似于,你在上海建了一栋别墅,但是你又试图将整栋别墅从 上海移动到北京,这个当然是不被允许的。c++的内存管理也是一样,他能让你申请和释放内存,但是不能让你改变内存的位置。
我相信,当能理解c++的每一个语句在内存中都做了什么的时候,对于加了*p, **p。 等等的语法也就想通了。