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;
}
注:数组参数只有第一维的长度是可以省略的。有长度省略的数组类型是一种未完成类型,只能使用在函数的参数类型上。