指针和数组有着密切的关系
,
任何能由数组下标完成的操作也都可用指针来实现
,
但程序中使用指针可使代码更紧凑、更灵活。
一、指向数组元素的指针
我们定义一个整型数组和一个指向整型的指针变量 :
int a[10], *p;
和前面介绍过的方法相同 , 可以使整型指针 p 指向数组中任何一个元素 , 假定给出赋值运算
p=&a[0];
此时 ,p 指向数组中的第 0 号元素 , 即 a[0], 指针变量 p 中包含了数组元素 a[0] 的地址 , 由于数组元素在内存中是连续存放的 , 因此 , 我们就可以通过指针变量 p 及其有关运算间接访问数组中的任何一个元素。
Turbo C 中 , 数组名是数组的第 0 号元素的地址 , 因此下面两个语句是等价的
p=&a[0];
p=a;
根据地址运算规则 ,a+1 为 a[1] 的地址 ,a+i 就为 a[i] 的地址。
下面我们用指针给出数组元素的地址和内容的几种表示形式:
(1). p+i 和 a+i 均表示 a[i] 的地址 , 或者讲 , 它们均指向数组第 i 号元素 , 即指向 a[i] 。
(2). *(p+i) 和 *(a+i) 都表示 p+i 和 a+i 所指对象的内容 , 即为 a[i] 。
(3). 指向数组元素的指针 , 也可以表示成数组的形式 , 也就是说 , 它允许指针变量带下标 , 如 p[i] 与 *(p+i) 等价。
假若 : p=a+5;
则 p[2] 就相当于 *(p+2), 由于 p 指向 a[5], 所以 p[2] 就相当于 a[7] 。而 p[-3] 就相当于 *(p-3), 它表示 a[2] 。
二、指向二维数组的指针
1. 二维数组元素的地址
为了说明问题 , 我们定义以下二维数组 :
int a[3][4]={{0,1,2,3}, {4,5,6,7}, {8,9,10,11}};
a 为二维数组名 , 此数组有 3 行 4 列 , 共 12 个元素。但也可这样来理解 , 数组 a 由三个元素组成 :a[0],a[1],a[2] 。而每个元素又是一个一维数组 , 且都含有 4 个元素 ( 相当于 4 列 ), 例如 ,a[0] 所代表的一维数组所包含的 4 个元素为 a[0][0], a[0][1], a[0][2], a[0][3] 。如图所示 :
______ _______________
a---| a[0] | ____ | 0 | 1 | 2 | 3 |
|______| |___|___|___|___|
| a[1] | ____ | 4 | 5 | 6 | 7 |
|______| |___|___|___|___|
| a[2] | ____ | 8 | 9 | 10| 11|
|______| |___|___|___|___|
但从二维数组的角度来看 ,a 代表二维数组的首地址 , 当然也可看成是二维数组第 0 行的首地址。 a+1 就代表第 1 行的首地址 ,a+2 就代表第 2 行的首地址。如果此二维数组的首地址为 1000, 由于第 0 行有 4 个整型元素 , 所以 a+1 为 1008,a+2 也就为 1016 。如图所示
_______________
(1000) ____ | 0 | 1 | 2 | 3 |
|___|___|___|___|
(1008) ____ | 4 | 5 | 6 | 7 |
|___|___|___|___|
(1016) ____ | 8 | 9 | 10| 11|
|___|___|___|___|
既然我们把 a[0],a[1],a[2] 看成是一维数组名 , 可以认为它们分别代表它们所对应的数组的首地址 , 也就是讲 ,a[0] 代表第 0 行中第 0 列元素的地址 , 即 &a[0][0], a[1] 是第 1 行中第 0 列元素的地址 , 即 &a[1][0], 根据地址运算规则 ,a[0]+1 即代表第 0 行第 1 列元素的地址 , 即 &a[0][1], 一般而言 ,a[i]+j 即代表第 i 行第 j 列元素的地址 , 即 &a[i][j] 。
另外 , 在二维数组中 , 我们还可用指针的形式来表示各元素的地址。如前所述 ,a[0] 与 *(a+0) 等价 ,a[1] 与 *(a+1) 等价 , 因此 a[i]+j 就与 *(a+i)+j 等价 , 它表示数组元素 a[i][j] 的地址。
因此 , 二维数组元素 a[i][j] 可表示成 *(a[i]+j) 或 *(*(a+i)+j), 它们都与 a[i][j] 等价 , 或者还可写成 (*(a+i))[j] 。
另外 , 要补充说明一下 , 果你编写一个程序输出打印 a 和 *a, 你可发现它们的值是相同的 , 这是为什么呢 ? 我们可这样来理解 :
首先 , 为了说明问题 , 我们把二维数组人为地看成由三个数组元素 a[0],a[1],a[2] 组成 , 将 a[0],a[1],a[2] 看成是数组名它们又分别是由 4 个元素组成的一维数组。因此 ,a 表示数组第 0 行的地址 , 而 *a 即为 a[0], 它是数组名 , 当然还是地址 , 它就是数组第 0 行第 0 列元素的地址。
2. 指向一个由 n 个元素所组成的数组指针
在 Turbo C 中 , 可定义如下的指针变量 :
int (*p)[3];
指针 p 为指向一个由 3 个元素所组成的整型数组指针。在定义中 , 圆括号是不能少的 , 否则它是指针数组 , 这将在后面介绍。这种数组的指针不同于前面介绍的整型指针 , 当整型指针指向一个整型数组的元素时 , 进行指针 ( 地址 ) 加 1 运算 , 表示指向数组的下一个元素 , 此时地址值增加了 2( 因为放大因子为 2), 而如上所定义的指向一个由 3 个元素组成的数组指针 , 进行地址加 1 运算时 , 其地址值增加了 6( 放大因子为 2x3=6),
这种数组指针在 Turbo C 中用得较少 , 但在处理二维数组时 , 还是很方便的。例如 :
int a[3][4], (*p)[4];
p=a;
开始时 p 指向二维数组第 0 行 , 当进行 p+1 运算时 , 根据地址运算规则 , 此时放大因子为 4x2=8, 所以此时正好指向二维数组的第 1 行。和二维数组元素地址计算的规则一样 ,*p+1 指向 a[0][1],*(p+i)+j 则指向数组元素 a[i][j] 。
例:
int a[3][4]={
{1,3,5,7},
{9,11,13,15},
{17,19,21,23}
};
main()
{
int i,(*b)[4];
b=a+1; /* b 指向二维数组的第 1 行 , 此时 *b[0] 是 a[1][0] */
for(i=1;i<=4;b=b[0]+2,i++) /* 修改 b 的指向 , 每次增加 2 */
printf("%d\t",*b[0]);
printf("\n");
for(i=0; i<3; i++)
{
b=a+i; /* 修改 b 的指向 , 每次跳过二维数组的一行 */
printf("%d\t",*(b[i]+1));
}
printf ("\n");
}
程序运行结果如下 :
9 13 17 21
3 11 19
三、字符指针
我们已经知道 , 字符串常量是由双引号括起来的字符序列 , 例如 :
"a string"
就是一个字符串常量 , 该字符串中因为字符 a 后面还有一个空格字符 , 所以它由 8 个字符序列组成。在程序中如出现字符串常量 C 编译程序就给字符串常量按排一存贮区域 , 这个区域是静态的 , 在整个程序运行的过程中始终占用 , 平时所讲的字符串常量的长度是指该字符串的字符个数 , 但在按排存贮区域时 , C 编译程序还自动给该字符串序列的末尾加上一个空字符 '\0', 用来标志字符串的结束 , 因此一个字符串常量所占的存贮区域的字节数总比它的字符个数多一个字节。
Turbo C 中操作一个字符串常量的方法有 :
(1). 把字符串常量存放在一个字符数组之中 , 例如 :
char s[]="a string";
数组 s 共有 9 个元素所组成 , 其中 s[8] 中的内容是 '\0' 。实际上 , 在字符数组定义的过程中 , 编译程序直接把字符串复写到数组中 , 即对数组 s 初始化。
(2). 用字符指针指向字符串 , 然后通过字符指针来访问字符串存贮区域。当字符串常量在表达式中出现时 ,
根据数组的类型转换规则 , 它被转换成字符指针。因此 , 若我们定义了一字符指针 cp:
char *cp;
于是可用 :
cp="a string";
使 cp 指向字符串常量中的第 0 号字符 a, 如图所示。
___________________________________
CP ----- | a | | s | t | r | i | n | g | \0|
|___|___|___|___|___|___|___|___|___|
以后我们可通过 cp 来访问这一存贮区域 , 如 *cp 或 cp[0] 就是字符 a, 而 cp[i] 或 *(cp+i) 就相当于字符串的第 i 号字符 , 但企图通过指针来修改字符串常量的行为是没有意义的。
四、指针数组
因为指针是变量 , 因此可设想用指向同一数据类型的指针来构成一个数组 , 这就是指针数组。数组中的每个元素都是指针变量 , 根据数组的定义 , 指针数组中每个元素都为指向同一数据类型的指针。指针数组的定义格式为 :
类型标识 * 数组名 [ 整型常量表达式 ];
例如 :
int *a[10];
定义了一个指针数组 , 数组中的每个元素都是指向整型量的指针 , 该数组由 10 个元素组成 , 即 a[0],a[1],a[2], ..., a[9], 它们均为指针变量。 a 为该指针数组名 , 和数组一样, a 是常量 , 不能对它进行增量运算。 a 为指针数组元素 a[0] 的地址 ,a+i 为 a[i] 的地址 ,*a 就是 a[0],*(a+i) 就是 a[i] 。
为什么要定义和使用指针数组呢 ? 主要是由于指针数组对处理字符串提供了更大的方便和灵活 , 使用二维数组对处理长度不等的正文效率低,而指针数组由于其中每个元素都为指针变量 , 因此通过地址运算来操作正文行是十分方便的。
指针数组和一般数组一样 , 允许指针数组在定义时初始化,但由于指针数组的每个元素是指针变量 , 它只能存放地址 , 所以对指向字符串的指针数组在说明赋初值时 , 是把存放字符串的首地址赋给指针数组的对应元素 ,
例如下面是一个书写函数 month_name(n), 函数返回一个指向包含第 n 月名字的字符指针 ( 关于函数指针和指针函数 , 下一节将专门介绍 ) 。
例 : 打印 1 月至 12 月的月名 :
char *month_name(int n)
{
static char *name[]={
"Illegal month",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
return((n<1||n>12)?name[0]:name[n]);
}
main()
{
int i;
for(i=0; i<13; i++)
printf("%s\n", month_name(i));
}
对于指针这一节,一定要多练习一些题。指针是一个很重要的概念,必须多接触实际的问题才能掌握它。
一、指向数组元素的指针
我们定义一个整型数组和一个指向整型的指针变量 :
int a[10], *p;
和前面介绍过的方法相同 , 可以使整型指针 p 指向数组中任何一个元素 , 假定给出赋值运算
p=&a[0];
此时 ,p 指向数组中的第 0 号元素 , 即 a[0], 指针变量 p 中包含了数组元素 a[0] 的地址 , 由于数组元素在内存中是连续存放的 , 因此 , 我们就可以通过指针变量 p 及其有关运算间接访问数组中的任何一个元素。
Turbo C 中 , 数组名是数组的第 0 号元素的地址 , 因此下面两个语句是等价的
p=&a[0];
p=a;
根据地址运算规则 ,a+1 为 a[1] 的地址 ,a+i 就为 a[i] 的地址。
下面我们用指针给出数组元素的地址和内容的几种表示形式:
(1). p+i 和 a+i 均表示 a[i] 的地址 , 或者讲 , 它们均指向数组第 i 号元素 , 即指向 a[i] 。
(2). *(p+i) 和 *(a+i) 都表示 p+i 和 a+i 所指对象的内容 , 即为 a[i] 。
(3). 指向数组元素的指针 , 也可以表示成数组的形式 , 也就是说 , 它允许指针变量带下标 , 如 p[i] 与 *(p+i) 等价。
假若 : p=a+5;
则 p[2] 就相当于 *(p+2), 由于 p 指向 a[5], 所以 p[2] 就相当于 a[7] 。而 p[-3] 就相当于 *(p-3), 它表示 a[2] 。
二、指向二维数组的指针
1. 二维数组元素的地址
为了说明问题 , 我们定义以下二维数组 :
int a[3][4]={{0,1,2,3}, {4,5,6,7}, {8,9,10,11}};
a 为二维数组名 , 此数组有 3 行 4 列 , 共 12 个元素。但也可这样来理解 , 数组 a 由三个元素组成 :a[0],a[1],a[2] 。而每个元素又是一个一维数组 , 且都含有 4 个元素 ( 相当于 4 列 ), 例如 ,a[0] 所代表的一维数组所包含的 4 个元素为 a[0][0], a[0][1], a[0][2], a[0][3] 。如图所示 :
______ _______________
a---| a[0] | ____ | 0 | 1 | 2 | 3 |
|______| |___|___|___|___|
| a[1] | ____ | 4 | 5 | 6 | 7 |
|______| |___|___|___|___|
| a[2] | ____ | 8 | 9 | 10| 11|
|______| |___|___|___|___|
但从二维数组的角度来看 ,a 代表二维数组的首地址 , 当然也可看成是二维数组第 0 行的首地址。 a+1 就代表第 1 行的首地址 ,a+2 就代表第 2 行的首地址。如果此二维数组的首地址为 1000, 由于第 0 行有 4 个整型元素 , 所以 a+1 为 1008,a+2 也就为 1016 。如图所示
_______________
(1000) ____ | 0 | 1 | 2 | 3 |
|___|___|___|___|
(1008) ____ | 4 | 5 | 6 | 7 |
|___|___|___|___|
(1016) ____ | 8 | 9 | 10| 11|
|___|___|___|___|
既然我们把 a[0],a[1],a[2] 看成是一维数组名 , 可以认为它们分别代表它们所对应的数组的首地址 , 也就是讲 ,a[0] 代表第 0 行中第 0 列元素的地址 , 即 &a[0][0], a[1] 是第 1 行中第 0 列元素的地址 , 即 &a[1][0], 根据地址运算规则 ,a[0]+1 即代表第 0 行第 1 列元素的地址 , 即 &a[0][1], 一般而言 ,a[i]+j 即代表第 i 行第 j 列元素的地址 , 即 &a[i][j] 。
另外 , 在二维数组中 , 我们还可用指针的形式来表示各元素的地址。如前所述 ,a[0] 与 *(a+0) 等价 ,a[1] 与 *(a+1) 等价 , 因此 a[i]+j 就与 *(a+i)+j 等价 , 它表示数组元素 a[i][j] 的地址。
因此 , 二维数组元素 a[i][j] 可表示成 *(a[i]+j) 或 *(*(a+i)+j), 它们都与 a[i][j] 等价 , 或者还可写成 (*(a+i))[j] 。
另外 , 要补充说明一下 , 果你编写一个程序输出打印 a 和 *a, 你可发现它们的值是相同的 , 这是为什么呢 ? 我们可这样来理解 :
首先 , 为了说明问题 , 我们把二维数组人为地看成由三个数组元素 a[0],a[1],a[2] 组成 , 将 a[0],a[1],a[2] 看成是数组名它们又分别是由 4 个元素组成的一维数组。因此 ,a 表示数组第 0 行的地址 , 而 *a 即为 a[0], 它是数组名 , 当然还是地址 , 它就是数组第 0 行第 0 列元素的地址。
2. 指向一个由 n 个元素所组成的数组指针
在 Turbo C 中 , 可定义如下的指针变量 :
int (*p)[3];
指针 p 为指向一个由 3 个元素所组成的整型数组指针。在定义中 , 圆括号是不能少的 , 否则它是指针数组 , 这将在后面介绍。这种数组的指针不同于前面介绍的整型指针 , 当整型指针指向一个整型数组的元素时 , 进行指针 ( 地址 ) 加 1 运算 , 表示指向数组的下一个元素 , 此时地址值增加了 2( 因为放大因子为 2), 而如上所定义的指向一个由 3 个元素组成的数组指针 , 进行地址加 1 运算时 , 其地址值增加了 6( 放大因子为 2x3=6),
这种数组指针在 Turbo C 中用得较少 , 但在处理二维数组时 , 还是很方便的。例如 :
int a[3][4], (*p)[4];
p=a;
开始时 p 指向二维数组第 0 行 , 当进行 p+1 运算时 , 根据地址运算规则 , 此时放大因子为 4x2=8, 所以此时正好指向二维数组的第 1 行。和二维数组元素地址计算的规则一样 ,*p+1 指向 a[0][1],*(p+i)+j 则指向数组元素 a[i][j] 。
例:
int a[3][4]={
{1,3,5,7},
{9,11,13,15},
{17,19,21,23}
};
main()
{
int i,(*b)[4];
b=a+1; /* b 指向二维数组的第 1 行 , 此时 *b[0] 是 a[1][0] */
for(i=1;i<=4;b=b[0]+2,i++) /* 修改 b 的指向 , 每次增加 2 */
printf("%d\t",*b[0]);
printf("\n");
for(i=0; i<3; i++)
{
b=a+i; /* 修改 b 的指向 , 每次跳过二维数组的一行 */
printf("%d\t",*(b[i]+1));
}
printf ("\n");
}
程序运行结果如下 :
9 13 17 21
3 11 19
三、字符指针
我们已经知道 , 字符串常量是由双引号括起来的字符序列 , 例如 :
"a string"
就是一个字符串常量 , 该字符串中因为字符 a 后面还有一个空格字符 , 所以它由 8 个字符序列组成。在程序中如出现字符串常量 C 编译程序就给字符串常量按排一存贮区域 , 这个区域是静态的 , 在整个程序运行的过程中始终占用 , 平时所讲的字符串常量的长度是指该字符串的字符个数 , 但在按排存贮区域时 , C 编译程序还自动给该字符串序列的末尾加上一个空字符 '\0', 用来标志字符串的结束 , 因此一个字符串常量所占的存贮区域的字节数总比它的字符个数多一个字节。
Turbo C 中操作一个字符串常量的方法有 :
(1). 把字符串常量存放在一个字符数组之中 , 例如 :
char s[]="a string";
数组 s 共有 9 个元素所组成 , 其中 s[8] 中的内容是 '\0' 。实际上 , 在字符数组定义的过程中 , 编译程序直接把字符串复写到数组中 , 即对数组 s 初始化。
(2). 用字符指针指向字符串 , 然后通过字符指针来访问字符串存贮区域。当字符串常量在表达式中出现时 ,
根据数组的类型转换规则 , 它被转换成字符指针。因此 , 若我们定义了一字符指针 cp:
char *cp;
于是可用 :
cp="a string";
使 cp 指向字符串常量中的第 0 号字符 a, 如图所示。
___________________________________
CP ----- | a | | s | t | r | i | n | g | \0|
|___|___|___|___|___|___|___|___|___|
以后我们可通过 cp 来访问这一存贮区域 , 如 *cp 或 cp[0] 就是字符 a, 而 cp[i] 或 *(cp+i) 就相当于字符串的第 i 号字符 , 但企图通过指针来修改字符串常量的行为是没有意义的。
四、指针数组
因为指针是变量 , 因此可设想用指向同一数据类型的指针来构成一个数组 , 这就是指针数组。数组中的每个元素都是指针变量 , 根据数组的定义 , 指针数组中每个元素都为指向同一数据类型的指针。指针数组的定义格式为 :
类型标识 * 数组名 [ 整型常量表达式 ];
例如 :
int *a[10];
定义了一个指针数组 , 数组中的每个元素都是指向整型量的指针 , 该数组由 10 个元素组成 , 即 a[0],a[1],a[2], ..., a[9], 它们均为指针变量。 a 为该指针数组名 , 和数组一样, a 是常量 , 不能对它进行增量运算。 a 为指针数组元素 a[0] 的地址 ,a+i 为 a[i] 的地址 ,*a 就是 a[0],*(a+i) 就是 a[i] 。
为什么要定义和使用指针数组呢 ? 主要是由于指针数组对处理字符串提供了更大的方便和灵活 , 使用二维数组对处理长度不等的正文效率低,而指针数组由于其中每个元素都为指针变量 , 因此通过地址运算来操作正文行是十分方便的。
指针数组和一般数组一样 , 允许指针数组在定义时初始化,但由于指针数组的每个元素是指针变量 , 它只能存放地址 , 所以对指向字符串的指针数组在说明赋初值时 , 是把存放字符串的首地址赋给指针数组的对应元素 ,
例如下面是一个书写函数 month_name(n), 函数返回一个指向包含第 n 月名字的字符指针 ( 关于函数指针和指针函数 , 下一节将专门介绍 ) 。
例 : 打印 1 月至 12 月的月名 :
char *month_name(int n)
{
static char *name[]={
"Illegal month",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
return((n<1||n>12)?name[0]:name[n]);
}
main()
{
int i;
for(i=0; i<13; i++)
printf("%s\n", month_name(i));
}
对于指针这一节,一定要多练习一些题。指针是一个很重要的概念,必须多接触实际的问题才能掌握它。