【问题标题】:Are casts as safe as unions?演员表和工会一样安全吗?
【发布时间】:2014-05-01 19:39:19
【问题描述】:

我想将浮点数等大型变量拆分为字节段,并通过 UART 逐字节串行发送。我正在使用 C/C++。

一种方法是深度复制我想要发送到联合的值,然后发送它。我认为这将是 100% 安全但缓慢。工会看起来像这样:

   union mySendUnion
   {
       mySendType sendVal;
       char[sizeof(mySendType)] sendArray; 
    }

另一种选择是将指向我要发送的值的指针转换为指向特定联合的指针。这还安全吗?

第三个选项可能是将指针转换为我要发送到字符的值,然后像这样增加一个指针:

            sendType myValue = 443.2;

    char* sendChar = (char*)myValue; 

    for(char i=0; i< sizeof(sendType) ; i++)
    {
        Serial.write(*(sendChar+j), 1);
    }

上面的指针算法我已经成功了,但我不确定它在所有情况下是否安全。我担心的是,如果我们使用 32 位处理器并想要发送浮点数怎么办。编译器选择将这个 32 位浮点数存储到一个内存单元中,但在每个 32 位单元中只存储一个字符。

然后,每个计数器增量都会使程序指针增加一个完整的内存单元,我们会错过浮点数。

C 标准中有什么东西可以防止这种情况发生吗,或者这可能是某个编译器的问题?

【问题讨论】:

  • 使用联合进行类型双关将导致 C++ 中的未定义行为,不应被视为安全
  • 这两种解决方案都不是安全的,都可能会失败。他们只是碰巧经常工作(大部分时间),人们一直在这样做......打字机更安全一些,因为它没有对齐问题
  • 你认为联合为什么会慢?
  • 确保考虑字节序;并非所有平台都以相同的顺序存储构成(例如)32 位浮点数的四个字节,因此您需要确保连接的两端在表示上达成一致。
  • @dwelch “类型转换更安全,因为它没有对齐问题”是什么意思?两种解决方案都是错误的(在 C++ 中,如果类型不同)或者两者都很好(在 C99 及更高版本中,当类型兼容或别名指针的引用类型为[un]signed char)或仅基于联合的解决方案是好的(在 C 中,当类型不同但双关指针不是 char * 时)或者只有基于指针的解决方案是好的(在 C++ 中,当类型不兼容并且别名指针的引用类型是 [un]signed char 时) .

标签: c++ c arduino embedded unions


【解决方案1】:

首先,您不能用“C/C++”编写代码。没有像“C/C++”这样的语言,因为它们是根本不同的语言。因此,关于工会的答案截然不同。

关于标题:

强制转换和联合一样安全吗?

不,通常不是,因为strict aliasing rule。也就是说,如果您使用指向不兼容类型的指针对某个类型的指针进行类型双关,则会导致未定义的行为。 此规则的唯一例外是当您通过指向(有符号或无符号)char 的指针对其进行别名来读取或操作对象的按字节表示时。与您的情况一样。 p>

然而,工会是完全不同的混蛋。在 C99 及更高版本中允许通过复制到联合和从联合中读取来进行类型双关,但在 C89 和所有 C++ 版本中会导致未定义的行为。

一个方向,如果您将原始联合作为实际对象,您还可以使用指向联合的指针安全地键入双关(在 C99 和更高版本中)。像这样:

union p {
    char c[sizeof(float)];
    float f;
} pun;
union p *punPtr = &pun;

punPtr->f = 3.14;
send_bytes(punPtr->c, sizeof(float));

因为“指向联合的指针指向其所有成员,反之亦然”(C99,我不记得确切的段落,大约是 6.2.5,IIRC)。 但在另一个方向上并非如此:

float f = 3.14;
union p *punPtr = &f;
send_bytes(punPtr->c, sizeof(float)); // triggers UB!

总结一下:以下代码sn-p在C89、C99、C11和C++中都有效:

float f = 3.14;
char *p = (char *)&f;
size_t i;
for (i = 0; i < sizeof f; i++) {
    send_byte(p[i]); // hypotetical function
}

以下内容仅在 C99 及更高版本中有效:

union {
    char c[sizeof(float)];
    float f;
} pun;

pun.f = 3.14;
send_bytes(pun.c, sizeof float); // another hypotetical function

但是,以下内容无效是有效的:

float f = 3.14;
unsigned *u = (unsigned *)&f;
printf("%u\n", *u); // undefined behavior triggered!

另一个始终保证有效的解决方案memcpy()memcpy() 函数在两个对象之间进行逐字节复制。 (不要让我开始说它“慢”——在大多数现代编译器和 stdlib 实现中,它是一个内在函数)。

【讨论】:

  • 你能否支持在 C99 及更高版本中允许通过复制到联合和从联合中读取来进行类型双关,但在 [C89 和] 所有 C++ 版本中会导致未定义的行为。
  • @DavidRodríguez-dribeas 对不起,我不明白,支持如何?
  • 我的印象是它在 C++ 中是合法的,但你声称它不是。您是否有支持该声明的报价?这是一个灰色地带,但很明显,我所知道的所有 C++ 编译器都支持类型双关语,甚至在 gcc 手册页中也是 推荐 的方式。跨度>
  • @DavidRodríguez-dribeas 啊,我明白了。我目前正在搜索 C++11 标准。
  • @DavidRodríguez-dribeas 例如,C++11, 9.2:“如果一个标准布局联合包含两个或多个共享相同初始序列的标准布局结构,并且如果标准布局联合对象当前包含这些标准布局结构之一,允许检查其中任何一个的公共初始部分。” - 以及后来,在 9.5 中:“在一个联合中,在任何时候最多可以有一个非静态数据成员处于活动状态,即最多可以将一个非静态数据成员的值存储在一个随时联合。” (这是上面关于标准的部分-...
【解决方案2】:

在字节流上发送浮点数据时的一般建议是使用一些serialization 技术,以确保数据格式定义明确(最好是架构中性,当心endianness 问题!)。

您可以使用XDR - 或者ASN1- 这是一种二进制格式(更多信息请参见xdr(3))。对于 C++,另请参阅 libs11n

除非速度或数据大小非常关键,否则我建议改为使用 JSONYAML 之类的文本格式(文本格式更冗长,但更易于调试和记录)。有几个很好的库支持它(例如,jsoncpp 用于 C++ 或 jansson 用于 C)。

请注意,串行端口非常慢(w.r.t. CPU)。所以序列化处理时间可以忽略不计。

无论您做什么,请记录序列化格式(即使是内部项目)。

【讨论】:

    【解决方案3】:

    转换为[[un]signed] char [const] * 是合法的,并且在阅读 时不会引起问题,所以这是一个不错的选择(也就是说,在修复char *sendChar = reinterpret_cast&lt;char*&gt;(&amp;myValue); 之后,因为你在它, 让它const)

    现在下一个问题出现在另一边,即阅读时,因为您不能安全地使用相同的方法进行阅读。一般来说,复制变量的成本远低于通过 UART 发送的成本,所以我会在读取串口时使用union

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-05-19
      • 2011-08-14
      • 2010-09-20
      • 1970-01-01
      • 2023-03-30
      • 1970-01-01
      相关资源
      最近更新 更多