cplusplus 共用体(union)

概述

  1. 共用体(union)是一种特殊的数据类型, 它允许在相同的内存位置存储不同的数据类型, 但任何时候只能有一个成员“活跃”. 这种设计使得共用体成为一种节省内存的工具,特别适用于需要以多种格式处理同一数据块的低级编程、协议解析或资源受限的嵌入式系统.

定义

#include <stdio.h>

union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;

    data.i = 10;
    printf("data.i = %d\n", data.i);

    data.f = 3.14;
    printf("data.f = %.2f\n", data.f);

    // 注意:此时 data.i 的值已经被覆盖
    printf("data.i (after writing f) = %d\n", data.i);

    return 0;
}

/** 
Output:

data.i = 10
data.f = 3.14
data.i (after writing f) = 1078523331
*/

使用 union 的缺陷

  1. 类型安全缺失

    + 点击展开
    #include <iostream>
    using namespace std;
    
    union U {
      int i;
      float f;
    };
    
    int main() {
      U u;
      u.i = 0x3f800000;		// 浮点数 1.0f 的二进制编码
      std::cout << u.f << std::endl;	
    }
    
    /**
    Output:
    1.0
    
    explain:
    1. u.i = 0x3f800000 把这段内存写成了 0x3f800000  
    2. u.f 读取时,把这段内存当成 float.  
    3. float 解释 0x3f800000 → 1.0.
     */
    

  2. 构造函数 / 析构函数管理困难

错误的版本
正确的版本
// 错误的版本
#include <iostream>
#include <string>

union U {
  std::string s;  // 非平凡类型
  int i;

	// 构造函数
  U() {
		// placement new(定位 new)语法
		// 在地址 &s 指向的内存上构造一个对象,但不分配新内存.
		// 它不会调用 operator new,不会分配堆内存,只是把对象构造在指定位置(&s).
    new (&s) std::string("hello");
    std::cout << "Construct string\n";
  }

  ~U() {          // 析构函数
    s.~basic_string();              // 手动析构 string
    std::cout << "Destruct string\n";
  }
};

int main() {
    U u;            // 调用构造函数,构造 string

    std::cout << u.s << std::endl;

    // 切换激活成员
    u.s.~basic_string();  // 必须手动析构 string
    u.i = 42;             // 激活 int 成员

    std::cout << u.i << std::endl;

    // 程序结束时,U 的析构函数会再次析构 string(但 string 已经被手动析构过一次)
    // → 这是未定义行为(double free)
}
// 正确的版本
#include <iostream>
#include <string>

struct U {
    union Storage {
        std::string s;
        int i;

        Storage() {}   // 不自动构造任何成员
        ~Storage() {}  // 不自动析构任何成员
    } data;

    bool active_string;

    U() : active_string(true) {
        new (&data.s) std::string("hello");
    }

    void setInt(int v) {
        if (active_string) {
            data.s.~basic_string();
            active_string = false;
        }
        data.i = v;
    }

    void setString(const std::string& str) {
        if (!active_string) {
            new (&data.s) std::string(str);
            active_string = true;
        } else {
            data.s = str;
        }
    }

    ~U() {
        if (active_string) {
            data.s.~basic_string();
        }
    }
};

int main() {
    U u;
    std::cout << u.data.s << std::endl;

    u.setInt(42);
    std::cout << u.data.i << std::endl;

    u.setString("world");
    std::cout << u.data.s << std::endl;

    return 0;
}

使用 std::variant 替代 union

  1. std::variant 是 C++17 引入的一种 类型安全的联合体(type‑safe union). 它的核心作用是:在同一块存储中保存多个可能类型之一,但自动管理构造/析构、记录当前激活类型,并保证类型安全.
  2. std::variant<T1, T2, …> 表示一个对象,它在任意时刻只能保存其中一种类型的值.

定义

#include <iostream>
#include <string>
#include <variant>

// helper for std::visit (C++17)
/**
template <class... Ts>: C++11引入的变长参数模板(Variadic Templates), 
struct overloaded : Ts...: 多重继承,继承所有 Ts.

*/

template <class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

int main() {
  using Value = std::variant<int, double, std::string>;

  // 构造
  Value v = 42;
  std::cout << "v holds int? " << std::holds_alternative<int>(v) << "\n";

  v = 3.14;
  std::cout << "v holds double? " << std::holds_alternative<double>(v) << "\n";

  v = std::string("hello variant");

  // 访问:std::get
  try {
    std::string s = std::get<std::string>(v);
    std::cout << "std::get<string>: " << s << "\n";
  } catch (const std::bad_variant_access& e) {
    std::cout << "bad_variant_access: " << e.what() << "\n";
  }

  // 访问:std::get_if
  if (auto p = std::get_if<double>(&v)) {
    std::cout << "get_if<double>: " << *p << "\n";
  } else {
    std::cout << "v is not double\n";
  }

  // 访问:std::visit
  std::visit(overloaded{
    [](int x)           { std::cout << "visit int: " << x << "\n"; },
    [](double d)        { std::cout << "visit double: " << d << "\n"; },
    [](const std::string& s) { std::cout << "visit string: " << s << "\n"; }
  }, v);

  // emplace
  v.emplace<int>(123);
  std::cout << "after emplace<int>: " << std::get<int>(v) << "\n";
  return 0;
}