C++结构联合与枚举

数组是相同类型的集合,相反,struct是任意类型元素的集合:

1
2
3
4
5
6
7
8
struct Address{
    const char* name;
    int number;
    const char* street;
    const char* town;
    char state[2];
    const char* zip;
};

声明Address类型的变量和声明其他类型变量的方式一模一样,可以用点运算符(.)为每个成员分别赋值:

1
2
3
4
5
void f(){
    Address jd;
    jd.name = "Jim Dandy";
    jd.number = 61;
}

struct类型的变量能使用{}的形式初始化:

1
2
3
4
5
6
7
8
Address jd = {
    "Jim Dandy",
    61,
    "South ST",
    "New Providence",
    {'N','J'},
    "07974"
};

不能用字符串"NJ"初始化jd.state。字符串以符号’\0’结尾,因此“NJ”实际上包含3个字符。
通常使用->运算符访问结构的内容:

1
2
3
void print_addr(Address* p){
    cout << p->name;
}

如果p是一个指针,则p->m等价于(*p).m。
除此之外,也能以引用的方式传递struct,并使用.运算符访问它:

1
2
3
void print_addr2(const Address& r){
    cout << p.name;
}

结构类型的对象可以被赋值、作为实参传入函数,或者作为函数的结果返回:

1
2
3
4
5
6
7
Address current;

Address set_current(Address next){
    address prev = current;
    current = next;
    return prev;
}

类型名字只要一出现就能马上使用了,无须等到该类型的声明全部完成:

1
2
3
struct Link{
    Link* previous;
};

但是,只有等到struct的声明全部完成,才能声明它的对象:

1
2
3
struct No_good{
    No_good member; //错误,递归定义
};

要想让两个或更多struct互相引用,必须提前声明好struct的名字:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct List;

struct Link{
    Link* pre;
    Link* suc;
    List* member_of;
    int data;
};

struct List{
    Link* head;
};

如果没有一开始声明List,则在稍后声明Link时使用List*类型的指针将造成错误。
可以在真正定义一个struct类型之前就使用它的名字,只要在此过程中不使用成员的名字和结构的大小就行。然而,直到struct的声明全部完成之前,它都是一个不完整的类型。

struct是一种class,它的成员默认是public的。struct可以包含成员函数,尤其是构造函数:

1
2
3
4
5
6
7
8
9
struct Points{
    vector<Point> elem;
    Points(Point p0){elem.push_back(p0);}
    Points(Point p0, Points p1){elem.push_back(p0);
    elem.push_back(p1);}
}

Points x0; //错误,缺少默认构造函数
// ....

如果只是按照默认的顺序初始化结构的成员,则不需要专门定义一个构造函数:

1
2
3
4
5
6
7
8
struct Points{
    int x, y;
}

Points p0; //如果位于局部作用域中,则p0未初始化
Points p1{}; 
Points p2{1};
Points p3{1,2};

如果需要改变实参的顺序、检验实参的有效性、修改实参或者建立不变式,则应该编写一个专门的构造函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Address{
    const char* name;
    int number;
    const char* street;
    const char* town;
    char state[2];
    const char* zip;

    Address(const string n, int nu, const string& s, const string& t, const string& st, int z);
};

可以构建struct的数组,也可以让struct包含数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Point{
    int x,y;
};


int x2 = points[2].x;

struct Array{
    Point elem[3];
};


int y2 = points2.elem[2].y;

把内置数组置于struct内部意味着我们可以把数组当成一个对象来用,可以在初始化和赋值时直接拷贝struct:

1
2
3
4
5
6
7
Array shift(Array a, Point b){
    for(int i =0 ; i != 3; ++i){
        a.elem[i].x += p.x;
        a.elem[i].y += p.y;
    }
    return a;
}

对两个struct来说,即使它们的成员相同,它们本身仍是不同的类型。

union是一种特殊的struct,它的所有成员都分配在同一个地址空间上。因此,一个union实际占用的空间大小与其最大的成员一样。自然的,在同一时刻union只能保存一个成员的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum Type{str, num};

struct Entry{
    char* name;
    Type t;
    char* s; //如果t==str,使用s
    int i; //如果t==num,使用i
};

void f(Entry* p){
    if(p->t == str)
        cout << p->s;
}

在此例中,成员s和i永远不会被同时使用,因此空间被浪费了。我们可以把它们指定成union的成员,解决上述问题:

1
2
3
4
union Value{
    char* s;
    int i;
};

枚举类型用于存放用户指定的一组整数值。枚举类型的每种取值各自对应一个名字,我们把这些值叫做枚举值:

1
enum class Color{red, green, blue};

上述代码定义了一个名为Color的枚举类型,它的枚举值时red、green和blue。“一个枚举"简称”一个enum“。 枚举类型分为两种:

  1. enum class,它的枚举值名字(如red)位于enum的局部作用域内,枚举值不会隐式的转换成其他类型。
  2. ”普通的enum“,它的枚举值名字与枚举类型本身位于同一个作用域中,枚举值隐式的转换成整数。

通常情况下,建议使用enum class,它很少会产生意料之外的结果。

enum class是一种限定了作用域的强类型枚举:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum class Traffic_light{red, yellow, green};
enum class Warning{green,yellow,orange,red};

Warning a1 = 7; //错误:不存在int向Warning的类型转换
int a2 = green; //错误:green位于它的作用域之外
int a3 = Warning::green; //错误:不存在Warning向int的类型转换
Warning a4 = Warning::green;

void f(Traffic_light x){
    if(x == 9) //错误,9不是一个Traffic_light
    if(x == red) //错误,当前作用域中没有red
    if(x == Warning::red) //错误:x不是一个Warning
    if(x == Traffic_light::red)
}

两个enum的枚举值不会互相冲突,它们位于各自enum class的作用域中。
枚举常用一些整数类型表示,每个枚举值是一个整数。我们把用于某个枚举的类型称为它的基础类型。基础类型必须是一种带符号或无符号的整数类型,默认是int,可以显式的指定:

1
enum class Warning:int{green,yellow,orange,red}; //sizeof(Warning) == sizeof(int)

如果认为上述定义太浪费空间,可以用char代替int:

1
enum class Warning:char{green,yellow,orange,red}; //sizeof(Warning) == 1

默认情况下,枚举值从0开始,依次递增:

1
2
3
4
static_cast<int>(Warning::green)==0
static_cast<int>(Warning::yellow)==1
static_cast<int>(Warning::orange)==2
static_cast<int>(Warning::red)==3

有了Warning之后,用Warning变量代替普通的int变量使得用户和编译器都能更好地了解该变量的真正用途:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void f(Warning key)
{
    switch(key){
    case Warning::green:
    //...
    case Warning:🍊
    //...
    case Warning::red:
    //...
        break;
    }
}

可以用整型常量表达式初始化枚举值:

1
2
3
4
5
6
7
enum class Print_flags{
    ackonwledge=1;
    paper_empty=2;
    busy=4;
    out_of_black=8;
    out_of_color=16;
};

enum属于用户自定义的类型,因此可以为它定义|和&运算符:

1
2
3
4
5
6
7
constexpr Printer_flags operator|(Print_flags a, Print_flags b){
    return static_cast<Print_flags>(static_cast<int>(a)|static_cast<int>(b))
}

constexpr Printer_flags operator&(Print_flags a, Print_flags b){
    return static_cast<Print_flags>(static_cast<int>(a)&static_cast<int>(b))
}

因为enum class不知吃隐式类型转换,所以必须在这里使用显式的类型转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void try_to_print(Printer_flags x)
{
    if(x&Printer_flags::ackonwledge){
        //...
    }
    else if(x&Printer_flags::busy){
        //...
    }
    else if(x&(Printer_flags::out_of_black | Printer_flags::out_of_color))
}

将operator}()和operator&()定义成constexpr函数,这样就能用于常量表达式:

1
2
3
4
5
6
7
void g(Printer_flags x){
    switch(x){
        casse Printer_flags::out_of_black | Printer_flags::out_of_color:
        //...
        break;
    }
}

C++允许先声明一个enum class,稍后再给出定义:

1
2
3
4
5
6
enum class Color_code:char; //声明
void foobar(Color_code* p); //使用声明
//...
enum class Color_code:char{ //定义
    ren,yellow,green,blue
};

一个整数类型的值可以显式的转换成枚举类型。如果这个值属于枚举的基础类型的取值范围,则转换是有效的;否则,如果超出了合理的表示范围,则转换的结果是未定义的:

1
2
3
4
5
6
7
enum class Flag:char{x=1,y=2,z=4,e=8};

Flag f0{}; //f0默认值是0
Flag f1 = 5; //类型错误,5不属于Flag
Flag f2 = Flag{5}; //错误。不许窄化成enum class
Flag f3 = static_cast<Flag>(5); //不近人情的转换
Flag f4 = static_cast<Flag>(999); //错误,999不是一个char类型的值

因为绝大多数整数值根本不再某一枚举类型的合理表示范围之内,因此不允许从整数到枚举类型的隐式转换。
每个枚举值对应一个整数,我们可以显式的把整数抽取出来:

1
2
int i = static_cast<int>(Flag::y); //i==2
char c = static_cast<char>(Flag::e); //c == 8

对enum class执行sizeof的结果是对其基础类型执行sizeof的结果,如果没有显式的指定基础类型,则枚举类型的尺寸等于sizeof(int)。

“普通的enum”是指C++在提出enum class之前提供的枚举类型。普通的enum的枚举值位于enum本身所在的作用域中,它们隐式的转换成某些整数类型的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
enum Traffic_light{red, yellow, green};
enum Warning{green,yellow,orange,red};

//错误:yellow被重复定义(取值相同)
//错误:red被重复定义(取值不同)

Warning a1 = 7; //错误:不存在int向Warning的类型转换
int a2 = green; //ok:green位于其作用域中,隐式转换成int类型
int a3 = Warning::green; //OK:Warning向int的类型转换
Warning a4 = Warning::green; //OK

void f(Traffic_light x){
    if(x == 9) //OK(d但是Traffic_light不包含枚举值9)
    if(x == red) //错误,当前作用域中有两个red
    if(x == Warning::red) //OK
    if(x == Traffic_light::red)
}

我们在同一个作用域的两个普通枚举中都定义了red,从而避免一个难以发现的错误。可以人为地消除枚举值的二义性,以实现对于普通enum的“清理”:

1
2
3
4
5
6
7
8
enum Traffic_light{tl_red, tl_yellow, tl_green};
enum Warning{green,yellow,orange,red};

void f(Traffic_light x){
    if(x == 9) //OK
    if(x == red) //OK
    if(x == Traffic_light::red)
} //错误:red不是一个Traffic_light类型的值   

从编译器角度来看,x==red是合法的,但它几乎肯定是一个程序缺陷。把名字注入外层作用域(当使用enum时会发生这种情况,但是使用enum class和class不会)的行为称为名字空间污染,在规模较大的程序中应避免这么做。
可以为普通的枚举指定基础类型,就像对enum class所做的一样。此时,允许先声明枚举类型,稍后再给出定义:

1
2
3
4
enum Traffic_light:char{tl_red, tl_yellow, tl_green}; //基础类型是char
enum Color_code:char; //声明
void foobar(Color_code* p); //使用声明
enum Color_code:char{green,yellow,orange,red}; //定义  

如果没有指定枚举的基础类型,则不能把它的声明和定义分开。

一个普通的enum可以是未命名的:

1
enum{arrow_up=1, arrow_down, arrow_sideways};

如果只需要一组整型变量,而不是用于声明变量的类型,则可以使用未命名的enum。