第10章 数组和指针

发布于 2021-08-30  615 次阅读


10.1 数组

程序员不可避免的要处理大量相关数据。
通常,数组可以高效快捷地处理这种数据。
数组和指针的关系
数组由数据类型相同的一系列元素组成。
使用数组时,通过声明数组告诉编译器数组中含有多少元素和这些元素的类型。编译器根据这些信息正确的创建数组。
普通元素可以使用的类型,数组元素都可以用。
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,也就是最后一个元素。

10.1.1 初始化数组

数组通常被用来存储程序需要的数据。
例如,一个含有12个整数元素的数组可以存储12个月的天数。
在这种情况下,在程序一开始就初始化数组比较好。
只存储单个值的变量有时也称为标量变量(scalar variable):
int fix = 1;
float flax = PI * 2;
 C 使用新的语法来初始化数组:

int main(void)
{
    int powers[8] = {1,2,4,6,8,16,32,64};
        //从ANSI C 开始支配这种初始化
    。。。
}
如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可以使用空格。
根据上面的初始化,把1赋给数组的首元素(powers[0]),以此类推(不支持ANSI的编译器会把这种形式的初始化识别为语法错误,在数组声明前加上关键字static可解决此问题。)
//Q:演示了一个小程序,打印每个月的天数
//day_mon1.c    --打印每个月的天数
//day_mon1.c    --打印每个月的天数
#include<stdio.h>
#define MONTHS 12

int main(void)
{
    int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
    int index;

    for(index = 0; index<MONTHS;index++)
    {
        printf("Month %2d has %2d days.\n",index+1,days[index]);
    }

    return 0;
 } 

file

这个程序还不够完善,每4年打错一个份的天数。
该程序用初始化列表初始化days[],列表(用花括号括起来)中用逗号分隔各值。
该程序使用了符号常量MONTHS表示数组大小,例如,如果要采用一年13个月的记法,只需要修改#define这行代码即可,不用在程序中查找所有使用过数组大小的地方。
注意:
使用const声明数组
有时需要把数组设置为只读。这样,程序只能从数组中检索值,不能把新值写入数组。要创建只读数组,应该用const声明和初始化数组。
因此,上述清单初始化数组应为:
const int day[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
这样修改后,程序在运行过程中就不能修改该数组中的内容。和普通变量一样,应该使用声明来初始化const数据,因为一旦声明为const,便不能给它赋值。
//Q:初始化数组失败
//no_data.c     --未初始化数组
#include<stdio.h>
#define SIZE 4
int main(void)
{
    int no_data[4];
    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;
 } 

file

使用数组前必须先初始化它。与普通变量相似,在使用数组元素之前,必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值。
注意: 存储类别警告
数组和其他变量类似,可以把数组创建成不同的存储类别(storage class),
记住:本章所描述的数组属于自动存储类别,意思是这些数组在函数内部声明,且声明时未使用关键字static。
初始化列表中的项数应该与数组的大小一致。
//初始化部分数组元素
#include<stdio.h>
#define SIZE 4
int main(void)
{
    int no_data[4] = {1492,1066};
    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;
 } 

file

如上所示,当初始化列表中的值少于数组元素的个数时,编译器会把剩余的元素都初始化为0.
但是,如果初始化列表的项数多于数组元素个数,则将其视为错误。
可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数。
//  让编译器计算元素的个数
#include<stdio.h>

int main(void)
{
    int days[] = {31,28,31,30,31,30,31,31,30,31,30,31};
    int index;

    for(index = 0; index<sizeof days/sizeof days[0];index++)
    {
        printf("Month %2d has %2d days.\n",index+1,days[index]);
    }

    return 0;
 } 

file

- 如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小
- 由计算机来计算数组的大小。sizeof 运算符给出它的运算对象的大小(以字节为单位)。所以sizeof days是震哥哥数组的大小(以字节为大小)。sizeof days[0]是数组中一个元素的大小(以字节为大小)。整个数组的大小除单个元素的大小就是数组元素的个数,
自动计算的弊端:无法察觉初始化列表中的项数有误

10.1.2 指定初始化器(C99)

C99增加了一个新特性:指定初始化器(designated initializer).利用该 特性可以初始化指定的元素。
例如,初始化数组中的最后一个元素。对于传统的C初始化语法,必须初始化最后一个元素之前的所有元素,才能初始化它:
int arr[6] = {0,0,0,0,0,212};   //传统的语法
而C99规定,可以在初始化了列表中使用带方括号的下标指明待初始化的元素:
 int arr[6]={[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",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,那么后面这些值将被用于初始化指定元素后面的元素。也就是说,在day[4]被初始化为31后,days[5],days[6]将被初始化为30和31.
第二,如果再此初始化指定的元素,那么初始化将会取代之前的初始化。
如果未指定元素大小?
int stuff[] = {1,[6]=23};
int staff[] = {1,[6]=4,9,10};
编译器会把数组的大小设置为足够装得下初始化的值。
stuff会拥有7个元素,staff拥有9个元素。

10.1.3 给数组元素赋值

声明数组后,可以借助数组下标(或索引)给数组元素赋值。
#include<stdio.h>
#define SIZE 50
int main(void)
{

    int counter,envens[SIZE];

    for(counter=0;counter<SIZE;counter++)
    {
        enens[counter]=2*counter;
    }
    ...
}
//这段代码中使用循环给数组的元素依次赋值。C不允许把数组作为一个单元赋给另一个数组,除初始化意外也不允许使用花括号列表的形式赋值。
//下面的代码演示了一些错误的赋值形式
//一些无效的数组赋值
#include<stdio.h>
#define SIZE 5
int main(void)
{
    int open[SIZE]={5,3,2,8};
    int yaks[SIZE];

    yarks = open;       //不允许!
    yarks[SIZE] = opne[SIZE];   //数组下标越界
    yarks[SIZE]={5,3,2,8};      //不起作用
}
    //open数组的最后一个元素是open[SIZE-1],所以open[SIZE]和yaks[SIZE]都超出了两个数组的末尾

10.1.4 数组边界

在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。
例如:
int doofi[20];
现在在使用该数组时,要确保程序中使用的数组下标在0~19的范围内,因为编译器不会检查出这种错误(但是,一些编译器会发出警告然后继续编译程序)
考虑下述代码
//  bounds.c    --数组下标越界
#include<stdio.h>
#define SIZE 4
int main(void)
{
    int value = 44;
    int arr[SIZE];
    int value2 = 88;
    int i;

    printf("value1 = %d,value2 = %d",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",value1,value2);
    printf("address of arr[-1]:&p\n",&arr[-1]);
    printf("address of arr[4]:&p\n",&arr[4]);
    printf("address of vallue1:&p\n",&value1);
    printf("address of value2:&p\n",&value2);

    return 0;
}

file

编译器似乎把value2存储在数组前前一个位置,把value1存储在数组的后一个位置。
使用越界的数组下标会导致程序改变其他变量的的值。
为什么会这样呢?
不检查边界,程序会更快。编译器没必要捕捉所有的下标错误。因为在程序运行之前,数组的下标值可能尚未确定。
因此为了安全起见,编译器必须在运行时添加额外的代码检查数组的每个下标值,这回降低程序的运行速度。C相信程序员能编写正确的代码,这样的程序运行速度更快。
注意:数组元素的编号从0开始。最好是在声明数组时使用符号常量来表示数组的大小:
#define SIZE 4
int main(void)
{
    int arr[SIZE];
    for(i=0;i<SIZE;i++)
    ...
}
这样做能确保整个程序中的数组大小始终一致。

10.1.5 指定数组的大小

//本章前面的程序示例都使用整型常量来声明数组:
#define SIZE 4
int main(void)
{
    int arr[SIZE];
    double lots[144];
    ...
}
//在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。sizeof表达式被视为整型常量,const值不是,表达式必须大于0
int n = 5;
int m = 8;
float a1[5];        //YES
float a2[5*2+1];        //yes
float a3[sizeof(int)+1];    //yes
float a4[-4];       //no
float a6[2.5];      //no
float a7[(int)2.5]; //yes
float a8[n];        //C99之前不允许
float a9[m];        //C99之前不允许
以前支持C90标准的编译器不允许后两种声明方式。而C99标准允许这样的声明,这创建了一一种新型的数组,称为边长数组(variable-length array)或简称VLA(C11放弃了这一创新举措,把VLA设定未可选,而不是语言必备的特征)
C99引入边长数组主要目的是为了让C称为更好的数值计算语言。

擦肩而过的概率