enum class 、enum struct 和 enum

至从C语言开始enum类型就被作为用户自定义分类有限集合常量的方法被引入到了语言当中,而且一度成为C++中定义编译期常量的唯一方法(后来在类中引入了静态整型常量)。
根据上面对enum类型的描述,有以下几个问题:
1.到底enum所定义出来的类型是一个什么样的类型呢?
2.作为一个用户自定义的类型其所占用的内存空间是多少呢?
3.使用enum类型是否真的能够起到有限集合常量的边界约束呢?
4.大家可能都知道enum类型和int类型具有隐示(自动)转换的规则,那么是否真的在任何地方都可以使用enum类型的变量来代替int类型的变量呢?

  1. 到底enum所定义出来的类型是一个什么样的类型呢?
    在C++中大家都知道仅仅有两种大的类型分类:POD类型(注(1))和类类型。
    enum所定义的类型其实属于POD类型,也就是说它会参与到POD类型的隐示转换规则当中去,所以才会出现enum类型与int类型之间的隐示转换现象。
    那么也就是说enum所定义的类型不具备名字空间限定能力(因为不属于类类型),其所定义的常量子具备和enum类型所在名字空间相同的可见性,由于自身没有名字限定能力,所以会出现名字冲突现象。
    如:
struct CEType
{
       enum EType1 { e1, e2 };
       enum EType2 { e1, e2 };
};

上面的例子会出现e1、e2名字冲突编译时错误,原因就在于枚举子(e1、e2)是CEType名字空间中的名字,同样在引用该CEType中的枚举子时必须采用CEType::e1这样的方式进行,而不是CEType::EType1::e1来进行引用。

注(1)POD类型:
你可以将 POD 类型看作是一种来自外太空的用绿色保护层包装的数据类型,POD 意为”Plain Old Data”(译者:如果一定要译成中文,那就叫”彻头彻尾的老数据”怎么样!)这就是 POD 类型的含义。
其确切定义相当粗糙(参见 C++ ISO 标准),其基本意思是 POD 类型包含与 C 兼容的原始数据。
例如,结构和整型是 POD 类型,但带有构造函数或虚拟函数的类则不是。
POD 类型没有虚拟函数,基类,用户定义的构造函数,拷贝构造,赋值操作符或析构函数。
为了将 POD 类型概念化,你可以通过拷贝其比特来拷贝它们。此外, POD 类型可以是非初始化的。

  1. 作为一个用户自定义的类型其所占用的内存空间是多少呢?
    该问题就是sizeof( EType1 )等于多少的问题,是不是每一个用户自定义的枚举类型都具有相同的尺寸呢?
    在大多数的32位编译器下(如:VC++、gcc等)一个枚举类型的尺寸其实就是一个sizeof( int )的大小,难道枚举类型的尺寸真的就应该是int类型的尺寸吗?
    其实不是这样的,在C++标准文档(ISO14882)中并没有这样来定义,
    标准中是这样说明的:”枚举类型的尺寸是以能够容纳最大枚举子的值的整数的尺寸”,
    同时标准中也说明了:”枚举类型中的枚举子的值必须要能够用一个int类型表述”,
    也就是说,枚举类型的尺寸不能够超过int类型的尺寸,但是是不是必须和int类型具有相同的尺寸呢?
    上面的标准已经说得很清楚了,只要能够容纳最大的枚举子的值的整数就可以了,那么就是说可以是char、short和int。
    例如:
    enum EType1 { e1 = CHAR_MAX };
    enum EType2 { e2 = SHRT_MAX };
    enum EType3 { e3 = INT_MAX };
    上面的三个枚举类型分别可以用char、short、int的内存空间进行表示,也就是:
    sizeof( EType1 ) == sizeof( char );
    sizeof( EType2 ) == sizeof( short );
    sizeof( EType3 ) == sizeof( int );
    那为什么在32位的编译器下都会将上面三个枚举类型的尺寸编译成int类型的尺寸呢?
    主要是从32位数据内存对其方面的要求进行考虑的,在某些计算机硬件环境下具有对齐的强制性要求(如:sun SPARC),
    有些则是因为采用一个完整的32位字长CPU处理效率非常高的原因(如:IA32)。
    所以不可以简单的假设枚举类型的尺寸就是int类型的尺寸,说不定会遇到一个编译器为了节约内存而采用上面的处理策略。
  2. 使用enum类型是否真的能够起到有限集合常量的边界约束呢?
    首先看一下下面这个例子:
enum EType { e1 = 0, e2 };
           void func1( EType e )
           {
               if ( e == e1 )
               {
                   // do something
               }
               // do something because e != e1 must e == e2
           }
           void func2( EType e )
           {
               if ( e == e1 )
               {
                   // do something
               }
               else if ( e == e2 )
               {
                   // do something
               }
           }

           func1( static_cast( 2  ) );
           func2( static_cast( -1 ) );

上面的代码应该很清楚的说明了这样一种异常的情况了,在使用一个操出范围的整型值调用func1函数时会导致函数采取不该采取的行为,而第二个函数可能会好一些他仅仅是忽略了超出范围的值。

这就说明枚举所定义的类型并不是一个真正强类型的有限常量集合,这样一种条件下和将上述的两个函数参数声明成为整数类型没有任何差异。所以以后要注意标准定义中枚举类型的陷阱。
(其实只有类类型才是真正的强类型)

  1. 是否真的在任何地方都可以使用enum类型的变量来代替int类型的变量呢?
    通过上面的讨论,其实枚举类型的变量和整型变量具有了太多的一致性和可互换性,那么是不是在每一个可以使用int类型的地方都可以很好的用枚举类型来替代呢?
    其实也不是这样的,毕竟枚举类型是一个在编译时可区分的类型,
    同时第2点的分析枚举类型不一定和int类型具有相同的尺寸,这两个差异就决定了在某些场合是不可以使用枚举类型来代替int类型的。
    如:
//  第一种情况:
               enum EType { e1 = 0, e2, e3 };
               EType val;
               std::cin >> val;
         //  第二种情况:
               enum EType { e1 = 0, e2, e3 };
               EType val;
               std::scanf( "%d", &val );

上面的两种情况看是基本上属于同一种类型的问题,其实不然。第一种情况会导致编译时错误,会因为std::cin没有定义对应的枚举类型的重载>>运算符而出错,这就说明枚举类型是一种独立和鉴别的类型;
而第二种情况不会有任何编译时问题,但是可能会导致scanf函数栈被破坏而使得程序运行非法,为什么会这样呢? 上面已经分析过了枚举类型变量的尺寸不一定和int类型相同,这样一来我们采用%d就是说将枚举类型变量val当作4字节的int变量来看待并进行参数压栈,而在某些编译器下sizeof( val )等于1字节,这样scanf函数就会将val变量地址中的后续的三字节地址也压入栈中,并对其进行赋值,也许val变量后续的三个字节的地址没有特殊含义可以被改写(比如是字节对齐的空地址空间),可能会认为他不会出现错误,其实不然,在scanf函数调用结束后会进行栈清理,这样一来会导致scanf函数清理了过多的地址空间,从而破坏了外围函数的栈指针的指向,从而必然会导致程序运行时错误。(个人理解:如果val变量后续的三个字节的地址空间不可以被改写,那么当scanf函数将val变量后续三个地址压入栈,在完成了操作之后,scanf函数调用结束进行栈清理,这时候就会把后续的三个字节地址清理,而此时是不应该清理后续地址的。)

由上面的说明枚举类型有那么多的缺点,那我们怎样才能够有一个类型安全的枚举类型呢?实际上,在最新的 C++0x 标准草案中有关于枚举作用域问题的提案,但最终的解决方案会是怎样的就无法未卜先知了,毕竟对于象 C++ 这样使用广泛的语言来说,任何特性的增删和修改都必须十分小心谨慎。

当然,我们可以使用一些迂回的方法来解决这个问题(C++ 总是能给我们很多惊喜和意外)。

例如,我们可以把枚举值放在一个结构里,并使用运算符重载来逼近枚举的特性:

struct FileAccess {
    enum __Enum {
        Read = 0x1,
        Write = 0x2
    };
    __Enum _value; // 枚举值

    FileAccess(int value = 0) : _value((__Enum)value) {}
    FileAccess& operator=(int value) {
        this->_value = (__Enum)value;
        return *this;
    }
    operator int() const {
        return this->_value;
    }
};

我们现在可以按照希望的方式使用这个枚举类型:

FileAccess access = FileAccess::Read;

并且,因为我们提供了到 int 类型的转换运算符,因此在需要 int 的地方都可以使用它,例如 switch 语句:

switch (access) {
    case FileAccess::Read:
        break;
    case FileAccess::Write:
        break;
}

当然我们不愿意每次都手工编写这样的结构。通过使用宏,我们可以很容易做到这一点:

#define DECLARE_ENUM(E) \
struct E \
{ \
public: \
    E(int value = 0) : _value((__Enum)value) { \
    } \
    E& operator=(int value) { \
        this->_value = (__Enum)value; \
        return *this; \
    } \
    operator int() const { \
        return this->_value; \
    } \
\
    enum __Enum {

#define END_ENUM() \
    }; \
\
private: \
    __Enum _value; \
};

我们现在可以按如下的方式定义前面的枚举,并且不比直接写 enum 复杂多少。

DECLARE_ENUM(FileAccess)
    Read = 0x1,
    Write = 0x2,
END_ENUM()

DECLARE_ENUM(FileShare)
    Read = 0x1,
    Write = 0x2,
END_ENUM()

1、枚举enum的用途浅例

写程序时,我们常常需要为某个对象关联一组可选alternative属性.例如,学生的成绩分A,B,C,D等,天气分sunny, cloudy, rainy等等。
更常见的,打开一个文件可能有三种状态:input, output和append. 典型做法是,对应定义3个常数,即:

const int input = 1;
      const int output = 2;
      const int append = 3;

然后,调用以下函数:

bool open_file(string file_name, int open_mode);

比如,

open_file("Phenix_and_the_Crane", append);

这种做法比较简单,但存在许多缺点,主要的一点就是无法限制传递给open_file函数的第2个参数的取值范围,只要传递int类型的值都是合法的。(当然,这样的情况下的应对措施就是在open_file函数内部判断第二个参数的取值,只有在1,2,3范围内才处理。)
使用枚举能在一定程度上减轻这种尴尬(注1),它不但能实现类似于之前定义三个常量的功能,还能够将这三个值组合起来成为独一无二的组。例如:

enum open_modes {input = 1, output, append};

以上定义了open_modes为枚举类型enumeration type。每一个命名了的枚举都是唯一的类型,是一个类型标示器type specifier。例如,我们可以重新写一个open_file函数:

bool open_file(string file_name, open_modes om);

在open_modes枚举中,input, output, append称为枚举子enumerator, 它们限定了open_modes定义的对象的取值范围。这个时候,调用open_file函数和之前的方法还是一模一样:

open_file("Phenix_and_the_Crane", append);

但是,如果传递给open_file的第二个参数不是open_modes枚举类型值的话(注1),那么编译器就会识别出错误;就算该参数取值等价于input, output, append中的某个,

也一样会出错哦!例如:

open_file("Phenix_and_the_Crane", 1);

2、枚举的定义

一个枚举是一个类型,可以保存一组由用户刻画的值。定义之类,枚举的使用很像一个整数类型。
枚举的定义具有以下形式,即以关键词enum开头,接着一个可选的枚举名,下来是由大括号{}包含着一个由逗号分隔的枚举子列表enumerators list:

enum [enumeration name] {enumerator1[=value1], enumerator2[=value2], ...};

3、枚举子的类型和取值
枚举子的类型就是它所在的那个枚举,例如前面说到的open_modes枚举中,input,output和append等枚举子的类型都是open_modes。这种做法,其实是为了赋予用户和编译器一些有关该变量拟议中的用途的提示。
默认下,第一个枚举子被赋值0,接下来的枚举子取值是前面一个枚举子的取值+1,例如:

enum weather {sunny, cloudy, rainy, windy};

其中

sunny == 0,
      cloudy == 1,
      rainy == 2,
      windy == 3;

以上是默认情况,有时候我们希望显式地指定某个枚举子的值,那么会出现什么情况呢?看看:

enum some_fruit {apple = 3, orange, banana = 4, bear};

好了,apple == 3, banana == 4; 那么orange和bear呢?记得前面说过一句,默认下”接下来的枚举子取值是前面一个枚举子的取值+1″。既然这两个枚举子没有显式赋值,那么就按照默认规则办事,所以 orange == 4, bear == 5.

从这个例子也可以看出,同一枚举中枚举子的取值不需要唯一。这样有什么用处呢?下面是个简单的例子:

enum some_big_cities {
                                 Guangzhou = 4,
                                 Shenzhen    = 4,
                                 Hongkong   = 4,
                                 Shanghai    = 2,
                                 Beijing         = 3,
                                 Chongqi      = 3
                              };

以上简单地按区域,将五个城市按照华南(4),华东(2), 华北(3)的几个城市分类了。

4、枚举变量的定义、初始化和赋值
既然每个枚举都是一个类型,那么由这个类型自然可以声明变量,例如,由前面定义的some_big_cities:
some_big_cities where_I_am;
需要注意的是,在声明where_I_am时没有初始化,如果这时打印where_I_am的值:

enum some_big_cities {
                             Guangzhou = 4,
                             Shenzhen = 4,
                             Hongkong = 4,
                             Shanghai = 2,
                             Beijing = 3,
                             Chongqi = 5                };
int main(void)
{
     some_big_cities wh;
     cout<;

输出将是the value is: 1. 然而,如果声明wh为全局变量,则另一种情况:

enum some_big_cities {              Guangzhou = 1,              Shenzhen = 1,               Hongkong = 1,
                        Shanghai = 2,              Beijing = 3,                 Chongqi = 5            };
some_big_cities wh;
int main(void)
{
   cout<;

输出将是the value is: 0;

以上结果是在Visual C++ 2005 Express中得到,不知道其它编译器情况如何,也不知为什么得到这样的结果。下来再找找资料。
定义一个枚举变量时,可以给它初始化,例如:

some_big_cities wh = Guangzhou;

注意等号右边只能取枚举子中的某一个;特别地,以Guangzhou为例,虽然Guangzhou==4, 但以下初始化是出错的:

some_big_cities wh = 4;

Visual C++ 2005编译器提示:

error C2440: ‘initializing’ : cannot convert from ‘int’ to ‘some_big_cities’
可见,不能直接地把一个整型赋值给一个枚举变量,因为枚举和整型是不同类型的,除非显式转换。关于枚举与整型的关系,后面再讲。
除了初始化,枚举变量也有赋值运算:

some_big_cities wh;
     wh = Guangzhou;
     wh = Shanghai;

或者

some_big_cities wh1 = Guangzhou;
    some_big_cities wh2 = Shanghai;
    wh2 = wh1;

5、枚举的取值范围

如果某个枚举中所有枚举子的值均非负,该枚举的表示范围就是[0:2^k-1],其中2^k是能使所有枚举子都位于此范围内的最小的2的幂;如果存在负的枚举值,该枚举的取值范围就是[-2^k,2^k-1].例如:

enum e1 {dark, light}; //范围0:1
    enum e3 {min = -10, max = 1000}; //范围-1024:1023

6、枚举与整型的关系

整型值只能显式地转换成一个枚举值,但是,如果转换的结果位于该枚举取值范围之外,则结果是无定义的。

enum e1 {dark = 1, light = 10};
     e1 VAR1 = e1(50); //无定义
     e1 VAR2 = e1(3); //编译通过

在这里也说明了不允许隐式地从整型转换到枚举的原因,因为大部分整型值在特定的枚举里没有对应的表示。

至于枚举可以当作特定的整型数来用的例子,从open_modes可以体会。
7、自定义运算符
枚举是用户自定义类型,所以在用户可以为它定义自身的操作,例如++或者<

Original: https://www.cnblogs.com/klb561/p/15954152.html
Author: konglingbin
Title: enum class 、enum struct 和 enum

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/552341/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 批量创建OU和AD账号

    #导出某个OU下的所有OU Get-ADOrganizationalUnit -Filter * -SearchBase "OU=智能中心,OU=Staff,DC=yy,…

    技术杂谈 2023年5月31日
    076
  • A*算法详解

    寻路算法——A*算法详解并附带实现代码 原文地址: http://www.gamedev.net/reference/articles/article2003.asp 概述 虽然掌…

    技术杂谈 2023年5月30日
    082
  • CVer想知道的都在这里了,一起分析下《中国计算机视觉人才调研报告》吧!

    最近闲来无事,老潘以一名普通算法工程师的角度,结合自身以及周围人的情况, 理性也感性地分析一下极市平台前些天发布的 2020年度中国计算机视觉人才调研报告。 以下的”计…

    技术杂谈 2023年7月10日
    095
  • Win10系统的SurfacePro4如何重装系统-1 SurfacePro专用的PE

    下载SurfacePro专用的PE(普通的PE可能不支持触摸屏操作,甚至没法启动Surface,所以务必要重新制作PE),下面提供百度云下载地址,下载之后,双击EXE,会进行检测 …

    技术杂谈 2023年5月31日
    080
  • CPU流水线与指令乱序执行

    青蛙见了蜈蚣,好奇地问:”蜈蚣大哥,我很好奇,你那么多条腿,走路的时候先迈哪一条啊?” 蜈蚣听后说:”青蛙老弟,我一直就这么走路,从没想过先迈哪…

    技术杂谈 2023年7月24日
    098
  • 十八、IO流(完结)

    十八、IO流 18.1 File 类 18.1.1 File 类介绍 java.io.File 类是 文件 和 目录 的 路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操…

    技术杂谈 2023年7月11日
    066
  • 批处理-日常小功能用法记录

    日常用到的一些批处理小命令记录 1、删除某个目录及其子目录下所有特定后缀的文件 假设目标目录为E:\PROJECT,目标后缀为.bakstep1:进入该目录 cd /d E:\PR…

    技术杂谈 2023年7月11日
    076
  • 【6】2022年8月

    8月21日 OMG!!我真的是懒骨头!不到最后一刻丝毫不紧张!! 兄弟,八月底了阿!! 你为了明年的计划,要想同一时间内赚5万和成功上岸,这太不可思议了! 你压力好大的,别到最后放…

    技术杂谈 2023年7月10日
    072
  • jquery中选择器的遍历方法

    jquery的遍历方法是.each()方法,可以支持多样化的选择器遍历场景。 1.索引值遍历。 如果该遍历的dom对象需要调用jquery的方法,可以使用$(this)将dom对象…

    技术杂谈 2023年6月1日
    083
  • 带头结点的单链表

    与顺序表相同,链表也是一种线性表。与顺序表不同的是,链表的物理存储结构是用一组地址任意的存储单元存储数据。它不像顺序表那样需要占据一段地址连续存储空间,而是将存储单元分散在内存的任…

    技术杂谈 2023年7月25日
    063
  • 日常踩坑_jpa的踩坑心得

    背景提要 使用jpa的出现了很多问题1、使用between做日期范围查询时报错2、使用@Query注解写原生sql时报错3、使用@where注解自动在sql后添加条件时查不出东西4…

    技术杂谈 2023年7月25日
    071
  • 电脑双屏改单屏后看不到文件问题的解决

    之前电脑用的双屏幕,后来改为了单屏幕,发现之前放到另一屏幕上的文件双击打开后看不到,似乎还停留在另一屏幕的位置处。 解决的方法如下: 1.打开对应的文件(此时不要点击其他地方,确保…

    技术杂谈 2023年5月31日
    0202
  • flex布局

    设置flex-direction值为row,然后设置flex-wrap值为wrap,此时,如果子元素总长超出容器长度,那么,子元素超出部分将会换行显示。可以看出,flex-dire…

    技术杂谈 2023年7月24日
    067
  • 基于ADS1292芯片的解决方案之源码解析

    A 硬件配置ok之后,就剩下软件了。接下来就是接口的配置信息下发了。上电流程如下所示: void ADS1292_PowerOnInit(void) { // ADS_CLKSEL…

    技术杂谈 2023年5月31日
    0103
  • Win下安装nvm

    nvm 是 windows 下切换 node 版本的管理工具,mac 下可以使用 TJ 写的 n 。 1、https://github.com/coreybutler/nvm-wi…

    技术杂谈 2023年6月1日
    096
  • 《Tornado介绍》—— 读后总结

    作者:xingoo 出处:http://www.cnblogs.com/xing901022 posted @2019-01-24 08:29 xingoo 阅读(1037 ) 评…

    技术杂谈 2023年5月30日
    0114
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球