Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第十三章 共用体与枚举

本章要点

第五章学习的结构体能将多个不同类型的数据打包,每个成员拥有独立的内存空间。本章讨论另外两种构造类型:共用体枚举

  • 共用体(union):多个成员共享同一块内存,同一时刻只能存一个成员的值
  • 枚举(enum):给整型常量起有意义的名字,替换“魔法数字“
  • 两者组合成**“带标签的联合体”**——用枚举标记共用体当前存的是哪种类型

一、共用体

1. 共用体(union)

1.1 从一种“多功能“储物格说起

结构体可以把不同类型的数据同时打包在一起,每个成员都有自己独立的空间。但有时我们需要的是一个“多合一“的存储单元:同一块空间,在不同的时候可以存放不同类型的数据,但一次只能容纳一种。

设想这样一个场景:一个变量在程序运行期间,有时需要存储整数,有时需要存储浮点数,有时需要存储字符——但这三种需求绝不会同时出现。如果用结构体来实现,三个成员各自占用各自的空间,会造成不必要的浪费。

共用体正是为这种场景设计的:所有成员共享同一块内存,你往其中写入一个新成员的值,就会覆盖掉旧成员的内容。

1.2 语法基本规则

共用体的定义语法和结构体非常相似,只需将关键字 struct 替换为 union

union 共用体名
{
    数据类型 成员1;
    数据类型 成员2;
    ...
};

声明变量同样延续了结构体的方式:

union 共用体名 变量名;

成员访问也使用点运算符 .

变量名.成员名

1.3 语法示例

#include <stdio.h>

union Data
{
    int i;
    float f;
    char c;
};

int main(void)
{
    union Data d;

    d.i = 100;
    printf("d.i = %d\n", d.i);   // 100

    d.f = 3.14f;
    printf("d.f = %f\n", d.f);   // 3.140000
    // 注意:此时 d.i 的值已经无效了,下面不要再读取 d.i

    d.c = 'A';
    printf("d.c = %c\n", d.c);   // A
    // 同理,写入 d.c 后,d.f 也不再是当前有效成员

    return 0;
}

输出

d.i = 100
d.f = 3.140000
d.c = A

这段代码没有在写入新成员后继续读取旧成员,因为那样的结果不可移植。它想说明的关键事实是:共用体成员共享同一块空间,写入一个成员后,应当只把最后写入的那个成员视为当前有效成员。

注意:共用体本身并不记录当前存储的是哪一个成员,这一信息需要程序员自行维护——要么凭逻辑记住,要么额外设置一个标记变量来区分当前有效的数据类型。这正是共用体与枚举最自然的结合点:用枚举标记类型,用共用体承载数据,构成**“带标签的联合体”**。下面就来讨论枚举。


二、枚举

1. 枚举(enum)

1.1 从“魔法数字“说起

在之前的程序中,我们经常会使用整数来表示某种状态或选项:

int day = 1;  // 1 表示星期一?还是星期日?
if (day == 1) { ... }

这类没有任何说明的裸数字被称为“魔法数字“(magic number)。随着时间的推移,连编写者自己都可能忘记这些数字的含义,更不用说其他人阅读代码时的困惑了。枚举的用途就是为一组相关联的整型常量赋予有意义的名字,让代码的含义一目了然。

1.2 语法基本规则

enum 枚举名
{
    标识符1,
    标识符2,
    标识符3,
    ...
};

关于枚举值的默认行为,有两条简单规则需要记住:

  1. 不手动指定:第一个标识符默认值为 0,其后的标识符依次加 1
  2. 手动指定某个值:后续未指定的标识符会在此基础上继续递增

声明枚举变量的语法与结构体类似:

enum 枚举名 变量名;

1.3 语法示例

#include <stdio.h>

// 定义星期枚举
enum Weekday
{
    MONDAY,     // 0
    TUESDAY,    // 1
    WEDNESDAY,  // 2
    THURSDAY,   // 3
    FRIDAY,     // 4
    SATURDAY,   // 5
    SUNDAY      // 6
};

int main(void)
{
    enum Weekday today = WEDNESDAY;

    printf("today = %d\n", today);  // 2

    if (today == WEDNESDAY)
    {
        printf("今天是星期三\n");
    }

    return 0;
}

输出

today = 2
今天是星期三

1.4 手动指定值

枚举的值也可以由程序员手动控制。既可以为所有成员全部指定,也可以只指定其中一部分,其余成员自动递增:

enum Color
{
    RED = 1,
    GREEN = 5,
    BLUE = 10
};

// 或者让部分自动递增
enum ErrorCode
{
    OK = 0,
    FILE_NOT_FOUND,      // 1
    PERMISSION_DENIED,   // 2
    UNKNOWN = 99
};

第一个未被手动指定的成员从 0 开始,其他未指定的成员在前一个已确定成员的值基础上加 1

1.5 枚举的优点

使用枚举带来的好处体现在三个层面:

  • 可读性if (today == FRIDAY) 远比 if (today == 4) 清晰,阅读代码的人无需记忆数字与含义的对应关系。
  • 组织性:枚举将一组相关的常量集中定义在一起,便于统一管理和修改,不会散落各处。
  • 类型提示:枚举是与实现所选整数类型兼容的独立类型。使用枚举变量能表达取值所属的集合;混用不同枚举时,一些编译器也会给出警告。

1.6 枚举与 switch 的经典配合

枚举与 switch 语句是一对天然的搭档。枚举提供命名清晰的常量,switch 则在不同分支中分派对应的处理逻辑:

enum Weekday today = SATURDAY;

switch (today)
{
    case MONDAY:
    case TUESDAY:
    case WEDNESDAY:
    case THURSDAY:
    case FRIDAY:
        printf("工作日\n");
        break;
    case SATURDAY:
    case SUNDAY:
        printf("周末\n");
        break;
    default:
        break;
}

三、动手练习

#include <stdio.h>

// 定义表示图形的共用体
union Shape
{
    double radius;       // 圆:半径
    struct { double w, h; } rect;  // 矩形:宽和高
};

// 定义图形类型枚举
enum ShapeType { CIRCLE, RECTANGLE };

int main(void)
{
    // 练习1:共用体与枚举配合
    enum ShapeType type = CIRCLE;
    union Shape s;

    if (type == CIRCLE)
    {
        s.radius = 5.0;
        printf("圆形,半径 = %.2f, 面积 = %.2f\n", s.radius, 3.14 * s.radius * s.radius);
    }
    else if (type == RECTANGLE)
    {
        s.rect.w = 3.0;
        s.rect.h = 4.0;
        printf("矩形,宽 = %.2f, 高 = %.2f, 面积 = %.2f\n", s.rect.w, s.rect.h, s.rect.w * s.rect.h);
    }

    // 练习2:用枚举表示月份,输出月份天数
    enum Month { JAN=1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC };
    enum Month m = MAR;

    printf("\n%d 月有 ", m);
    switch (m)
    {
        case JAN: case MAR: case MAY: case JUL: case AUG: case OCT: case DEC:
            printf("31 天\n"); break;
        case APR: case JUN: case SEP: case NOV:
            printf("30 天\n"); break;
        case FEB:
            printf("28 或 29 天\n"); break;
        default:
            printf("无效月份\n");
    }

    return 0;
}