深入浅出 C 语言—联合体

174 阅读5分钟

C 语言作为一个老牌语言,至今还是有他的一席之地,这主要归因于其精巧的设计和广泛的应用场景。拥有简洁高效的语法结构,能够以较少的代码实现复杂的功能,而且还直击底层,对硬件进行操作,因此在嵌入式系统开发领域无可替代的地位。无论是智能家居设备中的微控制器,还是汽车电子系统的核心芯片,C 语言都能精准地掌控底层硬件资源,实现高效稳定的运行。

联合体(union)

是一种特殊的自定义数据类型,其所有成员共享同一块内存空间。也就是说,在同一时间,联合体中只能有一个成员有效。这使得联合体非常适合表示那些在不同时间需要使用不同类型数据的变量。

union 数据类型名 
{ 
    成员列表; 
};

union 的声明有点类似于 struct,接下来我们就来声明一个 car union

union car
{
  char name[50];
  int price;
};

联合体的特点

  • 内存共享: 所有成员共用同一块内存空间。
  • 互斥访问: 在同一时间只能访问一个成员。
  • 节省空间: 联合体的大小等于其最宽成员的大小。
  • 类型转换: 可以将一个联合体变量强制转换为其成员类型。

联合体的应用场景

  • 表示多种类型的数据: 比如,一个变量可以存储整数、浮点数或字符串。
  • 节省内存: 当需要在不同情况下存储不同类型的数据时,使用联合体可以节省内存空间。
  • 自定义数据类型: 可以结合结构体和联合体来定义更复杂的数据类型。
struct value {
    enum value_type type;
    union {
        char *string;
        int integer;
        double floating;
    };
};

在 C 语言中,类型决定来存储数据需要的内存空间,这种类型大致描述了对象的内存布局。例如正使用 c 语言编写一款网络游戏,怪物出现在屏幕上。服务器会将怪物的数据发送给客户端,而客户端必须以某种方式存储这些数据以便将怪物呈现给用户。在这款游戏中设计不同类型的敌人,比如兽人、矮人、小矮妖,他们各自都有着各自不同的特性,所谓可以为每一种怪物设计一种类型。

  • orc_t 表示兽人类型
  • orc_t 表示矮人类型
  • leprechaun_t 小矮妖类型

那么问题来了,如果你要编写一个接收怪物相关信息并且必须将正确的类型存储到内存中的函数,你该怎么做呢?最简单但很繁琐的方法是设置一个变量 “orc_t”(兽人类型)、一个 “dwarf_t”(矮人类型)、一个 “leprechaun_t”(小矮妖类型)等等,然后根据接收到的信息只初始化对应的那一个变量。这种方法可行,但会浪费内存(尤其因为对所有敌人都得这么做),而且维护起来也很困难。

enum monster_type { ORC, DWARF, LEPRECHAUN };

typedef struct {
    int health;
} orc_t;

typedef struct {
    int health;
} dwarf_t;

typedef struct {
    int health;
} leprechaun_t;

显示怪物的血量,可以设计 show_health 方法来接收一个从服务器端返回的信息。get_monster_type 从信息提取类型,然后获取 monster_type, 根据 monster_type 不同

void show_health(Message msg) {
    int          health;
    orc_t        orc;
    dwarf_t      dwarf;
    leprechaun_t leprechaun;

    monster_type msg_monster_type = get_monster_type(msg);

    switch (msg_monster_type) {
        case (ORC):
            orc    = get_monster(msg);
            health = orc.health;
            break;
        case (DWARF):
            dwarf  = get_monster(msg);
            health = dwarf.health;
            break;
        case (LEPRECHAUN):
            leprechaun = get_monster(msg);
            health     = leprechaun.health;
            break;
        default:
            unknown_type_error();
    }

    printf("Monster health: %d\n", health);
}

为了简化这里事先分别准备好每种 monster 一个实例用于接收类型,不过这个无形中就浪费了内存。应该是根据返回信息来初始化一个指定类型即可,现在的做法有点浪费内存,现在怪物种类还不多,不过一旦上数量级,势必会浪费了大量内存资源

orc_t        orc;
dwarf_t      dwarf;
leprechaun_t leprechaun;

要解决上面的问题,我们可以借助 union 来解决这个问题,结合 union 的空间共享和互斥性的特点,就可以很好解决上面问题,因为在同一时刻只能有一个怪物类型。然后根据的 monster 类型,以其方式读来取数据,也就是根据类型来解读内存中的数据。

enum monster_type { ORC, DWARF, LEPRECHAUN };
typedef union {
    orc_t        orc;
    dwarf_t      dwarf;
    leprechaun_t leprechaun;
} monster_data;

void show_health(Message msg) {
    int          health;
    monster_data msg_monster      = get_monster(msg);
    monster_type msg_monster_type = get_monster_type(msg);

    switch (msg_monster_type) {
        case (ORC):
            health = msg_monster.orc.health;
            break;
        case (DWARF):
            health = msg_monster.dwarf.health;
            break;
        case (LEPRECHAUN):
            health = msg_monster.leprechaun.health;
            break;
        default:
            unknown_type_error();
    }

    printf("Monster health: %d\n", health);
}

上面的解决方案来应对这个 show_health 函数的情况,看起来没有什么问题,不过这个结构体还需要在其他场景或者情况下使用,为了避免问题,需要将类型内嵌到结构体中。

enum monster_type { ORC, DWARF, LEPRECHAUN };

typedef struct {
    monster_type type;

    typedef union {
        orc_t        orc;
        dwarf_t      dwarf;
        leprechaun_t leprechaun;
    } data;
} monster;

void show_health(Message msg) {
    int     health;
    monster msg_monster;
    msg_monster.data = get_monster(msg);
    msg_monster.type = get_monster_type(msg);

    switch (msg_monster.type) {
        case (ORC):
            health = msg_monster.data.orc.health;
            break;
        case (DWARF):
            health = msg_monster.data.dwarf.health;
            break;
        case (LEPRECHAUN):
            health = msg_monster.data.leprechaun.health;
            break;
        default:
            unknown_type_error();
    }

    printf("Monster health: %d\n", health);
}

好了通过一个例子我们已经看出了联合体在 c 语言中妙用,正是因为 c 语言简洁设计才给开发人员更多发挥空间,也正是因为简单 c 语言天生就有灵活性,具有更好生命力,在今天依然具有一定竞争力。

注意事项

  • 访问联合体的不同成员时,一定要清楚当前成员的值,否则可能会导致数据混乱。
  • 联合体的大小是由其最宽的成员决定的。
  • 联合体不能包含数组作为成员,但可以包含指向数组的指针。