指针与数组
1、定义
指针:C语言中某种数据类型的数据存储的内存地址,例如:指向各种整型的指针或者指向某个结构体的指针。
数组:若干个相同C语言数据类型的元素在连续内存中储存的一种形态。
数组在编译时就已经被确定下来,而指针直到运行时才能被真正的确定到底指向何方。所以数组的这些身份(内存)一旦确定下来就不能轻易的改变了,它们(内存)会伴随数组一生;
而指针则有很多的选择,在其一生他可以选择不同的生活方式,比如一个字符指针可以指向单个字符同时也可代表多个字符等。
指针和数组在C语言中使用频率是很高的,在极个别情况下,数组和指针是“通用的”,比如数组名表示这个数组第一个数据的指针。
如下代码
#include <stdio.h> char array[4] = {1, 2, 3, 4}; int main(void) { char * p; int i = 0; p = array; for (; i < 4; i++) { printf("*array = %d\n", *p++); } return (0); }
这里我们将数组名array作为数组第一个数据的指针赋值给p。但是不能写成*array++。准确来说数组名可以作为右值,不能作为左值(左值和右值的概念这里不再展开讲解)。
数组名的值其实是一个指针常量,这样我想你就明白了数组名为什么不能做为左值了。如果想用指针p访问array的下面2的数据,以下写法是合法的
char data; /*第一种写法*/ p = array; data = p[2]; /*第二种写法*/ p = array; data = *(p+2); /*第三种写法*/ p = array + 2; data = *p;
2、指针与二维数组
先说一下二维数组,二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
从概念上理解,a 的分布像一个矩阵:
0 1 2 3
4 5 6 7
8 9 10 11
但是内存是连续的,没有这样的“矩阵内存”,所以二维数组a分布是连续的一块内存。
C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。那么定义如下指针如何理解呢?
int (*p)[4];
括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
那么和下面定义有什么区别呢?
int *p[4];
这里就要先说明*和[]的优先级了,[]的优先级是高于*的,所以int *p[4];等同于int *(p[4]);。所以它是一个指针数组。
这里很绕,总接下
int (*p)[4];是数组指针,它指向二维数组中每个一维数组的类型,它指向的是一个数组。
int *p[4];是指针数组,它是一个数组,数组中每个数是指向int型的指针。
3、指针数组与数组指针
对于指针数组,说的已经很明确了,不再详细讲解,下面说一下数组指针。举例看一下
#include <stdio.h> int main() { int a[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}}; int(*p)[4]; p = a; printf("%d\n", sizeof(*(p + 1))); return (0); }
对于数组指针p如下
那么printf("%d\n", sizeof(*(p + 1)));的结果就是16。
如果想打印a[1][0]的值,代码如下
printf("%d\n", *(*(p + 1)));
如果想打印a[1][1]的值,代码如下
printf("%d\n", *(*(p + 1)+1));
这个自行体会,p是数组指针,它指向的是一个数组,所以对获取它指向的值,也就是*p,是指向一个数组还是一个值,指向a[0]。获取获取a[0][0],就需要写成**p。
对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。
最后再次捋一下数组指针和指针数组,
int *p1[4];是指针数组。
int (*p2)[4];是数组指针。
“[]”的优先级比“*”要高。
对于指针数组,p1先和“[]”结合,构成一个数组的定义,数组名为p1,int *修饰的是数组的内容,即数组的每个元素。那么它本质是一个数组,这个数组里有4个指向int类型数据的指针。
对于数组指针,“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那么它本质是一个指针,它指向一个包含4个int 类型数据的数组。
既然深入谈了指针数组和数组指针,就多聊一下。
#include <stdio.h> int main() { char a[5] = {'A', 'B', 'C', 'D'}; char(*p3)[5] = &a; char(*p4)[5] = a; return 0; }
上面代码是编译编译是报了waring的,报警如下
注意:不同的编译器编译结果可能不同,我的编译方法请参考《使用vscode编译C语言》。
p3 这个定义的“=”号两边的数据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。所以才有了上面的警告。
但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过编译器仍然警告你别这么用。
再举一个栗子
int vector[10]; int matrix[3][10]; int *vp,*vm; vp = vector; vm = matrix;
上面的代码第5行是错误的,因为vm是指向整型的指针,但是matrix不是指向正向的指针,他是指向整型数组的指针。下面是正确的写法
int matrix[3][10]; int (*vm)[10]; vm = matrix;
4、数组指针的应用
上面说了那么多,可能大部分开发者用不到,数组指针在很多时候都是可以代替二维数组的,有些程序员喜欢用指针数组来代替多维数组,一个常见的用法就是处理字符串。
#include <stdio.h> char *Names[] = { "Bill", "Sam", "Jim", "Paul", "Charles", 0}; void main() { char **nm = Names; while (*nm != 0) printf("%s \n", *nm++); }
具体运行我就不讲解了,运行结果如下
注意数组中的最后一个元素被初始化为0,while循环以次来判断是否到了数组末尾。具有零值的指针常常被用做循环数组的终止符。
这种零值的指针称为为空指针(NULL)。采用空指针作为终止符,在增删元素时就不必改动遍历数组的代码,因为此时数组仍然以空指针作为结束。
5、骚操作
写到这里想到一个“骚操作”,先看下面代码是否正确。
p[-1]=0;
初看这句代码,觉得奇怪,甚至觉得它就是错误,日常C语言开发基本有见到小标是负数的,但是仔细想想没有哪一本书说过下标能为负数的。看下面代码
void main() { int data[4] = {0, 1, 2, 3}; int *p; p = data +2; printf("p[-1] is %d\n",p[-1]); printf("*(p-1) is %d\n",*(p-1)); }
运行结果如下
不仅可以编译通过,还能正确的输出结果为1。这表明,C的下标引用和间接访问表达式是一样的。当然不鼓励这种骚操作,代码需要很强的可读性,而不是这样的骚操作,这里只是演示下标引用和简介表达式的关系。
点击查看本文所在的专辑:C语言进阶专辑