从有符号char转换为无符号char,然后再转回来?

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

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

我设法使这段代码起作用,其中像素值增加了30,但被限制为值255,但我不知道它是否安全或可移植:

```c++ jbyte* data = env->GetByteArrayElements(jByteArray, 0); for (int i = 0; i < size; ++i) { int tmp = data[i] & 0xff; tmp += 30; if (tmp > 255) { tmp = 255; } data[i] = static_cast(tmp); } env->ReleaseByteArrayElements(jByteArray, data, 0); ```
 #define CLAMP255(v) (v > 255 ? 255 : (v < 0 ? 0 : v))

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

我想知道如何在C和C++中实现这个。


4
当您像这样使用宏的参数时,最好添加括号:#define CLAMP255(v) ((v) > 255 ? 255 : ((v) < 0 ? 0 : (v))) - qbert220
5个回答

140
这是C++引入新的转换样式之一的原因,其中包括static_cast和reinterpret_cast。
从signed到unsigned的转换可能有两种意思:您可能希望unsigned变量包含signed变量值对无符号类型的最大值加1取模后的余数。也就是说,如果signed 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(是的,在反码中存在此值),并且使用静态转换会转换为0,但使用reinterpret_cast会转换为255。请注意,在反码的情况下,有符号char范围从-127到127,因此实际上无法表示128的unsigned值,由于有-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;

2
是的,根据您程序的语义,您可以安全地将有符号字符数组强制转换为无符号字符指针,这样您就可以有效地表示此内存不是有符号字符数组,而是无符号字符数组。但请注意,这将是一个reinterpret_cast,而不是static_cast,但从您描述问题的方式来看,我认为您需要一个reinterpret_cast。 - wich
你知道使用std::vector<unsigned char>数组的好的C++方法吗?我尝试了这个,但它并没有真正起作用: std::vector<char> buffer; std::vector<unsigned char> cache = std::vector<unsigned char>(reinterpret_cast<unsigned char*>(buffer.data()), reinterpret_cast<unsigned char*>(buffer.data() + buffer.size())); - serup
2
@serup 为什么它不起作用?对我来说它很好用。然而,我会稍微改一下措辞,像这样:std::vector<char> buffer; unsigned char* ptr = reinterpret_cast<unsigned char*>(buffer.data()); std::vector<unsigned char> cache(ptr, ptr + buffer.size()); 请注意,这将始终复制缓冲区,而普通数组方法则不会。 - wich
1
@serup,以下代码对我来说完全正常运行,并且可以避免缓冲区的复制。但是我不能百分之百确定这是否符合标准。std::vector<char> buffer; std::vector<unsigned char>& cache = reinterpret_cast<std::vector<unsigned char>&>(buffer); - wich
1
@serup 它会复制所有内容,std::move 只在容器元素本身包含指向其他内存的指针的情况下有用。在这种情况下,所指向的内存不会被复制,而是被“移动”。对于基本类型,如 charintfloat 等,它只是一个普通的复制。 - wich
显示剩余4条评论

2

是的,这是安全的。

c语言使用一种称为整数提升的特性,在执行计算之前增加值的位数。因此,您的CLAMP255宏将以整数(可能是32位)精度运行。结果赋值给jbyte,从而将整数精度降低回适合jbyte的8位。


你能否评论一下当signed char的值为-100时发生了什么。我对值被转换成什么,转换回去时会发生什么以及这是否安全感到困惑。 - rbcc
你从-100开始,用二进制表示为10011100。将其转换为无符号字符,结果为156。这个值用于计算(加30,然后测试是否小于0或大于255)。最终得到186(二进制为10111011),再将其转换回有符号字符,得到-70的值。所有这些都适用于8位数学运算。 - qbert220
如果你从-1(二进制11111111)开始,然后将其转换为无符号字符,你会得到255。如果你再加上30,那么你就会得到285。如果这是在8位数学中执行的(即没有整数提升),它将溢出并具有值29。然后它将在0-255范围内,因此不会被夹紧。由于我们有整数提升,我们有足够的精度来表示285,所以(v> 255)测试将为真,并且该值将被夹紧为255。 - qbert220
@rebecca,你的代码实际上从未看到数字-100,表达式(unsigned char)pixel,当像素值为-100时,将给出一个值156。 - wich

1
你有没有意识到,CLAMP255对于v < 0返回0,对于v >= 0返回255? 依我之见,应该将CLAMP255定义为:
#define CLAMP255(v) (v > 255 ? 255 : (v < 0 ? 0 : v))

差异:如果v不大于255且不小于0:返回v而不是255。

抱歉,我已经更新了。这是我简化代码时犯的错误。 - rbcc

0

有两种解释输入数据的方式;要么-128是最小值,127是最大值(即真正的有符号数据),要么0是最小值,127在中间,下一个“更高”的数字是-128,-1是“最高”值(也就是说,在二进制补码表示法中,最高位已经被误解为符号位)。

假设您指的是后者,那么正式正确的方式是

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

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


你是在说我现在所做的强制类型转换是安全的还是不安全的呢? - rbcc
强制类型转换在 C 标准法律角度上有些不安全,但在大多数常见系统上是足够安全的(具有 8 位字符和二进制补码算术的机器),因为我不知道任何编译器实现会在这里出错(尽管如果启用整数转换溢出检查,MSVC 将在此处生成运行时警告)。 - Simon Richter
无论使用什么架构来处理有符号字符,如果不使用二进制补码,这个程序都会出错。但是,简单的强制类型转换可以在任何有符号数实现中正常工作。 - wich
@wich:我有点糊涂了。您是在说我的示例代码使用强制类型转换是以安全的方式进行操作的正确方法吗? - rbcc
是否:是和否。当数据早期已被错误解释时,“加256”策略可用。 - Simon Richter

0

我不确定我是否完全理解了你的问题,如果我错了,请告诉我。

如果我理解正确,你正在读取技术上被签名为字符的jbytes,但实际上是从0到255范围内的像素值,并且你想知道在处理它们时应该如何处理,以避免损坏这些值。

那么,你应该执行以下操作:

  • 在执行任何其他操作之前将jbytes转换为无符号字符,这将确保恢复你试图操作的像素值

  • 在进行中间计算时使用更大的有符号整数类型,例如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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接