我对指针的深刻理解
在看完了《让你不再害怕指针.pdf》这本29页的书以后,感觉自己对指针的各种使用方法都会了(指针表达式那一块没仔细看,因为平时用不到这么复杂的情况)。但是忽然想起来,以前看MSDN的时候碰到双重指针就不理解,然而这本书却没有细讲,于是又搜了一些网上关于指针的文章看,竟然发现有些内容还是不太懂。于是忽然明白了,即使自己看完《让你不再害怕指针.pdf》也只能说是自己只是会用,不是真的理解了指针。
从天边漂泊着这朵乌云——双重指针的概念和使用出发,琢磨了好一阵,终于发现自己应该算是懂了指针的含义。下面说说我自己的理解,纯属个人理解,而且用不到汇编的知识。
真正理解1(这个说法绝对是我自己的发明):
只要看到出现了一个指针,就要立刻反应过来通常情况下它包括了5个东西,分别是:adr1,内存值1,adr2,内存值2,对象类型。分别一一分析并指明它们的意义和相应的值,这样在以后的使用过程中就应该没问题了。
(adr1)[内存值1]-------->(adr2)[内存值2]{对象类型}
指针 ----指向---->(存储在另一个地址的)对象
注意1:一旦定义一个指针,系统会在0x00010000到0x7FFFFFFF的范围内分配一个地址adr1,用来存放指针自身的这个变量。例如int* p;的时候,可以把p视为int*的一个变量,这个变量的值可以是任意某个int的内存地址。而存放这个变量的地址就在adr1。
注意2:内存值1通常就是adr2,但如果指针没有被初始化,这个值很可能会被赋予一个完全没有意义的值,比如0xCCCCCCCC,这时候adr2实际上不存在,自然也就不能再对内存值2进行操作了。而且0xCCCCCCCC位于系统管理的内存区域内,因此如果使用(*pointer)会立即出现系统访问错误。
总结:指针的打印值是adr2,这个值存储在adr1的内存区域里,但一般没必要关心adr1这个值。 也就是说,一般情况下它的自身地址adr1完全无用,它存储的值adr2却是有用的。
速记:每一个指针包括:两个地址(&p1,p1的打印值),一片区域值(对象值、对象内容)。双重指针包括三个地址(&p2,p2=&p1,p1),一片区域值。
例如: int *p1 = new int(100);
解释:指针自身占据了一块4字节的内存区域,所以它自身也有一个地址adr1,即&p1的打印值。这个内存块里存储了一个[内存值1],即另一个内存地址adr2,即p1的打印值。以adr2为起始的、长度为sizeof(对象类型)的整个内存区域,就是[内存值2]。有了这个对象的起始地址adr2以后,加上*以后,编译器就可以根据对象的类型自动判断这个指针可以正确操作的内存区块的长度,然后就可以对整个对象的内存区块,即[内存值2],进行操作使用了。
真正理解2(抄来的说法,加上我自己的解释):
指针是保存内存地址的变量。如果理解了这个简单的句子,那就理解了指针。
解释:每个一级指针都应该有2块内存区域,一块存储指针本身,另一块内存等待被开辟出来才能使用,它所存储所要指向的任意对象,比如Car类的实例。
以此类推,二级指针应该有3块内存区域,除了存储二级指针本身的那块区域已经被开辟出来,另外两块区域都有待开辟(一级指针等待通过定义来开辟内存,对象区域等待new来开辟内存)。因此二级指针当然不能被直接使用了。看到这里,即使是多级指针的使用也应该不在话下了吧?
// 真正理解3:结合delete语句,加强对指针的理解
应用程序只能管辖自己申请过的那部分内存,如果强行删除将导致应用程序越界崩溃。
指针本身是个局部变量。但关键字new分配的内存不会被自动释放,这样就造成了内存泄漏。(内存泄漏是指程序自身出错,而不是程序退出后系统不回收这些内存。)
其实删除指针以后(删除指针的用途:只是通知OS回收那部分内存而已!),但指针仍然可以使用它所指向的那个地址的内容(所以程序会崩溃),所以最好马上赋值为NULL。
如果你在此CScrollBar对象中分配了任何内存,则应重载CScrollBar析构函数来释放这些内存。
// 真正理解4:用比喻来理解(我自己的解释)
比如 Car* p = new Car();
那么这个p,就相当于这辆汽车的一个牌照(一个汽车可以有很多个牌照,但除了这个牌照,其它都是幽灵牌照附上来的),然后就可以牵着牛鼻子走了。(*p)则代表这辆车本身,这样也能理解(*this)的意义。
p本身是一个32bit的地址,但是可以代表整辆车,并且可以对它进行各种操作例。比如,我可以通过牌照p来使用这辆汽车的任意功能,比如p->start(),也包括销毁这辆车 delete p。销毁以后,车的实体不在了,但牌照还在(变成了一个幽灵牌照!),如果这时候再使用它进行各种操作当然会造成程序错误。当然,如果给这个幽灵牌照重新申请一个实体车,或者附在另一个与牌照类型相匹配的实体车上面,那么这个牌照又可以对它所代表的这辆车进行各种正常操作了。
根据以上理解,尝试对N重指针的理解:就是一辆有N个牌照的车。中间任意一个牌照都可以改变它所指向的下一个牌照或者汽车。但是最终都只有一辆车,并且只能对这辆车进行操作,中间只要有一个幽灵牌照,那么操作就会出现错误。即使不是幽灵牌照,而是出现牌照不匹配实体车的情况(强制转换或者无知造成的!),也一样会出现错误。
// 推论1:指针的使用分为初始化前,和初始化后(一般是指new以后)
未初始化时:当int *p1; 还没有被初始化的时候,只有adr1,而没有adr2的值。但有可能赋一个没有意义的值,比如此时p1=0xCCCCCCCC; 所以最好对指针进行初始化,int* p1=NULL; 如果p1未初始化,就把指针的值传入函数是没有意义的。因为在函数内new以后会给实参指针分配另一个地址,然而却与p1无关。
初始化以后:int* p1= new int(100),再把p1传入函数,函数里重新new,我断定这个p1不会被影响(没做实验)。因为实参被重新new了以后,就失去了对过去的object对象操作的能力。 在指针p1被new以后,把它传入到函数里,只有对旧的object对象进行操作才是有意义的。
// 推论2:强制转换(指针无论指向什么类型,它存储的都是一个32位的地址值,所以容易混淆)
指针难的地方,不是根念的理解,实际应用中,指针的类型转换是不做检查的,指针可以指向指针。类型转换可以任意转,再加上复杂的类,接口继承关系,由于没有任何限制,很容易搞出问题。难的地方主要是指这个。
在指针的强制类型转换:ptr1=(TYPE *)ptr2 中,如果sizeof(ptr2的类型)大于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2所指向的存储区时是安全的。
// 具体使用的情况1:双重指针的使用
如果想要在在C++的函数中分配(新的)内存,那么就只得使用指针的引用传递,或是二级指针。
对二级指针,一个空指针等待函数内部分配内存,并且要跳出函数后依然有效,就要使用二级指针,而且要预先在函数外提供二级指针所指向的一级指针。
原因1:如果传进去的是一级指针,那么肯定是未初始化的(如果已经指向某个对象,则没必要要求在函数内部重新申请内存指向另一个对象),而形参指针本身也是一个复制品,所以跳出函数后,形参没有了,函数所申请的对象内存还在,但是却没有指针能够找到它了。
原因2:同样的原因,形参丢失后,它与函数外部的二级指针指向同一个外部的一级指针,然后在函数内部使用一级指针申请内存对象,这样就不会丢失这个对象了。
// 具体使用的情况2:数组的指针
结构体和数组都是连续存储的,所以可以通过简单的偏移来访问各个元素。
在定义数组和函数的时候,系统会都自动分配一个指向其首地址的指针。
对于数组,其名称就是一个指针变量。比如int a[10]; 就同时定义了int* a=数组的首地址;
定义 a[10][10]的时候,a就是一个二级指针。它的基类型是有10个元素的一维数组,而不是int了。所以a=a+1的时候,就指向第二行数组。
其用法如下:*(*(a+j)+j) 即得到了a[i][j]元素。
例如:
int *p1; // adr1=???(不关心), value1=0xCCCCCCCC, adr2=0xCCCCCCCC, 因此value2值没有意义。因为0xCCCCCCCC是根本不是OS开辟的有效内存区域,所以不能在这个地方分配存储有效使用值。
int *p1=NULL; // adr1=???(不关心),value1=0,adr2=0,因此value2值没有意义。因为0x0是一个根本不能在这个地方分配内存。
int *p1=new int(100); // adr1=???(不关心),value1=adr2=0x12345678(一个可以有效使用的地址,这点与0xCCCCCCCC完全不同), value2=100
int **p1; // adr1=???(不关心),value1=0xCCCCCCCC,adr2=0xCCCCCCCC, 因此value2值没有意义。更关键的是,p1指向一个int*类型的数据,然而这个数据的内存地址还根本没有被分配(0xCCCCCCCC这个地址是不可以被使用和分配对象的),换句话说*p1位于0xCCCCCCCC,而这个位置随机赋予的未初始化值,想要直接用是不行的,所以直接来*p1=new int(200),一定是运行错误(语法正确,所以编译没问题)。一定要另外定义一个一级指针int* p2,并分配对象p2=new int(200),然后使得p1指向p2之后(p1=&p2),二级指针才可以正确操作运行。
int* p1=new int(100); int* p2=p1; // 此时p2的adr1不关心,但它的内存值1等于p1的内存值1,即*p2=*p1,所以p2可以操作p1所指向的那个int值。然而p2=new int(200)之后,p2的内存值1被改变了,于是就不能操作p1所指向的那个int值了。