第15讲:联合与枚举

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 在WSL Ubuntu中建立一个专门的目录用于课上练习,如~/course
$ mkdir ~/course
  • 打开VSCode并连接到WSL,打开此目录并新建文件,如~/course/main.cpp
  • 编辑main.cpp,随堂编程练习的代码请直接在此文件中编辑
main.cpp
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
  // ============= begin =============

  // =============  end  =============
  return 0;
}

温故:用指针实现动态类型

编程求解

在指针学习中,曾通过指针将一个内存区以不同类型进行解释,从而实现“动态类型”

dtypemain.cpp
#include <iostream>
#include "dtype.h"
int main() {
  int type;
  char buffer[sizeof(double)];
  int i; double d;
  std::cin >> type;
  // read in a value and write to buffer according to type
  switch (type) { // 0: int, 1: double
    case 0: std::cin >> i; writeI(buffer, i); break;
    case 1: std::cin >> d; writeD(buffer, d); break;
  }
  // read out the value from buffer according to type
  switch (type) {
    case 0: std::cout << readI(buffer) << std::endl; break;
    case 1: std::cout << readD(buffer) << std::endl; break;
  }
  return 0;
}
dtype.h
void writeI(void *buffer, int i);
void writeD(void *buffer, double d);
int readI(void *buffer);
double readD(void *buffer);
dtype.cpp
void writeI(void *buffer, int i) {
  *(int *)buffer = i;
}
void writeD(void *buffer, double d) {
  *(double *)buffer = d;
}
int readI(void *buffer) {
  return *(int *)buffer;
}
double readD(void *buffer) {
  return *(double *)buffer;
}

学习目标

  • 学习内容
    • 联合体
    • 枚举类型
    • 实现动态类型

学习目标

  • 理解联合体的内存布局
  • 掌握枚举类型的定义与使用
  • 应用联合体与枚举类型实现动态数据类型

综合使用结构体、联合体与枚举类型,实现动态类型的定义与使用

一、联合体

引入

考考你

试描述下面的数据表示问题有什么共性?

  • 科大学员要么是军人学员、要么是地方学员
  • 食堂每日供应的免费汤粥是紫菜蛋花汤、海带排骨汤、白粥、绿豆粥中的一种
  • 一个物理常数可以是实数也可以是复数

明察秋毫

需要表示的数据是多种类型中的一种

1.1 联合体的概念

  • 联合体是一种复合数据类型,用于在同一内存区域存储多种可能类型之一的数据
联合体
union Number {
  int i;
  float f;
  double d;
};

Number number;
  • Number是联合体类型,number是联合体变量,其内存区中存储的值
    • 要么是一个int
    • 要么是一个float
    • 要么是一个double
    • 但不能同时存储多个值

1.2 联合体的定义

  • 联合体的定义采用union关键字
    • union后跟联合体名称(标识符)
    • 联合体名称后面是成员列表
      • 成员列表用大括号括起来
      • 每个成员由类型与名称组成
      • 成员之间用分号分隔
    • 联合体定义以分号结束
联合体
union Number {
  int i;
  float f;
  double d;
};

Number number;

明察秋毫

  • 联合体本身是一种类型(用户定义的复合类型)
    • 联合体类型的变量称为联合体变量

1.2 联合体的定义

考考你

联合体的成员变量是否可以是任意类型?

“复杂”的联合体类型
struct Student {
  int id;          // 学号
  char name[16];   // 姓名
  char major[20];  // 专业
};
struct Teacher {
  short id;        // 教工号
  char name[16];   // 姓名
  char title[10];  // 职称
  char desc[100];  // 简介
};
union Member {
  Student s;       // 学员
  Teacher t;       // 教员
};

明察秋毫

  • 联合体的成员可以是任意类型,包括基础数据类型、联合体、数组、指针等
  • 但是,联合体成员不能是本联合体类型

1.3 联合体的存储

  • 联合体的成员共享内存中一块连续区域,任意时刻最多存储一个成员的值
  • 成员要满足所属类型的空间与对齐要求
layout.cpp
#include <iostream>
struct Student {
  int id;          // 学号
  char name[16];   // 姓名
  char major[20];  // 专业
};
struct Teacher {
  short id;        // 教工号
  char name[16];   // 姓名
  char title[10];  // 职称
  char desc[100];  // 简介
};
union Member {
  Student s;       // 学员
  Teacher t;       // 教员
};
layout.cpp
int main() {
  std::cout << sizeof(Student) << ' ' << alignof(Student) << " | ";
  std::cout << sizeof(Teacher) << ' ' << alignof(Teacher) << " | ";
  std::cout << sizeof(Member) << ' ' << alignof(Member) << " | ";
  std::cout << offsetof(Member, s) << ' ' << offsetof(Member, t) << std::endl;
  return 0;
}
$ g++ -o code/layout code/layout.cpp
$ ./code/layout
40 4 | 128 2 | 128 4 | 0 0

明察秋毫

联合体的体积至少为最大成员的体积、对齐至少为最大成员的对齐

1.3 联合体的存储

考考你

判断程序输出,分析联合体内存布局

layout-quiz.cpp
#include <iostream>
using namespace std;
union Number {
  int i;
  double d;
};
union NameOrID {
  int id;
  struct {
    char padding[2];
    char name[8];
  } name;
};
struct Object {
  int type;
  Number number;
  NameOrID nameOrID;
};
layout-quiz.cpp
int main() {
  cout << sizeof(Number) << ' ' << alignof(Number) << " | ";
  cout << sizeof(NameOrID) << ' ' << alignof(NameOrID) << " | ";
  cout << sizeof(Object) << ' ' << alignof(Object) << '\n';
  cout << offsetof(Number, i) << ' ' << offsetof(Number, d) << " | ";
  cout << offsetof(NameOrID, id) << ' ' << offsetof(NameOrID, name) << " | ";
  cout << offsetof(Object, type) << ' ' << offsetof(Object, number) << ' '
       << offsetof(Object, nameOrID) << '\n';
  return 0;
}
$ g++ -o code/layout-quiz code/layout-quiz.cpp
$ ./code/layout-quiz
8 8 | 12 4 | 32 8
0 0 | 0 0 | 0 8 16

1.3 联合体的存储

类型定义
#include <iostream>
using namespace std;
union Number {
  int i;
  double d;
};
union NameOrID {
  int id;
  struct {
    char padding[2];
    char name[8];
  } name;
};
struct Object {
  int type;
  Number number;
  NameOrID nameOrID;
};

内存布局

考考你

上图内存布局中有几处空洞?为什么会有这些空洞?

1.4 联合体的初始化

  • 联合体变量可在定义的同时进行初始化
init.cpp
#include <iostream>
using namespace std;
union Number {
  int i;
  double d;
};

int main() {
  Number n1 = {.i = 3};    // 指定初始化列表
  Number n2 = {.d = 3.14}; // 指定初始化列表
  Number n3 = 1;     // compile error
  Number n4 = 1.0;   // compile error
  Number n5 = {1};
  Number n6 = {1.0}; // compile error
  return 0;
}
$ g++ -o code/init code/init.cpp
code/init.cpp: In function ‘int main()’:
code/init.cpp:11:15: error: conversion from ‘int’ to non-scalar type ‘Number’ requested
   11 |   Number n3 = 1;     // compile error
      |               ^
code/init.cpp:12:15: error: conversion from ‘double’ to non-scalar type ‘Number’ requested
   12 |   Number n4 = 1.0;   // compile error
      |               ^~~
code/init.cpp:14:19: error: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ [-Wnarrowing]
   14 |   Number n6 = {1.0}; // compile error
      |                   ^

教员箴言

使用指定初始化列表初始化联合体变量!

1.5 访问联合体成员

  • 联合体成员的访问可以使用对象成员运算符.或指针成员运算符->
access.cpp
#include <cstring>
#include <iostream>
union Number {
  short s;
  int i;
};
int main() {
  Number n = {.i = 0x00010001}; // 指定初始化列表
  Number *p = &n;
  std::cout << n.s << ' ' << n.i << '\n';
  std::cout << p->s << ' ' << p->i << '\n';
  return 0;
}

考考你

联合体变量的成员只有一个有效,同时访问会怎样?

$ g++ -o code/access code/access.cpp
$ ./code/access
1 65537
1 65537

明察秋毫

访问哪个成员,就按照该成员的类型解释所属的内存!

1.6 匿名结构与匿名联合

  • 在结构体定义中使用匿名联合是一种惯用法
    • 当结构或联合类型仅用于定义一个结构体变量时,可以使用匿名方式
anonymous.cpp
#include <iostream>
struct Number {
  int type;
  union {
    int i;
    float f;
    double d;
    struct { float r, i; } fc;
    struct { double r, i; } dc;
  } value;
};
int main() {
  Number n1 = {.type = 0, .value.i = 3};              // type==0: int
  Number n2 = {.type = 1, .value.f = 3.14f};          // type==1: float
  Number n3 = {.type = 2, .value.d = 3.14};           // type==2: double
  Number n4 = {.type = 3, .value.fc = {3.14f, 2.71f}};// type==3: float complex
  Number n5 = {.type = 4, .value.dc = {3.14, 2.71}};  // type==4: double complex
  return 0;
}

二、枚举类型

引入

考考你

在上一页的例子中,使用整形变量type实现动态类型识别,有什么缺点?

anonymous.cpp
#include <iostream>
struct Number {
  int type;
  union {
    int i;
    float f;
    double d;
    struct { float r, i; } fc;
    struct { double r, i; } dc;
  } value;
};
int main() {
  Number n1 = {.type = 0, .value.i = 3};              // type==0: int
  Number n2 = {.type = 1, .value.f = 3.14f};          // type==1: float
  Number n3 = {.type = 2, .value.d = 3.14};           // type==2: double
  Number n4 = {.type = 3, .value.fc = {3.14f, 2.71f}};// type==3: float complex
  Number n5 = {.type = 4, .value.dc = {3.14, 2.71}};  // type==4: double complex
  return 0;
}

2.1 枚举类型的概念

  • 现实生活中,存在许多离散的、非数值的有限数据集
    • 一周有七天:Mon、Tue、Wed、Thu、Fri、Sat、Sun
    • 四季轮回:春、夏、秋、冬
    • 男女有别:男、女
  • 用整数表示这种数据集会带来不便
    • 表示不自然,需要记忆与整数的映射关系
    • 数据集较小,浪费存储空间
    • 容易出现非法值

注记

C++枚举类型用于表示离散的、非数值的有限数据集

2.2 枚举类型的定义

  • 枚举类型的定义采用enum关键字
    • enum后跟枚举类型名称(标识符)
    • 枚举类型名称后面是枚举值列表
      • 成员列表用大括号括起来
      • 每个枚举值使用一个名称(标识符)
      • 成员之间用逗号分隔
    • 枚举类型定义以分号结束
枚举类型
enum Season {
  Spring,
  Summer,
  Autumn,
  Winter
};

Season s = Spring;

明察秋毫

  • 枚举类型本身是一种类型(用户定义的复合类型)
    • 枚举类型类型的变量称为枚举变量

2.2 枚举值

  • 枚举值与一个整数对应,默认情况下第一个枚举值为0,后续枚举值依次递增
  • 可以显式设置枚举值对应的整数
  • 未设置的枚举值默认为前一个枚举值加1
枚举值
#include <iostream>
using namespace std;
enum Season { Spring, Summer, Autumn, Winter };
enum Sex { Male = 'M', Female = 'F' };
enum Weekday { Sun = 1, Mon, Tue, Wed, Thu, Fri, Sat };
int main() {
  cout << Spring << ' ' << Summer << ' ' << Autumn << ' ' << Winter << '\n';
  cout << Male << ' ' << Female << '\n';
  cout << Sun << ' ' << Mon << ' ' << Tue << ' ' << Wed << ' ' << Thu << ' '
       << Fri << ' ' << Sat << '\n';
}
$ g++ -o code/enumvalue code/enumvalue.cpp
$ ./code/enumvalue
0 1 2 3
77 70
1 2 3 4 5 6 7

2.3 枚举变量的取值

  • 给枚举变量赋值仅限于所属类型的枚举值
  • 虽然枚举值是整数,但不能将整数直接赋值给枚举变量
    • 可强制类型转换,小心转换的整数值超出枚举值范围
enumvar
enum Season { Spring, Summer, Autumn, Winter };
enum Sex { Male = 'M', Female = 'F' };
enum Weekday { Sun = 1, Mon, Tue, Wed, Thu, Fri, Sat };
int main() {
  Season season = Spring;
  Sex sex = Summer;
  Weekday Mon = 1;
  Weekday Tue = (Weekday)2;
  Weekday WHATISTHIS = (Weekday)100;
}

考考你

可否将超出枚举值范围的整数值转换为枚举类型?如第9行

$ g++ -o code/enumvar code/enumvar.cpp
code/enumvar.cpp: In function ‘int main()’:
code/enumvar.cpp:6:13: error: cannot convert ‘Season’ to ‘Sex’ in initialization
    6 |   Sex sex = Summer;
      |             ^~~~~~
      |             |
      |             Season
code/enumvar.cpp:7:17: error: invalid conversion from ‘int’ to ‘Weekday’ [-fpermissive]
    7 |   Weekday Mon = 1;
      |                 ^
      |                 |
      |                 int

枚举类型测试

测一测

下列有关枚举类型的说法中,正确的有

  1. 是一种自定义用于表示常量集合的数据类型
  2. 枚举值以字符串形式表示
  3. 枚举值其实一个整数
  4. 可以把整数值直接赋给一个枚举型变量

三、应用:实现动态类型

要求

  • 综合使用结构体、联合体与枚举类型实现一种动态类型Number
    • Number类型可表示一个int,或一个double,或一个单精度复数
    • 自定义结构体类型Complex表示单精度复数
    • 自定义枚举类型Type表示数据类型,使用匿名联合存储数据
动态类型操作
操作 函数签名 说明
写整数 void setInt(Number &n, int i)
写浮点数 void setDouble(Number &n, double d)
写复数 void setComplex(Number &n, Complex c)
读整数 int *getInt(Number &n) 若不是整数,返回nullptr
读浮点数 double *getDouble(Number &n) 若不是整数,返回nullptr
读复数 Complex *getComplex(Number &n) 若不是整数,返回nullptr
输出 void print(Number &n) 格式分别为12.0(1.2, 2.3)

3.1 类型定义

类型定义
// 整数、双精度实数或单精度复数
enum Type {

};

// 单精度复数
struct Complex {
  
};

// 使用匿名联合存储数据
struct Number {

};

3.2 写数据

写数据
// 向n中写入一个整数
void setInt(Number &n, int i) {

}
// 向n中写入一个双精度实数
void setDouble(Number &n, double d) {

}
// 向n中写入一个单精度复数
void setComplex(Number &n, Complex c) {

}

3.3 读数据

读数据
// 从n中读取一个整数,若n不是整数,返回nullptr
int *getInt(Number n) {

}
// 从n中读取一个双精度实数,若n不是双精度实数,返回nullptr
double *getDouble(Number n) {
}
// 从n中读取一个单精度复数,若n不是单精度复数,返回nullptr
Complex *getComplex(Number n) {

}

3.4 输出

输出数据
// 输出n,格式分别为`1`、`2.0`、`(1.2, 2.3)`
void print(Number &n) {

}

总结

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 20px
---
mindmap
联合体与枚举类型
  联合体
    联合体定义
    联合体存储布局
    联合体初始化
    访问联合体成员
    匿名联合
  枚举类型
    枚举类型定义
    枚举值
    枚举变量的取值
  动态数据类型

学习目标

  • 理解联合体的内存布局
  • 掌握枚举类型的定义与使用
  • 应用联合体与枚举类型实现动态数据类型

课后作业

  • 实训
    • C&C++结构实训(截止时间2025.04.16
    • 综合练习—C&C++结构(截止时间2025.04.21
    • 求解线性方程组实训正确性判定仍在改进,已延后截至
  • 预习
    • 教材8.5–8.6 链表

计算机程序设计