字符串、进制、位运算和字节流经常出现在协议解析、日志处理、状态字段和业务模拟题中。它们表面上是不同主题,实际都围绕同一条数据流:文本如何转换为数值,数值如何按位解释,字节如何按协议组合成字段 。
本篇以 C++17 为基线,完成一段十六进制报文的解析,并整理这些场景中最容易写错的边界。
一、std::string 必记操作 std::string 保存连续的字符序列,可以通过下标、迭代器和标准算法处理。
目的
写法
注意事项
长度
text.size()
返回 std::size_t
判空
text.empty()
比 text.size() == 0 更直接
读取字符
text[index]
不检查越界
带检查访问
text.at(index)
越界会抛出 std::out_of_range
截取
text.substr(pos, count)
count 超出剩余长度时会截到末尾;pos > size() 会抛异常
查找
text.find(target)
未找到时返回 std::string::npos
追加
text += suffix 或 text.append(suffix)
直接修改原字符串
插入/删除
insert()、erase()
操作后原下标和迭代器可能失效
查找结果不要保存到 int:
1 2 3 4 5 6 7 const std::string text = "device:error" ;const std::size_t separator = text.find (':' );if (separator != std::string::npos) { const std::string key = text.substr (0 , separator); const std::string value = text.substr (separator + 1 ); }
字符判断与大小写转换 std::isdigit()、std::isalpha()、std::tolower() 等函数位于 <cctype>。除 EOF 外,它们的参数必须能够表示为 unsigned char,因此稳妥写法是:
1 2 3 4 5 6 7 8 9 10 11 #include <cctype> bool isDigit (char ch) { return std::isdigit (static_cast <unsigned char >(ch)) != 0 ; } char toLower (char ch) { return static_cast <char >( std::tolower (static_cast <unsigned char >(ch)) ); }
如果题目只处理 ASCII 十六进制字符,直接比较字符范围往往更清楚:
1 2 3 4 5 bool isHexDigit (char ch) { return ('0' <= ch && ch <= '9' ) || ('a' <= ch && ch <= 'f' ) || ('A' <= ch && ch <= 'F' ); }
二、字符串与数值转换 1. 标准转换函数 1 2 3 4 const int decimal = std::stoi ("123" );const int hexadecimal = std::stoi ("7f" , nullptr , 16 );const long long largeValue = std::stoll ("9000000000" );const std::string text = std::to_string (42 );
std::stoi()、std::stoll() 可以接收基数参数,合法范围为 2 到 36,基数 0 表示根据前缀自动判断。转换可能抛出:
std::invalid_argument:没有可转换的字符;
std::out_of_range:结果超出目标类型范围。
业务模拟题如果明确要求非法输入返回错误码,就不能忽略这些情况。还要注意:默认情况下,std::stoi("12abc") 会成功解析前缀 12。需要整串合法时,应读取 pos 并确认 pos == text.size()。
1 2 3 std::size_t parsed = 0 ; const int value = std::stoi ("123" , &parsed, 10 );const bool allConsumed = parsed == 3 ;
2. 使用字符串流拆分一行 1 2 3 4 5 6 7 8 9 10 11 12 #include <sstream> std::string line = "sensor-7 36 online" ; std::istringstream input (line) ;std::string id; int temperature = 0 ;std::string state; if (input >> id >> temperature >> state) { }
当分隔符固定为逗号时,可以使用 std::getline(stream, field, ',')。如果字段允许引号、转义逗号或嵌套结构,就不能把简单的 getline() 拆分当成完整 CSV 解析器。
三、进制转换 1. 十进制转换为 2~36 进制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <algorithm> #include <cstdint> #include <stdexcept> #include <string> std::string toBase (std::uint64_t value, int base) { if (base < 2 || base > 36 ) { throw std::invalid_argument ("base must be in [2, 36]" ); } if (value == 0 ) { return "0" ; } constexpr char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ; std::string result; while (value > 0 ) { const std::uint64_t remainder = value % static_cast <std::uint64_t >(base); result.push_back (digits[remainder]); value /= static_cast <std::uint64_t >(base); } std::reverse (result.begin (), result.end ()); return result; }
循环每次把数值除以 base,因此时间复杂度和结果位数成正比,即 O(log_base(value));结果字符串占用相同数量级的空间。
这个函数明确只处理非负数。若题目允许负数,应先定义负号规则,并特别处理最小有符号整数不能直接取相反数的问题。
2. 固定格式的十六进制输出 1 2 3 4 5 6 7 8 9 10 #include <iomanip> #include <iostream> unsigned int value = 10 ;std::cout << std::uppercase << std::hex << std::setw (2 ) << std::setfill ('0' ) << value << '\n' ;
std::hex、std::uppercase 和 std::setfill() 会影响后续输出。需要继续输出十进制时,显式使用 std::dec。
四、位运算与掩码
运算
含义
常见用途
a & b
按位与
检查某些位是否为 1
a | b
按位或
设置某些位
a ^ b
按位异或
翻转位、查找只出现一次的值
~a
按位取反
构造清除掩码
a << k
左移
构造第 k 位或乘以 2 的幂
a >> k
右移
读取高位字段
位运算优先使用无符号整数,避免负数右移和有符号左移带来的实现差异或未定义行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <cstdint> bool hasBit (std::uint32_t value, unsigned int bit) { return (value & (std::uint32_t {1 } << bit)) != 0 ; } void setBit (std::uint32_t & value, unsigned int bit) { value |= (std::uint32_t {1 } << bit); } void clearBit (std::uint32_t & value, unsigned int bit) { value &= ~(std::uint32_t {1 } << bit); } void toggleBit (std::uint32_t & value, unsigned int bit) { value ^= (std::uint32_t {1 } << bit); }
调用前要保证 bit < 32。把 1 写成目标无符号类型,可以避免先以有符号 int 执行移位。
C++17 统计二进制中 1 的数量 1 2 3 4 5 6 7 8 9 10 #include <cstdint> int countBits (std::uint32_t value) { int count = 0 ; while (value != 0 ) { value &= value - 1 ; ++count; } return count; }
循环次数等于二进制中 1 的数量,最多执行 32 次,额外空间为 O(1)。
五、字节流的类型与字节序 1. char、std::uint8_t 与 std::byte
char 适合文本和字符序列;
std::uint8_t 在实现提供该类型时,适合需要算术与位运算的 8 位无符号数据;
std::byte 是 C++17 引入的枚举类,用于表达“这是一段原始内存”,不提供普通整数算术。
std::uint8_t 往往是 unsigned char 的别名,直接交给输出流可能被当作字符。显示数值时应转换:
1 2 std::uint8_t value = 65 ; std::cout << static_cast <unsigned int >(value) << '\n' ;
2. 大端与小端 假设两个字节依次为 0x12 0x34:
大端序把高位字节放在前面,表示 0x1234;
小端序把低位字节放在前面,表示 0x3412。
协议题必须按题目规定组合字节,不能直接把字节数组强制转换成整数指针。这种转换同时涉及宿主机字节序、对齐、对象生命周期和别名规则。
1 2 3 4 5 6 7 8 #include <cstdint> std::uint16_t readBigEndian16 (std::uint8_t high, std::uint8_t low) { return static_cast <std::uint16_t >( (static_cast <std::uint16_t >(high) << 8U ) | static_cast <std::uint16_t >(low) ); }
六、贯穿场景:解析十六进制报文 定义一段简化报文:
字段
字节数
说明
版本号
1
无符号整数
标志位
1
bit 0 为应答,bit 1 为压缩,bit 2 为紧急
载荷长度
2
大端序无符号整数
载荷
变长
本题限定为 ASCII 文本
输入是一组由空格分隔的两位十六进制字节:
完整程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <cstdint> #include <exception> #include <iostream> #include <stdexcept> #include <string> #include <vector> int hexDigitValue (char ch) { if ('0' <= ch && ch <= '9' ) { return ch - '0' ; } if ('a' <= ch && ch <= 'f' ) { return ch - 'a' + 10 ; } if ('A' <= ch && ch <= 'F' ) { return ch - 'A' + 10 ; } throw std::invalid_argument ("invalid hexadecimal digit" ); } std::uint8_t parseHexByte (const std::string& token) { if (token.size () != 2 ) { throw std::invalid_argument ("each byte must contain two hex digits" ); } const int high = hexDigitValue (token[0 ]); const int low = hexDigitValue (token[1 ]); return static_cast <std::uint8_t >((high << 4 ) | low); } std::uint16_t readBigEndian16 (std::uint8_t high, std::uint8_t low) { return static_cast <std::uint16_t >( (static_cast <std::uint16_t >(high) << 8U ) | static_cast <std::uint16_t >(low) ); } int main () { std::ios::sync_with_stdio (false ); std::cin.tie (nullptr ); try { std::vector<std::uint8_t > bytes; std::string token; while (std::cin >> token) { bytes.push_back (parseHexByte (token)); } if (bytes.size () < 4 ) { throw std::invalid_argument ("packet is shorter than its header" ); } const std::uint8_t version = bytes[0 ]; const std::uint8_t flags = bytes[1 ]; const std::uint16_t payloadLength = readBigEndian16 (bytes[2 ], bytes[3 ]); if (bytes.size () != static_cast <std::size_t >(4 + payloadLength)) { throw std::invalid_argument ("payload length does not match packet size" ); } const bool acknowledged = (flags & 0b0000'0001U ) != 0 ; const bool compressed = (flags & 0b0000'0010U ) != 0 ; const bool urgent = (flags & 0b0000'0100U ) != 0 ; const std::string payload (bytes.begin() + 4 , bytes.end()) ; std::cout << "version=" << static_cast <unsigned int >(version) << '\n' ; std::cout << std::boolalpha; std::cout << "acknowledged=" << acknowledged << '\n' ; std::cout << "compressed=" << compressed << '\n' ; std::cout << "urgent=" << urgent << '\n' ; std::cout << "length=" << payloadLength << '\n' ; std::cout << "payload=" << payload << '\n' ; } catch (const std::exception& error) { std::cout << "INVALID: " << error.what () << '\n' ; return 1 ; } return 0 ; }
输出:
1 2 3 4 5 6 version=1 acknowledged=true compressed=false urgent=true length=3 payload=ABC
执行过程:
每两个十六进制字符转换为一个 std::uint8_t;
先检查报文至少包含 4 字节头部;
将第 3、4 字节按大端序组合成载荷长度 3;
检查实际总长度是否恰好等于 4 + payloadLength;
使用掩码读取三个标志位;
本题已限定载荷是 ASCII,因此可以构造 std::string 输出。
程序扫描每个字节一次,时间复杂度为 O(n),保存完整报文需要 O(n) 空间。真实二进制载荷可能包含 0x00 或非文本字节,不能假定它是以空字符结尾的 C 字符串,也不应未经编码直接作为文本输出。
七、另外两类代表题 代表题二:合法整数前缀 给定字符串,判断它是否由可选正负号和至少一位十进制数字组成。不要直接依赖 std::stoi() 是否成功,因为题目要求整串合法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <cctype> #include <string> bool isInteger (const std::string& text) { if (text.empty ()) { return false ; } std::size_t index = 0 ; if (text[index] == '+' || text[index] == '-' ) { ++index; } if (index == text.size ()) { return false ; } for (; index < text.size (); ++index) { const unsigned char ch = static_cast <unsigned char >(text[index]); if (std::isdigit (ch) == 0 ) { return false ; } } return true ; }
时间复杂度为 O(n),额外空间为 O(1)。
代表题三:只出现一次的状态码 如果一个整数数组中除一个元素外,其余元素都恰好出现两次,可以利用 x ^ x == 0 和 x ^ 0 == x:
1 2 3 4 5 6 7 8 9 #include <vector> int findSingle (const std::vector<int >& values) { int answer = 0 ; for (int value : values) { answer ^= value; } return answer; }
该方法时间复杂度为 O(n)、额外空间为 O(1),但它严格依赖“其余元素恰好出现两次”的前提。出现次数规则改变时,算法也必须改变。
八、常见错误
忘记检查 find() 是否返回 std::string::npos;
把 std::string::size() 保存到可能溢出的 int;
认为 std::stoi() 默认要求整串都是数字;
把 std::uint8_t 直接输出,结果显示成字符;
使用 1 << bit 处理高位,却没有先转换为目标无符号类型;
用有符号负数做移位;
把协议字节数组直接转换为整数指针,忽略字节序和对齐;
把任意二进制载荷当作 UTF-8 或以 \0 结尾的字符串;
只检查报文最小长度,没有检查声明长度和实际长度是否一致。
九、考前速查清单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const std::size_t pos = text.find (target);if (pos != std::string::npos) { } const int value = std::stoi (text, nullptr , 16 );const bool set = (mask & (std::uint32_t {1 } << bit)) != 0 ;const std::uint16_t value16 = static_cast <std::uint16_t >( (static_cast <std::uint16_t >(high) << 8U ) | low );
文本:先确认是按空白分词,还是读取整行;
转换:确认是否允许部分成功、负数、前缀和异常;
位运算:优先使用无符号类型,确认位编号没有越界;
字节流:先检查最小长度,再读取字段;
多字节字段:按题目给定字节序手动组合;
载荷:区分文本与任意二进制数据。
十、C++20 可选补充 C++20 的 <bit> 提供 std::popcount()、std::rotl()、std::rotr() 和 std::endian,std::span 可以表达不拥有数据的连续区间。这些工具能让位操作和字节视图更清楚,但本系列的 C++17 代码不依赖它们。
十一、延伸练习
参考资料