C指针和数组
一、指针的运算
1) 指针的算术运算
指针可以和整数进行一些算术运算。
-
指针 +/- 整数
以加法为例。如果有基类型为
T
的指针:T a, *p = &a;
给定整数
n
,则运算p + n
的结果是一个新的指针值,其值等于:address(a) + sizeof(T) * n
而不是:
address(a) + 1
你可能已经注意到,这个新地址是未知单元的地址,也许不能访问!
-
指针 - 指针
两个指针的减法是有意义的,能得到一个整数值,表示两个指针之间的距离(以单元而不是字节计)。例如:
int a[10], *p = &a[2], *q = &a[5]; int b = q - p; //b的值是3,而不是12
-
两个指针的加法、乘法和除法是无意义的
例如,如果有指针
p
、q
,那么以下求这两个指针的中间地址的方法是错误的:int a[10], *p = &a[0], *q = &a[9]; int *t = (p + q) / 2; //error!
正确的方法应该是:
int *t = p + (q - p) / 2; //OK
2) 指针的条件运算
两个指针可以进行条件运算。例如:
p > q
:比较地址p是否大于地址q,即p指向的单元在内存中是否在q指向的单元之后。p == q
:比较两个地址是否相等,即两个指针是否指向了同一个单元。
在程序代码中,我们常进行指针是否为空的判断。例如:
if (p != NULL) ...
这条if
语句可以简写为:
if (p) ...
上述两条语句中的条件表达式是等效的。
虽然是等效的,但两个条件表达式的值却是不一样的。假设p
的值是10000,那么,表达式p != NULL
的值是 1,而元表达式p
的值是 10000。
之所以等效,是因为C语言对逻辑结果简单粗暴的处理方式:0即假,非0即真。
二、 指针指向数组元素
1) 指向数组元素的指针
如果有:
int a[10];
int *p;
p = &a[0];
那么我们说,指针p
指向了数组a
的元素;指针p
是指向数组元素的指针。
利用数组连续存储的特性,我们可以通过对指针的加减法使其指向数组的其他元素。例如:
++p; //p现在指向a[1]
上述关系的一种常见说法是:指针p指向了数组a。这也许是一种约定俗成的简略语,但实际上是不确切的。指向数组的指针定义完全不同。详见下文。
2) *和++运算符的组合
如果有表达式++*p
和*p++
,并将其值赋给变量a
,那么:
赋值语句 | 等价形式 | 分解形式 | 说明 |
---|---|---|---|
int a = ++*p; | a = ++(*p); | / | 自增p指向的单元 |
int a = *p++; | a = *(p++); | int *tmp = p++; a = *tmp; | 先后缀自增p的值。后缀自加表达式的结果为自加前的值 |
后缀自增运算符++的优先级高于间接寻址运算符*的。
三、数组名和指针的关系
C数组实际上是一块有起始地址但没有结尾标记的内存块。基于此,C认为数组的名字就表示了数组的起始地址(指针)。例如有:
int a[10];
这里,数组名a
被视为是一个常量指针,其值恒等于&a[0]
。那么,以下操作就是合情合理的:
*a = 7; //将7赋给a的0号单元
*(a+1) = 12; //a+1是1号单元的地址
既然如此,那么指针和数组就有着非常密切的关系。如果有:
int a[10], *p = a; //等价于 p = &a[0],因为a代表了a[0]的地址
那么有如下等价关系成立:
表达式 | 等价表达式1 | 等价表达式2 | 等价表达式3 |
---|---|---|---|
p | a | &a[0] | / |
*p | *a | a[0] | / |
p[i] | a[i] | / | / |
p + i | a + i | i + p | i + a |
*(p + i) | *(a + i) | *(i + a) | a[i] |
实际上,C编译在处理数组时,都将其转为换指针形式。
一个有趣又奇怪的事实是,既然*(a+i)
可以写成a[i]
,那么*(i+a)
就可以写成i[a]
,也就是说,a[i]
和i[a]
是等价的!
示例程序:
/*
* 指向数组元素的指针
*/
#include <stdio.h>
#define N 5
int main() {
int a[N] = {1, 2, 3, 4, 5};
int *p, *t;
//利用指向数组元素的指针遍历数组
//注意:a[N]这个单元不存在!
//但其地址总是可以获取的,并且在代码中只用到了这个地址,而没有访问这个地址上的单元,因此是安全的
for (t = p = &a[0]; p != &a[N]; ++p)
printf("%4d", *p);
printf("\np - t = %ld\n", p - t);
return 0;
}
相信大家已经注意到了,程序中用a[N]
的地址。我们知道,数组a
的最大合法下标应该是N-1
,那么为什么程序中可以这样用呢?
答案很简单:因为代码只是用了a[N]
这个虚拟元素的地址,而没有访问这个地址下的单元,所以代码是安全的。在某些场合,&a[N]
被称为哨兵(sentinal),标记数组的结尾。
此程序中的循环语句还可以写成:
for (t = p = a; p != a + N; ++p)
四、指向一维数组的指针
如果有:
int a[10], *p = a;
那么,p
的类型是 int*
,其基类型是int
;p
是指向数组元素的指针。
在p“眼中”,它指向的是单个整数单元;它并不“知道”数组有多长。
表达式++p
将使p
跳过一个int
单元。
如果将一维数组作为整体看待,并且用一个指针指向,那么这个指针就是指向数组的指针
-
数组的类型
如果有:
int a[10];
那么a
的类型是: int [10] 如果指针p
是一个指向数组(整体)的指针,那么它的基类型就应该这个。 -
指向数组的指针
指向数组的指针的定义为:
int (*p)[10];
上述类型的解读是:
- 定义中的新符号是
p
- ()迫使
p
先与*
先结合,因此p
一定是个指针 - 剩下的
int[10]
就是p
的基类型。很明显,这是一个一维数组类型
综上,指针
p
指向了一个长度为10的一维整型数组。 - 定义中的新符号是
-
p
的初始化。对比定义:
int a [10]; int (*p)[10];
如果要使
p
指向a
,那么*p
和a
就是等价的,那么就有:p = &a;
通过
p
访问数组元素的形式为:(*p)[i]
示例程序:
/*
* 指向一维数组的指针
*/
#include <stdio.h>
#define N 5
int main() {
int a[N] = {1, 2, 3, 4, 5};
int (*p)[N] = &a;
int i;
for (i = 0; i < N; ++i)
printf("%4d", (*p)[i]);
putchar('\n');
return 0;
}
五、指针和多维数组
这里仅以二维数组为例。
-
指向二维数组元素的指针
int a[3][5]; int *p; p = &a[0][0]; for (int i = 0; i < 3 * 5; ++i, ++p) *p = 1; //将a中的所有元素置为1
-
观察二维数组的不同视角
二维数组
int a[3][5]
除了可以视为是3X5的矩阵外,还可以用以下视角解读:- 是一个长度为3的一维数组:
a[3]
。 - 这个一维数组的基类型是int[5],即
a
的每个元素(即a
的每一行)都是一个长度为5的一维数组。
那么:
a[i]
是a
的一行,是个一维数组,那么a[i]
就是个简单指针,指向了它的0号元素(即a[i][0]
)。a[i]
等价于&a[i][0]
。a
是个指针(数组名就是指针),它指向了它的0号元素,而这个元素是个数组,因此a
就是指向数组的指针。a
等价于&a[0]
。
- 是一个长度为3的一维数组:
-
指针指向二维数组的行
int a[3][5]; int (*p)[5]; p = a; ++p; //++操作使p跳过a的一整行而不是一个元素!
示例程序:
/*
* 指向二维数组的指针
*/
#include <stdio.h>
#define M 3
#define N 5
int main() {
int a[M][N];
int *p; //指针p能指向数组的元素
int (*q)[N]; //指针q能指向一个长度为N的一维数组
int i, j;
//初始化二维数组
p = &a[0][0]; //p指向二维数组的首行首列元素
for (i = 0; i < M * N; ++i, ++p)
*p = M * N - i;
q = a; //q和a都是指向数组的指针,那么p[i]/a[i]是一维数组(名),即是个简单指针
for (i = 0; i < M; ++i, ++q) { //++q将使q跳过一个数组(即a的一行)而不是一个整型单元
p = *q; //q是指向一维数组的指针,则q = &a[i];p是指向数组元素的指针,因此p = *(a[i]) = *q
for (j = 0; j < N; ++j)
printf("%4d", p[j]);
putchar('\n');
}
return 0;
}
六、指针数组
顾名思义,是每个元素都是指针的数组:
int * a[10]; //a是一个一维数组;长度为10;每个元素都是整型指针
和数组指针的对比:
int (*a)[10]; //a是一个指针,指向一个长度为10的一维整型数组
七、数组作为参数
C将数组参数转换为指针参数。因此,数组参数和指针参数是等价的。
示例程序:
/*
* 数组参数与指针参数等价
*/
#include <stdio.h>
void increase(int *a, int len) {
// void increase(int a[], int len) {
for (int i = 0; i < len; ++i)
++a[i];
}
void print_r(int *a, int len) {
// void print_r(int a[], int len) {
for (int i = 0; i < len; ++i)
printf("%4d", a[i]);
putchar('\n');
}
int main() {
int x[] = {3, 7, 2, 8, 1, 9, 6, 4, 5, 0};
int len = sizeof(x) / sizeof(int);
print_r(x, len);
increase(x, len);
print_r(x, len);
return 0;
}
注:数组参数只有第一维的长度是可以省略的。有长度省略的数组类型是一种未完成类型,只能使用在函数的参数类型上。