第3章构造数据类型 引言 在实际应用中,除了使用基本数据类型描述所处理的问题外,人们经常要处理一些更复杂的数据对象,这些复杂的数据对象无法用单一的基本数据类型来描述,它们需要用一些简单的基本数据类型组合成比较复杂的数据类型才能予以描述和定义。为此,C++中提供了构造数据类型。 构造数据类型是用基本数据类型构造的用户自定义数据类型,用来对复杂的数据对象进行描述与处理。构造数据类型也称为自定义数据类型,包括枚举、数组、指针、字符串、引用类型、结构和联合。 学习目标 (1) 掌握枚举类型的使用; (2) 深入理解数组的概念,掌握数组应用的一般方法; (3) 深入理解指针的概念,掌握指针的使用; (4) 注意指针与数组的区别,会使用多重指针以及指针与数组的多种混合体,会分配动态数组; (5) 理解字符串的概念,会使用字符串; (6) 理解引用的概念,掌握引用型函数参数的用法; (7) 掌握结构与联合类型的使用,并注意二者的区别。 3.1枚 举 类 型 在生活中人们都有这样的常识: 一个星期有7天,分别是星期一、星期二、…、星期日; 交通灯只有红、黄、绿3种颜色。类似这样的情况还有很多例子,在计算机中可以用int、char等类型来表示这些数据。但是,如果将星期一至星期日表示为1~7的整数,一方面在程序中容易将它们与其他不表示星期的整数混淆; 另外,由于它们只能取有限的几种可能值,这样在程序中对数据的合法性检查成为一件比较麻烦的事。C++中的枚举类型就是专门用来解决这类问题的。 3.1.1枚举类型的定义 如果一个变量只有几种可能的取值,可以使用枚举类型来定义。枚举类型属于用户自定义数据类型。所谓“枚举”,是指将变量所有可能的取值一一列举出来,变量的取值只限于列举出来的常量。 枚举类型的声明的一般形式如下: enum枚举类型名{枚举常量1,枚举常量2,…,枚举常量n}; 其中:  枚举类型名以及枚举常量为标识符,遵循标识符的取名规则。  在定义一个枚举类型时定义了多个常量,供枚举类型变量取值,我们称此常量为枚举常量。当没给各枚举常量指定值时,其值依次默认为0、1、2、…在定义枚举类型时,也可以使用赋值号另行指定枚举常量的值。 例如: enum weekday { SUN,MON,TUE,WED,THU,FRI,SAT }; 定义了7个枚举常量以及枚举类型weekday。枚举常量具有默认的整数与之对应: SUN的值为0、MON的值为1、TUE的值为2、…、SAT的值为6。 enum city{ Beijing,Shanghai,Tianjin=5,Chongqing}; 枚举常量Beijing的值为0,Shanghai的值为1,Tianjin的值为5。对于没有指定值的枚举常量,编译器会将前一个常量值加1(下一个整数)赋给它,所以Chongqing的值为6。 枚举类型定义了以后就可以使用枚举常量、枚举类型来定义变量了,定义枚举变量的方法与定义其他变量的方法一样。例如: enum city city1,city2; city city1,city2; 用两种方法定义了city1、city2两个枚举类型的变量名。 枚举类型变量也可以在定义枚举类型的同时定义,例如: enum city{ Beijing,Shanghai,Tianjin=5,Chongqing} city1,city2; 在定义枚举类型的同时定义枚举类型变量可以省略枚举类型名,例如: enum { Beijing,Shanghai,Tianjin=5,Chongqing} city1,city2; 在定义变量时可以顺便给出初值,若不给初值,默认初值为随机的无意义的数。 3.1.2枚举类型的使用 定义一个枚举类型后,就可以直接使用各个枚举常量了。用枚举类型建立枚举变量后可以对枚举变量实施赋值以及进行其他运算,包括对枚举变量进行赋值,其值要求为同一枚举类型,否则在编译时会出错。 例如: weekday d1,d2,d3,d4; d1=SUN; d2=6;//错误 d3=Shanghai; //错误 其中,对d2所赋之值是整数6,不是枚举常量; 可以将一个整型值强制转换成同类型的枚举常量赋给枚举变量: d2=(weekday)6; 对d3的赋值不是同类型的枚举常量。 枚举常量、枚举类型的变量可进行算术运算、关系运算。对枚举类型实施算术、关系运算时,枚举值转换成整型值参加运算,结果为整型值。如果要将结果赋给枚举变量,还要将结果转换成枚举值。 例如: d1=d1+2;//是错误的,因为结果为int型 需要将它强制转换成枚举型: d1=(weekday)(d1+2); d1++; //也是错误的 枚举常量、枚举类型的变量可直接进行各种形式的关系运算。 例如: if(city1==3); if(city2>=Beijing); if(Shanghai==1); if(city1>SUN); 另外,枚举类型变量不能直接进行输入,例如: cin>>d1; //错误 ☆注意: (1) 枚举常量是常量,不是变量,所以不能对枚举常量进行赋值。在上例中不能进行“Shanghai=Beijing; ”的赋值。 (2) 枚举常量的值不是列举的字符串,其值为整数。 (3) 编译器对赋给枚举变量的对象(数)进行类型检查,如类型不相符则发出警告。当类型相同,而值超出此类枚举类型的枚举常量范围时也是正常的。 【例31】输入城市代号,输出城市名称。 1/********************************************************** 2*程序名:p3_1.cpp * 3*功能:枚举类型的使用,输入城市代号,输出城市名称 * 4**********************************************************/ 5#include 6using namespace std; 7enum city{ Beijing,Shanghai,Tianjin=6,Chongqing}; 8int main() 9{ 10int n; 11cout<<"Input a city number ("<>n; 13while(n>=Beijing){ 14switch(n) { 15case Beijing: cout<<"Beijing"<>n; 22} 23return 0; 24} 运行结果: Input a city number (-1 to exit): 1↙ Shanghai 8↙ Invalid city number! -1↙ 3.2数组 在程序中经常需要处理成批的数据,如一个班学生某门功课的成绩,这类数据有一个共同的特点: 它们有若干个同类型的数据元素,并且各个数据元素之间存在某种次序关系。如果用单个变量表示数据元素,一方面要建立很多变量,另一方面无法体现数据元素之间的关系。C++以及其他高级语言提供了数组这种数据类型来表示上述数据。 数组是一组在内存中依次连续存放的、具有同一类型的数据变量所组成的集合体。其中的每个变量称为数组元素,它们属于同一种数据类型,数组元素用数组名与带方括号的数组下标一起标识。数组可以是一维的,也可以是多维的。 3.2.1一维数组的定义与使用 数组属于构造数据类型,在使用之前必须先进行类型定义。 1. 一维数组的定义 定义一维数组的一般形式如下: 数据类型数组名[常量表达式]; 其中:  数组元素的类型可以是void型以外的任何一种基本数据类型,也可以是已经定义的构造数据类型。  数组名是用户自定义的标识符,用来表示数组的名称,代表数组元素在内存中的起始地址,是一个地址常量。  常量表达式必须是unsigned int类型的正整数,表示数组的大小或长度,也就是数组所包含数据元素的个数。  []是数组下标运算符,在数组定义时用来限定数组元素的个数。 例如,下面定义了两个不同类型的数组: int a[5]; //定义了一个5个元素的整型数组a weekday b[10];  //定义了一个10个元素的枚举数组b,weekday为已定义的枚举类型 数据类型相同的多个数组可以在同一条语句中予以定义。例如: int a1[10],a2[20]; //同时定义了两个整型数组 数据类型相同的简单变量和数组也可以在一个语句中定义。例如: int x,a[20]; //同时定义了一个整型变量和一个整型数组 数组在定义之后,系统将会从内存中为其分配一块连续的存储空间,从第1个数据元素开始依次存放各个数组元素。例如,定义的数组a的内存排列(分配)示意如图31所示。 一维数组所占内存大小(字节数)的计算公式如下: n*sizeof(元素类型); 或 sizeof(数组名); 其中,n为数组的长度。 若定义“int a[100]; sizeof(a)=100*sizeof(int)=400”,数组a所占内存的大小为400个字节。 图31一维数组的内存排列示意图 在定义数组时可以使用类型定义typedef为数组类型取一个名字,格式如下: typedef数据类型数组名[常量表达式]; 例如: typedef int A[5]; 定义了一个整型数组A,同时A是一个类型名。因此,可以使用类型A定义变量: A b; 定义了与A类型、长度相同的数组b。 2. 一维数组的初始化 一维数组的初始化是指在定义数组的同时给数组中的元素赋值。其一般语法格式如下: 数据类型数组名[常量表达式] ={初值1,初值2,…,初值n};初值表 其中:  {初值1,初值2,…,初值n}称为初值表,初值之间用逗号分隔,所有初值用{ }括起来。  初值可以是一个变量表达式,初值与数组元素的对应关系是初值i为数组的第i个元素,所以,初值个数n不能超过数组的大小。  若初值表中的初值个数(项数)小于数组的大小,则未指定值的数组元素被赋值为0,但初值表中的项数不能为0。例如: weekday b[10]={MON,WED,FRI}; 经过以上定义和初始化后,b的前3个元素的值分别为MON、WED、FRI,其余元素的值为默认值0。  当对全部数组元素赋初值时,可以省略数组的大小,此时数组的实际大小就是初值列表中初值的个数。例如: char str[] = {'a','b','c','d','e' }; 则数组str的实际大小为5。  在函数中定义数组时,如果没有给出初值表,数组不被初始化,其数组元素的值为随机值; 在函数外定义数组,如果没有初始化,其数组元素的值为0。  数组初值表可以用一个逗号结尾,其效果和没有逗号一样。例如: int a[2]={1,2};与int a[2]={1,2,};相同 int b[]={1,2};与int b[]={1,2,};相同 ☆注意: 在定义数组时,编译器必须知道数组的大小,并据此为整个数组分配适当大小的内存空间。因此,数组元素的个数一定是常量表达式,只有在定义数组时进行初始化才能省略数组的大小。 3. 一维数组的存取 对一维数组实施的存取操作有两类,即存取数组元素与读取数组元素的地址。数组元素是通过数组名及下标来标识的,这种带下标的数组元素也称为下标变量,下标变量可以像简单变量一样参与各种运算。存取一维数组元素的格式如下: 数组名[下标表达式]; 其中:  下标表达式可以是变量表达式,用来标识数组元素,不同于数组定义时用来确定数组长度的常量表达式。  当定义了一个长度为n的一维数组a时,C++规定数组的下标从0开始,依次为0、1、2、3、…、n-1,对应的数组元素分别是a[0]、a[1]、…、a[n-1],因此下标表达式的值要在0~n-1范围内。例如: a[1+2]=100; //将数组a的第4个元素赋值100 【例32】学生成绩排序。 分析: 学生成绩由键盘输入,当输入一个负数时,输入完毕。 采用直观的“选择排序法”进行排序,基本步骤如下: ① 将a[0]依次与a[1]~a[n-1]比较,选出大者与a[0]交换,最后a[0]为a[0]~a[n-1]中的最大者; ② 将a[1]依次与a[2]~a[n-1]比较,选出大者与a[1]交换,最后a[1]为a[1]~a[n-1]中的最大者; ③ 同理,从i=2到i=n-1,将a[i]依次与a[i+1]~a[n-1]比较,选出较大者存于a[i]中。 1/****************************************** 2*程序名: p3_2.cpp* 3*功能: 数组应用——选择排序 * 4******************************************/ 5#include 6using namespace std; 7int main() 8{const int MaxN=5; 9int n,a[MaxN],i,j; 10for (n=0;n>a[n]; //输入数组元素 13if(a[n]<0) 14break; 15} 16 17//对数组元素逐趟进行选择排序 18for(i=0;i 6using namespace std; 7int main() 8{ 9const int MaxN=100,CourseN=5; 10int n,score[MaxN][CourseN+1]={0}; 11float aver[CourseN+1]={0}; 12for(n=0;n>score[n][j]; 16if(score[n][0]<0) break;   //输入-1,结束输入 17} 18for(int i=0;i 7using namespace std; 8const col=5; 9enum dir {Asc,Des}; 10void sort(int a[][col],int n,int Cn,dir D)//排序 11{ 12int t[col]; //用于暂存一行数据 13for(int i=0;ia[j][Cn]&&D==Asc) 16{ 17memcpy(t,a[i],sizeof(t)); //交换数组行 18memcpy(a[i],a[j],sizeof(t)); 19memcpy(a[j],t,sizeof(t)); 20} 21} 22int main(){ 23{ 24const CourseN=5; 25int n,score[][CourseN]={{20140101,1,82,86,0}, 26{20140203,2,80,80,0}, 27{20140204,2,86,90,0}, 28{20140205,2,90,83,0}, 29{20140102,1,75,86,0}}; 30n=sizeof(score)/sizeof(score[0]); 31for(int i=0;i 6using namespace std; 7const NameLen=20; 8void order(char name[][NameLen],int n)//字符串排序 9{ 10char temp[NameLen]; 11for(int i=0;i0)  //比较两个字符串的大小 14{ 15strcpy(temp,name[i]);   //字符串交换 16strcpy(name[i],name[j]); 17strcpy(name[j],temp); 18} 19} 20int find(char name[][NameLen],int n,char searchname[NameLen]) 21{ 22for(int i=0;i0)  //未找完,但找不到,返回0 26return 0; 27return 0;   //找完,找不到,返回0 28} 2930int main() 31{ 32char NameTab[][NameLen]={"GongJing","LiuNa","HuangPin","ZhouZijun", 33"LianXiaolei","ChenHailing","CuiPeng","LiuPing"}; 34char searchname[NameLen]; 35int n=sizeof(NameTab)/NameLen; 36order(NameTab,n); 37for(int i=0;i>searchname; 41if(n=find(NameTab,n, searchname)) 42cout<<"Position:"< 6using namespace std; 7int main() 8{ 9int m,n; 10int **dm; 11cout<<"input matrix size m,n:"; 12cin>>m>>n; 13dm=new int * [m]; //建立m个指针,存储m行 14for(int i=0;i>dm[i][j]; 21} 22for(i=0;i 6using namespace std; 7int main() 8{ 9float (*p)[3][4]; 10int i,j,k; 11p = new float[2][3][4]; 12for (i=0; i<2; i++) 13for (j=0; j<3; j++) 14for (k=0; k<4; k++) 15*(*(*(p+i)+j)+k)=i*100+j*10+k; //通过指针访问数组元素 16for (i=0; i<2; i++) 17{ 18for (j=0; j<3; j++) 19{ 20for (k=0; k<4; k++) 21//将指针cp作为数组名使用,通过数组名和下标访问数组元素 22cout< 6using namespace std; 7void swap_i(int *num1,int *num2)//整型数交换 8{int t; 9t=*num1; 10*num1=*num2; 11*num2=t; 12} 13void swap(void *num1,void *num2,int size)  //所有类型数据交换 14{char *first=(char *)num1,*second=(char *)num2; 15for(int k=0;k 6using namespace std; 7char *ladd(char *s1,char *s2) 8{ 9int n1,n2,n; 10char *res,c=0; 11n1=strlen(s1);//n1=数字串s1的长度 12n2=strlen(s2);   //n2=数字串s2的长度 13n = n1>n2 ? n1 :n2;   //数字串s1、s2的最大长度 14res=new char [n+2];   //申请存结果串的内存 15for(int i=n+1;i>=0;i--)   //将s1从低位开始搬到res,没有数字的位 //以及最高位填'0' 16res[i] = i>n-n1 ? s1[i-n-1+n1] :'0'; 17for(i=n;i>=0;i--) 18{ 19char tchar; 20tchar = i>n-n2 ? res[i]-'0'+s2[i-n+n2-1]-'0'+c :res[i]-'0'+c; //将数字符变成数 21c = tchar>9 ? 1 :0;   //设进位 22res[i] = c>0 ? tchar-10+'0' :tchar+'0';  //将数字变成数字字符 23} 24return res; 25} 26int main() 27{ 28char num1[100],num2[100],*num; 29cin>>num1>>num2; 30num=ladd(num1,num2); 31cout< 6using namespace std; 7int add(int a,int b) { 8return a+b; 9} 10int sub(int a,int b) { 11return a-b; 12} 13int mul(int a,int b) { 14return a*b; 15} 16int divi(int a,int b) { 17if (b==0) return 0x7fffffff; 18else return a/b; 19} 20int (*menu[])(int a,int b)={add,sub,mul,divi}; 21int main() 22{ 23int num1,num2,choice; 24cout<<"Select operator:"<>choice; 30cout<<"Input number(a,b):"; 31cin>>num1>>num2; 32cout<<"Result:"< 6using namespace std; 7void swap(int &refx,int &refy) 8{ 9int temp; 10temp=refx; 11refx=refy; 12refy=temp; 13} 14int main() 15{ 16int x=3,y=5; 17cout<<"before swap:x="< 结构变量名的成员名; (5) 结构变量之间可以相互赋值,结构变量之间的赋值等价于各个成员之间逐一相互赋值。如果成员是数组,则将数组元素一一赋值。 例如: s001=s002; 相当于进行以下赋值: s001.no=s002.no; s001.birthday=s002.birthday; ... s001.score=s002.score; 对于s001.name相当于进行了: memcpy(s001.name,s002.name,sizeof(s002.name)); 但对于结构变量中的指针,C++只是将地址进行了赋值。结构变量间的赋值称为结构式拷贝,属于浅拷贝(后面章节将详细介绍)。 【例313】使用结构数组存储学生信息,按学生成绩从高到低排序。 分析: 按成绩排序时需要进行数组元素交换,利用结构式拷贝对结构体中存储学生姓名的字符数组进行复制,从而将成绩与姓名一并交换。 1/******************************************* 2*程序名: p3_13.cpp* 3*功能: 学生成绩的排序,结构数组应用 * 4*******************************************/ 5#include 6using namespace std; 7struct student 8{ 9char name[20]; 10float score; 11}; 12int input(student s[],int n)//返回实际输入人数 13{ 14for(int i=0;i>s[i].name>>s[i].score; 17if (s[i].score<0) break; 18} 19return i; 20} 21void output(student s[],int n) 22{for(int i=0;inext=NULL head=newp; (2) 创建新结点。 newp=new student; (3) 插入新结点。 如果要插入一个结点,首先要找到插入位置。假定要建立一个按成绩从高到低排列的链表,查找插入位置需要从头结点开始遍历链表,直到找到这样的一个结点,其后继结点的成绩比要插入结点的成绩低,插入点就在此结点之后。假定p指向当前结点,插入新结点newp的步骤如图38所示。 图38在链表中插入结点 (4) 删除结点。 如果要删除一个结点,一定要有一个指向此结点前驱结点的指针,除非是头结点。删除结点的操作步骤如图39所示。 图39删除链表中的结点 【例314】使用单向链表按学生成绩从高到低的顺序存储学生信息,从中删除不及格学生的信息。 1/*********************************************** 2*程序名: p3_14.cpp* 3*功能: 单向链表的排序、查找、插入、删除 * 4***********************************************/ 5#include 6using namespace std; 7struct student 8{ 9char name[20]; 10float score; 11struct student *next; 12}; 13typedef student NODE; 14NODE *Search(NODE *head,int key) { //查找关键字小于key的结点的前驱 15NODE *p; 16p=head; 17while(p->next!=NULL) { 18if(p->next->scorenext; 21} 22return p; 23} 24void InsertNode(NODE *p,NODE *newp) { //在p之后插入结点newp 25newp->next=p->next; 26p->next=newp; 27} 28void DelNode(NODE *p) {   //删除p结点的一个后继结点 29NODE *q; 30if(p->next!=NULL) { 31q=p->next; 32p->next=q->next; 33delete q; 34} 35} 36void DelList(NODE *head) {   //销毁整个链表 37NODE *p; 38p=head; 39while(head->next!=NULL) { 40head=head->next; 41delete p; 42p=head; 43} 44delete head; 45} 46void DispList(NODE *head) {   //显示链表各元素 47NODE *p; 48p=head; 49while(p->next!=NULL) { 50cout<next->name<<"\t"<next->score<next; 52} 53} 54int main() 55{ 56NODE *newp,*head,*p; 57char name[20]; 58float score,low=60; 59if((newp=new NODE)==NULL) { 60cout<<"new NODE fail!"<next=NULL; 65cout<<"Input name and score(–1 to exit):"<>name>>score; 67while(score>0) 68{ 69if((newp=new NODE)==NULL) 70{ 71cout<<"new NODE fail!"<name,name); 75newp->score=score; 76newp->next=NULL; 77p=Search(head,score); 78InsertNode(p,newp); 79cin>>name>>score; 80} 81cout<<"Before delete:"<next!=NULL;p=Search(head,low)) 84DelNode(p); 85cout<<"After delete:"< 联合变量的成员名; 3. 联合变量在内存中的排列 在定义联合变量后,联合变量在内存中获得了共同的内存,由于各个成员的数据类型不同,因此长度也不同。各成员在内存中的排列均从低地址开始,遵循“低地址低字节,高地址高字节”的原则。 例如: UData u; strcpy(u.str,"123456789"); //给成员u.str赋初值 其内存图如图310所示。 图310联合体在内存中的排列 【例315】显示联合体中各成员的内存排列。 1/***************************************************** 2*程序名: p3_15.cpp* 3*功能: 显示联合体中各类型成员的内存排列 * 4*****************************************************/ 5#include 6using namespace std; 7int main() { 8union UData 9{ 10charCh; 11short Sint; 12long Lint; 13unsigned Uint; 14float f; 15double d; 16char str[10]; 17}; 18UData u; 19strcpy(u.str,"123456789"); 20cout<<"char:"<<'\t'< using namespace std; int main() { char a[]="Hello,World"; char *ptr=a; while (*ptr) { if(*ptr>='a'&&*ptr<='z') cout<>s; (23) 对于指向同一块连续内存的两个指针变量不能进行的运算是()。 A. < B. = C. + D. - (24) 若有语句“int*point,a = 4; ”和“point = &a; ”,下面均代表地址的一组选项是()。 A. a,point,*&a B. &*a,&a,*point C. *&point,*point,&a D. &a,&*point,point (25) 已有定义“int k = 2; int *ptr1,*ptr2; ”,且ptr1和ptr2均指向变量k,下面不能正确执行的赋值语句是()。 A. k = *ptr1 + *ptr2; B. ptr2 = k; C. ptr1 = ptr2; D. k = *ptr1 *(*ptr2); (26) 若有说明“int i,j = 2,*p = &i; ”,则能完成i = j赋值功能的语句是()。 A. i = *p; B. *p = *&j; C. i = &j; D. i = **p; (27) 若有定义“int a[8]; ”,则以下表达式中不能代表数组元素a[1]的地址的是()。 A. &a[0] + 1 B. &a[1] C. &a[0]++ D. a + 1 (28) 若有以下语句且0≤k<6,则正确表示数组元素地址的表达式是()。 int x[] = {1,3,5,7,9,11},*ptr = x,k; A. x++ B. &ptr C. &ptr[k] D. &(x+1) (29) 下面程序段的运行结果是()。 char *p = "abcdefgh"; p+=3; cout<y?x:y);} int main() { int m(3),n(4); max(m,n)--; cout< using namespace std; int main() { float x[8] = ①; float aver(0),max②,min③; for(int i=0;i < 8;i++){ cin>>x[i]; if(x[i]>max) ④; if(⑤) min=x[i]; aver += x[i]; cout< using namespace std; int main() { ①; int x[M][N] = {1,5,6,4,2,7,4,3,8,2,3,1}; for(②;i < M;i++) { int t = 0; for(③;j < N;j++) if(④) ⑤; cout< using namespace std; int &add(int x,int y) { return x+y; } int main() { int n(2),m(10); cout<<(add(n,m)+= 10)<