1.6 数组
迄今为止,程序所使用的数据类型都是基本数据类型,如int、float、double等。但C++还允许用户按一定的规则进行数据类型的构造,如定义数组、指针、结构和引用等,这些类型统称为构造类型。数组是应用最广泛的构造类型之一,它有一维、二维和多维,分别应用于不同的场合。
1.6.1 一维数组
在C++中,数组是相同类型的元素的有序集合,每一个元素在内存中占用相同大小的内存单元,这些内存单元在内存空间中都是连续存放的。和变量一样,在使用数组前需要对数组进行定义以通知编译为其开辟相应的内存空间。数组一旦定义后,就可在程序中引用和操作这些数组元素,每个元素所对应的内存空间或在数组中的位置可用统一的数组名,通过下标运算符“[ ]”指定下标序号来唯一确定。这里先来讨论一维数组。
1.一维数组的定义和引用
C++中,一维数组的一般定义格式如下:
<数据类型> <数组名>[<常量表达式>];
其中,方括号“[ ]”是区分数组和变量的特征符号。方括号中的常量表达式的值必须是一个确定的整型数值,且数值必须大于0,它反映维的大小,对于一维数组来说,也可以是一维数组元素的个数或一维数组的大小、数组的长度。数据类型用来指定数组中元素的数据类型,以及每一个元素所占内存空间的大小,它必须是C++合法的数据类型。数组名与变量名一样,遵循标识符命名规则。例如:
int a[10];
这个定义会使编译为其分配10个int元素连续的内存空间。其中,a表示数组名,方括号里的10表示该数组有10个元素,每个元素的类型都是int。在定义中,还可将同类型的变量或其他数组的定义写在一行语句中,但它们必须用逗号隔开。例如:
int a[10],b[20],n;
其中,a和b被定义成整型数组,n是整型变量。
一般地,数组方括号中的常量表达式中不能包含变量,但可以包括常量和符号常量,如:
int a[4-2]; // 合法,表达式4-2是一个确定的值2 float b[3*6]; // 合法,表达式3*6是一个确定的值18 const int size = 18; int c[size]; // 合法,size是一个标识符常量 int SIZE = 18; int d[SIZE]; // 不合法,SIZE是一个变量,不能用做下标大小的定义 int d[0]; //ANSI/ISO C++不合法,定义时,下标必须大于0
需要说明的是,当数组的数据类型是char时,则该数组称为字符数组。由于字符数组的每一个元素都用来存储一个字符,因此字符数组可用来存放一个字符序列,即字符串(后面还会专门讨论)。
数组定义后,就可以用下标运算符通过指定下标序号来引用和操作数组中的元素,引用时按下列格式:
<数组名> [<下标表达式>]
其中,方括号“[ ]”是C++的下标运算符,下标表达式的值就是下标序号,反映该元素在数组中的位置。需要说明的是:
(1)C++数组的下标序号总是从0开始。例如,若有数组定义:
int a[5];
则由于开始的下标序号为0,也就是说,a[0]是数组a的第一个元素。由于数组a被定义成具有5个元素的一维数组,因此a[0],a[1],a[2],a[3],a[4]是数组a的5个元素,a[4]是a的最后一个元素。注意:这里的数组a没有a[5]这个数组元素。可见,在引用一维数组元素时,若数组定义时指定的大小为n,则下标序号范围为0~(n-1)。
(2)在引用数组元素时,下标序号的值必须为整型,因此它可以是一个整型常量,也可以是一个整型变量,或是结果为一个整型值的表达式等。例如:
int d[10]; // … for (int i=0; i<10; i++) cout<<d[i]<< "\t";
代码中的d[i]就是一个对数组d元素的合法引用,i是一个整型变量,用来指定下标序号。当i=0时,引用的是元素d[0],当i=1时,引用的是元素d[1]……以此类推。要注意,若将for中的i<10误写成i<=10,则当i=10时,数组d的下标序号已超界,而这一错误是不会在编译与连接中反映出来的,因此在程序设计中必须保证数组下标序号取值的正确性。
(3)数组定义后,编译会根据数组的大小开辟相应的内存空间,并依照下标序号从小到大的次序依次存放数组中的各个元素。显然,所开辟的内存大小(字节数)应等于数组元素个数*sizeof (数组类型)。
(4)事实上,数组中的每一个元素都可看成一个与数组数据类型相同的变量。并且,若设元素为X[n](X表示数组名,n表示合法的下标序号),则引用X[n]就是引用该元素所对应的内存空间,它与变量名一样,都是对相应内存空间的标识。因此在程序中对数组元素进行赋值或其他处理时,它的操作与变量相同。
2.一维数组的初始化和赋值
与变量一样,在引用前还可对一维数组进行初始化或赋初值。数组元素既可以在数组定义的同时赋初值,即初始化,也可以在定义后赋值。一维数组的初始化格式如下:
<数据类型> <数组名>[<常量表达式>]={初值列表};
它在数组定义格式中,在方括号之后,用“={初值列表}”的形式进行初始化。其中,初值列表中的初值个数不得多于数组元素个数,且多个初值之间要用逗号隔开。例如:
int a[5]={1,2,3,4,5};
是将花括号“{ }”里的初值(整数)1,2,3,4,5依次填充到数组a的内存空间中,即将初值依次赋给数组a的各个元素。它的作用与下列的赋值语句相同:
a[0]=1; a[1]=2; a[2]=3; a[3]=4; a[4]=5;
对于一维数组的初始化和赋值需注意以下几点:
(1)可以给其中的一部分元素赋初值。例如:
int b[5]={1,2}; //A
是使数组b的元素b[0] = 1,b[1] = 2。
(2)在对数组进行初始化时,若没有明确列举元素值的元素,则其值均为0,即“A”中的元素b[2],b[3],b[4]的值均为默认的值0。正因如此,若有:
int b[5]={0};
则使得数组b的各个元素的值均设为0。
(3)在初始化的“={初值列表}”中,花括号中的初值可以是常量,也可以是常量表达式,但不能有变量。例如:
double f[5]={1.0,3.0*3.14,8.0}; // 合法 double d=8.0; double f[5]={1.0,3.0*3.14,d}; // 不合法,d是变量
(4)在对全部一维数组元素赋初值时,有时可以不指定一维数组的长度。例如:
int c[]={1,2,3,4,5};
编译将根据数值的个数自动设定数组c的长度,这里是5。要注意,必须让系统在编译时知道数组的大小。若只有:
int c[]; // 不合法,未指定数组大小
则是错误的。
(5)初始化数组的值的个数不能多于数组元素的个数。例如:
int e[5]={1,2,3,4,5,6}; // 错误,初始化值个数多于数组元素个数
(6)“={初值列表}”的方式只限于数组的初始化,不能出现在赋值语句中。例如:
int c[4]; // 合法 c[4]={1,2,3,4}; // 错误
(7)在“={初值列表}”方式中,逗号前面必须要有表达式或值。例如:
int f[5]={1,,3,4,5}; // 错误,第2个逗号前面没有表达式或值 int g[5]={1,2,3,}; //ANSI/ISO C++中合法 int h[5]={}; //ANSI/ISO C++中合法,等价于int h[5]={0};
(8)两个一维数组不能直接进行赋值“=”运算,但数组元素可以。例如:
int a1[4]={1,2,3,4}; // 合法 int a2[4]; // 合法 a2=a1; // 错误,数组名表示一个地址常量,不能做左值(后面还会讨论) a2[0]=a1[0]; // 合法:数组元素就是一个变量 a1[2]=a2[1]; // 合法:数组元素就是一个变量
1.6.2 二维数组
在C++数组定义中,数组的维数是通过方括号的对数来指定的。显然,若在数组定义时指定多对方括号,则定义的是多维数组。最常用的多维数组是二维数组,这里就来讨论一下。
1.二维数组的定义和引用
二维数组定义的格式如下:
<数据类型> <数组名>[<常量表达式1>][<常量表达式2>];
从中可以看出,二维数组定义的格式与一维数组基本相同,只是多了一对方括号。同样,若定义一个三维数组,则在二维数组定义格式的基础上再增加一对方括号,以此类推。显然,对于数组定义的统一格式可表示为:
<数据类型> <数组名>[<常量表达式1>][<常量表达式2>]…[<常量表达式n>];
其中,各对方括号中的常量表达式用来表示相应维的大小。例如:
float b[2][3]; char c[4][5][6];
其中,b是二维数组,每个元素的类型都是float型。c是三维数组,每个元素的类型都是字符型。
需要说明的是:
(1)要注意数组定义中维的高低。如图1.9所示,四维数组d的维的次序从右向左逐渐升高,最右边的是最低维,最左边的是最高维。
图1.9 多维数组的维次序
(2)对于多维数组来说,数组元素的个数是各维所指定的大小的乘积。例如,上述定义的二维数组b中的元素个数为2×3=6个,三维数组c中的元素个数为4×5×6=120个。
一旦定义了二维数组,就可以通过下面的格式来引用数组中的元素:
<数组名> [<下标表达式1>][<下标表达式2>]
对于多维数组来说,引用的格式为:
<数组名> [<下标表达式1>][<下标表达式2>]…[<下标表达式n>]
这里的下标表达式1、下标表达式2等分别与数组定义时的“维”相对应。也就是说,对于上述定义“float b[2][3];”中的二维数组b来说,其元素引用时需写成b[i][j]的形式。其中,每一维的下标序号都从0开始,且都小于相应维定义时指定的大小,即i的取值只能是0和1,j的取值只能是0、1和2。再如,对于上述定义“char c[4][5][6];”中的三维数组c来说,其合法的元素引用应是c[i][j][k]的形式,其中,i的取值只能是0~3的整数,j的取值只能是0~4的整数, k的取值只能是0~5的整数。
注意:
这里的下标序号都不能超过定义时所对应的维的大小。
与一维数组一样,通过对二维或多维数组元素的引用,二维或多维数组元素可与相同数据类型的普通变量一样进行赋值、算术运算及输入/输出等操作。
2.二维数组元素在内存空间存放的次序
由于内存空间是一维的,因此需要搞清二维或多维数组元素在内存中存放的次序。在C++中,表示维数的方括号的次序高低是从右向左逐渐升高,类似于十进制数中的个位、十位、千位……的变化次序。在内存中依次存放的数组元素的下标序号总是从低维到高维顺序变化。例如:
int a[3][4];
则a在内存中存放的元素的次序按数组元素下标序号变化的次序顺序排列,这样若在程序中通过循环嵌套来引用二维或多维数组的元素,则是最方便的方法。此时,循环嵌套的层数应与数组维数相同。例如:
int a[3][4], i, j; // 输入二维数组a的元素值 for (i=0; i<3; i++) for (j=0; j<4; j++) cin>>a[i][j]; // 输出二维数组a的全部元素 for (i=0; i<3; i++) { for (j=0; j<4; j++) cout<<a[i][j]<< "\t"; cout<<endl; }
3.二维数组的初始化和赋值
在程序设计中,常将二维数组看成一个具有行和列的数据表,例如:
int a[3][4];
由于它在内存空间的存放次序可以写成:
a[0]:a[0][0], a[0][1], a[0][2], a[0][3] // 第0行 a[1]:a[1][0], a[1][1], a[1][2], a[1][3] // 第1行 a[2]:a[2][0], a[2][1], a[2][2], a[2][3] // 第2行
因此,可以认为在数组a[3][4]中,3表示行数,4表示列数。故在进行二维数组初始化时一般以“行”为单位。
在C++中,“行”的数据由“{}”构成,且每一对“{}”根据其书写的次序依次对应二维数组的第0行、第1行、第2行……第i行。例如:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
其中,{1, 2, 3, 4}是对第0行元素进行初始化,{5, 6, 7, 8}是对第1行元素进行初始化,{9, 10, 11, 12}是对第2行元素进行初始化。它们依次进行,行与行之间用逗号分隔。每对花括号里的数据个数均不能大于列数。它等价于:
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; // 依次对元素进行初始化
需要说明的是:
(1)可以只对部分元素赋初值,例如:
int a[3][4]={{1,2},{3},{4,5,6}};
凡没有明确列举元素值的元素,其值均为0,即等同于:
int a[3][4]={{1,2,0,0},{3,0,0,0},{4,5,6,0}};
又如:
int a[3][4] = {1, 2, 3};
此时数据中没有花括号,则将其按元素在内存空间的存放次序依次赋初值,即a[0][0] = 1,a[0][1]= 2,a[0][2] = 3,其余各个元素的初值为0。
(2)对于多维数组来说,若有初始化,则定义数组时可以忽略最高维的大小,但其他维的大小不能省略。也就是说,在二维数组定义中,最左边方括号的大小可以不指定,但最右边方括号的大小必须指定。例如:
int b[][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; // 结果为b[3][4] int b[][4] = {{1, 2, 3, 4}, {5, 6}, {7},{ 8, 9, 10, 11}, 12}; // 结果为b[5][4] int b[][4]={1,2,3}; // 结果为b[1][4] int b[][4]={{1},2,3}; // 结果为b[2][4]
当定义的数组的数据类型为char时,该数组就称为字符数组,字符数组的每个元素都是字符。由于字符数组存放的是一个字符序列,因而它跟字符串常量有着密切的关系。在C++中,可用字符串常量来初始化字符数组,或通过字符数组名来引用字符串等。
1.一维字符数组
对于一维字符数组来说,它的初始化有两种方式。一是:
char ch[]={'H','e','l','l','o','!','\0'};
二是使用字符串常量来给字符数组赋初值。例如:
char ch[]={"Hello!"};
其中的花括号可以省略,即:
1.6.3 字符数组
char ch[]="Hello!";
这几种方式都是使ch[0]= 'H',ch[1]= 'e',ch[2]= 'l',ch[3]= 'l',ch[4]= 'o',ch[5]= '!',ch[6]= '\0'。
注意:
由于字符串常量总是以'\0'作为结束符,它虽不是字符串的内容,但却占据了一个存储空间。因此当字符数组用字符串常量方式进行初始化时,要注意数组的长度还应包含字符串的结束符'\0',一定要注意字符数组与其他数组的这种区别。
需要说明的是,如果指定的数组长度大于字符串中的字符个数,那么其余的元素将自动设定为'\0'。例如:
char ch[9]="Hello!";
因"Hello!"的字符个数为6,但还要包括一个空字符'\0',故数组长度至少是7,从ch[6]开始到ch[8]都等于空字符'\0'。
要注意,不能将字符串常量直接通过赋值语句赋给一个字符数组。例如,下列赋值语句是错误的:
char str[20]; str="Hello!"; // 错误
因为这里字符数组名str是一个指针常量(以后还会讨论),不能放在赋值运算符左边。
2.二维字符数组
一维字符数组常用于存取一个字符串,而二维字符数组可存取多个字符串。例如:
char str[][20]={"How", "are", "you"};
这时,数组元素str[0][0]表示一个char字符,值为'H';而str[0]表示字符串"How",str[1]表示字符串"are" ,str[2]表示字符串"you"。由于省略了二维字符数组的最高维的大小,编译会根据初始化的字符串常量,自动设为3。要注意,二维字符数组的最右边的大小应不小于初始化初值列表中最长字符串常量的字符个数+1。
1.6.4 数组与函数
以前所讨论的函数调用都是按实参和形参的对应关系将实际参数的值传递给形参,这种参数传递称为值传递。在值传递方式下,函数本身不对实参进行操作,也就是说,即使形参的值在函数中发生了变化,实参的值也不会受到影响。但如果传递函数的参数是某个内存空间的地址,则对这一个函数的调用就是按地址传递的函数调用,简称传址调用。由于函数形参和实参都是指向同一个内存空间的地址,形参值的改变也就是实参地址所指向的内存空间的内容改变,从而实参的值也将随之改变。通过地址传递,可以由函数带回一个或多个值。
数组也可作为函数的形参和实参,若数组元素作为函数的实参,则其用法与一般变量相同。当数组名作为函数的实参和形参时,由于数组名表示数组内存空间的首地址,因此是函数的地址传递。
【例Ex_StrChange】 改变字符串中的内容
#include <iostream.h> #include <string.h> void change(char str1[20]); int main() { char name[10] = "Ding"; cout<<name<<endl; change(name); // 调用时,只需指定数组名 cout<<name<<endl; return 0; } void change(char str1[20]) { strcpy( str1, "Zheng" ); }
代码中,函数change的形参是一个字符数组,调用时只要将数组名作为实参即可。由于函数传递的是数组的地址,因此函数中对形参改变的内容,也同样反映到实参中。
程序运行结果如下:
Ding
Zheng
需要说明的是:
(1)函数调用时,实参数组与形参数组的数据类型应一致,如不一致,结果将出错。
(2)形参数组也可以不指定大小,在定义数组时数组名后面跟一个空的方括号。为了满足在被调用函数中处理数组元素的需要,可以另设一个参数,传递数组元素的个数。例如:
float ave(int data[],int n);