说一说指针在使用过程中容易混淆不清、出问题的点。
1. 引入
若要说清楚指针,就要先说说内存。内存,每一个字节都有着自己的地址,早年间有个词常说,那就是32位机器,和64位机器(现在32位的机器越来越少),其中的32位和64位,指的便是内存寻址的位数,直白点说就是机器的系统用来存储内存地址的位数。32位,最多能表示2^32个字节,也就是4G个字节,所以在32位机器上运行的程序最多能使用的内存大小就是4G字节。但随着技术发展,4G字节的内存已渐渐不能满足生产生活需求,于是,64位机器应运而生,最多能表示16E个字节,这是一个相当大的数字,虽然目前64位的机器已能满足需求,但也说不定哪天,128位的机器就展露了头脚。
而指针,存储的就是这个地址。
定义一个char类型的变量,它存储的是字符,长度是1个字节。定义一个int类型的变量,它存储的整数,抛开不同类型机器因素,这里认为它的长度是4个字节。定义一个指针类型的变量,在32位的机器上,它存储的就是一个32位的地址,长度为4个字节(1byte=8bits),在64位机器上为8个字节。这里还要说明一点,不管一个变量在内存中占用几个字节,占用4个字节的int也好,占用8个字节的double也罢,它的地址都是低位的第一个地址。比如,一个int占用的4个字节为,0x0000_0001,0x0000_0002,0x0000_0003,0x0000_0004,那么它的地址就是低位的0x0000_0001,double等其他类型,同理。
2. 指针的类型
上面有提到,只要是指针类型的变量,那么它存储的值便是一个内存地址,既然存储的都是地址,那为什么还要有不同的类型呢,比如int型的指针int*
、char型的指针char*
?主要目的有两个,其一,是用指针类型来限定指针如何解释它所指向的内存,其二,便是限定指针的移动。
先说指针如何解释它所指向的内存。假定32位机器的大顶端机器,
1 | 内存地址为0x0000_0001的字节,存储的值是0x41 |
简单说就是4个连续的字节,存储的都是0x41。上面有提,不管什么类型的指针,存储的值都是这个变量的低位字节的地址,现定义一个int类型的指针int* pi
令它指向0x0000_0001,再定义一个char* pc
,也令它指向0x0000_0001,也就是定义两个不同类型的指针,但是让它们都指向同一个地址。当把它们的值打出来的时候,你会发现pi指向的值为0x4141_4141,pc指向的值为’A’,也就是0x41。指向同样的地址,值却不同,这就是指针类型对内存解释的限定。
再说如何限定指针的移动。就像我们知道的,指针加1就是向后移动,指向下一个值,指针减1就是向前移动,指向前一个值,那么问题来了,每次移动要移动的长度是多少呢,即,每次要移动多少字节呢?指针的类型便会限定指针移动的字节数量,还是上面那个例子,pi加1之后,它会向高位内存移动int的长度,4个字节,随后指向0x0000_0005,而pc加1后,它只会移动1个字节,指向0x0000_0002,因为char的长度为1个字节。关于此,也可以将指针的类型,理解为它的跳跃能力,pi的跳跃能力是4个字节,而pc的跳跃能力是1个字节。
好了,关于指针的类型就说这么多,因为不想写代码,所以假定了一个很直白、也很理想的例子,仅供参考。
3. 数组和指针
数组由相同类型的一些列元素组成,使用中括号[]
声明,关于定义不多赘述,主要还是说数组和指针。
1 | int arr[5] = {1, 2, 3, 4, 5}; |
像上面这样,一眼看上去,是不是有些迷乱,甚至还有点不知所措?不要担心,路是要一步一步走,让我们逐一击破。
arr是声明的一个长度为5的整型数组,arr的值就是数组第一个元素的地址,也就是arr[0]
的地址,第一个元素是个int,所以int型的指针pa是可以指向这个元素的,同时,int型指针pa0,也可以指向arr[0]
,这个应该不难理解。
用中括号[]
从数组中取值的操作,本质上和指针操作,是一样的,也就是说,
1 | arr[0] = *pa; |
但是,还是有一点区别,数组是有长度的,也就是sizeof(arr)/sizeof(arr[0])
的值,这里等于5,指针只要不超过最大内存地址,便可以一直加1向高位移动,但数组一旦超过了声明时的长度,便会报错数组脚标越界。
接下来说容易让人疑惑的对数组取地址:&arr
。对数组取地址后,返回的值的类型是数组型指针,如果用一个int型的指针来接收这个值就会报错,而是需要声明一个数组型的指针来接收此值,也就是
1 | int(* parr)[5] = &arr; |
注意这里的括号,如果不加括号,那么parr的类型就是一个普通数组,里面元素的值是int型指针,要用int型指针来初始化,如果觉得乱,那么横向对比着看就很清晰
1 | int value = 5; |
int型数组和int*
型数组,相同点都是数组,不同点,一个元素类型是int,一个元素类型是int型指针。
int型指针和数组型指针的区别在于,跳跃能力不同,int型指针加1后向高位内存移动一个int的长度,即4个字节,而数组指针在加1后,会向高位内存移动一个数组的长度,即,4x5=20个字节。数组型指针取值,和数组取值相同,这也算对得起名字里的数组,这么看来,数组型指针是不是很像二维数组了,
1 | int arr2[2][3] = {{1, 2, 3}, {11, 12, 13}}; |
4. const
调用函数时,经常看到返回值或是参数的类型是const char*
,还有char const*
,关于const,其实有个很好记的小窍门:const修饰谁,谁就不可变
1 | int a = 10; |
5. void*
前文写JNI的时候有提到过,类型是void的指针,表示这个指针的跳跃能力未可知,需要在使用的时候手动指定,以便能解释它所指向的内存,多用于通用函数的参数,如内存复制的memcpy,函数本身并不关心传进来的是什么类型的指针,跳跃能力如何,它只负责把从src指针指向的位置开始,复制指定数量的字节的值,到dst指针指向的位置,即可。