C语言学习栏目目录
目录
1 初始化数组
2 指定初始化器(C99)
3 给数组元素赋值
4 数组边界
5 指定数组的大小
本章源码 编译环境:VS2019
前面介绍过,数组由数据类型相同的一系列元素组成。需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确地创建数组。普通变量可以使用的类型数组元素都可以用。考虑下面的数组声明:
/* 一些数组声明*/
int main(void)
{
float candy[365]; /* 内含365个float类型元素的数组 */
char code[12]; /*内含12个char类型元素的数组*/
int states[50]; /*内含50个int类型元素的数组 */
...
}
方括号([])表明candy、code和states都是数组,方括号中的数字表明数组中的元素个数。要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。数组元素的编号从0开始,所以candy[0]表示candy数组的第1个元素,candy[364]表示第365个元素,也就是最后一个元素。大家对这些内容应该比较熟悉,下面我们介绍一些新内容。
1 初始化数组
数组通常被用来储存程序需要的数据。例如,一个内含12个整数元素的数组可以储存12个月的天数。在这种情况下,在程序一开始就初始化数组比较好。下面介绍初始化数组的方法。
只储存单个值的变量有时也称为标量变量(scalar variable),我们已经
很熟悉如何初始化这种变量:
int fix = 1;
float flax = PI * 2;
代码中的PI已定义为宏。C使用新的语法来初始化数组,如下所示:
int main(void)
{
int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初始化 */
...
}
如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可以使用空格。根据上面的初始化,把 1 赋给数组的首元素(powers[0]),以此类推(不支持ANSI的编译器会把这种形式的初始化识别为语法错误,在数组声明前加上关键字static可解决此问题。后面将详细讨论这个关键字)。
下程序清单演示了一个小程序,打印每个月的天数
/************************************************************************
功能:打印每个月的天数
/************************************************************************/
#include<stdio.h>
int main(void)
{
int MONTHS = 12;
int days[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
int index;
for (index = 0;index < MONTHS;index++)
printf(" %2d 月有 %2d 天.\n", index + 1, days[index]);
return 0;
}
该程序的输出如下:
1 月有 31 天.
2 月有 28 天.
3 月有 31 天.
4 月有 30 天.
5 月有 31 天.
6 月有 30 天.
7 月有 31 天.
8 月有 31 天.
9 月有 30 天.
10 月有 31 天.
11 月有 30 天.
12 月有 31 天.
这个程序还不够完善,每4年打错一个月份的天数(即,2月份的天数)。该程序用初始化列表初始化days[],列表(用花括号括起来)中用逗号分隔各值。
注意该例使用了符号常量 MONTHS 表示数组大小,这是我们推荐且常用的做法。例如,如果要采用一年13个月的记法,只需修改#define这行代码即可,不用在程序中查找所有使用过数组大小的地方。注意 使用const声明数组有时需要把数组设置为只读。这样,程序只能从数组中检索值,不能把新值写入数组。要创建只读数组,应该用const声明和初始化数组。因此,
程序清单10.1中初始化数组应改成:
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
这样修改后,程序在运行过程中就不能修改该数组中的内容。和普通变量一样,应该使用声明来初始化 const 数据,因为一旦声明为 const,便不能再给它赋值。明确了这一点,就可以在后面的例子中使用const了。
/************************************************************************
功能: 为初始化数组
/************************************************************************/
#include<stdio.h>
#define SIZE 4
int main(void)
{
int no_data[SIZE]; //未初始化数组
int i;
printf("%2s%14s\n", "i", "no_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, no_data[i]);
return 0;
}
该程序的输出如下(系统不同,输出的结果可能不同 我是在win10 vs2019编译环境下):
i no_data[i]
0 -858993460
1 -858993460
2 -858993460
3 -858993460
使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值,因此,读者运行该程序后的输出会与该示例不同。
注意 存储类别警告
数组和其他变量类似,可以把数组创建成不同的存储类别(storage class)。后面将介绍存储类别的相关内容,现在只需记住:本章描述的数组属于自动存储类别,意思是这些数组在函数内部声明,且声明时未使用关键字static。到目前为止,本书所用的变量和数组都是自动存储类别。在这里提到存储类别的原因是,不同的存储类别有不同的属性,所以不能把本章的内容推广到其他存储类别。对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为0。初始化列表中的项数应与数组的大小一致。如果不一致会怎样?我们还是以上一个程序为例,但初始化列表中缺少两个元素,如下程序清单所示:
/************************************************************************
功能: 部分初始化数组
/************************************************************************/
#include<stdio.h>
#define SIZE 4
int main(void)
{
int some_data[SIZE] = { 1492,1066 };
int i;
printf("%2s%14s\n", "i", "some_data[i]");
for(i = 0;i < SIZE;i++)
printf("%2d%14d\n", i, some_data[i]);
return 0;
}
下面是该程序的输出:
i some_data[i]
0 1492
1 1066
2 0
3 0
如上所示,编译器做得很好。当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;但是,如果部分初始化数组,剩余的元素就会被初始化为0。
如果初始化列表的项数多于数组元素个数,编译器可没那么仁慈,它会毫不留情地将其视为错误。但是,没必要因此嘲笑编译器。其实,可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数(见下程序清单)
/************************************************************************
功能: 让编译器计算元素个数
/************************************************************************/
#include<stdio.h>
int main(void)
{
const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31 };
int index;
for(index = 0;index < sizeof days / sizeof days[0];index++)
printf(" %2d 月有 %d 天.\n", index + 1, days[index]);
return 0;
}
在上程序清单中,要注意以下两点。如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。
注意for循环中的测试条件。由于人工计算容易出错,所以让计算机来计算数组的大小。sizeof运算符给出它的运算对象的大小(以字节为单位)。所以sizeof days是整个数组的大小(以字节为单位)sizeof day[0]是数组中一个元素的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。
下面是该程序的输出:
1 月有 31 天.
2 月有 28 天.
3 月有 31 天.
4 月有 30 天.
5 月有 31 天.
6 月有 30 天.
7 月有 31 天.
8 月有 31 天.
9 月有 30 天.
10 月有 31 天.
我们的本意是防止初始化值的个数超过数组的大小,让程序找出数组大小。我们初始化时用了10个值,结果就只打印了10个值!这就是自动计数的弊端:无法察觉初始化列表中的项数有误。还有一种初始化数组的方法,但这种方法仅限于初始化字符数组。我们
在下一章中介绍。
2 指定初始化器(C99)
C99 增加了一个新特性:指定初始化器(designated initializer)。利用该特性可以初始化指定的数组元素。例如,只初始化数组中的最后一个元素。对于传统的C初始化语法,必须初始化最后一个元素之前的所有元素,才能初始化它:
int arr[6] = {0,0,0,0,0,212}; // 传统的语法
而C99规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素:
int arr[6] = {[5] = 212}; // 把arr[5]初始化为212
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0。下程序清单中的初始化比较复杂。
/************************************************************************
功能:使用指定初始化器
/************************************************************************/
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int days[MONTHS] ={ 31, 28,[4] = 31, 30, 31,[1] = 29 };
int i;
for (i = 0; i < MONTHS; i++)
printf("%2d %d\n", i + 1, days[i]);
return 0;
}
该程序在支持C99的编译器中输出如下:
1 31
2 29
3 0
4 0
5 31
6 30
7 31
8 0
9 0
10 0
11 0
12 0
以上输出揭示了指定初始化器的两个重要特性。第一,如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段:[4] = 31,30,31,那么后面这些值将被用于初始化指定元素后面的元素。也就是说,在days[4]被初始化为31后,days[5]和days[6]将分别被初始化为30和31。第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。例如,上程序清单中,初始化列表开始时把days[1]初始化为28,但是days[1]又被后面的指定初始化[1] = 29初始化为29。
如果未指定元素大小会怎样?
int stuff[] = {1, [6] = 23}; //会发生什么?
int staff[] = {1, [6] = 4, 9, 10}; //会发生什么?
编译器会把数组的大小设置为足够装得下初始化的值。所以,stuff数组有7个元素,编号为0~6;而staff数组的元素比stuff数组多两个(即有9个元素)。
3 给数组元素赋值
声明数组后,可以借助数组下标(或索引)给数组元素赋值。例如,下面的程序段给数组的所有元素赋值:
/* 给数组的元素赋值 */
#include <stdio.h>
#define SIZE 50
int main(void)
{
int counter, evens[SIZE];
for (counter = 0; counter < SIZE; counter++)
evens[counter] = 2 * counter;
...
}
注意这段代码中使用循环给数组的元素依次赋值。C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。下面的代码段演示了一些错误的赋值形式:
/* 一些无效的数组赋值 */
#define SIZE 5
int main(void)
{
int oxen[SIZE] = {5,3,2,8}; //初始化没问题
int yaks[SIZE];
yaks = oxen; // 不允许
yaks[SIZE] = oxen[SIZE]; // 数组下标越界
yaks[SIZE] = {5,3,2,8}; // 不起作用
return 0;
}
oxen数组的最后一个元素是oxen[SIZE-1],所以oxen[SIZE]和yaks[SIZE]都超出了两个数组的末尾。
4 数组边界
在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。例如,假设有下面的声明:
int doofi[20];
那么在使用该数组时,要确保程序中使用的数组下标在0~19的范围内,因为编译器不会检查出这种错误(但是,一些编译器发出警告,然后继续编译程序)。考虑下程序清单的问题。该程序创建了一个内含4个元素的数组,然后错误地使用了-1~6的下标。
/************************************************************************
功能: 数组下标越界
/************************************************************************/
#include <stdio.h>
#define SIZE 4
int main(void)
{
int value1 = 44;
int arr[SIZE];
int value2 = 88;
int i;
printf("value1 = %d, value2 = %d\n", value1, value2);
for(i = -1; i <= SIZE; i++)
arr[i] = 2 * i + 1;
for(i = -1; i < 7; i++)
printf("%2d %d\n", i, arr[i]);
printf("value1 = %d, value2 = %d\n", value1, value2);
printf("address of arr[-1]: %p\n", &arr[-1]);
printf("address of arr[4]: %p\n", &arr[4]);
printf("address of value1: %p\n", &value1);
printf("address of value2: %p\n", &value2);
return 0;
}
编译器不会检查数组下标是否使用得当。在C标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。下面是使用VS2019的输出示例:(执行结果会终止:Run-Time Check Failure #2 - Stack around the variable 'arr' was corrupted.)
value1 = 44, value2 = 88
-1 -1
0 1
1 3
2 5
3 7
4 9
5 -858993460
6 44
value1 = 44, value2 = 88
address of arr[-1]: 0097F958
address of arr[4]: 0097F96C
address of value1: 0097F974
address of value2: 0097F950
注意,该编译器似乎把value2储存在数组的前一个位置,把value1储存在数组的后一个位置(其他编译器在内存中储存数据的顺序可能不同)。在上面的输出中,arr[-1]与value2对应的内存地址相同, arr[4]和value1对应的内存地址相同。因此,使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。
C 语言为何会允许这种麻烦事发生?这要归功于 C 信任程序员的原则。不检查边界,C 程序可以运行更快。编译器没必要捕获所有的下标错误,因为在程序运行之前,数组的下标值可能尚未确定。因此,为安全起见,编译器必须在运行时添加额外代码检查数组的每个下标值,这会降低程序的运行速度。C 相信程序员能编写正确的代码,这样的程序运行速度更快。但并不是所有的程序员都能做到这一点,所以就出现了下标越界的问题。
还要记住一点:数组元素的编号从0开始。最好是在声明数组时使用符号常量来表示数组的大小:
#define SIZE 4
int main(void)
{
int arr[SIZE];
for (i = 0; i < SIZE; i++)
....
这样做能确保整个程序中的数组大小始终一致。
5 指定数组的大小
本章前面的程序示例都使用整型常量来声明数组:
#define SIZE 4
int main(void)
{
int arr[SIZE]; // 整数符号常量
double lots[144]; // 整数字面常量
...
在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof表达式被视为整型常量,但是(与C++不同)const值不是。另外,表达式的值必须大于0:
int n = 5;
int m = 8;
float a1[5]; // 可以
float a2[5*2 + 1]; //可以
float a3[sizeof(int) + 1]; //可以
float a4[-4]; // 不可以,数组大小必须大于0
float a5[0]; // 不可以,数组大小必须大于0
float a6[2.5]; // 不可以,数组大小必须是整数
float a7[(int)2.5]; // 可以,已被强制转换为整型常量
float a8[n]; // C99之前不允许
float a9[m]; // C99之前不允许
上面的注释表明,以前支持C90标准的编译器不允许后两种声明方式。而C99标准允许这样声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称 VLA(C11 放弃了这一创新的举措,把VLA设定为可选,而不是语言必备的特性)。
C99引入变长数组主要是为了让C成为更好的数值计算语言。例如,VLA简化了把FORTRAN现有的数值计算例程库转换为C代码的过程。VLA有一些限制,例如,声明VLA时不能进行初始化。在充分了解经典的C数组后,我们再详细介绍VLA。