15

简单地使用 void* 与 union 有什么区别?例子:

struct my_struct {
    short datatype;
    void *data;
}

struct my_struct {
    short datatype;
    union {
        char* c;
        int* i;
        long* l;
    };
};

两者都可以用来完成完全相同的事情,但是使用 union 还是 void* 更好?

4

11 回答 11

18

我的图书馆里正好有这个案例。我们有一个通用的字符串映射模块,它可以为索引使用不同的大小,8、16 或 32 位(出于历史原因)。所以代码充满了这样的代码:

if(map->idxSiz == 1) 
   return ((BYTE *)map->idx)[Pos] = ...whatever
else
   if(map->idxSiz == 2) 
     return ((WORD *)map->idx)[Pos] = ...whatever
   else
     return ((LONG *)map->idx)[Pos] = ...whatever

有100行这样的。作为第一步,我将其更改为联合,我发现它更具可读性。

switch(map->idxSiz) {
  case 1: return map->idx.u8[Pos] = ...whatever
  case 2: return map->idx.u16[Pos] = ...whatever
  case 3: return map->idx.u32[Pos] = ...whatever
}

这让我更清楚地看到发生了什么。然后我可以决定idxSiz只使用 32 位索引来完全删除变体。但这只有在代码变得更具可读性时才有可能。

PS:这只是我们项目的一小部分,大约有 100'000 行代码由不再存在的人编写。为了不破坏应用程序,代码的更改必须是渐进的。

结论:即使人们不太习惯 union 变体,我还是更喜欢它,因为它可以使代码更易于阅读。在大型项目中,可读性非常重要,即使只有您自己,稍后也会阅读代码。

编辑:添加了评论,因为评论不格式化代码:

switch 之前的更改(现在是真正的代码)

switch(this->IdxSiz) { 
  case 2: ((uint16_t*)this->iSort)[Pos-1] = (uint16_t)this->header.nUz; break; 
  case 4: ((uint32_t*)this->iSort)[Pos-1] = this->header.nUz; break; 
}

改为

switch(this->IdxSiz) { 
  case 2: this->iSort.u16[Pos-1] = this->header.nUz; break; 
  case 4: this->iSort.u32[Pos-1] = this->header.nUz; break; 
}

我不应该把我在代码中所做的所有美化都结合起来,只显示那一步。但是我在无法访问代码的家里发布了我的答案。

于 2009-11-30T20:10:27.357 回答
12

在我看来,void 指针和显式转换是更好的方法,因为对于每个经验丰富的 C 程序员来说,意图是什么都很明显。

编辑澄清:如果我在程序中看到上述联合,我会问自己作者是否想限制存储数据的类型。也许执行了一些仅对整数类型有意义的完整性检查。但是如果我看到一个void指针,我直接知道作者设计的数据结构是用来保存任意数据的。因此,我也可以将它用于新引入的结构类型。请注意,我可能无法更改原始代码,例如,如果它是 3rd 方库的一部分。

于 2009-11-30T18:27:02.503 回答
7

使用联合来保存实际对象而不是指针更为常见。

我认为我尊重的大多数 C 开发人员都不会费心将不同的指针联合在一起。如果需要通用指针,那么使用void *肯定是“C方式”。该语言牺牲了很多安全性,以允许您故意对事物类型进行别名;考虑到我们为此功能付出的代价,我们不妨在它简化代码时使用它。这就是为什么从严格类型中逃脱一直存在的原因。

于 2009-11-30T19:18:54.497 回答
5

union方法要求您先验地知道可能使用的所有类型。该void *方法允许存储在编写相关代码时甚至可能不存在的数据类型(尽管这种未知数据类型做很多事情可能会很棘手,例如需要将指针传递给要在该数据上调用的函数而不是可以直接处理)。

编辑:由于似乎对如何使用未知数据类型存在一些误解:在大多数情况下,您提供某种“注册”功能。在典型情况下,您传递指向函数的指针,这些函数可以对正在存储的项目执行所需的所有操作。它生成并返回一个新索引,用于标识类型的值。然后,当您要存储该类型的对象时,将其标识符设置为您从注册中返回的值,并且当与对象一起使用的代码需要对该对象执行某些操作时,它会通过调用适当的函数您传入的指针。在典型情况下,这些指向函数的指针将位于struct,它会简单地将这些结构存储(指向)在一个数组中。它从注册返回的标识符值只是存储该特定结构的那些结构的数组的索引。

于 2009-11-30T18:28:03.960 回答
2

尽管如今使用 union 并不常见,但由于 union 对您的使用场景更为明确,因此非常适合。在第一个代码示例中,它不理解数据的内容。

于 2009-11-30T18:27:48.680 回答
2

我的偏好是走工会路线。void* 的强制转换是一种钝器,通过正确键入的指针访问数据可以提供一些额外的安全性。

于 2009-11-30T18:30:23.487 回答
2

抛硬币。联合更常用于非指针类型,所以这里看起来有点奇怪。然而,它提供的显式类型规范是体面的隐式文档。只要您始终知道您只会访问指针, void* 就可以了。不要开始将整数放在那里并依赖 sizeof(void*) == sizeof (int)。

我不觉得任何一种方式最终都比另一种方式有任何优势。

于 2009-11-30T18:34:45.630 回答
2

在您的示例中它有点模糊,因为您使用的是指针,因此是间接的。但union肯定有它的优点。

想象:

struct my_struct {
   short datatype;
   union {
       char c;
       int i;
       long l;
   };
};

现在您不必担心价值部分的分配来自哪里。没有单独malloc()的或类似的东西。您可能会发现访问->c->i->l的速度要快一些。(虽然这可能只有在有很多这些访问时才会有所作为。)

于 2009-11-30T18:35:34.270 回答
2

这实际上取决于您要解决的问题。没有这种情况,真的不可能评估哪个会更好。

例如,如果您尝试构建一个可以处理任意数据类型的通用容器(如列表或队列),那么 void 指针方法更可取。OTOH,如果您将自己限制为一小组原始数据类型,那么联合方法可以为您节省一些时间和精力。

于 2009-11-30T19:51:05.477 回答
2

如果您在其他编译器上使用 -fstrict-aliasing (gcc) 或类似选项构建代码,那么您必须非常小心如何进行转换。您可以随意转换指针,但是当您取消引用它时,用于取消引用的指针类型必须与原始类型匹配(有一些例外)。例如,您不能执行以下操作:

无效富(无效* p)
{
   短 * pSubSetOfInt = (短 *)p ;
   *pSubSetOfInt = 0xFFFF ;
}

无效咕()
{
   int intValue = 0 ;

   富(&int值);

   printf("0x%X\n", intValue);
}

如果这会打印 0(例如)而不是 0xFFFF 或 0xFFFF0000,请不要感到惊讶,正如您在优化构建时所期望的那样。使这段代码工作的一种方法是使用联合来做同样的事情,而且代码也可能更容易理解。

于 2009-11-30T20:36:57.750 回答
1

union 为最大的成员保留了足够的空间,它们不必相同,因为 void* 具有固定大小,而 union 可以用于任意大小。

#include <stdio.h>
#include <stdlib.h>

struct m1 {
   union {
    char c[100];
   };
};

struct m2 {
    void * c;
 };


 int
 main()
 {
printf("sizeof m1 is %d ",sizeof(struct m1));
printf("sizeof m2 is %d",sizeof(struct m2));
exit(EXIT_SUCCESS);
 }

输出:m1 的大小为 100 m2 的大小为 4

编辑:假设您只使用与 void* 大小相同的指针,我认为联合更好,因为当您尝试使用整数指针设置 .c 等时,您将获得一些错误检测。void* ,除非您正在创建自己的分配器,否则无论好坏,它绝对是快速而肮脏的。

于 2009-11-30T20:55:46.017 回答