一、指针的运算

1) 指针的算术运算

指针可以和整数进行一些算术运算。

  1. 指针 +/- 整数

    以加法为例。如果有基类型为T的指针:

     T a, *p = &a; 
    

    给定整数n,则运算p + n的结果是一个新的指针值,其值等于:

     address(a) + sizeof(T) * n
    

    而不是:

     address(a) + 1
    

    你可能已经注意到,这个新地址是未知单元的地址,也许不能访问!

  2. 指针 - 指针

    两个指针的减法是有意义的,能得到一个整数值,表示两个指针之间的距离(以单元而不是字节计)。例如:

     int a[10], *p = &a[2], *q = &a[5];
     int b = q - p; //b的值是3,而不是12
    
  3. 两个指针的加法、乘法和除法是无意义的

    例如,如果有指针pq,那么以下求这两个指针的中间地址的方法是错误的:

     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*,其基类型是intp指向数组元素的指针

在p“眼中”,它指向的是单个整数单元;它并不“知道”数组有多长。

表达式++p将使p跳过一个int单元。

如果将一维数组作为整体看待,并且用一个指针指向,那么这个指针就是指向数组的指针

  1. 数组的类型

    如果有:int a[10]; 那么a的类型是: int [10] 如果指针p是一个指向数组(整体)的指针,那么它的基类型就应该这个。

  2. 指向数组的指针

    指向数组的指针的定义为:

     int (*p)[10];
    

    上述类型的解读是:

    • 定义中的新符号是p
    • ()迫使p先与*先结合,因此p一定是个指针
    • 剩下的int[10]就是p的基类型。很明显,这是一个一维数组类型

    综上,指针p指向了一个长度为10的一维整型数组。

  3. p的初始化。

    对比定义:

     int  a  [10];
     int (*p)[10];
    

    如果要使p指向a,那么*pa就是等价的,那么就有:

     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;
}

五、指针和多维数组

这里仅以二维数组为例。

  1. 指向二维数组元素的指针

     int a[3][5];
     int *p;
     p = &a[0][0];
     for (int i = 0; i < 3 * 5; ++i, ++p) *p = 1; //将a中的所有元素置为1
    
  2. 观察二维数组的不同视角

    二维数组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;
}

注:数组参数只有第一维的长度是可以省略的。有长度省略的数组类型是一种未完成类型,只能使用在函数的参数类型上。