C++了解联合和结构

C++ understanding Unions and Structs

本文关键字:结构 了解 C++      更新时间:2023-10-16

我来从事一个正在进行的项目,其中一些工会的定义如下:

/* header.h */
typedef union my_union_t {
  float data[4];
  struct {
    float varA;
    float varB;
    float varC;
    float varD;
  };
} my_union;

如果我理解得很好,联合是为了节省空间,所以 sizeof(my_union_t( = 其中变量的 MAX。使用上面的语句而不是这个语句有什么好处:

typedef struct my_struct {
  float varA;
  float varB;
  float varC;
  float varD;
};

分配给他们俩的空间不是一样吗?

我怎样才能初始化varA,varB...从my_union

联合通常用于

实现对象(类型字段和数据类型的联合(等变体或实现序列化。

你使用工会的方式是灾难的秘诀。

您假设union中的struct正在包装float,然后之间没有间隙!该标准保证float data[4];是连续的,但不能保证结构元素。您唯一知道的另一件事是 varA 的地址;与data[0]的地址相同。

切勿以这种方式使用工会。

至于你的问题:"我怎样才能初始化varA,varB......从my_union?答案是,以正常的冗长方式访问结构成员,而不是通过data[]数组。

Union 主要不是为了节省空间,而是为了实现 sum 类型(为此,您将union放在一些structclass中,或者还有一个区分字段来保留运行时标记(。另外,我建议您使用最近的C++标准,至少C++11,因为它对联合有更好的支持(例如,允许更容易地联合对象及其构造或初始化(。

使用联合的优点是能够将第 n 个浮点数(0 <= n <= 3(索引为u.data[n]

在某个仅my_union u;代码声明的变量中分配联合字段,例如 u.varB = 3.14;在您的情况下具有与u.data[1] = 3.14;相同的效果


之无愧的联合的一个很好的例子是一个可变对象,它可以容纳一个int或一个string(在这种情况下你不能使用派生类(:

class IntOrString {
   bool isint;
   union {
      int num; // when isint is true
      str::string str; // when isint is false
   };
 public:
   IntOrString(int n=0) : isint(true), num(n) {};
   IntOrString(std::string s) : isint(false), str(s) {};
   IntOrString(const IntOrString& o): isint(o.isint) 
      { if (isint) num = o.num; else str = o.str); };
   IntOrString(IntOrString&&p) : isint(p.isint) 
      { if (isint) num = std::move (p.num); 
        else str = std::move (p.str); };
   ~IntOrString() { if (isint) num=0; else str->~std::string(); }; 
   void set (int n) 
     { if (!isint) str->~std::string(); isint=true; num=n; };
   void set (std::string s) { str = s; isint=false; };
   bool is_int() const { return isint; };
   int as_int() const { return (isint?num:0; };
   const std::string as_string() const { return (isint?"":str;};
 }; 

请注意str字段析构函数的显式调用。另请注意,您可以在标准容器中安全地使用IntOrString ( std::vector<IntOrString> (

另请参阅C++未来版本中的 std::optional(从概念上讲,它是与void的标记联合(

顺便说一句,在 Ocaml 中,您只需编写代码:

 type intorstring = Integer of int | String of string;;

您将使用模式匹配。如果你想让它可变,你需要做一个记录或引用它。

您最好以C++惯用的方式使用 union -s(有关一般建议,请参阅此处(。

我认为理解工会的最好方法是举出两个常见的实际例子。

第一个示例是处理图像。想象一下,你有和RGB图像,它被安排在一个长缓冲区中。
大多数人会做的是将缓冲区表示为char*,然后按 3 循环它以获得 R,G,B。

相反,您可以做的是做一个小联合,并使用它来循环图像缓冲区:

union RGB
{
   char raw[3];
   struct 
   {
      char R;
      char G;
      char B;
   } colors;
}
RGB* pixel = buffer[0];
///pixel.colors.R == The red color in the first pixel.

联合的另一个非常有用的用途是使用寄存器和位字段。
假设您有一个 32 位值,它代表某个硬件寄存器或其他东西。
有时,为了节省空间,您可以将 32 位拆分为位字段,但您还希望将该寄存器的整体表示形式表示为 32 位类型。
这显然节省了许多程序员无缘无故使用的位移计算。

union MySpecialRegister
{
    uint32_t register;
    struct 
    {
       unsigned int firstField           : 5;
       unsigned int somethingInTheMiddle : 25;
       unsigned int lastField            : 6; 
    } data;
}
// Now you can read the raw register into the register field
// then you can read the fields using the inner data struct

优点是使用联合,您可以通过两种不同的方式访问相同的内存。

在您的示例中,联合包含四个浮点数。您可以将这些浮点数作为varA,varB...这可能是更具描述性的名称,或者您可以访问与数组 data[0]、data[1]...这在循环中可能更有用。

通过联合,您还可以对不同类型的数据使用相同的内存,您可能会发现这对于编写函数以告诉您使用的是大端序还是小端序 CPU 之类的事情很有用。

不,这不是为了节省空间。它用于将一些二进制数据表示为各种数据类型的能力。例如

#include <iostream>
#include <stdint.h>
union Foo{
   int x;
   struct y
   {
      unsigned char b0, b1, b2, b3;
   };
   char z[sizeof(int)];
};
int main()
{
   Foo bar;
   bar.x = 100;
   std::cout << std::hex; // to show number in hexadec repr;
   for(size_t i = 0; i < sizeof(int); i++)
   {
      std::cout << "0x" << (int)bar.z[i] << " "; // int is just to show values as numbers, not a characters
   }
   return 0;
}

输出: 0x64 0x0 0x0 0x0 相同的值存储在结构 bar.y 中,但不存储在数组中,而是存储在结构成员中。这是因为我的机器有一个小端序。如果它很大,则输出将被反转:0x0 0x0 0x0 0x64

您可以使用reinterpret_cast实现相同的目的:

#include <iostream>
#include <stdint.h>
int main()
{
   int x = 100;
   char * xBytes = reinterpret_cast<char*>(&x);
   std::cout << std::hex; // to show number in hexadec repr;
   for (size_t i = 0; i < sizeof(int); i++)
   {
      std::cout << "0x" << (int)xBytes[i] << " "; // (int) is just to show values as numbers, not a characters
   }
   return 0;
}

例如,当您需要读取一些二进制文件时,它很有用,该文件是在与您的字节序不同的计算机上编写的。您可以只访问字节数组形式的值,并根据需要交换这些字节。

此外,当您必须处理位字段时,它很有用,但这是一个完全不同的故事:)

首先:避免访问相同内存但访问不同类型的联合!

工会根本没有节省空间。只在同一内存区域定义多个名称!而且您一次只能在联合中存储其中一个元素。

如果你有

union X
    {
        int x;
        char y[4];
     };

您可以存储 int 或 4 个字符,但不能同时存储两者!一般的问题是,没有人知道哪些数据实际存储在联合中。如果您存储 int 并读取字符,编译器将不会检查该字符,也不会进行运行时检查。解决方案通常是在联合的结构中提供额外的数据元素,该联合包含实际存储的数据类型作为枚举。

 struct Y
 {
      enum { IS_CHAR, IS_INT } tinfo;
      union
      {
           int x;
           char y[4];
       };
  }

但是在 c++ 中,你总是应该使用类或结构,它们可以从一个可能为空的父类派生,如下所示:

class Base
{
};
class Int_Type: public Base
{
    ...
    int x;
};
class Char_Type: public Base
{
     ...
    char y[4];
};

因此,您可以设备指向实际上可以为您保存 Int 或 Char 类型的基的指针。使用虚函数,您可以通过面向对象的编程方式访问成员。

正如Basile的回答中已经提到的,一个有用的情况是通过不同的名称访问同一类型

union X 
{
      struct data
      {
          float a;
          float b;
      };
      float arr[2];
 };

这允许对具有相同类型的相同数据的不同访问方式。应完全避免使用存储在相同内存中的不同类型!