【C语言】指针进阶:指针和数组
文章目录
- 【C语言】指针进阶:指针和数组
- 1 指针的算术运算
- 1.1 指针加上整数
- 1.2 指针减去整数
- 1.3 两个指针相减
- 1.4 指针比较
- 1.5 指向复合字面量的指针
- 2 指针用于数组处理
- * 运算符和 ++ 运算符的组合
- 3 用数组名作为指针
- 用指针作为数组名
- 4 指针和多维数组
- 4.1 处理多维数组的元素
- 4.2 处理多维数组的行
- 4.3 处理多维数组的列
- 4.4 用多维数组名作为指针
【C语言】指针进阶:指针和数组
1 指针的算术运算
在以往的学习中可知,指针可以指向数组元素。例如:
int a[10], *p;
p = &a[0];
通过如上代码,可以使 p
指向 a[0]
,如下图所示:
现在可以通过 p
访问 a[0]
。例如,通过以下写法把 5 存入 a[0]
中:
*p = 5;
把指针 p
指向数组看起来也没有什么大不了的。但是,通过在 p
上执行指针算术运算(或者地址算术运算)可以访问数组 a
的其他所有元素。C语言支持 3 种(而且只有 3 种)格式的指针算术运算:
- 指针加上整数;
- 指针减去整数;
- 两个指针相减。
下面的所有例子都假设有如下声明:
int a[10], *p, *q, i;
1.1 指针加上整数
指针 p
加上整数 j
产生指向特定元素的指针,这个特定元素是 p
原先指向的元素后的 j
个 位置。更确切地说, 如果 p
指向数组元素 a[i]
,那么 p + j
指向 a[i + j]
(当然,前提 是 a[i + j]
必须存在)。
使用如下示例说明指针的加法运算:
1.2 指针减去整数
如果 p
指向数组元素 a[i]
,那么 p - j
指向 a[i - j]
。例如:
1.3 两个指针相减
当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。因此,如果 p
指向 a[i]
且 q
指向 a[j]
,那么 p - q
就等于 i - j
。例如:
注意:如果指针指向的不是数组元素,则不能对该指针执行算术运算。此外,只有在 2 个指针指向同一个数组时,把它们进行相减才有意义。
1.4 指针比较
可以用关系运算符(<、<=、>
和 >=
)和判等运算符( ==
和 !=
)进行指针比较。只有在两个 指针指向同一数组时,用关系运算符进行的指针比较才有意义。比较的结果依赖于数组中两个 元素的相对位置。例如,在下面的赋值后 p <= q
的值是 0,而 p >= q
的值是 1。
p = &a[5];
q = &a[1];
1.5 指向复合字面量的指针
指针指向由复合字面量创建的数组中的某个元素是合法的。复合字面量是 C99 的一个特性,可以用于创建没有名称的数组。考虑如下的例子:
int *p = (int []){3, 0, 3, 4, 1};
p
指向一个 5 元素数组的第一个元素,这个数组包括 5 个整数:3、0、3、4 和 1。
使用复合字面量可以减少一些麻烦,我们不再需要先声明一个数组变量,然后用指针 p
指向数组的第一个元素:
int a[] = {3, 0, 3, 4, 1};
int *p = &a[0];
2 指针用于数组处理
指针的算术运算允许通过对指针变量进行重复自增来访问数组的元素。如下代码片段所示:
#define N 10
int a[N], sum, *p;sum = 0;
for (p = &a[0]; p < &a[N]; p++)sum += *p;
在这个示例中,指针变量 p
初始指向 a[0]
,每次执行循环时对 p
进行自增。因此 p
先指向 a[1]
,然后指向 a[2]
,以此类推。在 p
指向数组 a
的最后一个元素的后一位置时,循环终止。
特别说明:
for
语句中的条件 p < &a[N]
值得特别说明一下。尽管元素 a[N]
不存在(数组 a
的下标为 0~N-1
),但是对它使用取地址运算符是合法的。
因为循环不会尝试检查 a[N]
的值,所以在上述方式下使用 a[N]
是非常安全的。执行循环体时 p
依次等于 &a[0], &a[1], …, &a[N-1]
,但是 当 p
等于 &a[N]
时,循环终止。
当然,改用下标可以很容易地写出不使用指针的循环。支持采用指针算术运算的最常见论调是,这样做可以节省执行时间。但是,这依赖于具体的实现——对有些编译器来说,实际上依靠下标的循环会产生更好的代码。
* 运算符和 ++ 运算符的组合
思考一个简单的例子:把值存入一个数组元素中,然后前进到下一个元素。利用数组下标可以这样写:
a[i++] = j;
如果 p
指向数组元素,那么相应的语句将是
*p++ = j;
因为后缀 ++
的优先级高于 *
,所以编译器把上述语句看作
*(p++) = j;
p++
的值是 p
。(因为使用后缀 ++
,所以 p
只有在表达式计算出来后才可以自增。)因此,*(p++)
的值将是 *p
,即 p
当前指向的对象。 当然,*p++
不是唯一合法的 *
和 ++
的组合。例如,可以编写 (*p)++
,这个表达式返回 p
指向的对象,然后对该对象进行自增( p
本身是不变化的)。更多的组合如下表:
表达式 | 含义 |
---|---|
*p++ 或 *(p++) | 自增前表达式的值是 *p ,以后再自增 p |
(*p)++ | 自增前表达式的值是 *p ,以后再自增 *p |
*++p 或 *(++p) | 先自增 p,自增后表达式的值是 *p |
++*p 或 ++(*p) | 先自增 *p ,自增后表达式的值是 *p |
3 用数组名作为指针
可以用数组的名字作为指向数组第一个元素的指针。
int a[10];
// 把7存储到a[0]
*a = 7;
// 把12存储到a[1]
*(a + 1) = 12;
通常情况下,a + i
等同于 &a[i]
(两者都表示指向数组 a
中元素 i
的指针),并且 *(a+i)
等价于 a[i]
(两者都表示元素 i
本身)。换句话说,可以把数组的取下标操作看作指针算术运算的一种形式。
特别说明:
虽然可以把数组名用作指针,但是不能给数组名赋新的值。。试图使数组名指向其他地方是错误的:
while (*a != 0) a++; /*** WRONG ***/
这一限制不会对我们造成什么损失。我们可以把 a
复制给一个指针变量,然后改变该指针变量:
p = a;
while (*p != 0) p++;
用指针作为数组名
既然可以用数组名作为指针,C语言是否允许把指针看作数组名进行取下标操作呢?答案是肯定的,下面是一个例子:
#define N 10
...
int a[N], i, sum = 0, *p = a;
...
for (i = 0; i < N; i++) sum += p[i];
编译器把 p[i]
看作 *(p+i)
,这是指针算术运算非常正规的用法。
4 指针和多维数组
4.1 处理多维数组的元素
C语言按行主序存储二维数组;换句话说,先是第 0 行的元素,接着是第 1 行的,依此类推。r
行的数组可表示如下:
使用指针时可以利用这一布局特点。如果使指针 p
指向二维数组中的第一个元素(即第 0 行第 0 列的元素),就可以通过重复自增 p
的方法访问数组中的每一个元素。
常规初始化二维数组方法为2层 for
循环:
int a[NUM_ROWS][NUM_COLS];
//初始化所有元素为0
int row, col;
for (row = 0; row < NUM_ROWS; row++) for (col = 0; col < NUM_COLS; col++) a[row][col] = 0;
但是,如果把 a
看作一维的整型数组,那么就可以把上述两个循环改成一个循环了:
int *p;
for (p = &a[0][0]; p <= &a[NUM_ROWS-1][NUM_COLS-1]; p++) *p = 0;
特别说明: 虽然把二维数组当成一维数组来处理看上去像在搞欺骗,但是对大多数C语言编译器 而言这样做是合法的。这样做是否是个好主意则要另当别论。这个方法明显破坏了程序的可读性。
4.2 处理多维数组的行
处理二维数组的一行中的元素,该怎么办呢?再次选择使用指针变量 p
。为了访问到第 i
行的元素,需要初始化 p
使其指向数组 a
中第 i
行的元素 0:
p = &a[i][0];
对于任意的二维数组 a
来说,由于表达式 a[i]
是指向第 i
行中第一个元素(元素0)的指针, 上面的语句可以简写为:
p = a[i];
为了了解原理,回顾一下把数组取下标和指针算术运算关联起来的那个神奇公式:对于任意数组 a
来说,表达式 a[i]
等价于 *(a + i)
。因此 &a[i][0]
等同于 &(*(a[i] + 0))
,而后者等 价于 &*a[i]
;又因为 &
和 *
运算符可以抵消,所以也就等同于 a[i]
。
下面的循环对数组 a
的第 i
行清零,其中用到了这一简化:
int a[NUM_ROWS][NUM_COLS], *p, i; for (p = a[i]; p < a[i] + NUM_COLS; p++) *p = 0;
因为 a[i]
是指向数组 a
的第 i
行的指针,所以可以把 a[i]
传递给需要用一维数组作为实际参数的函数。换句话说,使用一维数组的函数也可以使用二维数组中的一行。
4.3 处理多维数组的列
处理二维数组的一列中的元素就没那么容易了,因为数组是按行而不是按列存储的。下面的循环对数组 a
的第 i
列清零:
int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i;
for (p = &a[0]; p < &a[NUM_ROWS]; p++) (*p)[i] = 0;
这里把 p
声明为指向长度为 NUM_COLS
的整型数组的指针。在 (*p)[NUM_COLS]
中,*p
是需要使用括号的;如果没有括号,编译器将认为 p
是指针数组,而不是指向数组的指针。表达式p++
把 p
移到下一行的开始位置。在表达式 (*p)[i]
中,*p
代表 a
的一整行,因此 (*p)[i]
选中了 该行第 i
列的那个元素。(*p)[i]
中的括号是必要的,因为编译器会将*p[i]
解释为 *(p[i])
。
4.4 用多维数组名作为指针
就像一维数组的名字可以用作指针一样,无论数组的维数是多少都可以采用任意数组的名字作为指针。但是,需要特别小心。思考下列数组:
int a[NUM_ROWS][NUM_COLS];
a
不是指向 a[0][0]
的指针,而是指向 a[0]
的指针。
从C语言的角度来看,这样做是有意义的。 C语言认为 a
不是二维数组而是一维数组,并且这个一维数组的每个元素又是一维数组。用作指针时, a
的类型是 int (*)[NUM_COLS]
(指向长度为 NUM_COLS
的整型数组的指针)。
了解 a
指向的是 a[0]
有助于简化处理二维数组元素的循环。例如,为了把数组 a
的第 i
列清零,可以用 :
for (p = &a[0]; p < &a[NUM_ROWS]; p++) (*p)[i] = 0;
取代:
for (p = a; p < a + NUM_ROWS; p++) (*p)[i] = 0;