C语言的枚举类型(enum)用于表达对象所属的类别。比如,人分男女,大学的学生则又分为专科生、本科生、硕士研究生和博士研究生。C语言中的联合(union)类型则为我们提供了操纵和解读“数据”的独特方式,它允许对同一块内存以不同的方式进行解读和操纵。
在C++程序中,上述两项特性使用得不多,故本章内容以在线方式提供。至于C++中的枚举类(enum class),我们在第13章讨论。
版权声明
本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。
本文不可以以纸质出版为目的进行改编、摘抄。
10. 枚举与联合
性灵出万象,风骨超常伦。 — 高适
10.1 枚举型
对事物进行分类是人类的技能之一。人分男女,大学的学生则又分为专科生、本科生、硕士研究生和博士研究生。与现实世界相对应,在程序当中,我们也常常需要表达对象所属的类别。而枚举类型,则是完成该任务的工具之一。
下述C语言代码定义了一个名为ColorType的枚举(enumeration)类型。
1 | //Project - ColorType |
对于编译器而言,枚举类型的实质就是整数。上述定义中,编译器会从0开始,给各个枚举项确定对应的整数值。同时,程序也可以给某些枚举项指定值,如本例中的yellow被指定为100。那些在yellow之后的枚举项,则依次被确定100+1,100+2,100+3。上述程序中的第5行注释列出了所有枚举项所对应的整数值。
上述定义之后,在C语言里便可以把enum ColorType当成一个新的数据类型来使用。在C++里,可以直接把ColorType当成数据类型使用,不必加上enum关键字。
1 | enum ColorType color = blue; |
上述代码定义了一个名为color的对象/变量,其类型为enum ColorType,初始值为blue,即整数102。
1 | color = green; |
任何ColorType的枚举项都可以赋值给color对象,也可以直接把整数赋值给color对象。当把一个不在ColorType枚举项范围内的整数,比如999赋值给color时,编译器会给出警告。
1 | color = (enum ColorType)103; //等价于violet |
有的编译器可能不喜欢你直接把整数赋值给枚举类型的对象,从而给出类型不一致的警告。严格地说,上述赋值语句的左边是enum ColorType类型,而103则是整数字面量。可以通过类型转换消除这种警告。
1 | printf("violt = %d\n",violet); |
所有的枚举项,均可视为整数类型的常量,上述代码的输出值应为103。
1 | //Project - ColorType |
也可以把枚举类型的对象作为switch分支语句的“整数表达式”。上述getColor()函数将enum ColorType对象转换成对应的英文字符串。
1 | printf("color = %s\n",getColor(green)); |
借助于getColor()函数,上行代码将枚举项green转换成字符串”green”并输出。
在作者的开发环境及编译器下,sizeof(color)及sizeof(enum ColorType)均为4,这说明编译器内部将该枚举类型按4字节整数处理。枚举型对应整数,但到底对应1个字节(unsigned char, char),2个字节(unsigned short, short),还是4个字节的整数(unsigned int, int)则取决于编译器的决定。但可以放心,编译器最终确定的整数类型一定可以容纳所有枚举项对应的整数值。
读者可能会问:既然枚举类型的实质就是整数,那直接用整数0、1、2、3 … 来表示红、橙、黄、绿不就可以了?要枚举型为何用?答案是枚举型可以改善程序的可读性,对程序的阅读者而言,red比0更容易理解和记忆。
练习巩固 👣 |
---|
10-1(枚举月份)定义枚举型Month,其中的枚举项以英文月份名January、February等表示;实现如下的getDays()函数,并编写代码加以验证。 说明:函数返回指定年月的天数,getDays(2022,April)应返回30,请注意区分闰年。 |
10.2 typedef语句
在编写单片机的C语言程序时,电气工程师特别喜欢清楚地了解每个整数类型的字节数,以及符号特性,即是有符号还是无符号整数。这个目的可以通过typedef语句实现。typedef源自英文type definition。
1 | //Project - TypeDef |
相对于unsigned char,unsigned short这种类型名称,UINT8,UINT16具有更好的解释性。在上述typedef之后,UINT8、INT32等类型直接与unsigned char,int通用。
1 | UINT8 b = 0x77; //b的类型实为unsigned char |
如果用sizeof操作符求上述b、s、c对象的字节数,分别应为1、2和4。
在第7章中,我们曾经借助typedef定义了一个名为biggerFunc的函数指针类型:
1 | typedef bool (*biggerFunc)(const string&, const string&); |
定义完成后,biggerFunc可以当成数据类型来使用,上述代码第2行中的变量f即是类型为biggerFunc的函数指针。
借助于typedef,我们也可以简化枚举类型的使用语法。下述代码将一个枚举类型定义为一个名为GenderType的数据类型:
1 | //Project - GenderType |
在C语言里,前述枚举类型ColorType必须结合enum关键字来使用,而使用typedef定义的GenderType可以直接使用。
1 | enum ColorType color = red; //必须结合enum关键字使用 |
10.3 枚举类
传统C/C++语言中的枚举类型有个很大的缺点。在下述GenderType枚举类型引入后,male、female作为一个枚举项,被视为整数常量。
1 | enum GenderType { |
male、female这两个名字处于全局名字空间。male、female是很普通的命名,它们的存在“污染”了名字空间。
【C++ 11】引入了enum class(枚举类),示例如下:
1 | //Project - EnumClass |
上述代码定义了一个名为GenderType的枚举类(enum class),其有两个枚举项,分别为male和female。基于类似的规则,male和female分别与整数0和1对应。上述定义中,GenderType冒号之后的unsigned char则显式地指定了该枚举类按unsigned char进行存储。如果省略该冒号及其之后的存储类型指示,枚举类的存储格式将由编译器自行确定。
要点🎯 域解析符 |
---|
“::”称为域解析符。Rocket::Engine可以理解为火箭(Rocket)里的发动机(Engine),以区别于Car::Engine(轿车里的发动机),和Engine(发动机)。 |
相较于传统的枚举类型,enum class带来诸多益处。首先,其枚举项不再“污染”名字空间,必须通过域解析符::来使用,见下述代码:
1 | GenderType gender = GenderType::male; //::为域解析符 |
作为一个整体,GenderType::male具有更好的自解释性且不容易导致意外重名。
此外,不同于传统枚举型,enum class不允许其枚举项与整数之间的隐式类型转换,但显式类型转换是允许的。这种更严格的类型要求可以减少因为疏忽而导致的软件缺陷。
1 | //gender = 1; //错误:不允许进行整数与enum class之间的隐式类型转换 |
如果对上述gender对象应用sizeof操作符,返回的字节数应为1,因为GenderType的定义过程中指定了背后的存储类型:unsigned char。
练习巩固 👣 |
---|
10-2(春花夏蝉秋实冬雪)请定义枚举类Season,并设计一个名为season的函数,使得下述代码可以得到期望的执行结果。 |
1 | int main(){ |
10.4 联合
C语言中的联合(union)类型为我们提供了操纵和解读“数据”的独特方式,它允许对同一块内存以不同的方式进行解读和操纵。
1 | union UINT { |
上述代码定义了一个名为UINT的联合类型。该类型提供了两个成员,分别是unsigned int类型的intValue,以及元素类型为unsigned char的长度为4的字符数组bytes。这两个成员的内存空间是共享的,即一个union UNIT类型的对象只占4个字节的空间。当以成员intValue进行操作时,这4个字节的内存被当成一个unsigned int进行操纵和解读;当以成员bytes进行操作时,这4个字节的内存被当成一个4字节的字符数组进行操纵和解读。
我们通过下述C语言程序来解释联合类型的使用方法。
1 | //Project - UnionExample |
上述程序的执行结果为:
1 | &v = 000000000061FE1C, &v.intValue = 000000000061FE1C, v.bytes = 000000000061FE1C |
说明:在读者的计算机上,执行结果中的地址很可能与本书不同。
要点🎯 |
---|
“.”被称为成员操作符,a.b意为对象a的b成员。 |
🚩第10行:C语言中,union UINT作为一个整体,代表名为UNIT的联合类型。在C++语言中,使用UINT类型时,前面的union关键字可以省略。此处的v是一个对象,其类型为union UINT。因为联合对象的多个成员是内存共享的,所以v的初始值必须以{ }包裹起来,.intValue指明了v初始化的实际动作是把0x11223344赋值给v的intValue成员。作者在这里故意使用了十六进制,因为十六进制每位占4个比特,每两位占1个字节。v定义并初始化以后,其内存结构可以用图10-1表示。
如图10-1所示,v占据了地址为0x0061FE1C、0x0061FE1D、0x0061FE1E和0x0061FE1F的连续4个字节的存储空间。v的所谓数据成员,无非这4字节内存的不同视图(view)而已。从v.intValue的角度看,这是一个地址为0x0061FE1C的32位无符号整数;从v.bytes的角度看,这是一个从地址0x0061FE1C开始的4个元素的字符数组。
读者可能注意到,图10-1 中,4个字节从低地址往高地址方向读,依次是0x44、0x33、0x22和0x11,其顺序与作为无符号整数的v.intValue的值正好相反。这是因为,Intel的x86系列CPU执行Little Endian的字节顺序(小端序),高位字节(0x11)存高地址(0x0061FE1F)。
🚩第12 ~ 13行:依次打印v的地址,v.intValue的地址,v.bytes的地址(数组名即为地址)。从执行结果的第1行可见,3个地址值相同。这证实,联合对象v的不同成员间是共享内存的。此处的v.intValue应用了”.”操作符,读者可以形象地将其理解为“v对象的intValue”。
🚩第15 ~ 16行:依次打印v.bytes的4个元素。这相当于从v.bytes的角度去解释v.intValue的数据。执行结果的第2行证实,v.intValue最高位字节的0x11存在了v.bytes[3]里。如刚才所述,这是小端序导致的。
🚩第18 ~ 19行:对v.bytes成员进行赋值,然后再以v.intValue成员解释数据。执行结果的第3行证实,对v.bytes的修改即是对v.intValue的修改。
🚩第21行:打印sizeof(v),执行结果证实,联合对象v占4个字节的空间。
请阅读下述C语言程序:
1 | //Project - MoreUnion |
上述代码的执行结果为:
1 | sizeof(union UMore) = 8 |
说明:在读者的计算机上,执行结果中的地址很可能与本书不同。
要点🎯 |
---|
“->”称为指向操作符,如果p是一个指针,p->a表示p所指向的对象的a成员。p->a与(*p).a等价。 |
上述程序中,联合类型UMore的三个成员分别占据8个、1个及4个字节的空间。从执行结果看,UMore类型的联合对象v占据8个字节的空间,正好是各成员空间尺寸的最大值。同时,还应注意到v的三个成员的地址相同。这意味着,当我们操作v.cValue时,仅会影响v的第0个字节,其余7个字节不受影响。
🚩第13 ~ 14行:当联合对象初始化时,如果没有指明被初始化的成员,则会默认初始化第1个成员。执行结果的第2行证实,v.dValue被初始化为33.22。
🚩第16 ~ 17行:打印v的dValue、cValue及iValue成员的地址。执行结果证实,三者的地址相同。
🚩第19 ~ 20行:定义了一个指向v的指针p。p->iValue表示指针p所指向的联合对象的iValue成员。从执行结果可见,将8个字节double数据的前4个字节当成int来解读,结果是“莫名其妙”的。
练习巩固 👣 |
---|
10-3(判定大小端序)结合typedef定义一个联合类型Endian,使得下述代码能正确判断CPU的大小端序。请解释该程序的工作原理。提示:联合体包括无符号整数成员i和无符号字符成员b。 |
1 | int main(){ |