【问题标题】:Converting from signed char to unsigned char and back again?从有符号字符转换为无符号字符然后再转换回来?
【发布时间】:2011-02-18 11:47:28
【问题描述】:

我正在使用 JNI,并且有一个 jbyte 类型的数组,其中 jbyte 表示为有符号字符,即范围从 -128 到 127。jbytes 表示图像像素。对于图像处理,我们通常希望像素分量的范围为 0 到 255。因此我想将 jbyte 值转换为 0 到 255 的范围(即与 unsigned char 相同的范围),对值进行一些计算,然后存储结果再次为 jbyte。

如何安全地进行这些转换?

我设法让这段代码工作,其中像素值增加了 30,但限制为值 255,但我不明白它是否安全或可移植:

 #define CLAMP255(v) (v > 255 ? 255 : (v < 0 ? 0 : v))

 jbyte pixel = ...
 pixel = CLAMP_255((unsigned char)pixel + 30);

我很想知道如何在 C 和 C++ 中做到这一点。

【问题讨论】:

  • 您可能应该在使用宏的参数时添加括号,如下所示:#define CLAMP255(v) ((v) &gt; 255 ? 255 : ((v) &lt; 0 ? 0 : (v)))

标签: c++ c java-native-interface


【解决方案1】:

这也是 C++ 引入新的演员风格的原因之一,其中包括 static_castreinterpret_cast

说从有符号到无符号的转换可能意味着两件事,你可能意味着你希望无符号变量包含有符号变量的值,以你的无符号类型的最大值 + 1 为模。也就是说,如果你有符号char 的值为 -128,然后将 CHAR_MAX+1 添加为 128,如果值为 -1,则将 CHAR_MAX+1 添加为 255,这就是 static_cast 所做的。另一方面,您可能意味着将某个变量引用的内存的位值解释为无符号字节,而不管系统上使用的有符号整数表示形式如何,即如果它具有位值 0b10000000 它应该评估值 128,位值 0b11111111 为 255,这是通过 reinterpret_cast 完成的。

现在,对于二进制补码表示,这恰好是完全相同的东西,因为 -128 表示为 0b10000000 而 -1 表示为 0b11111111 以及两者之间的所有表示。然而,其他计算机(通常是较旧的架构)可能会使用不同的符号表示,例如符号和大小或反码。在一个补码中,0b10000000 位值不会是 -128,而是 -127,因此静态强制转换为 unsigned char 将使此为 129,而 reinterpret_cast 将使此为 128。此外,在一个补码中,0b11111111 位值将不是-1,而是-0,(是的,这个值存在于反码中),并且将通过static_cast转换为0值,但通过reinterpret_cast转换为255。请注意,在反码的情况下,无符号值 128 实际上不能用有符号字符表示,因为它的范围是 -127 到 127,因为 -0 值。

我不得不说,绝大多数计算机都将使用二进制补码,这使得整个问题对于您的代码将运行的任何地方都没有实际意义。想想 60 年代的时间框架,您可能只会在非常古老的架构中看到除二进制补码以外的系统。

语法归结为:

signed char x = -100;
unsigned char y;

y = (unsigned char)x;                    // C static
y = *(unsigned char*)(&x);               // C reinterpret
y = static_cast<unsigned char>(x);       // C++ static
y = reinterpret_cast<unsigned char&>(x); // C++ reinterpret

使用数组以一种不错的 C++ 方式执行此操作:

jbyte memory_buffer[nr_pixels];
unsigned char* pixels = reinterpret_cast<unsigned char*>(memory_buffer);

或C方式:

unsigned char* pixels = (unsigned char*)memory_buffer;

【讨论】:

  • 哇,好帖子!是的,所以我想用 static_cast 来保证安全。不过,我对您的示例中的指针有点困惑。我可以将有符号的 char* 指针转换为无符号的 char* 指针,然后用后一个指针安全地读/写吗?安全地说,我的意思是从那时起,我可以将后一个指针视为指向一个无符号字符数组?这将使我的代码更清晰,因为这意味着我不必来回转换。它似乎通过快速测试有效,但我不确定它是否安全。
  • 是的,考虑到程序的语义,您可以安全地将一个有符号字符数组转换为指向无符号字符的指针,您可以有效地说,这个内存不是一个有符号字符数组,而是一个数组无符号字符。但是请注意,这将是一个 reinterpret_cast,而不是静态转换,但从您描述问题的方式来看,我认为重新解释转换是您想要的。
  • @serup 为什么它真的不起作用?这对我来说可以。不过,我的措辞会略有不同; std::vector&lt;char&gt; buffer; unsigned char* ptr = reinterpret_cast&lt;unsigned char*&gt;(buffer.data()); std::vector&lt;unsigned char&gt; cache(ptr, ptr + buffer.size()); 请注意,这将始终复制缓冲区,而普通数组方法不会。
  • @serup 以下对我来说确实很好,并且可以避免缓冲区的副本。但是,我不是 100% 确定这是否可以保证按标准工作。 std::vector&lt;char&gt; buffer; std::vector&lt;unsigned char&gt;&amp; cache = reinterpret_cast&lt;std::vector&lt;unsigned char&gt;&amp;&gt;(buffer);
  • @serup 它将复制所有内容,std::move 仅在容器元素本身包含指向其他内存的指针的情况下才有用。在这种情况下,指向的内存不会被复制,而是“移动”。对于charintfloat 等基本类型,它只是一个普通的副本。
【解决方案2】:

是的,这是安全的。

c 语言使用一种称为整数提升的功能来增加值中的位数,然后再执行计算。因此,您的 CLAMP255 宏将以整数(可能是 32 位)精度运行。结果分配给一个 jbyte,这会将整数精度降低到适合 jbyte 的 8 位。

【讨论】:

  • 你能评论一下正在发生的事情吗?签名字符的值为-100。我对值转换为什么、转换回什么以及这是否安全感到困惑。
  • 您从 -100 开始,即二进制的 10011100。将其转换为无符号字符,结果为 156。这是用于计算的值(加 30,然后测试 255)。您最终将得到 186(10111011 二进制),它被转换回有符号字符,其值为 -70。无论如何,这一切都适合 8 位数学。
  • 如果你从 -1(11111111 二进制)开始,然后将其转换为无符号字符,你会得到 255。如果你加上 30,那么你会得到 285。如果这是在 8 中执行的位数学(即没有整数提升)它会溢出并具有值 29。然后它将在 0-255 范围内,因此不会被钳制。由于我们有整数提升,我们有足够的精度来表示 285,因此 (v > 255) 测试将为真,并且该值将被限制为 255。
  • @rebecca 您的代码实际上永远不会看到数字 -100,像素值为 -100 的表达式 (unsigned char)pixel 已经为您提供了 156 的值。
【解决方案3】:

您是否意识到,对于 v = 0,则返回 255?
恕我直言,CLAMP255 应定义为:

#define CLAMP255(v) (v > 255 ? 255 : (v < 0 ? 0 : v))

区别:如果 v 不大于 255 且不小于 0:返回 v 而不是 255

【讨论】:

  • 糟糕,我已经更新了。这是我在简化代码时犯的一个错误。
【解决方案4】:

有两种方法可以解释输入数据; -128 是最低值,127 是最高值(即真正的有符号数据),或者 0 是最低值,127 介于中间,下一个“更高”的数字是 -128,其中 -1 是“最高”值(即,最高有效位已被误解为二进制补码表示法中的符号位。

假设你的意思是后者,形式上正确的方式是

signed char in = ...
unsigned char out = (in < 0)?(in + 256):in;

至少 gcc 正确识别为无操作。

【讨论】:

  • 你是说我正在做的演员是安全的还是不安全的?
  • 从 C 标准律师的角度来看,演员阵容有点不安全,但在大多数常见系统(具有 8 位字符和二进制补码算法的机器)上足够安全,因为我知道没有编译器实现可以这样做这里是错误的(尽管如果启用整数转换溢出检查,MSVC 会在此处生成运行时警告)。
  • 好吧,这在任何不使用二进制补码作为有符号字符的架构上都会出错,无论有符号数字的实现如何,简单转换都可以工作。
  • @wich:我有点糊涂了。你是说我的带有强制转换的示例代码是以安全的方式处理事情的正确方法吗?
  • wich:是和不是。 “添加 256”策略适用于数据早前已被误解的情况。
【解决方案5】:

我不能 100% 确定我理解你的问题,如果我错了,请告诉我。

如果我没看错,您正在阅读的 jbytes 是 技术上 签名的字符,但 实际上 像素值范围从 0 到 255,并且您想知道如何应该在不破坏过程中的值的情况下处理它们。

然后,您应该执行以下操作:

  • 在执行任何其他操作之前将 jbytes 转换为 unsigned char,这肯定会恢复您尝试操作的像素值

  • 在进行中间计算时使用更大的有符号整数类型,例如 int,以确保可以检测和处理上溢和下溢(特别是,强制转换为有符号类型可能会强制编译器将每种类型提升为无符号类型,在这种情况下,您以后将无法检测到下溢)

  • 分配回 jbyte 时,您需要将值限制在 0-255 范围内,转换为无符号字符,然后再次转换为有符号字符:我不确定第一次转换是否严格有必要,但如果两者都做就不会错

例如:

inline int fromJByte(jbyte pixel) {
    // cast to unsigned char re-interprets values as 0-255
    // cast to int will make intermediate calculations safer
    return static_cast<int>(static_cast<unsigned char>(pixel));
}

inline jbyte fromInt(int pixel) {
    if(pixel < 0)
        pixel = 0;

    if(pixel > 255)
        pixel = 255;

    return static_cast<jbyte>(static_cast<unsigned char>(pixel));
}

jbyte in = ...
int intermediate = fromJByte(in) + 30;
jbyte out = fromInt(intermediate);

【讨论】:

    猜你喜欢
    • 2012-06-03
    • 2013-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    • 1970-01-01
    • 2011-11-08
    • 2011-06-14
    相关资源
    最近更新 更多