1.9 结构、共用和自定义
程序中所描述的数据往往来源于日常生活,比如一个学生有多门课程成绩,此时用一维数组来组织数据则可满足需要。若是多个学生有多门课程成绩,则此时用二维数组来组织仍可满足,但若还有每门课程的学分数据,则用三维数组就无法反映其对应关系了。事实上,可将数据这个概念扩展为信息,每条信息看做一条记录。显然,对记录的描述就不能用简单的一维或多维数组来组织,而应该使用从C语言继承下来的结构体类型来构成。除结构体之外,C++还允许构造共用体等类型,它们从另一个方面来诠释数据类型的构造方法。
1.9.1 结构体
结构体是从C语言继承下来的一种构造数据类型,它是由多种类型的数据(变量)组成的整体。组成结构类型的各个分量称为结构的数据成员(简称为成员,或称为成员变量)。
1.结构类型声明
在C++中,结构类型的声明可按下列格式进行:
struct [结构类型名] { <成员定义1>; <成员定义2>; … <成员定义n>; };
结构类型声明是以关键字struct开始的,结构类型名应是一个有效的合法的标识符,若该结构类型变量以后不再定义,结构类型名也可不指定。结构体中的每个成员都必须通过成员定义来确定其数据类型和成员名。要注意:
(1)成员的数据类型可以是基本数据类型,也可以是数组、结构等构造类型或其他已声明的合法的数据类型。
(2)结构类型的声明仅仅是一个数据类型的说明,编译不会为其分配内存空间,只有当用结构类型定义结构类型的变量时,编译才会为这种变量分配内存空间。
(3)结构类型声明中最后的分号“;”不要漏掉。
例如,若声明的学生成绩结构类型为:
struct STUDENT { int no; // 学号 float score[3]; // 三门课程成绩 float edit[3]; // 三门课程的学分 float total,ave; // 总成绩和平均成绩 float alledit; // 总学分 }; // 分号不能漏掉
则结构体中的成员变量有no(学号)、score[3](三门课程成绩)、edit[3](三门课程的学分)、total (总成绩)、ave(平均成绩)和alledit(总学分)。需要说明的是:
(1)在结构体中,成员变量定义与一般变量定义规则相同。例如,若成员变量的数据类型相同,可写在一行定义语句中,如total和ave成员变量的定义。
(2)结构体中的成员变量的定义次序只会影响在内存空间中的分配顺序(当定义该结构类型变量时),而对所声明的结构类型没有影响。
(3)结构类型名是区分不同类型的标志。例如,若再声明一个结构类型PERSON,其成员变量都与STUDENT相同,但却是两个不同的结构类型。结构类型名通常用大写来表示,以便与其他类型名相区别。
2.结构类型变量的定义
一旦在程序中声明了一个结构类型,就为程序增添了一种新的数据类型,也就可以用这种数据类型定义该结构类型的变量。虽然结构类型变量的定义与基本数据类型定义基本相同,但也有一些区别。在C++中,定义一个结构类型变量可有3种方式。
(1)先声明结构类型,再定义结构类型变量,称为声明之后定义方式(推荐方式)。
这种方式与基本数据类型定义格式相同,即:
[struct]<结构类型名> <变量名1>[,<变量名2>, … <变量名n>];
例如:
struct STUDENT stu1,stu2;
其中,结构类型名STUDENT前面的关键字struct可以省略。一旦定义了结构类型变量,编译就会为其分配相应的内存空间,其内存空间的大小就是声明时指定的各个成员所占的内存空间的大小之和。
(2)在结构类型声明的同时定义结构类型变量,称为声明之时定义方式。
这种方式是将结构类型的声明和变量的定义同时进行。在格式上,被定义的结构类型变量名应写在最后的花括号和分号之间,多个变量名之间要用逗号隔开。例如:
struct STUDENT { //… }stu1,stu2; // 定义结构类型变量
(3)在声明结构类型时,省略结构类型名,直接定义结构类型变量。
由于这种方式一般只用于程序不再二次使用该结构类型的场合,因此称这方式为一次性定义方式。例如:
struct { //… }stu1,stu2; // 定义结构类型变量
此时应将左花括号“{”和关键字struct写在一行上,以便与其他方式相区别,也增强了程序的可读性。
3.结构类型变量的初始化
与一般变量和数组一样,结构类型变量也允许在定义的同时赋初值,即结构类型变量的初始化,其一般形式是在定义的结构类型变量后面加上“= {<初值列表>};”。例如:
STUDENT stu1={1001,90,95,75,3,2,2};
它是将花括号中的初值按其成员变量定义的顺序依次给成员变量赋初值,也就是说,此时stu1中的no = 1001,score[0] = 90,score[1] = 95,score[2] = 75,edit[0] = 3,edit[1] = 2,edit[2] =2。由于其他成员变量的初值未被指定,因此它们的值是默认值或不确定。
需要说明的是,可以在上述stu1的初值列表中,适当地增加一些花括号,以增加可读性,如stu1的成员score和edit都是一维数组,因此可以这样初始化:
STUDENT stu1={1001,{90,95,75},{3,2,2}};
此时初值中花括号仅起分隔作用。但若是对结构类型数组进行初始化,则不能这么做。
前面已提及,结构类型中成员的数据类型还可以是另一个已定义的结构类型,例如:
struct POINT { int x, y; }; struct RECT { POINT ptLeftTop; // 使用已定义过的结构类型POINT int nWidth; int nHeight; };
此时对RECT变量初始化时,可使用花括号来增强其可读性。例如:
RECT rc1={{20,30},100,80}; // 里面的花括号是为了增强可读性
4.结构类型变量的引用
当一个结构类型变量定义后,就可引用这个变量。使用时,应遵循下列规则:
(1)只能引用结构类型变量中的成员变量,并使用下列格式:
<结构体变量名>.<成员变量名>
例如:
struct POINT { int x, y; } spot = {20, 30}; cout<<spot.x<<spot.y;
其中,“.”是成员运算符,它的优先级很高,仅次于域运算符“::”,因而可以把spot.x和spot.y作为一个整体来看待,它可以像普通变量那样进行赋值或进行其他各种运算。
(2)若成员本身又是一个结构类型变量,则引用时需要用多个成员运算符一级一级地找到最低一级的成员。例如:
struct RECT { POINT ptLeftTop; POINT ptRightDown; } rc = {{10,20},{40,50}};
则有:
cout<<rc.ptLeftTop.x<< rc.ptLeftTop.y;
(3)多数情况下,结构类型相同的变量之间可以直接赋值,这种赋值等效于各个成员的依次赋值。如:
struct POINT { int x, y; }; POINT pt1={10,20}; POINT pt2=pt1; // 将pt1直接赋给pt2 cout<<pt2.x<<"\t"<<pt2.y<<endl; // 输出 10 20
其中,pt2 = pt1等效于pt2.x = pt1.x; pt2.y = pt1.y;
数组是相同数据类型的元素的集合,当然元素的数据类型也可以是结构类型。由结构类型的元素组成的数组称为结构数组。
1.结构数组的初始化
在定义结构数组的同时,也可对其进行初始化,其方法与数组相同。但要注意,由于结构类型声明的是一条记录信息,而一条记录在二维线性表中就表示一个行,因此一维结构数组的初始化形式应与二维普通数组相同。例如:
struct STUDENT { int no; // 学号 float score[3]; // 三门课程成绩 float edit[3]; // 三门课程的学分 float total,ave; // 总成绩和平均成绩 float alledit; // 总学分 }; STUDENT stu[3]={{1001,90,95,75,3,2,2}, {1002, 80, 90, 78, 3, 2, 2}, {1003, 75, 80, 72, 3, 2, 2}};
1.9.2 结构数组
此时初值中的花括号起到类似二维数组中的行的作用,并与二维数组初始化中的花括号的使用规则相同。这里依次将初值中的第1对花括号里的数值赋给元素stu[0]中的成员,将初值中的第2对花括号里的数值赋给元素stu[1]中的成员,将初值中的第3对花括号里的数值赋给元素stu[2]中的成员。需要说明的是,与普通数组初始化相同,在结构数组初始化中,凡成员未被指定初值时,这些成员的初值均为0。
2.结构数组元素的引用
一旦定义了结构数组,就可以在程序中引用结构数组元素。由于结构数组元素等同于一个同类型的结构变量,因此它的引用与结构变量相类似,格式如下:
<结构数组名>[<下标表达式>].<成员>
例如:
for (int i=0; i< sizeof(stu)/sizeof(STUDENT); i++) { stu[i].total =stu[i].score[0]+stu[i].score[1]+stu[i].score[2]; stu[i].ave =stu[i].total/3.0; stu[i].alledit =stu[i].edit[0]+stu[i].edit[1]+stu[i].edit[2]; if (stu[i].ave > stu[nMax].ave) nMax = i; }
1.9.3 结构与函数
当结构类型变量作为函数的参数时,它与普通变量一样,由于结构类型变量不是地址,因此这种传递是值传递方式,整个结构都将被复制到形参中去。
【例Ex_StructValue】 将结构体的值作为参数传给函数
#include <iostream.h> struct PERSON { int age; // 年龄 float weight; // 体重 char name[25]; // 姓名 }; void print(PERSON one) { cout <<one.name<<"\t" <<one.age<<"\t" <<one.weight<<"\n"; } PERSON all[4]={ {20,60,"Zhang"}, {28,50,"Fang"}, {33,78,"Ding"}, {19,65,"Chen"}}; int main() { for(int i=0;i<4;i++) print(all[i]); return 0; }
程序运行结果如下:
Zhang 20 60
Fang 28 50
Ding 33 78
Chen 19 65
print函数的参数是PERSON结构变量,main函数调用了4次print函数,实参为PERSON结构数组的元素。
事实上,结构体还可以作为一个函数的返回值。
【例Ex_StructReturn】 将结构体的值作为参数传给函数
#include <iostream.h> struct PERSON { int age; // 年龄 float weight; // 体重 char name[25]; // 姓名 }; void print(PERSON one) { cout <<one.name<<"\t" <<one.age<<"\t" <<one.weight<<"\n"; } PERSON getperson() { PERSON temp; cout<<"请输入姓名、年龄和体重:"; cin>>temp.name>>temp.age>>temp.weight; return temp; } int main() { PERSON one=getperson(); print(one); return 0; }
程序运行结果如下:
请输入姓名、年龄和体重:ding 4190↵
ding 41 90
由于函数getperson返回的是一个结构类型的值,因此可以先在函数中定义一个局部作用域的临时结构体变量temp,当用户输入的数据保存在temp后,通过return返回。
1.9.4 结构指针
当定义一个指针变量的数据类型是结构类型时,这样的指针变量就称为结构指针变量,它指向结构体类型变量。
【例Ex_StructPointer】 指针在结构体中的应用
#include <iostream.h> #include <string.h> struct PERSON { int age; // 年龄 char sex; // 性别 float weight; // 体重 char name[25]; // 姓名 }; int main() { struct PERSON one; struct PERSON *p; // 指向PERSON类型的指针变量 p=&one; p->age=32; p->sex=’M’; p->weight=(float)80.2; strcpy(p->name,"LiMing"); cout<<"姓名:"<<(*p).name<<endl; cout<<"性别:"<<(*p).sex<<endl; cout<<"年龄:"<<(*p).age<<endl; cout<<"体重(kg):"<<(*p).weight<<endl; return 0; }
程序运行结果如下:
姓名:LiMing
性别:M
年龄:32
体重(kg):80.2
程序中,“->”称为指向运算符,它的左边必须是一个指针变量,它等效于指针变量所指向的结构体类型变量,如p->name和(*p).name是等价的,都是引用结构PERSON类型变量one中的成员name,由于成员运算符“.”优先于“*”运算符,所以(*p).name中*p两侧的括号不能省略,否则*p.name与*(p.name)等价,但这里的*(p.name)是错误的。
若将结构体变量看成一个整体,那么指向结构体变量数组的指针操作和指向数组的指针操作是一样的。例如若有:
PERSON many[10],*pp; pp=many; // 等价于pp=&many[0];
则pp+i与many+i是等价的,(pp+i)->name与many[i].name是等价的,等等。
事实上,结构指针变量也可作为函数的参数或返回值,由于其使用方法与一般指针变量相类似,因此这里不再赘述。
在C++中,共用体的功能和语句都和结构体相同,但它们最大的区别是:共用体在任一时刻只有一个成员处于活动状态,且共用体变量所占的内存长度等于各个成员中最长成员的长度,而结构体变量所占的内存长度等于各个成员的长度之和。在共用体中,各个成员所占内存的字节数各不相同,但都是从同一地址开始的。
1.9.5 共用体
定义一个共用体可用下列格式:
union <共用体名> { <成员定义1>; <成员定义2>; … <成员定义n>; }[共用体变量名表]; // 注意最后的分号不要忘记
例如:
union NumericType { int iValue; // 整型变量,4字节长 long lValue; // 长整型变量,4字节长 float fValue; // 实型,8字节长 };
这时,系统为NumericType开辟了8字节的内存空间,因为成员fValue是实型,故它所占空间最大。需要说明的是,共用体除了关键字(union)与结构体不同外,其使用方法均与结构体相同。
【例Ex_Union】 共用体的使用
#include <iostream.h> #include <string.h> union PERSON { int age; // 年龄 float weight; // 体重 char name[25]; // 姓名 }; void print(PERSON one) { cout <<one.name<<"\t" <<one.age<<"\t" <<one.weight<<"\n"; } int main() { PERSON all={33}; // all.age=33 print(all); // 只有all.age有效 all.weight=80; print(all); // 只有all.weight有效 strcpy(all.name,"ding"); print(all); // 只有all.name有效 return 0; }
程序运行结果如下:
! 33 4.62428e-044 1117782016 80 ding 1735289188 1.12587e+024
1.9.6 使用typedef
在C++中可使用关键字typedef来为一个已定义的合法的类型名增加新名称,从而使相同类型具有不同的类型名,这样做的好处有两个:一是可以按统一的命名规则定义一套类型名称体系,从而可以提高程序的移植性;二是可以将一些难以理解的、冗长的数据类型名重新命名,使其变得容易理解和阅读。例如,若为const char *类型名增加新的名称CSTR,则在程序中不仅书写方便,而且更具可读性。这里就不同数据类型来说明typedef的使用方法。
1.为基本数据类型名添加新的类型名
当使用typedef为基本数据类型名增加新的名称时,可使用下列格式:
typedef <基本数据类型名> <新的类型名>;
其功能是将新的类型名赋予基本数据类型的含义。其中,基本数据类型名可以是char、short、int、long、float、double等,也可以是带有const、unsigned或其他修饰符的基本类型名。例如:
typedef int Int; typedef unsigned int UInt; typedef const int CInt;
注意:
书写时typedef及类型名之间必须要有一个或多个空格,且一条typedef语句只能定义一个新的类型名。这样,上述三条typedef语句就使得在原先基本数据类型名的基础上增加了Int、UInt和CInt类型名。之后,就可直接使用这些新的类型名来定义变量了。例如:
UInt a,b; // 等效于unsigned int a,b; CInt c=8; // 等效于const int a=8;
再如,若有:
typedef short Int16; typedef int Int32;
则新的类型名Int16和Int32可分别反映16位和32位的整型。这在32位系统中,类型名和实际是吻合的。若在16位系统中,为了使Int16和Int32也具有上述含义,则可用typedef语句重新定义:
typedef int Int16; typedef long Int32;
这样就保证了程序的可移植性。
2.为数组类型名增加新的类型名
当使用typedef为数组类型名增加新的名称时,可使用下列格式:
typedef <数组类型名> <新的类型名>[<下标>];
其功能是将新的类型名作为一个数组类型名,下标用来指定数组的大小。例如:
typedef int Ints[10]; typedef float Floats[20];
则新的类型名Ints和Floats分别表示具有10个元素的整型数组类型和具有20个元素的单精度实型数组类型。这样,若有:
Ints a; // 等效于int a[10]; Floats b; // 等效于float b[20];
3.为结构类型名增加新的类型名
当使用typedef为结构类型名增加新的类型名称时,可使用下列格式:
typedef struct [结构类型名] { <成员定义>; … } <新的类型名> ;
这种格式是在结构类型声明的同时进行的,其功能是将新的类型名作为此结构类型的一个新名称。例如:
typedef struct student { … } STUDENT; STUDENT stu1; // 等效于struct student stu1;
4.为指针类型名增加新的类型名称
由于指针类型不容易理解,因此typedef常用于指针类型名的重新命名。例如:
typedef int* PInt; typedef float* PFloat; typedef char* String; PInt a,b; // 等效于int*a,*b;
则PInt、PFloat和String分别被声明成整型指针类型名、单精度实型指针类型名和字符指针类型名。由于字符指针类型常用来操作一个字符串,因此常将字符指针类型名声明为String或STR。
可见,用typedef为一个已有的类型名声明新的类型名称的方法时可按下列步骤进行:
① 用已有的类型名写出定义一个变量的格式,如int a;
② 在格式中将变量名换成要声明的新的类型名称,如int Int;
③ 在最前面添加上关键字typedef即可完成声明,如typedef int Int;
④ 之后,就可使用新的类型名定义变量了。
需要说明的是,与struct、enum和union构造类型不同的是,typedef不能用于定义变量,也不会产生新的数据类型,它所声明的仅仅是一个已有数据类型的别名。另外,typedef声明的标识符也有作用域范围,也遵循先声明后使用的原则。
以上是C++语言最基础的内容。下一章将讨论C++所支持的面向对象的程序设计方法。