时间:2014.04.19
地点:基地
————————————————————————————
一、谈谈指针的危险
指针会用则灵活巧妙,不会用则是大麻烦。一句话:指针只是一个内存地址,你能以你可以想象得到的任何手段借助这个地址修改内存里存储的内容。很灵活吧,但也很可怕。
————————————————————————————
二、思考指针的模型
1.指针的数学模型
通常来说我们简单的只是把指针看做地址,表示内存位置的数字,这是一种数学模型。
2.指针的空间模型
指针的空间模型即常见的箭头模型,它的意义在于告诉程序”看那个地方“。
当通过*运算符对指针解引用时,实际上是让程序在内存中更深入一步,从地址的角度上看指针,解引用就是跳到指针表示的地址对应的内存中,取得里面的内容。而当用&运算符取一个地址时,从地址角度看,程序是获得对应内存位置的数值,并保存为指针的形式。
3.指针的类型转换
指针只是内存地址,因此指针的类型比较脆弱,指向各类类型的指针在大小上是一样的,编译器也允许通过C风格的类型转换将任意指针类型转换为其它任意的指针类型。比如:
Document* documentPtr=getDocument();
char* myCharPtr=(char*)documentPtr;
这是C风格的指针类型转换,但余地太大,使得容易发生错误,具有风险,试想下,我们对指向不同类型的指针进行转换有什么意义呢?
但有时确实还有意义,比如在继承关系当中。这时可用静态类型转换。但在继承体系中完成指针类型的转换还有更安全的方式,那就是动态类型转换。
Document* documentPtr=getDocument();
char* myCharPtr=static_cast<char*>(documentPtr);
4.常量的指针,关于const到底保护谁
关键字const到底保护谁,取决于所采用的语法。如果const放在类型前面,则说明保护的是指向的值,在为数组的情况下,数组中的每个元素都是const的。看下面例子:
这样的代码函数体中的第一行在编译时是不能通过的,因为指针所指向类型的值受const保护。但第二行可以,因为指针本身在这里不受const保护。如果你想保护指针不受修改,那么const关键字应该紧放在指针变量名前面,比如这样的代码:void test(const int*inProtectedInt,int* anotherPtr) { *inProtectedInt=7; inProtectedInt=anotherPtr; }
因为指针所指向的类型受const保护,指针本身也受const保护,所以他们两个都是不能被修改的。编译不能通过。但在实际中,很少有必要保护指针,void test(const int* const inProtectedInt,int* anotherPtr) { *inProtectedInt=7; inProtectedInt=anotherPtr; }
5.数组和指针
数组名很像指针,但是不是指针。
我们通过指针不仅可以指向基于堆的数组,也可以通过指针语法来访问基于堆栈的数组元素。数组的地址就是第0个元素的地址。编译器会知道当你通过变量名引用整个数组时,实际上引用的是第0个元素的地址。下面代码是在堆栈上创建了一个数组,但我们可通过一个指针来访问这个数组。
int main() { int my_array[10]; int* my_ptr=my_array; my_ptr[4]=5; }
a.向函数传递堆栈数组
向函数传递数组的时候,通过指针引用基于堆栈的数组能力非常的有用,嗯,是有用啊,因为数组往往太大,这样我们就不需要拷贝。但引用堆栈上的数组时记得要显式地传入数组的大小,因为指针指针传入的指针不不含任何与数组大小相关的信息,指针只是告诉函数,从内存的这个点切入,接下来的都可能是你想要操作的内存块,至于到哪里打止,这无从考之,所以你还需要告诉函数,这个块的大小。void func(int* array,size_t size) { for(size_t i=0;i<size;I++) { the_array[i]*=2; } }
b.向函数传入堆数组
在上面我们已经看到了,向函数传入堆栈数组时,调用者可以传入数组变量就好,因为编译器会自动把这个数组变量当做指向数组的指针处理,也可以显式地传入第一个元素的地址,前面也已经说了,因为数组是占据内存中的一个连续块,函数只要找到这块内存从哪里开始的一个切入点,然后我们还告诉函数,这个数组的大小,这样,我们就可以对数组进行操作了。
而对于堆上的数组也类似,而且基于堆的数组在创建时往往指针已经存在,只有把这个指针的值传入就可以了。看下面代码:
从这我们应该明确两点:1是函数对数组进行处理是要知道数组的一个切入点,即数组是从内存中的哪个位置开始的,而以什么方式告诉函数无所谓,比如数组名可以,数组第一个元素的地址也可以;2是还需要显式告诉函数数组在哪个地方结束,我们一般是选择告诉函数数组的大小。size_t arrSize=4; int* heapArray=new int[arrSize]; heapArray[0]=0; heapArray[1]=1; .... func(heapArray,arrSize); delete[] heapArray; heapArray=nullptr; int stactArray[]={5,6,7,11}; arrSize=sizeof(stackArray)/sizeof(stackArray[0]); func(stackArray,arrSize); func(&stackArray[0],arrSize);
需要说明的是,数组参数传递的语义和指针参数传递的语义很相似,当数组名被传递给一个函数时,编译器将其视为一个指针。即使参数是一个数组,也是一样。比如:
在这里,函数中的参数是一个数组形式给出,但我们传递进去数组名后其还是表现为指针形式,指向该数组的起始地址。void fun(int array[],size_t size) { ...... }
6.并非所有的指针都是数组
然后指针和数组尽管在很多方面都有相似,共享很多属性,但还是有区别的,它们是不一样的。指针本身的意义只是标识一块内存地址,可以指向一个随机内存,一个对象或数组,可以使用指针的数组语法,但这样并不总是正确,因为指针并不是数组,比如:
指针ptr是一个有效的指针,但不是一个数组,但我们也还是可以用指针的数组语法去访问这个指针指向的值,比如 ptr[0] ptr[1]等等,可这样做有风险,因为在这里并不是指向一个数组,所以ptr[0] ptr[1]的 你的企图是什么呢。ptr[0]还好,但ptr[1]是未定义的,里面有可能放得是很危险的物品对不对。int* ptr=new int;
所以总的一句来讲:通过指针可以自动引用数组,但并不是所有指针都是数组。
————————————————————————————
7.指针的运算
C++编译器通过声明的指针类型允许执行指针运算,比如声明一个指向int的指针,然后将指针变量进行加1操作,那么这个指针在内存中表现为向前移动一个int的大小,即指向下一个元素。这样的运算对于数组操作是很有用的,因为数组在内存中包含了同构的数据序列。比如在堆中声明如下数组:
上面两种方式是等价的,都是将指针array向前移动两个int大小的位置,前面一句和后面一句实则都是同过解引用设置对应内存中的值。int* array=new int[8]; array[2]=33; *(array+2)=33;
指针运算的一个有用的应用是减法运算,将一个指针减去另一个同类型的指针得到的是两个指针之间类型的元素的个数,而不是两个指针之间字节数的绝对值。