c语言自定义类型深度解析:联合(Union)与枚举(Enum)

下面代码结果为什么这样:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
	union un
	{
		char c;
		int i;

	};

int main()
{

	union un u= { 0 };
	printf("%zd\n", sizeof(u));

	printf("%p\n", &u);
	printf("%p\n", &u.c);
	printf("%p\n", &u.i);


	return 0;

}

一、联合体(Union):内存共用的 "空间魔法师"

联合体,也叫共用体,核心特性是所有成员共享同一块内存空间。这和结构体(成员各自占用独立内存)形成了鲜明对比,也是它节省内存的关键所在。

1. 联合体类型的声明

联合体的声明语法和结构体非常相似,仅关键字不同(union 替代 struct),常见三种声明方式:

cpp 复制代码
#include <stdio.h>

// 1. 基本声明:先定义类型,再定义变量
union Un {
    char c;    // 字符型成员
    int i;     // 整型成员
};

// 2. 声明同时定义变量
union Un2 {
    short s;
    long l;
} un2_var;

// 3. 匿名联合体:无类型名,仅能声明时定义变量
union {
    float f;
    int arr[2];
} anon_un;

int main() {
    union Un un = {0};  // 定义联合体变量并初始化
    printf("联合体Un的大小:%d\n", sizeof(un));  // 输出结果:4
    return 0;
}

为什么 union Un 的大小是 4?答案藏在联合体的核心特点里。

2. 联合体的核心特点

(1)所有成员共享同一块内存

联合体变量的起始地址,与每个成员的起始地址完全相同。我们用代码验证:

cpp 复制代码
union Un un = {0};
printf("&un = %p\n", &un);
printf("&un.i = %p\n", &(un.i));
printf("&un.c = %p\n", &(un.c));
(2)修改一个成员,会覆盖其他成员

由于内存共用,对一个成员赋值后,其他成员的值会被 "冲掉"。看下面的例子:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
	union un
	{
		char c;
		int i;

	};

int main()
{

	union un u= { 0 };
	printf("%zd\n", sizeof(u));

   u.i=0x12345678;
   u.c = 0x55;//55转换十进制为85,即'U'

	return 0;

}

visual studio采用小端存贮,调试查看变量内存:

改变c的时候,i的值也发生了变化,因为他们共有内存

(3)与结构体的内存布局对比

用相同的成员分别定义结构体和联合体,内存占用差异明显:

cpp 复制代码
#include <stdio.h>

// 结构体:成员独立占用内存(含内存对齐)
struct S {
    char c;
    int i;
};
// 联合体:成员共享内存
union Un {
    char c;
    int i;
};

int main() {
    printf("struct S的大小:%d\n", sizeof(struct S));  // 输出:8(含3字节对齐填充)
    printf("union Un的大小:%d\n", sizeof(union Un));  // 输出:4(占最大成员大小)
    return 0;
}

结构体的内存布局会有对齐填充(浪费部分空间),而联合体完全复用内存,空间效率更高。

3. 联合体大小的计算

联合体的大小遵循两个规则,缺一不可:

  1. 联合体的大小 至少是最大成员的大小(必须能容纳最大的成员);
  2. 若最大成员大小不是 "最大对齐数" 的整数倍,需对齐到最大对齐数的整数倍。

对齐数:每个成员的对齐数通常等于其自身大小(如 char 对齐数 1、int 对齐数 4、short 对齐数 2,编译器可能有差异)

cpp 复制代码
// 示例1:union Un1
#include <stdio.h>
union Un1 {
    char c[5];  // 大小5,对齐数1
    int i;      // 大小4,对齐数4
};
// 最大成员大小5,最大对齐数4;5不是4的整数倍,需补3字节→总大小8

// 示例2:union Un2
union Un2 {
    short c[7]; // 大小14(7×2),short 对齐数2
    int i;      // 大小4,int 对齐数4
};
// 最大成员大小14,最大对齐数4;14不是4的整数倍,需补2字节→总大小16

int main() {
    printf("sizeof(Un1) = %d\n", sizeof(union Un1));  // 输出:8
    printf("sizeof(Un2) = %d\n", sizeof(union Un2));  // 输出:16
    return 0;
}

4. 联合体的实用场景

(1)节省内存:多类型属性复用空间

想象一个礼品店有 3 种商品:图书、杯子、衬衫。它们有共同的属性(库存、价格、类型),但也有各自独有的属性(比如图书有书名 / 作者,衬衫有颜色 / 尺寸)。

如果为每种商品单独建一个结构体,会浪费内存;用「结构体 + 联合体」的方式,就能让不同商品的专属属性共享同一块内存空间,大大节省内存。:

cpp 复制代码
struct gift_list {
    int stock_number;  // 公共属性:库存量
    double price;      // 公共属性:价格
    int item_type;     // 公共属性:商品类型(1=图书,2=杯子,3=衬衫)
    
    // 专属属性用联合体复用内存
    union {
        struct {
            char title[20];  // 书名
            char author[20]; // 作者
            int num_pages;   // 页数
        } book;
        struct {
            char design[30]; // 设计方案
        } mug;
        struct {
            char design[30]; // 设计方案
            int colors;      // 颜色
            int sizes;       // 尺寸
        } shirt;
    } item;
};

这样一来,专属属性只占用最大成员(shirt)的内存,大幅节省空间

cpp 复制代码
#include<stdio.h>
// 引入字符串操作头文件,用于strcpy/strncpy
#include<string.h>

struct gift_list {
    int stock_number;  // 公共属性:库存量
    double price;      // 公共属性:价格
    int item_type;     // 公共属性:商品类型(1=图书,2=杯子,3=衬衫)

    // 专属属性用联合体复用内存
    union {
        struct {
            char title[20];  // 书名
            char author[20]; // 作者
            int num_pages;   // 页数
        } book;
        struct {
            char design[30]; // 设计方案
        } mug;
        struct {
            char design[30]; // 设计方案
            int colors;      // 颜色
            int sizes;       // 尺寸
        } shirt;
    } item;
};

int main()
{
    // 1. 定义一个礼品变量(图书类型)
    struct gift_list gift1;

    // 2. 给公共属性赋值
    gift1.stock_number = 88;    // 库存88本
    gift1.price = 69.9;         // 价格69.9元
    gift1.item_type = 1;        // 标记为图书类型

    // 3. 给图书专属属性赋值(因为item_type=1,所以操作book成员)
    // 使用strncpy避免字符串超长导致内存越界,最后一位留空字符
    strncpy(gift1.item.book.title, "C语言从入门到精通", 19);
    gift1.item.book.title[19] = '\0'; // 确保字符串以'\0'结尾
    strncpy(gift1.item.book.author, "王编程", 19);
    gift1.item.book.author[19] = '\0';
    gift1.item.book.num_pages = 450;  // 页数450页

    // 4. 打印输出gift1的所有信息,验证赋值是否成功
    printf("===== 图书礼品信息 =====\n");
    printf("库存量:%d\n", gift1.stock_number);
    printf("价格:%.2f 元\n", gift1.price);
    printf("商品类型:%d(图书)\n", gift1.item_type);
    printf("书名:%s\n", gift1.item.book.title);
    printf("作者:%s\n", gift1.item.book.author);
    printf("页数:%d 页\n", gift1.item.book.num_pages);
    printf("========================\n\n");

    // 【拓展】再演示一个衬衫类型的礼品赋值和打印
    struct gift_list gift2;
    // 公共属性
    gift2.stock_number = 50;
    gift2.price = 139.9;
    gift2.item_type = 3; // 标记为衬衫
    // 衬衫专属属性
    strncpy(gift2.item.shirt.design, "纯棉格子款", 29);
    gift2.item.shirt.design[29] = '\0';
    gift2.item.shirt.colors = 4;  // 4种颜色
    gift2.item.shirt.sizes = 5;   // 5种尺寸

    // 打印衬衫信息
    printf("===== 衬衫礼品信息 =====\n");
    printf("库存量:%d\n", gift2.stock_number);
    printf("价格:%.2f 元\n", gift2.price);
    printf("商品类型:%d(衬衫)\n", gift2.item_type);
    printf("设计方案:%s\n", gift2.item.shirt.design);
    printf("颜色数量:%d 种\n", gift2.item.shirt.colors);
    printf("尺寸数量:%d 种\n", gift2.item.shirt.sizes);
    printf("========================\n");

    return 0;
}
(2)判断机器字节序(大端 / 小端)

这是联合体的经典用法!字节序指多字节数据在内存中的存储顺序:

  • 小端:低字节存低地址;
  • 大端:低字节存高地址。

用联合体实现判断函数:

cpp 复制代码
// 返回1:小端;返回0:大端
int check_sys() {
    union {
        int i;     // 4字节
        char c;    // 1字节(仅占用i的低地址字节)
    } un;
    un.i = 1;       // 十六进制:0x00000001
    return un.c;    // 若c=1→小端(低字节1存在低地址);若c=0→大端
} 

int main() {

    if (check_sys() == 1) {
        printf("当前机器是小端存储\n");
    }
    else {
        printf("当前机器是大端存储\n");
    }
    return 0;
}

二、枚举(Enum):优雅的 "常量集合"

枚举,顾名思义就是 "一 一列举",用于定义一组离散的命名常量(如星期、性别、状态码),让代码更具可读性。

1. 枚举类型的声明

枚举的关键字是 enum,语法格式如下:

cpp 复制代码
// 基本声明:默认枚举常量从0开始递增
enum Day {  // 枚举类型:星期
    Mon,    // 0
    Tues,   // 1
    Wed,    // 2
    Thur,   // 3
    Fri,    // 4
    Sat,    // 5
    Sun     // 6
};

// 声明时指定枚举常量的值
enum Color {
    RED = 2,   // 手动指定为2
    GREEN = 4, // 手动指定为4
    BLUE = 8   // 手动指定为8
};

// 声明同时定义变量
enum Sex {
    MALE,    // 0
    FEMALE,  // 1
    SECRET   // 2
} person_sex;

注意:枚举常量是整型常量,默认从 0 开始,依次递增 1;若手动指定某个常量的值,后续未指定的常量会在前一个基础上递增 1

2. 枚举的核心优点

为什么不用 #define 定义常量,非要用枚举?枚举的 5 大优势:

  1. 可读性更强RED2 更直观,一眼能明白含义;
  2. 类型安全 :枚举变量只能赋值为对应的枚举常量,#define 无类型检查(比如 #define RED 2 后,可给任意整型变量赋值 2,无法限制范围);
  3. 便于调试 :预处理阶段会删除 #define 定义的符号,调试时只能看到数值(名字被去掉,只有数值给变量);而枚举常量会保留名称,调试更清晰;
  4. 批量定义 :一次可定义多个常量,无需重复写 #define
  5. 遵循作用域规则:若枚举声明在函数内,仅能在该函数内使用,避免全局污染。

例子:

cpp 复制代码
#include<stdio.h>
// 定义枚举类型:计算器操作选项(EXIT=0, ADD=1, SUB=2, MUL=3, DIV=4)
enum option
{
    EXIT,    // 退出
    ADD,     // 加法
    SUB,     // 减法
    MUL,     // 乘法
    DIV      // 除法
};

int main() {
    enum option choice; // 定义枚举变量存储用户选择
    double a, b;
    while (1) {
        // 1. 显示简单菜单
        printf("\n简易计算器:\n");
        printf("%d-退出 | %d-加法 | %d-减法 | %d-乘法 | %d-除法\n", EXIT, ADD, SUB, MUL, DIV);
        printf("请选择:");
        scanf("%d", (int*)&choice); // 枚举本质是整型,强转地址

        // 2. 退出逻辑
        if (choice == EXIT) {
            printf("退出程序!\n");
            break;
        }

        // 3. 输入运算数
        printf("输入两个数(空格分隔):");
        scanf("%lf %lf", &a, &b);

        // 4. 枚举配合switch执行运算(核心用法)
        switch (choice) {
        case ADD: printf("结果:%.2f\n", a + b); break;
        case SUB: printf("结果:%.2f\n", a - b); break;
        case MUL: printf("结果:%.2f\n", a * b); break;
        case DIV:
            if (b == 0) printf("除数不能为0!\n");
            else printf("结果:%.2f\n", a / b);
            break;
        default: printf("选择错误!\n");
        }
    }
    return 0;
}
(1)给枚举变量赋值

枚举变量只能赋值为对应的枚举常量,C 语言允许整型隐式转换(不推荐),C++ 则严格禁止:

cpp 复制代码
enum Color {
    RED = 1,
    GREEN = 2,
    BLUE = 4
};

int main() {
    enum Color clr1 = GREEN;  // 合法:用枚举常量赋值
    enum Color clr2 = 2;      // C语言允许(不推荐),C++报错
    enum Color clr3 = (enum Color)4;  // 显式转换,更规范
    
    printf("clr1 = %d\n", clr1);  // 输出:2
    return 0;
}
相关推荐
FJW0208142 小时前
Python推导式与生成器
开发语言·python
leiming62 小时前
手写Linux C UDP通信
linux·c语言·udp
xb11322 小时前
C# WinForms界面设计
开发语言·c#
期末考复习中,蓝桥杯都没时间学了2 小时前
力扣刷题记录2
算法·leetcode·职场和发展
高洁012 小时前
知识图谱如何结合 RAG实现更精确的知识问答
人工智能·算法·机器学习·数据挖掘·知识图谱
-Rane2 小时前
【C++】内存管理
开发语言·c++
DARLING Zero two♡2 小时前
【计算机网络】简学深悟启示录:序列化&&反序列化
开发语言·计算机网络·php
ID_180079054732 小时前
乐天(Letian)商品详情API接口的调用示例与代码实现
开发语言·python
爱喝可乐的老王2 小时前
机器学习监督学习模型----KNN
人工智能·算法·机器学习