C语言union指针如何正确使用与访问?

99ANYc3cd6
预计阅读时长 21 分钟
位置: 首页 C语言 正文

什么是 union (联合体)?

union 是一种特殊的自定义数据类型,它允许你在同一个内存位置存储不同类型的数据。

c语言 union指针
(图片来源网络,侵删)

struct(结构体)不同,struct 会为每个成员分配独立的内存空间,而 union 的所有成员共享同一块内存,这块内存的大小等于其成员中占用空间最大的那个成员的大小。

union 的核心特点:

  1. 内存共享:所有成员共用一块内存。
  2. 大小由最大成员决定sizeof(union) 等于其最大成员的 sizeof
  3. 同时只能使用一个成员:你在任何时刻只能安全地使用其中一个成员,如果你写入了一个成员,然后以另一个成员的类型去读取,得到的是未定义的行为或毫无意义的乱码。

union 的基本语法和示例

#include <stdio.h>
#include <string.h>
// 定义一个联合体
DataUnion {
    int i;
    float f;
    char str[20];
};
int main() {
    DataUnion data;
    // data.i, data.f, data.str 都指向同一块内存
    printf("Size of DataUnion: %zu bytes\n", sizeof(DataUnion)); // 输出通常是 20 (由 str[20] 决定)
    // 使用成员 i
    data.i = 10;
    printf("data.i = %d\n", data.i); // 输出 10
    // 使用成员 f
    // 写入 data.f 会覆盖 data.i 在内存中的内容
    data.f = 220.5;
    printf("data.f = %.2f\n", data.f); // 输出 220.50
    // 如果现在再读取 data.i,得到的是垃圾值
    printf("data.i (after f) = %d\n", data.i); // 输出一个无意义的数字
    // 使用成员 str
    // 写入 data.str 会覆盖 data.f 在内存中的内容
    strcpy(data.str, "Hello C");
    printf("data.str = %s\n", data.str); // 输出 Hello C
    // 如果现在再读取 data.f,得到的是垃圾值
    printf("data.f (after str) = %.2f\n", data.f); // 输出一个无意义的数字
    return 0;
}

这个例子清晰地展示了 union 的“覆盖”特性,当你使用一个成员时,其他成员的数据就会被破坏。


union 指针

就像你可以有指向 struct 的指针一样,你也可以有指向 union 的指针,使用指针通常是为了:

  • 通过函数传递 union(避免大结构体的值拷贝开销)。
  • 动态地管理 union 对象。

声明和初始化 union 指针

DataUnion data;         // 定义一个联合体变量
DataUnion *ptr;         // 定义一个指向 DataUnion 类型的指针
ptr = &data;            // 让指针 ptr 指向 data 的内存地址

通过指针访问 union 成员

有两种方式通过指针访问 union 的成员:

c语言 union指针
(图片来源网络,侵删)
  1. 使用 -> (间接成员访问运算符):这是最常见、最推荐的方式。

    ptr->i = 100;      // 等价于 (*ptr).i = 100;
    printf("Value via pointer: %d\n", ptr->i);
  2. *使用 `(解引用) 和.` (成员访问运算符)**:

    (*ptr).f = 99.99;
    printf("Value via dereferenced pointer: %.2f\n", (*ptr).f);

    由于 的优先级高于 ,所以必须用括号将 *ptr 括起来。


union 指针的实际应用场景

union 指针最常见的用途是实现多态类型双关,即用一个数据表示多种不同的含义,一个经典的例子是网络编程中的 IP 地址。

示例:处理 IPv4 和 IPv6 地址

一个 IPv4 地址是一个 4 字节的整数,而一个 IPv6 地址是一个 16 字节的数组,我们可以用 union 来优雅地处理这两种情况。

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h> // 用于 inet_pton 和 ntohl
// 定义一个联合体来存储 IP 地址
// 它可以存储 IPv4 (4字节) 或 IPv6 (16字节)
union IPAddr {
    struct {
        uint32_t addr; // IPv4 地址,存储为网络字节序的无符号长整型
    } v4;
    struct {
        uint8_t addr[16]; // IPv6 地址,存储为16字节的数组
    } v6;
};
// 函数通过指针接收 IP 地址并打印
void printIP(const char *name, union IPAddr *ip) {
    printf("Printing IP for %s:\n", name);
    // 在实际应用中,你需要一个标志位来区分是 v4 还是 v6
    // 这里我们简化处理,假设调用者知道类型
    // 检查 addr[0] 是否为0来判断是否可能是IPv4(这是一个简化的启发式方法)
    if (ip->v6.addr[0] == 0 && ip->v6.addr[1] == 0 && ip->v6.addr[2] == 0 && ip->v6.addr[3] == 0 && ip->v6.addr[4] == 0 && ip->v6.addr[5] == 0 && ip->v6.addr[6] == 0 && ip->v6.addr[7] == 0 && ip->v6.addr[8] == 0 && ip->v6.addr[9] == 0 && ip->v6.addr[10] == 0 && ip->v6.addr[11] == 0) {
        // 简单判断,可能是IPv4映射的IPv6地址
        // 更好的方法是在 union 外面加一个 type 字段
        uint32_t v4_addr = ntohl(ip->v4.addr); // 从网络字节序转换为主机字节序
        printf("  IPv4 Address: %u.%u.%u.%u\n",
               (v4_addr >> 24) & 0xFF,
               (v4_addr >> 16) & 0xFF,
               (v4_addr >> 8) & 0xFF,
               v4_addr & 0xFF);
    } else {
        printf("  IPv6 Address: ");
        for (int i = 0; i < 16; i += 2) {
            printf("%02x%02x", ip->v6.addr[i], ip->v6.addr[i+1]);
            if (i < 14) printf(":");
        }
        printf("\n");
    }
}
int main() {
    // --- 示例 1: IPv4 地址 ---
    union IPAddr ipv4_addr;
    // 将 "192.168.1.1" 转换为网络字节序的32位整数
    inet_pton(AF_INET, "192.168.1.1", &(ipv4_addr.v4.addr));
    // 通过指针传递给函数
    printIP("My Router", &ipv4_addr);
    // --- 示例 2: IPv6 地址 ---
    union IPAddr ipv6_addr;
    // 将 "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 转换为16字节数组
    inet_pton(AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ipv6_addr.v6.addr);
    // 通过指针传递给函数
    printIP("Server", &ipv6_addr);
    return 0;
}

分析这个例子:

  • 我们定义了一个 union IPAddr,它既能容纳 4 字节的 IPv4 地址,也能容纳 16 字节的 IPv6 地址。
  • 我们使用 struct 来封装 union 的成员,使代码更清晰(v4.addr vs ip.addr)。
  • printIP 函数接收一个 union IPAddr 的指针,这使得无论传入的是 IPv4 还是 IPv6 地址,函数都能用同一种方式处理。
  • 重要提示:上面的代码为了简化,没有包含一个类型标识符,一个更健壮的设计是在 union 外面加一个 enum 成员来明确当前存储的是哪种类型。

最佳实践和注意事项

  1. 永远不要同时读写多个成员union 的核心就是共享内存,写入一个成员后,再读取另一个成员是危险的,除非你完全清楚内存布局并且有意为之(如类型双关)。

  2. 使用类型标志:当一个 union 可能存储多种类型时,最好在 union 定义中加入一个 enum 成员来记录当前存储的是哪种类型。

    enum DataType { TYPE_INT, TYPE_FLOAT, TYPE_STRING };
    typedef struct {
        enum DataType type;
        union {
            int i;
            float f;
            char str[20];
        } data;
    } SafeData;
    // 使用时
    SafeData myData;
    myData.type = TYPE_INT;
    myData.data.i = 123;

    这样,在读取 union 之前,你可以先检查 type 成员,确保你读取的是正确的类型,从而避免错误。

  3. 注意字节序和对齐:当 union 用于处理二进制数据(如网络协议、文件格式)时,必须注意字节序(大端/小端)和对齐问题,跨平台使用时,要确保数据转换正确。

  4. 指针运算:可以对 union 指针进行指针运算(如 ptr++),它会移动到下一个 union 对象的起始地址,移动的 sizeof(union) 个字节。

特性 struct union
内存分配 为每个成员分配独立的内存空间。 所有成员共享同一块内存空间。
大小 sizeof(struct) = 所有成员大小之和(考虑对齐)。 sizeof(union) = 最大成员的大小。
成员使用 可以同时使用所有成员。 任何时刻只能安全地使用一个成员。
指针 可以有指向 struct 的指针,用 ->(*ptr). 访问成员。 可以有指向 union 的指针,用 ->(*ptr). 访问成员。
主要用途 将不同类型的数据组合成一个逻辑实体。 在同一内存位置存储不同类型的数据,用于多态、类型转换或节省内存。

union 指针是 C 语言中一个强大但需要谨慎使用的工具,它让你能够以非常灵活的方式处理内存,但也要求你对内存管理有深刻的理解。

-- 展开阅读全文 --
头像
织梦网站如何查看原有文章?
« 上一篇 今天
织梦dede在线留言如何实现?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]