C语言超详细讲解指针的概念与使用
一、指针与一维数组
1. 指针与数组基础
先说明几点干货:
1. 数组是变量的集合,并且数组中的多个变量在内存空间上是连续存储的。
2. 数组名是数组的入口地址,同时也是首元素的地址,数组名是一个地址常量,不能更改。
3. 数组的指针是指数组在内存中的起始地址,数组元素的地址是指数组元素在内存中的其实地址。
对于第一点数组中的变量在内存空间上是连续的相信没有什么疑问,这点在讲解数组的时候就已经提到过了。对于第二点,可以得到,数组名就是一个地址,并且是整个数组的内存起始地址。数组名一个是常量地址我们不能对数组名进行赋值操作,数组名是数组的起始地址,数组的第一个元素的地址也是数组的起始地址,他们两个在数值上是相等的。对于第三点,举个例子,假如定义一个 int 类型变量 a,int 占四个字节,假如他的第一个字节的地址是 0x00000010, 第四个字节的地址那么就是 0x00000013。而 &a 的地址是 a 的内存空间上的首地址,即 &a 的值为 0x00000010。
通过下面示例可以来证明一下上面讲述的结论。
源代码:
#include <stdio.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int b = 20; char *pb = &b; /*a是地址常量,不能被赋值*/ //a = pb; /*在数值上 a &a[0] &a 的数值是相等的 */ printf("a = %p, &a[0] = %p, &a = %p\n", a, &a[0], &a); printf("&b = %p, pb = %p, pb + 3 = %p\n", &b, pb, pb + 3); return 0; }
运行结果:
a = 0xbfb82f38, &a[0] = 0xbfb82f38, &a = 0xbfb82f38
&b = 0xbfb82f30, pb = 0xbfb82f30, pb + 3 = 0xbfb82f33
2. 指针与数组
以一个实例开始,数组的打印你会几种方法?
源代码:
#include <stdio.h> int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p = a, i; //1. 数组元素访问法 for (i = 0; i < 10; i++) { printf("a[%d] = %-3d", i, a[i]); } putchar(10); //等价于 putchar('\n'), 因为 \n 的 ASCII 码就是 10 //2. 数组地址偏移访问法 for (i = 0; i < 10; i++) { printf("a[%d] = %-3d", i, *(a + i)); } putchar(10); //3. 指针元素访问法 for (i = 0; i < 10; i++) { printf("a[%d] = %-3d", i, p[i]); } putchar(10); //4. 指针地址偏移访问法 for (i = 0; i < 10; i++) { printf("a[%d] = %-3d", i, *(p + i)); } putchar(10); return 0; }
运行结果:
a[0] = 1 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[5] = 6 a[6] = 7 a[7] = 8 a[8] = 9 a[9] = 10
a[0] = 1 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[5] = 6 a[6] = 7 a[7] = 8 a[8] = 9 a[9] = 10a[0] = 1 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[5] = 6 a[6] = 7 a[7] = 8 a[8] = 9 a[9] = 10
a[0] = 1 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[5] = 6 a[6] = 7 a[7] = 8 a[8] = 9 a[9] = 10
从上面可以看到,如果一个指针p指向数组a,那么 a[i],*(a+i), p[i],*(p+i) 这四种写法是完全等价的,都是访问数组中第 i+1 个元素。其实数组对于元素的访问根本上就是地址的偏移,a[i] 之所以能够访问到第 i+1 个元素其实他所进行的操作和 *(a+i) 是一样的,都是在地址 a 的基础上偏移 i 个单位的内存单元进行元素访问。a 是一个地址,p 也是一个地址,且当 p 指向 a 的时候,能够以 a[i] 来访问第 i+1 元素,那么同理也能以 p[i] 的方式来访问第 i+1 个元素。同样的 *(p+i) 也是以地址偏移的方式来进行数组元素的访问。其实这几种写法唯一不同的就是 a 是地址常量,不能被赋值,也就是 a 不被允许再指向其他的内存空间,而 p 是指针变量,可以被任意赋值,可以指向其他的内存空间。
3. 一个思考
通过上面的讲解请大家看一下下面程序应该输出多少?
#include <stdio.h> int main() { int a[10] = {1, 2, 3, 4, 5}; int *p = &a[1]; p[0] = 20; p[1] = 30; p[-1] = 10; printf("a[0] = %d, a[1] = %d, a[2] = %d\n", a[0], a[1], a[2]); return 0; }
其实大家可能或许能够猜到程序的输出结果是10,20,30。但是可能并不是所有人都能够清楚的明白编译器是怎么个处理逻辑的,在这里来为大家再做进一步的讲解,让大家对指针和数组的关系有更深一步的认识。
上图简单从地址偏移角度来画出了指针的指向,大家先看右边,地址 a 在数值上指向数组的首地址,那么 a+1 就是偏移了一个 int 类型,也就是 4 字节,所以 a+1 指向数组的第二个元素。大家再看左边,p 指向 a[1] 的首地址,那么从指针变量 p 的角度来看,p[0] 就是他指向的元素 a[1],所以 p[0] 和 a[1] 是完全相等的,呢么 p[1] ,也可以写成 *(p+1) 指向的就是 a[2] 元素的首地址,因为 p+1 要在 p 的基础上偏移 4 字节,p 指向 a[1] 的首地址,那么 p+1 就是指向 a[2] 的首地址,再用取值运算符 *(p+1) 的值就是a[2] 的值也就是 3。p+1 理解了那么 p-1 也就不能理解了,他们两个只是偏移的方向不同,p+1 是向右移,也就是地址增加的方向一定,而 p-1 是向左移,向地址减小的方向移动。
二、指针与字符串
C语言处理字符串通常实讲字符串放在字符数组中,因为C语言没有字符串类型,而字符串在地址空间上是连续的,而数组元素在内存空间上也是连续,字符串就是若干字符的集合,所以就用字符数组来处理字符串,对于常量字符串也可以用指针对其直接指向操作。
关于指针与字符串的关系大家可以先看看下面程序:
#include <stdio.h> int main() { char a[] = "hello world"; char *b = "hello world"; a[1] = 'z'; //b[1] = 'z'; //error printf("a = %s\n", a); return 0; }
其实如上面注释所示,可以对 a 里面的元素进行赋值操作,而不能对 b 里面的元素进行赋值操作。为什么呢?其实原因与内存单元的分配有关。a 是一个局部变量数组,局部变量分配在栈上,栈上的内容具有可读可写操作,所以对 a 里面的元素进行赋值操作没有问题。而 b 是一个指针,也是一个局部变量,但是 b 指向的是一个字符串常量,字符串常量存储在只读区,只读区里面的内容只有读权限而没有写权限,这点尤其注意,所以我们上述操作中对 b 指向的内容进行写操作编译器是会报错的。报错内容就是段错误。导致段错误的原因就是访问了非法内存,非法内存一般就是内存地址不存在或者对于该块地址没有权限。
三、指针和二维数组
1. 指针数组与数组指针
该部分主要讲解指针数组与数组指针。可能对于初学者而言对指针数组和数组指针比价容易弄混,其实记后面两个字就可以,指针数组 后面两个字是数组,说明指针数组是一个数组,那么数组里面存储的内容就是前两个字 指针。数组指针 后面两个字是指针,说明数组指针是一个指针,那么这个指针指向那里,前面两个字就有体现,数组指针指向一个数组。一句话概括之,指针数组是一个数组,数组里面每个元素存储的是一个指针;数组指针是一个指针,是指向数组的指针。
指针数组的定义方法:
char a[5]; //字符数组
char *a[5]; //指针数组
数组指针的定义方法:
char a[5]; //字符数组
char (*a)[5] //数组指针
上面数组指针和指针数组的定义方法很像,其实不管是这里的指针数组 数组指针 还是后面文章中会讲解的 指针函数 函数指针,其实分辨他们有一个诀窍,那就是右左法则,何谓右左法则,即在运算符的优先级范围内,先往右看,再往左看。打个比方,看上面定义的指针数组,先找到 a ,a 的右边与 a 结合是一个数组,那么这个定义就是一个数组,是个什么样的数组呢?再往左看,a 的左边与 a 结合是一个指针,那么就是一个指针数组。再来看看数组指针,先找到 a,a 的右边是一个括号,有括号先看括号里面的内容,也就是往左看,括号里面的内容是一个指针,是个什么样的指针呢?再往右看,是一个数组,所以就是数组指针。掌握了这个方法,不管是给出定义来辨别名字,还是告诉你名字让其写出定义都不在话下。
2. 指针数组
指针数组是一个数组,里面每个成员都是指针,定义一个指针数组相当于定义了多个指针变量的集合。 例如 int *a[3];
这是一个指针数组,数组里面每个成员都是指向int类型地址的。为了让大家更加熟悉指针数组的使用,大家请看下面这个例子:
源代码:
#include <stdio.h> int main() { int a = 1, b= 2, c = 3; int *p[3]; //定义一个指针数组,数组里面有3个成员,每个成员都是指向int类型的指针。 /*数组成员的初始化*/ p[0] = &a; p[1] = &b; p[2] = &c; /*通过指针改变其指向地址中的内容*/ *p[0] = 10; *p[1] = 20; *p[2] = 30; printf("a = %d, b = %d, c = %d\n", a, b, c); return 0; }
运行结果:
a = 10, b = 20, c = 30
通过上面例子大家就不难发现,定义好指针数组之后,数组成员的使用方法和普通指针的使用是一样的,定义好一个指针数组唯独就是可以一次性定义好多个指向相同类型的指针。其实大家想一下,我们当时引入数组的时候说C语言引入数组就是因为数组可以一次性定义好多个具有相同类型的普通变量,其实这里的指针数组也是一样的,不同的是,普通数组里面的成员都是普通变量,而指针数组里面的成员都是指针。
3. 数组指针
数组指针是一个指针,指向一个数组的指针。 例如 int (*a)[3];
这是一个数组指针,指向的是一个3列的数组,a+1 就相当于步进为1列,1列有3个int类型,相当于步进了 3 * 4 = 12(字节)。示例如下:
源代码:
#include <stdio.h> int main(int argc, const char *argv[]) { int a[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; /*定义数组指针*/ int (*p)[3] = a; int (*q)[2] = a; /*数组指针步进加1为1行*/ printf("p = %p, p + 1 = %p\n", p, p+1); /*先对行指针进行一次解引用就是普通指针,普通指针步进就是指向的普通数据类型的大小*/ *(*(p+1) + 1) = 56; printf("%d\n", a[1][1]); printf("q = %p, q + 1 = %p\n", q, q+1); *(*(q+1) + 1) = 56; printf("%d\n", a[1][0]); return 0; }
运行结果:
p = 0xbfc208ec, p + 1 = 0xbfc208f8
56
q = 0xbfc208ec, q + 1 = 0xbfc208f4
56
图示如下:
数组指针的步进大家一定要清楚,步进主要看定义时数组个数的大小,比如 int (*p)[3];
步进加1就是步进 int [3] 大小;int (*q)[2];
步进加1就是 int [2] 大小。数组指针就是行指针,因为数组指针指向相同行的数组时,指针偏移加1就是1行。