char与unsigned char位运算行为差异及字节合并异常原因解析
我尝试用位左移<<和按位或|操作将两个字节合并为uint16_t类型的16位数字,代码为uint16_t universe = (hb << 8) | lb;。但当低位字节lb的值大于0x80时,会出现异常结果(比如预期得到511,实际得到65535),而高位字节hb不受影响。
进一步测试发现:0x70 == '\x70'成立,但0x80 != '\x80'在部分场景下成立,且signed char与char在值>=0x80时处理方式不同。尽管char与unsigned char内存大小相同,但二者在位运算中的表现存在明显差异。
复现代码
#include <string> #include <iostream> using namespace std; int main() { // 结果不同的情况 uint8_t hb = 0x01; uint8_t lb = 0xff; uint16_t universe = (hb << 8) | lb; cout<<universe << endl; // 输出511 string line = string("\x01\xff"); universe = (line[0] << 8) | line[1]; cout<<universe << endl; // 输出65535 // 另一组结果不同的情况 hb = 0x10; lb = 0x80; universe = (hb << 8) | lb; cout<<universe << endl; // 输出4224 line = string("\x10\x80"); universe = (line.at(0) << 8) | line.at(1); cout<<universe << endl; // 输出65408 universe = ('\x10' << 8) | '\x80'; cout<<universe << endl; // 同样输出65408 // 结果正常的情况 hb = 0x83; lb = 0x70; universe = (hb << 8) | lb; cout<<universe << endl; // 输出33648 line = string("\x83\x70"); universe = (line[0] << 8) | line[1]; cout<<universe << endl; // 输出33648 // 仅当低位字节>0x80时出现异常 if(0x70 == '\x70') cout << "same 0x70" << endl; // 输出same unsigned char nn = '\x80'; if(0x80 == nn) cout << "now its same 0x80 again " << endl; // 输出same uint8_t n = '\x80'; if(0x80 == n) cout << "now its same 0x80 again " << endl; // 输出same if(0x80 == '\x80') cout << "not same 0x80" << endl; // 不输出(实际不相等) if(nn == '\x80') cout << "not same 0x80" << endl; // 不输出(实际不相等) char nnn = '\x80'; if(nnn == '\x80') cout << "same 0x80" << endl; // 输出same cout << sizeof(nn) << sizeof(n) << sizeof(lb)<< sizeof(lb)<<endl; // 输出1111 // 高位字节>0x80但低位正常时结果正常 hb = 0x90; lb = 0x70; universe = (hb << 8) | lb; cout<<universe << endl; // 输出36976 line = string("\x90\x70"); universe = (line[0] << 8) | line[1]; cout<<universe << endl; // 输出36976 return 0; }
解决方法
将char类型转换为uint8_t后再进行位运算即可得到正确结果:
#include <string> #include <iostream> using namespace std; int main() { string line = string("\x01\xff"); uint8_t hb = line[0]; uint8_t lb = line[1]; uint16_t universe = (hb << 8) | lb; cout<<universe << endl; // 输出预期的511 return 0; }
核心问题解答
1. 为何char与unsigned char在位运算中会被区别对待?
这是因为**char在C++中是有符号还是无符号由编译器决定**,大部分编译器默认将char视为signed char。当char的值大于等于0x80时,最高位为1,作为有符号数会被解释为负数。
在进行位运算(比如<<或|)时,C++会先执行整数提升:将小于int的类型(包括char、unsigned char、uint8_t等)提升为int类型。
- 对于
unsigned char(或uint8_t),提升为int时会进行零扩展:高位补0,比如0xff会变成0x000000ff。 - 对于
signed char(默认char),提升为int时会进行符号扩展:如果原数最高位是1(负数),高位会全部补1,比如0xff会变成0xffffffff,0x80会变成0xffffff80。
当执行(line[0] << 8) | line[1]时,line[1]作为signed char被提升为int后是全1的负数,按位或操作会把结果的所有位都置1,最终赋值给uint16_t时就变成了0xffff(即65535)。
2. 为何高位字节hb不受影响?
因为高位字节在左移8位后,原来的字节会被移动到uint16_t的高8位,而符号扩展出来的高位会被uint16_t的截断操作丢弃。
比如line[0]是0x83(作为signed char是负数),提升为int后是0xffffff83,左移8位后变成0xffff8300。当赋值给uint16_t时,只会保留低16位,即0x8300,这和预期的结果一致。而低位字节在按位或时,符号扩展出来的高位会覆盖高8位的内容,导致结果异常。
内容的提问来源于stack exchange,提问作者Alex

