图片网站cms,wordpress知名中国网站,竞价专员是做什么的,晋中住房保障和城乡建设局网站再论C语言数组 C语言处理数组的方式是它广受欢迎的原因之一。C语言对数组的处理是非常有效的#xff0c;其原因有以下三点#xff1a;第一#xff0c;除少数翻译器出于谨慎会作一些繁琐的规定外#xff0c;C语言的数组下标是在一个很低的层次上处理的。但这个优点也有一个反…再论C语言数组 C语言处理数组的方式是它广受欢迎的原因之一。C语言对数组的处理是非常有效的其原因有以下三点第一除少数翻译器出于谨慎会作一些繁琐的规定外C语言的数组下标是在一个很低的层次上处理的。但这个优点也有一个反作用即在程序运行时你无法知道一个数组到底有多大或者一个数组下标是否有效。ANSI/ISOC标准没有对使用越界下标的行为作出定义因此一个越界下标有可能导致这样几种后果(1) 程序仍能正确运行(2) 程序会异常终止或崩溃(3) 程序能继续运行但无法得出正确的结果(4) 其它情况。换句话说你不知道程序此后会做出什么反应这会带来很大的麻烦。有些人就是抓住这一点来批评C语言的认为C语言只不过是一种高级的汇编语言。然而尽管C程序出错时的表现有些可怕但谁也不能否认一个经过仔细编写和调试的C程序运行起来是非常快的。第二数组和指针能非常和谐地在一起工作。当数组出现在一个表达式中时它和指向数组中第一个元素的指针是等价的因此数组和指针几乎可以互换使用。此外使用指针要比使用数组下标快两倍。第三将数组作为参数传递给函数和将指向数组中第一个元素的指针传递给函数是完全等价的。将数组作为参数传递给函数时可以采用值传递和地址传递两种方式前者需要完整地拷贝初始数组但比较安全后者的速度要快得多但编写程序时要多加小心。C和ANSIC中都有const关键字利用它可以使地址传递方式和值传递方式一样安全。数组和指针之间的这种联系会引起一些混乱例如以下两种定义是完全相同的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中有多少个元素这样说明的类型称为不完整类型。在程序中适当地说明一些不完整类型是很常见的也是一种很好的编程习惯。是的对数组a[MAX](MAX是一个编译时可知的值)来说它的第一个和最后一个元素分别是a[o]和aLMAX-1)。在其它一些语言中情况可能有所不同例如在BASIC语言中数组a[MAX]的元素是从a[1]到a[MAX]在Pascal语言中则两种方式都可行。注意a[MAX]是一个有效的地址但该地址中的值并不是数组a的一个元素。上述这种差别有时会引起混乱因为当你说“数组中的第一个元素”时实际上是指“数组中下标为。的元素”这里的“第一个”的意思和“最后一个”相反。尽管你可以假造一个下标从1开始的数组但在实际编程中不应该这样做。下文将介绍这种技巧并说明为什么不应该这样做的原因。因为指针和数组几乎是相同的因此你可以定义一个指针使它可以象一个数组一样引用另一个数组中的所有元素但引用时前者的下标是从1开始的/*dont do this!!*/int a0[MAX]int *a1a0-1; /*a0[-1)*/现在a0[0]和a1[1)是相同的而a0[MAX-1]和a1[MAX]是相同的。然而在实际编程中不应该这样做其原因有以下两点第一这种方法可能行不通。这种行为是ANSI/ISOC标准所没有定义的(并且是应该避免的)而a0[-1)完全有可能不是一个有效的地址(见93)。对于某些编译程序你的程序可能根本不会出问题在有些情况下对于任何编译程序你的程序可能都不会出问题但是谁能保证你的程序永远不会出问题呢? 第二这种方式背离了C语言的常规风格。人们已经习惯了C语言中数组下标的工作方式如果你的程序使用了另外一种方式别人就很难读懂你的程序而经过一段时间以后连你自己都可能很难读懂这个程序了。 你可以使用数组后面第一个元素的地址但你不可以查看该地址中的值。对大多数编译程序来说如果你写如下语句 int ia[MAX]j; 那么i和j都有可能存放在数组a最后一个元素后面的地址中。为了判断跟在数组a后面的是i还是j你可以把i或j的地址和数组a后面第一个元素的地址进行比较即判断ia[MAX]或ja[MAX]是否为真。这种方法通常可行但不能保证。 问题的关键是如果你将某些数据存入a[MAX]中往往就会破坏原来紧跟在数组a后面的数据。即使查看a[MAX]的值也是应该避免的尽管这样做一般不会引出什么问题。 为什么在C程序中有时要用到a[MAX]呢?因为很多C程序员习惯通过指针遍历一个数组中的所有元素即用 for(i0;iMAXi) { /*do something*/ } 代替 for(pa; pa[MAX]p) { /*do something*/ } 这种方式在已有的C程序中是随处可见的因此ANSIC标准规定这种方式是可行的。 如果你的程序是在理想的计算机上运行即它的取址范围是从00000000到FFFFFFFF那么你大可以放心但是实际情况往往不会这么简单。 在有些计算机上地址是由两部分组成的第一部分是一个指向某一块内存的起始点的指针(即基地址)第二部分是相对于这块内存的起始点的地址偏移量。这种地址结构被称为段地址结构子程序调用通常就是通过在栈指针上加上一个地址偏移量来实现的。采用段地址结构的最典型的例子是基于Intel 8086的计算机所有的MS-DOS程序都在这种计算机上运行(在基于Pentium芯片的计算机上大多数MS-DOS程序也在与8086兼容的模式下运行)。即使是性能优越的具有线性地址空间的RISC芯片也提供了寄存器变址寻址方式即用一个寄存器保存指向某一块内存的起始点的指针用另一个寄存器保存地址偏移量。 如果你的程序使用段地址结构而在基地址处刚好存放着数组a0(即基地址指针和a0[0]相同)这会引出什么问题呢?既然基地址无法(有效地)改变而偏移量也不可能是负值因此“位于a0[0]前面的元素”这种说法就没有意义了ANSIC标准明确规定引用这个元素的行为是没有定义的这也就是91中所提到的方法可能行不通的原因。 同样如果数组a(其元素个数为MAX)刚好存放在某段内存的尾部那么地址a[MAX]就是没有意义的如果你的程序中使用了a[MAX]而编译程序又要检查a[MAX]是否有效那么编译程序必然就会报告没有足够的内存来存放数组a。 尽管在编写基于WindowsUNIX或Macintosh的程序时不会遇到上述问题但是C语言不仅仅是为这几种情况设计的C语言必须适应各种各样的环境例如用微处理器控制的烤面包炉防抱死刹车系统MS-DOS等等。严格按C语言标准编写的程序能被顺利地编译并能服务于任何目的但是有时程序员也可以适度地背离C语言的标准这要视程序员、编译程序和程序用户三者的具体要求而定。 不可以。当把数组作为函数的参数时你无法在程序运行时通过数组参数本身告诉函数该数组的大小因为函数的数组参数相当于指向该数组第一个元素的指针。这意味着把数组传递给函数的效率非常高也意味着程序员必须通过某种机制告诉函数数组参数的大小。 为了告诉函数数组参数的大小人们通常采用以下两种方法 第一种方法是将数组和表示数组大小的值一起传递给函数例如memcpy()函数就是这样做的 char source[MAX]dest[MAX] /* */ memcpy(destsourceMAX); 第二种方法是引入某种规则来结束一个数组例如在C语言中字符串总是以ASCII字符NUL(\0)结束而一个指针数组总是以空指针结束。请看下述函数它的参数是一个以空指针结束的字符指针数组这个空指针告诉该函数什么时候停止工作 void printMany(char *strings口) { int i; i0 while(strings[i]!NULL) { puts(strings[i]); i; } } 正象95中所说的那样C程序员经常用指针来代替数组下标因此大多数C程序员通常会将上述函数编写得更隐蔽一些 void printMany(char *strings[]) { while(*strings) { puts(*strings) } } 尽管你不能改变一个数组名的值但是strings是一个数组参数相当于一个指针因此可以对它进行自增运算并且可以在调用puts()函数时对strings进行自增运算。在上例中while(*strings) 就相当于 while(*strings !NULL) 在写函数文档(例如在函数前面加上注释或者写一份备忘录或者写一份设计文档)时写进函数是如何知道数组参数的大小是非常重要的例如你可以非常简略地写上“以空指针结束”或“数组elephants中有numElephants个元素”(如果你在程序中用数字13表示数组的大小你可以写进“数组arr中有13个元素”这样的描述然而用确切的数字表示数组的大小不是一种好的编程习惯)。 与使用下标相比使用指针能使C编译程序更容易地产生优质的代码。假设你的程序中有这样一段代码 /* X la some type */ X a[MAX]; X *p; /*pointer*/ X x; /*element*/ int i; /*index*/ 为了历数组a中的所有元素你可以采用这样一种循环方式(方式a) /*version (a)*/ for (i 0; iMAX; i) { xa[i]; /* do something with x * / } 你也可以采用这样一种循环方式(方式b) /*veraion(b)*/ for (p a; pa[MAX]; p ) { x*p; /* do aomething with x * / } 这两种方式有什么区别呢?两种方式中的初始情况和递增运算是相同的作为循环条件的比较表达式也是相同的(下文中将进一步讨论这一点)。区别在于“xa[]”和“x*p”前者要确定a[i]的地址因此需要将i和类型x的大小相乘后再与数组a中第一个元素的地址相加 后者只需间接引用指针p。间接引用是快速的而乘法运算却比较慢。 这是一种“微效率”现象它可能对程序的总体效率有影响也可能没有影响。对方式a来说如果循环体中的操作是将数组中的元素相加或者只是移动数组中的元素那么每次循环中大部分时间就消耗在使用数组下标上如果循环体中的操作是某种I/O操作或者是函数调用那么使用数组下标所消耗的时间是微不足道的。 在有些情况下乘法运算的开销会降低。例如当类型x的大小为1时经过优化就可以将乘法运算省去(一个值乘以1仍然等于这个值)当类型x的大小是2的幂时(此时类型x通常是系统固有类型)乘法运算就可以被优化为左移位运算(就象一个十进制的数乘以10一样)。 在方式b中每次循环都要计算a[MAX]这需要多大代价呢?这和每次计算a[i]的代价相同吗?答案是不同因为在循环过程中a[MAX]是不变的。任何一种合格的编译程序都只会在循环开始时计算一次a[MAX]而在以后的每次循环中重复使用这次计算所得的值。 在编译程序确认在循环过程中a和MAX都不变的前提下方式b和以下代码的效果是相同的 /* how the compiler implements version (b) */ X *temp a[MAX]; /* optimization */ for (p a; p temp; p ) { x *p; /*do something with x * / } 遍历数组元素还可以有另外两种方式即以递减而不是递增的顺序遍历数组元素。对按顺序打印数组元素这样的任务来说后两种方式没有什么优势但是对数组元素相加这样的任务来说后两种方式比前两种方式更好。通过下标并且以递减顺序遍历数组元素的方式(方式c)如下所示(人们通常认为将一个值和。比较的代价要比将一个值和一个非零值比较的代价小 /* version (c) */ for (i MAX - 1; i0; --i) { xa[i]; /* do aomcthing with x * / } 通过指针并以递减顺序遍历数组元素的方式(方式d)如下所示其中作为循环条件的比较表达式显得很简洁 /* version (d) */ for (p a[MAX - 1]; pa; --p ) { x *P; /*do something with x * / } 与方式d类似的代码是很常见的但不是绝对正确的因为循环结束的条件是p小于a而这有时是不可能的(见93)。 通常人们会认为“任何合格的能优化代码的编译程序都会为这4种方式产生相同的代码”但实际上许多编译程序都没能做到这一点。笔者曾编写过一个测试程序(其中类型x的大小不是2的幂循环体中的操作是一些无关紧要的操作)并用4种差别很大的编译程序编译这个程序结果发现方式b总是比方式a快得多有时要快两倍可见使用指针和使用下标的效果是有很大差别的(有一点是一致的即4种编译程序都对a[MAX]进行了前文提到过的优化)。 那么在遍历数组元素时以递减顺序进行和以递增顺序进行有什么不同呢?对于其中的两种编译程序方式c和方式d的速度基本上和方式a相同而方式b明显是最快的(可能是因为其比较操作的代价较小但是否可以认为以递减顺序进行要比以递增顺序进行慢一些呢?)对于其中的另外两种编译程序方式c的速度和方式a基本相同(使用下标要慢一些)但方式d的速度比方式b要稍快一些。 总而言之在编写一个可移植性好、效率高的程序时为了遍历数组元素使用指针比使用下标能使程序获得更快的速度在使用指针时应该采用方式b尽管方式d一般也能工作但编译程序为方式d产生的代码可能会慢一些。 需要补充的是上述技巧只是一种细微的优化因为通常都是循环体中的操作消耗了大部分运行时间许多C程序员往往会舍本求末忽视这种实际情况希望你不要犯相同的错误。不可以尽管在一个很常见的特例中好象可以这样做。 数组名不能被放在赋值运算符的左边(它不是一个左值更不是一个可修改的左值)。一个数组是一个对象而它的数组名就是指向这个对象的第一个元素的指针。 如果一个数组是用extern或static说明-的则它的数组名是在连接时可知的一个常量你不能修改这样一个数组名的值就象你不能修改7的值一样。 给数组名赋值是毫无根据的。一个指针的含义是“这里有一个元素它的前后可能还有其它元素”一个数组名的含义是“这里是一个数组中的第一个元素它的前面没有数组元素并且只有通过数组下标才能引用它后面的数组元素”。因此如果需要使用指针就应该使用指针。 有一个很常见的特例在这个特例中好象可以修改一个数组名的值 void f(chara[12]) { a; /*legal!*/ } 秘密在于函数的数组参数并不是真正的数组而是实实在在的指针因此上例和下例是等价的 void f(char *a) { a /*certainlylegal*/ } 如果你希望上述函数中的数组名不能被修改你可以将上述函数写成下面这样但为此你必须使用指针句法: void{(char *const a) { a; /*illegal*/ } 在上例中参数a是一个左值但它前面的const关键字说明了它是不能被修改的。 前者是指向数组中第一个元素的指针后者是指向整个数组的指针。 注意笔者建议读者读到这里时暂时放下本书写一下指向一个含MAX个元素的字符数组的指针变量的说明。提示使用括号。希望你不要敷衍了事因为只有这样你才能真正了解C语言表示复杂指针的句法的奥秘。下文将介绍如何获得指向整个数组的指针。 数组是一种类型它有三个要素即基本类型(数组元素的类型)大小(当数组被说明为不完整类型时除外)数组的值(整个数组的值)。你可以用一个指针指向整个数组的值 char a[MAX]; /*arrayOfMAXcharacters*/ char *p; /*pointer to one character*/ /*pa is declared below*/ paal pa; /* a[0] */ 在运行了上述这段代码后你就会发现p和pa的打印结果是一个相同的值即p和pa指向同一个地址。但是p和pa指向的对象是不同的。 以下这种定义并不能获得一个指向整个数组的值的指针 char *(ap[MAX]); 上述定义和以下定义是相同的它们的含义都是“ap是一个含MAX个字符指针的数组” char *ap[MAX]; 并不是所有的常量都可以用来定义一个数组的初始大小在C程序中只有C语言的常量表达式才能用来定义一个数组的初始大小。然而在C中情况有所不同。 一个常量表达式的值在程序运行期间是不变的并且是编译程序能计算出来的一个值。在定义数组的大小时你必须使用常量表达式例如你可以使用数字 char a[512]; 或者使用一个预定义的常量标识符 #define MAX 512 /* */ char a[MAX]; 或者使用一个sizeof表达式 char a[sizeof(structcacheObject)]; 或者使用一个由常量表达式组成的表达式 char buf[sizeof(struct cacheObject) *MAX]; 或者使用枚举常量。 在C中一个初始化了的constint变量并不是一个常量表达式 int max512; /* not a constant expression in C */ char buffer[max]; /* notvalid C */ 然而在C中用const int变量定义数组的大小是完全合法的并且是C所推荐的。尽管这会增加C编译程序的负担(即跟踪const int变量的值)而C编译程序没有这种负担但这也使C程序摆脱了对C预处理程序的依赖。 数组的元素可以是任意一种类型而字符串是一种特殊的数组它使用了一种众所周知的确定其长度的规则。 有两种类型的语言一种简单地将字符串看作是一个字符数组另一种将字符串看作是一种特殊的类型。C属于前一种但有一点补充即C字符串是以一个NUL字符结束的。数组的值和数组中第一个元素的地址(或指向该元素的指针)是相同的因此通常一个C字符串和一个字符指针是等价的。 一个数组的长度可以是任意的。当数组名用作函数的参数时函数无法通过数组名本身知道数组的大小因此必须引入某种规则。对字符串来说这种规则就是字符串的最后一个字符是ASCII字符NUL(\0)。 在C中int类型值的字面值可以是42这样的值字符的字面值可以是‘*’这样的值浮点型值的字面值可以是4.2el这样的单精度值或双精度值。 注意实际上一个char类型字面值是一个int类型字面值的另一种表示方式只不过使用了一种有趣的句法例如当42和*都表示char类型的值时它们是两个完全相同的值。然而在C中情况有所不同C有真正的char类型字面值和char类型函数参数并且通常会更仔细地区分char类型和int类型整数数组和字符数组没有字面值。然而如果没有字符串字面值程序编写起来就会很困难因此C提供了字符串字面值。需要注意的是按照惯例C字符串总是以NUL字符结束因此C字符串的字面值也以NUL字符结束例如“six times nine”的长度是15个字符(包括NUL终止符)而不是你看得见的14个字符。 关于字符串字面值还有一条鲜为人知但非常有用的规则如果程序中有两条紧挨着的字符串字面值编译程序会将它们当作一条长的字符串字面值来对待并且只使用一个NUL终止符。也就是说“Hello”world”和“Helloworld”是相同的而以下这段代码中的几条字符串字面值也可以任意分割组合 char message[] ”This is an extremely long prompt\n” ”How long is it?\n” ”Its so long\n” ”It wouldnt fit On one line\n” 在定义一个字符串变量时你需要有一个足以容纳该字符串的数组或者指针并且要保证为NUL终止符留出空间例如以下这段代码中就有一个问题 char greeting[12] strcpy(greeting”Helloworld”) /*trouble*/ 在上例中greeting只有容纳12个字符的空间而“Helloworld”的长度为13个字符(包括NUL终止符)因此NUL字符会被拷贝到greeting以外的某个位置这可能会毁掉greetlng附近内存空间中的某些数据。再请看下例 char greeting[12]”Helloworld”/*notastring*/ 上例是没有问题的但此时greeting是一个字符数组而不是一个字符串。因为上例没有为NUL终止符留出空间所以greeting不包含NUL字符。更好一些的方法是这样写 char greeting[]”Helloworld”; 这样编译程序就会计算出需要多少空间来容纳所有内容包括NUL字符。 字符串字面值是字符(char类型)数组而不是字符常量(const char类型)数组。尽管ANSIC委员会可以将字符串字面值重新定义为字符常量数组但这会使已有的数百万行代码突然无法通过编译从而引起巨大的混乱。如果你试图修改字符串字面值中的内容编译程序是 不会阻止你的但你不应该这样做。编译程序可能会选择禁止修改的内存区域来存放字符串字面值例如ROM或者由内存映射寄存器禁止写操作的内存区域。但是即使字符串字面值被存放在允许修改的内存区域中编译程序还可能会使它们被共享。例如如果你写了以下代码(并且字符串字面值是允许修改的) char *pmessage char *qmessage p[4]\0; /* p now points to”mess”*/ 编译程序就会作出两种可能的反应一种是为p和q创建两个独立的字符串在这种情况下q仍然是“message”一种是只创建一个字符串(p和q都指向它)在这种情况下q将变成“mess”。 注意有人称这种现象为“C的幽默”正是因为这种幽默绝大多数C程序员才会整天被自己编写的程序所困扰难得忙里偷闲一次。