📄 第9章 数 组.txt
字号:
C语言编程常见问题解答
发表日期:2003年10月2日 已经有1733位读者读过此文
第9章 数 组
C语言处理数组的方式是它广受欢迎的原因之一。C语言对数组的处理是非常有效的,其原因有以下三点:
第一,除少数翻译器出于谨慎会作一些繁琐的规定外,C语言的数组下标是在一个很低的层次上处理的。但这个优点也有一个反作用,即在程序运行时你无法知道一个数组到底有多大,或者一个数组下标是否有效。ANSI/ISOC标准没有对使用越界下标的行为作出定义,因此,一个越界下标有可能导致这样几种后果:
(1) 程序仍能正确运行;
(2) 程序会异常终止或崩溃;
(3) 程序能继续运行,但无法得出正确的结果;
(4) 其它情况。
换句话说,你不知道程序此后会做出什么反应,这会带来很大的麻烦。有些人就是抓住这一点来批评C语言的,认为C语言只不过是一种高级的汇编语言。然而,尽管C程序出错时的表现有些可怕,但谁也不能否认一个经过仔细编写和调试的C程序运行起来是非常快的。
第二,数组和指针能非常和谐地在一起工作。当数组出现在一个表达式中时,它和指向数组中第一个元素的指针是等价的,因此数组和指针几乎可以互换使用。此外,使用指针要比使用数组下标快两倍(请参见9.5中的例子)。
第三,将数组作为参数传递给函数和将指向数组中第一个元素的指针传递给函数是完全·
等价的。将数组作为参数传递给函数时可以采用值传递和地址传递两种方式,前者需要完整地拷贝初始数组,但比较安全;后者的速度要快得多,但编写程序时要多加小心。C++和ANSIC中都有const关键字,利用它可以使地址传递方式和值传递方式一样安全。如果你想了解更多的细节,请参见2.4,8.6和第7章“指针和内存分配”开头部分的介绍。
数组和指针之间的这种联系会引起一些混乱,例如以下两种定义是完全相同的:
void f(chara[MAX])
{
/*... */
}
void f(char *a)
{ ·
/*... */
}
注意:MAX是一个编译时可知的值,例如用#define预处理指令定义的值。
这种情况正是前文中提到的第三个优点,也是大多数C程序员所熟知的。这也是唯一一种数组和指针完全相同的情况,在其它情况下,数组和指针并不完全相同。例如,当作如下定义 (可以出现在函数说明以外的任何地方)时:
char a[MAX];
系统将分配MAX个字符的内存空间。当作如下说明时:
char *a;
系统将分配一个字符指针所需的内存空间,可能只能容纳2个或4个字符。如果你在源文件中作如下定义:
char a[MAX];
但在头文件作如下说明;
extern char *a;
就会导致可怕的后果。为了避免出现这种情况,最好的办法是保证上述说明和定义的一致性,例如,如果在源文件中作如下定义:
char a[MAX];
那么在相应的头文件中就作如下说明,
externchar a[];
上述说明告诉头文件a是一个数组,不是一个指针,但它并不指示数组a中有多少个元素,这样说明的类型称为不完整类型。在程序中适当地说明一些不完整类型是很常见的,也是一种很好的编程习惯。
9.1 数组的下标总是从0开始吗?
是的,对数组a[MAX](MAX是一个编译时可知的值)来说,它的第一个和最后一个元素分别是a[o]和aLMAX-1)。在其它一些语言中,情况可能有所不同,例如在BASIC语言中数组a[MAX]的元素是从a[1]到a[MAX],在Pascal语言中则两种方式都可行。
注意:a[MAX]是一个有效的地址,但该地址中的值并不是数组a的一个元素(见9。2)。
上述这种差别有时会引起混乱,因为当你说“数组中的第一个元素”时,实际上是指“数组中下标为。的元素”,这里的“第一个”的意思和“最后一个”相反。
尽管你可以假造一个下标从1开始的数组,但在实际编程中不应该这样做。下文将介绍这种技巧,并说明为什么不应该这样做的原因。
因为指针和数组几乎是相同的,因此你可以定义一个指针,使它可以象一个数组一样引用另一个数组中的所有元素,但引用时前者的下标是从1开始的:
/*don't do this!!*/
int a0[MAX],
int *a1=a0-1; /*&a0[-1)*/
现在,a0[0]和a1[1)是相同的,而a0[MAX-1]和a1[MAX]是相同的。然而,在实际编程中不应该这样做,其原因有以下两点:
第一,这种方法可能行不通。这种行为是ANSI/ISOC标准所没有定义的(并且是应该避免的),而&a0[-1)完全有可能不是一个有效的地址(见9.3)。对于某些编译程序,你的程序可能根本不会出问题;在有些情况下,对于任何编译程序,你的程序可能都不会出问题;但是,谁能保证你的程序永远不会出问题呢?
第二,这种方式背离了C语言的常规风格。人们已经习惯了C语言中数组下标的工作方式,如果你的程序使用了另外一种方式,别人就很难读懂你的程序,而经过一段时间以后,连你自己都可能很难读懂这个程序了。
请参见:
9.2 可以使用数组后面第一个元素的地址吗?
9.3 为什么要小心对待位于数组后面的那些元素的地址呢?
9.2 可以使用数组后面第一个元素的地址吗?
你可以使用数组后面第一个元素的地址,但你不可以查看该地址中的值。对大多数编译程序来说,如果你写如下语句:
int i,a[MAX],j;
那么i和j都有可能存放在数组a最后一个元素后面的地址中。为了判断跟在数组a后面的是i还是j,你可以把i或j的地址和数组a后面第一个元素的地址进行比较,即判断"&i==&a[MAX]"或"&j==&a[MAX]"是否为真。这种方法通常可行,但不能保证。
问题的关键是:如果你将某些数据存入a[MAX]中,往往就会破坏原来紧跟在数组a后面的数据。即使查看a[MAX]的值也是应该避免的,尽管这样做一般不会引出什么问题。
为什么在C程序中有时要用到&a[MAX]呢?因为很多C程序员习惯通过指针遍历一个数组中的所有元素,即用
for(i=0;i<MAX;++i)
{
/*do something*/
}
代替
for(p=a; p<&a[MAX];++p)
{
/*do something*/
}
这种方式在已有的C程序中是随处可见的,因此ANSIC标准规定这种方式是可行的。
请参见:
9.3 为什么要小心对待位于数组后面的那些元素的地址呢?
9.5 通过指针或带下标的数组名都可以访问数组中的元素,哪一种方式更好呢?
9.3 为什么要小心对待位于数组后面的那些元素的地址呢?
如果你的程序是在理想的计算机上运行,即它的取址范围是从00000000到FFFFFFFF,那么你大可以放心,但是,实际情况往往不会这么简单。
在有些计算机上,地址是由两部分组成的,第一部分是一个指向某一块内存的起始点的指,针(即基地址),第二部分是相对于这块内存的起始点的地址偏移量。这种地址结构被称为段地址结构,子程序调用通常就是通过在栈指针上加上一个地址偏移量来实现的。采用段地址结构的最典型的例子是基于Intel 8086的计算机,所有的MS-DOS程序都在这种计算机上运行(在基于Pentium芯片的计算机上,大多数MS-DOS程序也在与8086兼容的模式下运行)。即使是性能优越的具有线性地址空间的RISC芯片,也提供了寄存器变址寻址方式,即用一个寄存器保存指向某一块内存的起始点的指针,用另一个寄存器保存地址偏移量。
如果你的程序使用段地址结构,而在基地址处刚好存放着数组a0(即基地址指针和&a0[0]相同),这会引出什么问题呢?既然基地址无法(有效地)改变,而偏移量也不可能是负值,因此“位于a0[0]前面的元素”这种说法就没有意义了,ANSIC标准明确规定引用这个元素的行为是没有定义的,这也就是9.1中所提到的方法可能行不通的原因。
同样,如果数组a(其元素个数为MAX)刚好存放在某段内存的尾部,那么地址&a[MAX]就是没有意义的,如果你的程序中使用了&a[MAX],而编译程序又要检查&a[MAX]是否有效,那么编译程序必然就会报告没有足够的内存来存放数组a。
尽管在编写基于Windows,UNIX或Macintosh的程序时不会遇到上述问题,但是C语言不仅仅是为这几种情况设计的,C语言必须适应各种各样的环境,例如用微处理器控制的烤面包炉,防抱死刹车系统,MS-DOS,等等。严格按C语言标准编写的程序能被顺利地编译并能服务于任何目的,但是,有时程序员也可以适度地背离C语言的标准,这要视程序员、编译程序和程序用户三者的具体要求而定。
请参见:
9. 1数组的下标总是从0开始吗?
9.2可以使用数组后面第一个元素的地址吗?
9.4 在把数组作为参数传递给函数时,可以通过sizeof运算符告诉函数数组的大小吗?
不可以。当把数组作为函数的参数时,你无法在程序运行时通过数组参数本身告诉函数该数组的大小,因为函数的数组参数相当于指向该数组第一个元素的指针。这意味着把数组传递给函数的效率非常高,也意味着程序员必须通过某种机制告诉函数数组参数的大小。
为了告诉函数数组参数的大小,人们通常采用以下两种方法:
第一种方法是将数组和表示数组大小的值一起传递给函数,例如memcpy()函数就是这样做的:
char source[MAX],dest[MAX];
/*... */
memcpy(dest,source,MAX);
第二种方法是引入某种规则来结束一个数组,例如在C语言中字符串总是以ASCII字符NUL('\0')结束,而一个指针数组总是以空指针结束。请看下述函数,它的参数是一个以空指
针结束的字符指针数组,这个空指针告诉该函数什么时候停止工作:
void printMany(char *strings口)
{
int i;
i=0;
while(strings[i]!=NULL)
{
puts(strings[i]);
++i;
}
}
正象9.5中所说的那样,C程序员经常用指针来代替数组下标,因此大多数C程序员通常会将上述函数编写得更隐蔽一些:
void printMany(char *strings[])
{
while(*strings)
{
puts(*strings++);
}
}
尽管你不能改变一个数组名的值,但是strings是一个数组参数,相当于一个指针,因此可以对它进行自增运算,并且可以在调用puts()函数时对strings进行自增运算。在上例中,while(*strings)
就相当于
while(*strings !=NULL)
在写函数文档(例如在函数前面加上注释,或者写一份备忘录,或者写一份设计文档)时,写进函数是如何知道数组参数的大小是非常重要的,例如,你可以非常简略地写上“以空指针结束”或“数组elephants中有numElephants个元素”(如果你在程序中用数字13表示数组的大小,你可以写进“数组arr中有13个元素”这样的描述,然而用确切的数字表示数组的大小不是一种好的编程习惯)。
请参见:
9.5通过指针或带下标的数组名都可以访问数组中的元素,哪一种方式更好呢?
9.6可以把另外一个地址赋给一个数组名吗?
9.5 通过指针或带下标的数组名都可以访问数组中的元素,哪一种方式更好呢?
与使用下标相比,使用指针能使C编译程序更容易地产生优质的代码。
假设你的程序中有这样一段代码:
/* X la some type */
X a[MAX];
X *p; /*pointer*/
X x; /*element*/
int i; /*index*/
为了历数组a中的所有元素,你可以采用这样一种循环方式(方式a)
/*version (a)*/
for (i = 0; i<MAX; ++i)
{
x=a[i];
/* do something with x * /
}
你也可以采用这样一种循环方式(方式b)
/*veraion(b)*/
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -