11.1 数组

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。

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值