对于C和C++的初学者而言,计算机系统的硬件体系和操作系统对应用程序的调用之类的问题过于复杂。其实需要掌握的最重点的内容是,要分清“指针值”和“指针变量”的区别,以及指向的类型。

如何理解指针

除了仅仅用来表示“这是个地址”的void*之外,无论是什么指针,都是“指向某种类型”的。但是所有的指针所表示的都是一个地址,地址本身和“指向的类型”是没有关系的。
所有的指针都可以抽象成T * p;其中T是一种类型(整数、实数、结构体、指针本身、数组、有特定参数个数、类型和返回值类型的函数,等等),p变量保存了一个地址。
指针变量的规定是,T不同的指针不能相互比较、赋值、相减。对指针的一切使用,按这个原则来理解,一定不会错。

指针和数组

T a[];里,a数组“变量”是个地址,它可以直接当“不能改变指向”的T*来用。本质上,a就是一个0x12345678的数(指针值!)。因为这个数是个固定值,所以不需要在内存里放变量的部分开一块空间保存着。
T* p;里,p可以放一个地址,但是它放着的地址是可以变的,程序生成的时候并不知道它会放什么地址,所以T*需要在内存里放变量的部分开一块空间保存当前值。
因为存在“有”和“没有”变量用内存空间保存地址的区别,所以T[]T*是不同的类型!一个很明显的区别是,T[N]的大小,是T的大小*N,如果N很大,T[N]的大小也可以很大;而T*的大小是个固定值,一般32位系统上是4字节,64位系统上是8字节。
但是,可以把一个“确定的地址”赋值给指针变量,比如p=0x80000000,而a的确是个“确定的地址”,所以p=a;是合理的。
现在来看二维数组。大家都知道,T不同的指针T1 * p1;T2 * p2;是不能直接相互赋值的。那么T a[][]T** p呢?让T1 = T[]T2 = T*,那么,aT1[]pT2*,而T1[]的值可以当T1*来用,于是它们分别是T1*T2*,这是不同类型的指针。因此,如下的尝试

int * * p;
int a[2][100];
p = (int * *)a;

是错误的,即使强转了,指针指向的东西也是不一样的,p[0]认为自己指向的东西是4字节或8字节的int*,其实是400字节(古老编译器上可能是200字节)的int[100]
那怎么做才对呢?让p变成T1*,也就是指向正确大小的T[N]。这就是“指针数组的指针”。

int (* p)[100];
int a[2][100];
p = a;

或者让p指向一个正确的T2(也就是T*),而不是一个常数地址(a)表示的T[]

int * * p;
int a[2][100];
int * rows[2] = { a[0], a[1] };
p = rows;

传指针变量

著名的swap函数到底做了什么?
我们来看它的声明和实现:

void swap(int* x, int* y);
void swap(int* x, int* y)
{
  int t;
  t = *x; *x = *y; *y = t;
}

这里int不重要,我们还是让它变成T吧。于是有如下伪代码(T1=T*):

void swap(T1 x, T1 y)
{
  /* 这是错误的方法 */
  /* T1 t; */
  /* t = x; x = y; y = t; */

  /* 这是正确的方法 */
  T t;
  t = *x; *x = *y; *y = t;
}
T a, b;
swap(&a, &b);

传给函数的参数都是原来那个参数的复制品。所谓“复制品”,也就是说不是原来那个参数。所以,对xy本身进行修改——因为它们是指针所以就是改指向——是不会带到外面去的。换个角度来看,也不可能在上面的调用之后,变量ab交换了地址,因为变量自己的地址是固定的。
但是同一个程序里,地址是所有函数共用的,对一个地址上的东西的修改,在其它函数里能看得到;这与这个地址用什么变量表示无关,它们的值一样就行。所以,在swap里修改了T* x这个指针(值是调用swap的函数里a的地址)指向的地址——也就是a的地址——的内容的话,这个地址上的变量a的内容就变了。注意,对*x的修改不是对x的修改,x从头到尾没变过。

对复杂指针声明的理解

函数指针看上去很可怕?记不住int*p[];int(*p)[];哪个是什么?没关系,有个轻松简单又准确的方法。
我总结的口诀是:扔参数表,先右后左,去掉括号。下面给几个例子。
先看一下怎么从一个简单的变量声明变成一个数组、指针或函数。

int n;
int a[100];
int *p;  /* 等价于 int (*p); */
int f(int a, int b);

变数组是右边加[],变指针是左边加*(然后在*和变量名外加上()包起来),变函数是右边加()包起来的参数表。
那么再变一次呢?

int (*pa)[100];
int *fp(int x);
int (*pf)(int a);

pa是“数组的指针”——“变指针是左边加*,然后在*和变量名外加上()包起来”;fp是“返回指针的函数”——“变函数是右边加()包起来的参数表”;pf是“函数的指针”——“变指针是左边加*,然后在*和变量名外加上()包起来”。
看出点规律吗?
理解的时候,反着上面三条来做就行了。不过要注意顺序,顺序就是“先右后左”。

例1

int (*af[4])(int a, int b);

这是什么?
一眼看上去,(int a, int b)这个长得是个参数表的样子,我们在找核心标识符的时候不需要关心参数表具体内容,所以先扔掉参数表

int (*af[4])(参数表1);    参数表1=int a, int b

于是标识符只有af了,它的意义就是整个声明的核心了。“先右”,af右边是[4],所以af是一个数组。我们改写成不符合语法但是比较好理解的格式:

af是(“int (*af元素)(参数表1)”)的[4]数组;    参数表1=int a, int b

af元素”是什么?右边没东西了,看左边,是*,所以是指针。继续改写:

af是(“int (af指针指向的东西)(参数表1)”指针)的[4]数组;    参数表1=int a, int b

af指针指向的东西”是什么?两边都没东西了,去掉括号是等价的,继续改写:

af是(“int af指针指向的东西(参数表1)”指针)的[4]数组;    参数表1=int a, int b

“先右”,“af指针指向的东西”右边是参数表,所以“af指针指向的东西”是函数。改写:

af是(指向有(参数表1)的返回“int af返回值”的函数的指针)的[4]数组;    参数表1=int a, int b

退化成变量了,返回值是int

af是(指向有(参数表1)的返回int的函数的指针)的[4]数组;    参数表1=int a, int b

整理成人类语言:
af是函数指针的数组,长度为4,其中的函数指针指向参数为(int a, int b),返回int的函数。
对不对呢?用如下代码测试一下。

#include <stdio.h>
int func0 (int a, int b) { return a + b + 0; }
int func1 (int a, int b) { return a + b + 1; }
int func2 (int a, int b) { return a + b + 2; }
int func3 (int a, int b) { return a + b + 3; }
int main(int argc, char* argv[], char* env[]) {
  int (*af[4])(int, int);
  int i;
  af[0] = func0;
  af[1] = func1;
  af[2] = func2;
  af[3] = func3;
  for (i = 0; i < 4; ++i)
    printf("func%d(%d00,%d0)=%d\n", i, i, i, af[i](i*100, i*10));
  return 0;
}

结果:

func0(000,00)=0
func1(100,10)=111
func2(200,20)=222
func3(300,30)=333

的确是对的。

例2

int (*pa)[4];

有点眼熟吧。这又是什么?
标识符只有pa,它的意义就是整个声明的核心了。“先右”,没有东西,“后左”边,是*,所以是指针。改写:

pa是指向(int (pa指向的东西)[4])的指针;

pa指向的东西”是int[4]数组。所以结论是(人类语言):
pa是指向数组的指针,数组元素为整型,长度为4。

一个例子

题目

写一个用二阶(二维)指针访问二维数组的程序。

错误的解答

#include <stdio.h>

#define COLUMN 2
#define ROW 3

int main(int argc, char* argv[], char* env[]) {
  int i, j;
  int * * p = NULL;
  int arr[ROW][COLUMN] = { { 0, 1 }, { 2, 3 }, { 4, 5 } };

  /* 按二维数组的方式来访问,没有问题 */
  printf("按二维数组的方式访问\n");
  for (i = 0; i < ROW; ++i) {
    for (j = 0; j < COLUMN; ++j) printf("%4d", arr[i][j]);
    printf("\n");
  }

  /* 按二阶指针的方式访问,报错“CodeTest.exe 中的 0x00163d81 处未处理的异常:
     0xC0000005: 读取位置 0x00000000 时发生访问冲突” */
  printf("按二阶指针的方式访问\n");
  p = (int * *)arr;
  for (i = 0; i < ROW; ++i) {
    for (j = 0; j < COLUMN; ++j) printf("%4d", p[i][j]);
    printf("\n");
  }

  return 0;
}

为什么错误

我们来看看p = (int * *)arr;发生了什么。在目前最常见的电脑平台(32或64位的CPU、32位操作系统)上,指针们都是4字节,而int也是4字节、int的低位在低字节(小端序)。于是:

表1 p和arr在内存里的状态

对应的变量 对应的值 地址(字节) 指向这里的p[i] p[i]指向的值
p 开始是NULL
赋值后是0x00100000
0x0010100B
0x0010100A
0x00101009
0x00101008(假设)
…… 各种东西 ……
arr[2][1] 5 0x00100017
0x00100016
0x00100015
0x00100014
arr[2][0] 4 0x00100013
0x00100012
0x00100011
0x00100010
arr[1][1] 3 0x0010000F
0x0010000E
0x0010000D
0x0010000C
arr[1][0] 2 0x0010000B
0x0010000A
0x00100009
0x00100008
p[2] 0x00000002
arr[0][1] 1 0x00100007
0x00100006
0x00100005
0x00100004
p[1] 0x00000001
arr[0][0] 0 0x00100003
0x00100002
0x00100001
0x00100000(假设)
p[0] 0x00000000

如果arrp的地址如上表里的值,那么在p = (int * *)arr;之后,p的值是0x00100000,可以解释成“0x00100000地址处有一个int*0x00100000地址开始有一堆int*”。
那么p[0]是什么呢?p[0]就是“0x00100000地址处的那个int*”,也就是说,从0x00100000地址开始、int*的大小(字节数)个字节,拼起来,当作一个int*。由于现在intint*大小相同,所以这个值就是arr[0][0],也就是0。
最后一步,p[0][0]是什么呢?是p[0]这个int*指向的一串int里的头一个,即地址0x00000000处的int。于是,执行的时候,程序得到的指示是,“从0x00000000地址处取一个int”,然而这个地址是不可以访问的,于是报错。
大家可以试试把arr[0][0]换成其它不是0的小数值(4,8,12之类的),它是多少,错误信息里“读取位置”后面就是多少。

应该怎么做

应该怎么做呢?
大家想一下这个错误的本质问题在哪里。本质问题在于,p指向的0x00100000地址,其实这个地址开始的数据的正确的类型是int[ROW][COLUMN],所以p[0]对应的类型应该是int[COLUMN]的,这和int*是有区别的,前者是“一片内存的起始地址的数值”,后者是“一个地址的变量”。
所以要改对的话,有两种方案,要么让p[0]不再表示int*,而是表示int[COLUMN];要么p[0]还是表示一个int*,但是这个int*自己有自己对应的变量(本质上是有自己的内存空间),而不是和其它变量抢同一个位置(不是指抢int*指向的地址,而是指int*变量本身占别人的地方)。
对应的两种写法是:

#include <stdio.h>

#define COLUMN 2
#define ROW 3

int main(int argc, char* argv[], char* env[]) {
  int i, j;
  int (* p)[COLUMN] = NULL;
  int arr[ROW][COLUMN] = { { 0, 1 }, { 2, 3 }, { 4, 5 } };

  /* 按二维数组的方式来访问,没有问题 */
  printf("按二维数组的方式访问\n");
  for (i = 0; i < ROW; ++i) {
    for (j = 0; j < COLUMN; ++j) printf("%4d", arr[i][j]);
    printf("\n");
  }

  /* 按二阶指针的方式访问,报错“CodeTest.exe 中的 0x00163d81 处未处理的异常:
     0xC0000005: 读取位置 0x00000000 时发生访问冲突” */
  printf("按二阶指针的方式访问\n");
  p = arr;
  for (i = 0; i < ROW; ++i) {
    for (j = 0; j < COLUMN; ++j) printf("%4d", p[i][j]);
    printf("\n");
  }

  return 0;
}

和:

#include <stdio.h>

#define COLUMN 2
#define ROW 3

int main(int argc, char* argv[], char* env[]) {
  int i, j;
  int * * p = NULL;
  int arr[ROW][COLUMN] = { { 0, 1 }, { 2, 3 }, { 4, 5 } };
  int * rows[ROW] = { arr[0], arr[1], arr[2] };

  /* 按二维数组的方式来访问,没有问题 */
  printf("按二维数组的方式访问\n");
  for (i = 0; i < ROW; ++i) {
    for (j = 0; j < COLUMN; ++j) printf("%4d", arr[i][j]);
    printf("\n");
  }

  /* 按二阶指针的方式访问,报错“CodeTest.exe 中的 0x00163d81 处未处理的异常:
     0xC0000005: 读取位置 0x00000000 时发生访问冲突” */
  printf("按二阶指针的方式访问\n");
  p = rows;
  for (i = 0; i < ROW; ++i) {
    for (j = 0; j < COLUMN; ++j) printf("%4d", p[i][j]);
    printf("\n");
  }

  return 0;
}

数组做函数参数

数组做参数的时候,只要有自己用循环来遍历数组元素的情况,函数参数就应该有一项数组元素数,这样是比较安全的做法。
而为了便于修改和检查,数组长度的那个常数值最好用#define定义成宏(在C++更推荐写const int定义常数变量,支持C++11的编译器更推荐写constexpr int定义,具体可以自己查一下资料)。