数组
数组由数据类型相同的一系列元素组成。 需要使用数组时, 通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。 编译器根据这些信息正确地创建数组。
/* 一些数组声明*/
int main(void)
{
float candy[365]; /* 内含365个float类型元素的数组 */
char code[12]; /*内含12个char类型元素的数组*/
int states[50]; /*内含50个int类型元素的数组 */
...
}
方括号([]) 表明candy、 code和states都是数组, 方括号中的数字表明数组中的元素个数。
要访问数组中的元素, 通过使用数组下标数(也称为索引) 表示数组中的各元素。
初始化数组
数组通常被用来储存程序需要的数据。只储存单个值的变量有时也称为标量变量(scalar variable)
用以逗号分隔的值列表(用花括号括起来) 来初始化数组,各值之间用逗号分隔。 在逗号和值之间可以使用空格。
每4年打错一个月份的天数(即, 2月份的天数) 。 该程序用初始化列表初始化days[], 列表(用花括号括起来) 中用逗号分隔各值。符号常量 MONTHS 表示数组大小
使用const声明数组有时需要把数组设置为只读。 这样, 程序只能从数组中检索值, 不能把新值写入数组。 要创建只读数组, 应该用const声明和初始化数组。
在使用数组元素之前,必须先给它们赋初值。 编译器使用的值是内存相应位置上的现有值。
注意 存储类别警告
数组和其他变量类似, 可以把数组创建成不同的存储类别(storageclass)。
存储类别的原因是, 不同的存储类别有不同的属性。
初始化列表中的项数应与数组的大小一致。
当初始化列表中的值少于数组元素个数时, 编译器会把剩余的元素都初始化为0。 也就是说, 如果不初始化数组,数组元素和未初始化的普通变量一样, 其中储存的都是垃圾值; 但是, 如果部分初始化数组, 剩余的元素就会被初始化为0。
如果初始化列表的项数多于数组元素个数,编译器会毫不留情地将其视为错误。
如果初始化数组时省略方括号中的数字, 编译器会根据初始化列表中的项数来确定数组的大小。sizeof运算符给出它的运算对象的大小(以字节为单位) 。 所以sizeof days是整个数组的大小(以字节为单位) , sizeof day[0]是数组中一个元素的大小(以字节为单位) 。 整个数组的大小除以单个元素的大小就是数组元素的个数。
指定初始化器(C99)
C99 增加了一个新特性: 指定初始化器(designated initializer) 。 利用该特性可以初始化指定的数组元素。
而C99规定, 可以在初始化列表中使用带方括号的下标指明待初始化的元素。
对于一般的初始化, 在初始化一个元素后, 未初始化的元素都会被设置为0。
初始化器的两个重要特性,第一, 如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素。第二, 如果再次初始化指定的元素, 那么最后的初始化将会取代之前的初始化。
编译器会把数组的大小设置为足够装得下初始化的值。
给数组元素赋值
声明数组后, 可以借助数组下标(或索引) 给数组元素赋值。
C 不允许把数组作为一个单元赋给另一个数组, 除初始化以外也不允许使用花括号列表的形式赋值。
数组边界
在使用数组时, 要防止数组下标超出边界。 也就是说, 必须确保下标是有效的值。
创建了一个内含4个元素的数组, 然后错误地使用了-1~6的下标。
在C标准中, 使用越界下标的结果是未定义的。
该编译器似乎把value2储存在数组的前一个位置, 把value1储存在数组的后一个位置(其他编译器在内存中储存数据的顺序可能不同)。使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同, 有些会导致程序异常中止。
数组元素的编号从0开始。 最好是在声明数组时使用符号常量来表示数组的大小这样做能确保整个程序中的数组大小始终一致
指定数组大小
#define SIZE 4
int main(void)
{
intarr[SIZE]; // 整数符号常量
double lots[144]; // 整数字面常量。
....
}
在C99标准之前, 声明数组时只能在方括号中使用整型常量表达式。 所谓整型常量表达式, 是由整型常量构成的表达式。 sizeof表达式被视为整型常量, 但是(与C++不同) const值不是。
C99引入变长数组主要是为了让C成为更好的数值计算语言。
多维数组
二维视图有助于帮助理解二维数组的两个下标。在计算机内部,这样的数组是按顺序储存的, 从第1个内含12个元素的数组开始, 然后是第2个内含12个元素的数组, 以此类推。
程序使用了两个嵌套for循环。 第1个嵌套for循环的内层循环, 在year不变的情况下, 遍历month计算某年的总降水量; 而外层循环, 改变year的值, 重复遍历month, 计算5年的总降水量。
初始化二维数组
初始化二维数组是建立在初始化一维数组的基础上。
如果某列表中的数值个数超出了数组每行的元素个数, 则会出错, 但是这并不会影响其他行的初始化。
初始化时也可省略内部的花括号, 只保留最外面的一对花括号。只要保证初始化的数值个数正确, 初始化的效果与上面相同。 但是如果初始化的数值不够, 则按照先后顺序逐行初始化, 直到用完所有的值。 后面没有值初始化的元素被统一初始化为0。
因为储存在数组rain中的数据不能修改, 所以程序使用了const关键字声明该数组。
其他多维数组
处理三维数组要使用3重嵌套循环, 处理四维数组要使用4重嵌套循环。 对于其他多维数组, 以此类推。
指针和数组
指针提供一种以符号形式使用地址的方法。使用指针的程序更有效率。 尤其是, 指针能有效地处理数组。数组表示法其实是在变相地使用指针。
第2行打印的是两个数组开始的地址, 下一行打印的是指针加1后的地址, 以此类推。
在C中, 指针加1指的是增加一个存储单元。 对数组而言, 这意味着把加1后的地址是下一个元素的地址, 而不是下一个字节的地址 这是为什么必须声明指针所指向对象类型的原因之一。 只知道地址不够, 因为计算机要知道储存对象需要多少字节(即使指针指向的是标量变量, 也要知道变量的类型, 否则*pt 就无法正确地取回地址上的值) 。
指针的值是它所指向对象的地址。 地址的表示方式依赖于计算机内部的硬件。 许多计算机(包括PC和Macintosh) 都是按字节编址, 意思是内存中的每个字节都按顺序编号。 这里, 一个较大对象的地址(如double类型的变量) 通常是该对象第一个字节的地址。
在指针前面使用*运算符可以得到该指针所指向对象的值。
指针加1, 指针的值递增它所指向类型的大小(以字节为单位) 。
数组和指针的关系十分密切, 可以使用指针标识数组的元素和获得元素的值。 从本质上看, 同一个对象有两种表示法。C语言标准在描述数组表示法时确实借助了指针。
days是数组首元素的地址, days + index是元素days[index]的地址, 而*(days + index)则是该元素的值, 相当于days[index]。 for循环依次引用数组中的每个元素, 并打印各元素的内容。
函数、数组和指针
total = sum(marbles); // 可能的函数调用
数组名是该数组首元素的地址, 所以实际参数marbles是一个储存int类型值的地址, 应把它赋给一个指针形式参数, 即该形参是一个指向int的指针
int sum(int * ar); // 对应的函数原型
sum()获得了该数组首元素的地址, 知道要在该位置上找出一个整数。 注意, 该参数并未包含数组元素个数的信息。我们有两种方法让函数获得这一信息。 第一种方法是, 在函数代码中写上固定的数组大小
指针表示数组名, 也可以用数组名表示指针。 +=运算符把右侧运算对象加到左侧运算对象上。
注意 声明数组形参数组名是该数组首元素的地址, 作为实际参数的数组名要求形式参数是一个与之匹配的指针。
marbles是一个数组, ar是一个指向marbles数组首元素的指针, 利用C中数组和指针的特殊关系, 可以用数组表示法来表示指针ar。
使用指针形参
sum()函数使用一个指针形参标识数组的开始, 用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型) 。 但是这并不是给函数传递必备信息的唯一方法。 还有一种方法是传递两个指针, 第1个指针指明数组的开始处(与前面用法相同) , 第2个指针指明数组的结束处。
指针start开始指向marbles数组的首元素, 所以赋值表达式total += *start把首元素(20) 加给total。 然后, 表达式start++递增指针变量start, 使其指向数组的下一个元素。 因为start是指向int的指针, start递增1相当于其值递增int类型的大小。
因为while循环的测试条件是一个不相等的关系, 所以循环最后处理的一个元素是end所指向位置的前一个元素。 这意味着end指向的位置实际上在数组最后一个元素的后面。 C保证在给数组分配空间时, 指向数组后面第一个位置的指针仍是有效的指针。
total += *start++;
一元运算符*和++的优先级相同, 但结合律是从右往左, 所以start++先求值, 然后才是*start。 也就是说, 指针start先递增后指向。 使用后缀形式(即start++而不是++start) 意味着先把指针指向位置上的值加到total上, 然后再递增指针。 如果使用*++start, 顺序则反过来, 先递增指针, 再使用指针指向位置上的值。 如果使用(*start)++, 则先使用start指向的值, 再递增该值, 而不是递增指针。 这样, 指针将一直指向同一个位置, 但是该位置上的值发生了变化。
只有(*p3)++改变了数组元素的值, 其他两个操作分别把p1和p2指向数组的下一个元素
指针表示法和数组表示法
处理数组的函数实际上用指针作为参数, 但是在编写这样的函数时, 可以选择是使用数组表示法还是指针表示法。使用数组表示法, 让函数是处理数组的这一意图更加明显。
C语言, ar[i]和*(ar+1)这两个表达式都是等价的。 无论ar是数组名还是指针变量, 这两个表达式都没问题。 但是, 只有当ar是指针变量时, 才能使用ar++这样的表达式。
指针表示法(尤其与递增运算符一起使用时) 更接近机器语言, 因此一些编译器在编译时能生成效率更高的代码。