C++ 中的类型双关、union 与类型双关:让一块内存有多个“名字”

在 C++ 里,除了常见的 类型转换 (conversion),我们还可以通过 类型双关(type punning)的方式,把一段已有内存"重新解释"为另一种类型。这种技巧在底层编程、调试内存布局时很常见,但同时也要注意其局限与风险。


类型转换 vs. 类型双关

  • 类型转换(cast/conversion)
cpp 复制代码
int a = 50; 
double value = (double)a;

这里发生的是 语义转换
int 的值 50 被转换成一个新的 double 值 50.0。

两者在内存上并不共享,valuea 的地址完全不同。


  • 类型双关(type punning)
cpp 复制代码
int a = 50; 
double value = *(double*)&a;

这里没有新变量分配,而是把 a 的地址强制解释为 double*,并读取内存。

也就是说,读到的是 同一块内存,但按照 double 的方式去解释

问题是:

  • int 只有 4 个字节,而 double 通常需要 8 个字节

  • 强制读取会访问到 a 之后的 4 个字节(未知内容)

  • 这属于 未定义行为(UB)


为什么会 UB?

  1. 对齐问题(alignment)

    某些 CPU 要求 double* 必须是 8 字节对齐,否则解引用会导致崩溃。

  2. 对象大小不匹配
    int 只有 4 字节,把它当成 double(8 字节)来读写,必然会访问到超出范围的内存。

  3. 严格别名规则(Strict Aliasing Rule)

    C++ 标准规定:

    一个对象不能通过"非兼容类型"的指针来访问,否则就是未定义行为。

    唯一的例外是 char* / unsigned char* / std::byte*,因为它们专门用来观察原始字节。

    换句话说:

    cpp 复制代码
    int a = 50; 
    char* p = (char*)&a; //  合法
    double* q = (double*)&a; //  UB

示例:结构体的内存布局

我们先定义一个简单的结构体:

cpp 复制代码
struct Entity
{
    int x, y;
};

Entity e = { 5, 8 };

在绝大多数编译器下,Entity 的内存布局就是两个 int 连续存放:

cpp 复制代码
地址:  &e
内存:  05 00 00 00   08 00 00 00   cc cc cc cc ...
        e.x=5        e.y=8         未初始化数据
  • cc cc cc cc 是调试器里未初始化内存的填充字节。

  • 由于 Entity 内部只有两个 int,没有额外的填充字节(padding),所以结构体大小就是 sizeof(int) * 2

  • 如果结构体是空的(没有成员变量),C++ 仍然会让它占用 至少 1 个字节,这样结构体对象才能有地址。


示例:把结构体当数组访问

cpp 复制代码
int* position = (int*)&e;
std::cout << position[0] << ", " << position[1] << std::endl;

输出结果:

cpp 复制代码
5,8

因为 Entity 内部就是两个连续的 int,所以这样访问有效。

但请注意:一旦结构体包含了 doublechar 等不同类型,或者编译器插入了填充字节,就可能出问题。


示例:用字节偏移访问成员

cpp 复制代码
int y = *(int*)((char*)&e + 4);
std::cout << y << std::endl;  // 输出 8

解释:

  1. &e → 结构体地址

  2. (char*)&e → 转成字节指针

  3. + 4 → 偏移 4 个字节,正好是 y 的位置

  4. 转回 int*,解引用取值


示例:结构体方法返回内部指针

cpp 复制代码
struct Entity
{
    int x, y;

    int* GetPositions()
    {
        return &x;
    }
};

Entity e = { 5, 8 };
int* position = e.GetPositions();

position[0] = 2;   // 修改 e.x
position[1] = 10;  // 修改 e.y

输出结果:

cpp 复制代码
2, 10

因为 xy 在内存上连续存放,&x 也能作为"数组起点"。


总结

  • 类型转换 → 创建新对象,值发生转换,安全。

  • 类型双关 → 同一内存不同解释,高效但可能 UB。

  • 未定义行为原因:对齐、大小不匹配、严格别名规则。

  • 调试场景 可用 reinterpret_cast实际工程 推荐 std::memcpychar* 来访问底层内存。

C++ 中的 union 与类型双关:让一块内存有多个"名字"

在 C++ 中,我们经常会遇到这样的需求:同一块内存,用不同的方式去访问 。这就是所谓的 类型双关(type punning)

一种常见手段是使用指针强制转换(reinterpret_cast),但这种方式既不直观,又容易触发 严格别名规则(Strict Aliasing Rule) 导致未定义行为。

相比之下,union 提供了更简洁的方式:多个成员共享同一块内存。这使得我们可以给同一个数据起多个不同的"名字",代码既易读又高效。

基础示例:float 与 int 共用内存

cpp 复制代码
struct myUnion
{
    union
    {
        float a;
        int b;
    };
};

int main()
{
    myUnion u;
    u.a = 2.0f;
    std::cout << u.a << ", " << u.b << std::endl;
}

输出结果:

cpp 复制代码
2, 1073741824

解释:

  • u.a = 2.0f; → 把浮点数 2.0f 的二进制形式写入 union。

  • u.bu.a 共享同一块内存,于是 u.b 直接读取了那 4 个字节,并把它解释为 int

  • 1073741824 就是 2.0f原始二进制位模式 按照 int 的方式解读出来的值。

这就是 union 的类型双关:一份内存,多个解释。


Vector4 与 Vector2 的关系(指针版)

假设我们有二维向量和四维向量:

cpp 复制代码
/*虽然 Vector4 里已经有 x 和 y,但 GetA() 的作用是:

让 (x, y) 能作为 一个整体 Vector2 返回/传递*/

struct Vector2
{
    float x, y;
};

struct Vector4
{
    float x, y, z, w;

    Vector2 GetA()
    {
        return *(Vector2*)&x; // 把 &x 当作 Vector2 的起点
    }
};

这里的 GetA() 方法是通过 reinterpret_cast 来实现的:

它把 &x 强制解释成 Vector2*,再解引用,得到 Vector2(x, y)

虽然能用,但有两个问题:

  1. 严格别名规则可能导致 UB(未定义行为)。

  2. 代码不够直观。

所以更推荐用 union 来实现。


用 union 改写 Vector4(更直观)

第一步:把 Vector4 的 4 个分量放进匿名结构体里。

cpp 复制代码
struct Vector4
{
    union
    {
        struct
        {
            float x, y, z, w;
        };
    };
};

为什么不直接写 float x, y, z, w; 在 union 里?

因为 union 的成员是 重叠存储 的,如果直接写 4 个 float,它们都会占用同一地址,彼此覆盖。

匿名 struct 的好处

  • 你不用写 v.s.x 这种访问方式,而是直接 v.x

  • C++ 允许匿名 struct 作为 union 的成员,这样变量名会被"提升"到外层作用域。


支持多种访问方式的 Vector4

进一步改写,让 Vector4 既可以按单个分量访问,也可以按 Vector2 访问:

cpp 复制代码
struct Vector4
{
    union
    {
        struct
        {
            float x, y, z, w;
        };
        struct
        {
            Vector2 a, b; // a=(x,y),b=(z,w)
        };
    };
};

现在 Vector4 有两种视图:

  • x, y, z, w 四个浮点数

  • a, b 两个 Vector2

由于它们在 union 中共享内存:

  • a 对应 x, y

  • b 对应 z, w


使用示例

cpp 复制代码
Vector4 vector = { 1.0f, 2.0f, 3.0f, 4.0f };

PrintVector2(vector.a);   // 输出 1, 2

vector.z = 500.0f;        // 修改 z
PrintVector2(vector.b);   // 输出 500, 4

运行结果:

cpp 复制代码
1, 2 
500, 4

解释:

  • vector.a → 访问 {x, y},输出 (1, 2)

  • 设置 vector.z = 500.0f;,其实就是修改了 b.x 的内存。

  • 所以 vector.b 输出 (500, 4)


总结

  • union 的本质:所有成员共享同一块内存。

  • 常见用途

    1. 给同一个变量多个"名字"(float ↔ int)。

    2. 给一组数据多种访问方式(Vector4 ↔ 两个 Vector2)。

  • 注意事项

    • 匿名 union / 匿名 struct 可以让成员直接暴露,更简洁。

    • C++ 标准对 union 的"活跃成员"有一定限制,但主流编译器广泛支持这种用法。

    • 这种技巧在 数学向量库图形 API底层协议解析 里特别常见。

相关推荐
chao_7892 小时前
Union 和 Optional 区别
开发语言·数据结构·python·fastapi
hsjkdhs2 小时前
C++之类的组合
开发语言·c++·算法
奔跑吧邓邓子2 小时前
【C++实战(57)】C++20新特性实战:解锁C++编程新姿势
c++·实战·c++20·c++20新特性
charlie1145141912 小时前
精读 C++20 设计模式:行为型设计模式——观察者模式
c++·学习·观察者模式·设计模式·程序设计·c++20
疯狂的Alex2 小时前
【C#避坑实战系列文章16】性能优化(CPU / 内存占用过高问题解决)
开发语言·性能优化·c#
象骑士Hack2 小时前
dev c++工具下载 dev c++安装包下载 dev c++软件网盘资源分享
开发语言·c++
青草地溪水旁3 小时前
设计模式(C++)详解——观察者模式(Observer)(2)
c++·观察者模式·设计模式
charlie1145141913 小时前
精读 C++20 设计模式:行为型设计模式 — 备忘录模式
c++·学习·设计模式·c++20·备忘录模式
铍镁钙锶钡镭3 小时前
FFmpeg 解封装简单流程
开发语言·ffmpeg·php