一维数组和指针具有如第一篇5.4节表5-1所示的关系。但要注意不要用错。
18.1.1 使用数组偏移量造成数组越界
【例18.1】有如下程序:
#include <stdio.h>
int main
(
)
{
int i
, a={1
,2
,3
,4}
,*p=a
;
for
( i=0
; i<5
; i++
,++p
)
printf
(\"%dt%ut%un\"
,*
(a+i
),a+i
,p
);
printf
(\"%ut%u n\"
,a
,p
);
printf
(\"%dt%dt%dt%dn\"
,*
(a+5
),a+5
,p
,*p
);
return 0
;
}
编译没有出错信息。输出结果如下:
1 1245036 1245036
2 1245040 1245040
3 1245044 1245044
4 1245048 1245048
4 1245052 1245052
1245036 1245056
1245120 1245056 1245056 1245120
找出程序中的错误。
其实,C语言中一维数组的大小必须在编译期间确定下来。也就是说,在定义数组时,数组的大小就是一个确定的常数。即使用语句
int a={1
,2
,3
,4}
;
定义数组,在编译时也会将数组的大小确定下来(这里数组的大小为4),不允许再变动。
数组的下标是从0开始到4结束。所以循环语句的i应使用“i<4”,最后一个有效的数组元素是a[3],输出语句超出边界,而p则越界两个元素的存储地址。第5行的输出都是第1次越界的信息。这时,指针还要执行一次加1操作,所以它的指向是1245056,而a是数组名,所以仍然是存储数组的首地址,也就是a[0]的存储首地址1245036,这就是第6行的输出内容。
将a执行a+5,从而验证了它和p的内容一样,而*(a+5)则和*p的一致,这就是第7行的输出。
由此可见,必须知道数组的边界,如果越界,就会像指针越界一样,造成错误甚至使系统崩溃。
由以上分析知,应删除最后一个输出语句并将循环改为如下形式:
for
( i=0
; i<4
; i++
, ++p
)
18.1.2 使用数组名进行错误运算
【例18.2】找出下面程序的错误。
#include <stdio.h>
int main
(
)
{
int i
, a={1
,2
,3
,4
,5}
,*p=a
;
p=a
;
for
( i=0
; i<5
; i++
,++a
,++p
)
printf
(\"%d %d \"
,*a
,*p
);
printf
(\"n\"
);
p=&a[4]
;
for
( i=0
; i<5
; i++
,--p
)
printf
(\"%d %d \"
,*
(a-i
),*p
);
printf
(\"n\"
);
a+2
;
printf
(\"%dn\"
,*a
);
return 0
;
}
错在混淆了数组和指针。对指针p来说,它可以是--p和++p。但对数组来说,a是数组名,始终代表数组存储的首地址。它虽然也相当于指针,但只是用来表示指向数组存储首地址的指针,本身不能作为左值,即“a=a+1”和“a=a-1”都是错误的。至于表达式“*(a+i)”,只是取“a[i]”数组的内容,i出现在表达式a+i中,只是表示相对a的地址偏移量,a的值并没有变化,所以是正确的。这个循环语句可以修改为:
for
( i=0
; i<5
; i++
,++p
)
printf
(\"%d %d \"
,*
(a+i
),*p
);
显然,第2个循环语句也是错的。a始终是数组名,所以a-1就越界了。从后面反序输出的起始数组是a[4],地址是&a[4],所以偏移量-i,正确的形式为:
for
( i=0
; i<5
; i++
,--p
)
printf
(\"%d %d \"
, *
(&a[4]-i
), *p
);
语句“a+2;”是无意义的,对程序运行的结果没有影响,但编译系统给出警告信息。
//改正后的完整程序
#include <stdio.h>
int main
(
)
{
int i
, a={1
,2
,3
,4
,5}
,*p=a
;
p=a
;
for
( i=0
; i<5
; i++
,++p
)
printf
(\"%d %d \"
,*
(a+i
),*p
);
printf
(\"n\"
);
p=&a[4]
;
for
( i=0
; i<5
; i++
,--p
)
printf
(\"%d %d \"
,*
(&a[4]-i
),*p
);
printf
(\"n\"
);
printf
(\"%dn\"
,*a
);
return 0
;
}
程序运行结果如下:
1 1 2 2 3 3 4 4 5 5
5 5 4 4 3 3 2 2 1 1
1
18.1.3 错误使用数组下标和指向数组指针的下标
【例18.3】找出下面程序的错误。
#include <stdio.h>
int main
(
)
{
int i
, a={1
,2
,3
,4
,5}
,*p=a
;
p=a
;
for
( i=0
; i<5
; i++
)
printf
(\"%d %d \"
, a[i]
, p[i]
);
printf
(\"n\"
);
p = &a[4]
;
for
( i=0
; i<5
; i++
)
printf
(\"%d %d \"
, *a[4-i]
, p[4-i]
);
printf
(\"n\"
);
printf
(\"%d %dn\"
,*a
,*p
);
return 0
;
}
第1个循环没有问题。第2个循环的关键是它们起始的下标。执行语句
p=&a[4]
;
之后,对指针而言,p[0]对应的是a[4],而p[1]则越界。p[-1]是a[3]。所以它的下标的计算方法是错的,应该使用p[-i]。a的表示方法与指针不一样,使用a[4-i]是正确的。计算时一定要注意,C语言的数组下标是从0开始。这里的错误是把a[4-i]误认为数组元素的指针,其实这里是标准的数组表示方法,不能使用“*”号。
执行完循环语句之后,p本身的值没有发生变化,仍然指向最后一个数组元素a[4],所以*p是最后一个数组元素的值,而a始终是数组名,*a就是第1个数组元素的值。
//
修改后的正确程序
#include <stdio.h>
int main
(
)
{
int i
, a={1
,2
,3
,4
,5}
,*p=a
;
p=a
;
for
( i=0
; i<5
; i++
)
printf
(\"%d %d \"
,a[i]
,p[i]
);
printf
(\"n\"
);
p=&a[4]
;
for
( i=0
; i<5
; i++
)
printf
(\"%d %d \"
,a[4-i]
,p[-i]
);
printf
(\"n\"
);
printf
(\"%d %dn\"
,*a
,*p
);
return 0
;
}
程序运行结果如下:
1 1 2 2 3 3 4 4 5 5
5 5 4 4 3 3 2 2 1 1
1 5
18.1.4 小结
从上面几个例子可以看出,使用数组和指针是相辅相成的,如果设计得好,能使程序简洁有效,达到事半功倍的效果。
1.不对称边界
C语言数组a[n]共有n个有效元素,其下标从0开始(这是有效元素的下标),至n结束,但n不是数组的有效元素,而是它不能达到的上界。有效上界是n-1。
元素个数=n-1-0+1=n
这就带来一个便利,声明数组时就给出了数组的个数,例如double b[10]就是具有10个实数的数组。而n是不可能达到的上界,区间为[0,10)。而在循环输出或赋值时,循环值小于这个n值,从而使计算简化为:
元素个数=n
对定义的数组a[n]而言,a[i](包括元素a[i])前面有i个元素,后面有n-i个元素,一共有n个元素。
2.指针的下标
a是数组的名字,也就是指向数组存储首地址的指针,a[0]是起点,a[-1]越界,下标不能为负值。
如果定义一个指向数组的指针p,则p的下标可正可负。P[0]就是初始化指针指向的数组元素,p[-1]越界。如果执行
p=&a[2]
;
语句,则p[0]=a[2],p[-1]=a[1],p[1]=a[3]。简言之,大于0是数组从该元素开始的正序,小于0是逆序。指针就像一朵云,可以到处飘荡,指针使用稍有不慎,就会出错。
3.灵活运用这些特征
编程中利用这些特征既可提高效率,又可避免错误。
【例18.4】有数组a[20]的值分别为:
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
现在编制了如下程序,目的是把前10个的值修改为
1 2 3 4 5 6 7 8 9 10
并把这十个值输出以检查程序是否正确。下面的程序对吗?
#include <stdio.h>
int main
(
)
{
int *p
, i
, a[20]
;
p = a
;
for
(i=0
; i<20
; i++
)
a[i]=11+i
;
for
(i=1
; i<=10
; i++
,*p++
)
*p=i
;
for
(i=0
; i<10
; i++
, p++
)
printf
(\"%4d\"
, *p
);
printf
(\"n\"
);
return 0
;
}
【解答】不对。对p操作会改变指向,但这是必要条件,不是充分条件。所以不要以为必须对p操作才会改变指向。指针变量的移动,使指针指向的地址也同步变化,即
for
(i=1
; i<=10
; i++
,*p++
)
*p=i
;
语句“*p++”的作用与“p++”一样,都移动了指针的指向。由此可见,在读入数据时,指针变量已经指向数组a[20]的第十一个元素的地址,即a[10]的地址。所以输出结果是
21 22 23 24 25 26 27 28 29 30
应先把指针的初始值回到&a[0],即把指针修改为指向a[0]。在输出之前简单地使用
p=a
;
语句即可实现。正确的程序在最后两句之前增加一句,即:
p=a
;
for
(i=0
; i<10
; i++
, p++
)
printf
(\"%4d\"
, *p
);
实际上,直接使用偏移量的概念编制程序,因为不移动指针指向,实现起来就非常简单。下面是完整的程序。
#include <stdio.h>
int main
(
)
{
int *p
, i
, a[20]
;
p = a
;
for
(i=0
; i<20
; i++
)
a[i]=11+i
;
for
(i=0
; i<=10
; i++
)
*
(p+i
)=1+i
;
for
(i=0
; i<10
; i++
)
printf
(\"%4d\"
, *
(p+i
));
printf
(\"n\"
);
return 0
;
}
程序输出结果如下:
1 2 3 4 5 6 7 8 9 10