字符串、进制、位运算和字节流经常出现在协议解析、日志处理、状态字段和业务模拟题中。它们表面上是不同主题,实际都围绕同一条数据流:文本如何转换为数值,数值如何按位解释,字节如何按协议组合成字段

本篇以 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 += suffixtext.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() 可以接收基数参数,合法范围为 236,基数 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'; // 0A

std::hexstd::uppercasestd::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; // 每次清除最低位的 1。
++count;
}
return count;
}

循环次数等于二进制中 1 的数量,最多执行 32 次,额外空间为 O(1)

五、字节流的类型与字节序

1. charstd::uint8_tstd::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'; // 65,而不是字符 A

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
01 05 00 03 41 42 43

完整程序如下:

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
// verify: cpp17
#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

执行过程:

  1. 每两个十六进制字符转换为一个 std::uint8_t
  2. 先检查报文至少包含 4 字节头部;
  3. 将第 3、4 字节按大端序组合成载荷长度 3
  4. 检查实际总长度是否恰好等于 4 + payloadLength
  5. 使用掩码读取三个标志位;
  6. 本题已限定载荷是 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 == 0x ^ 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) {
// found
}

// 进制文本转整数
const int value = std::stoi(text, nullptr, 16);

// 第 bit 位是否为 1
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::endianstd::span 可以表达不拥有数据的连续区间。这些工具能让位操作和字节视图更清楚,但本系列的 C++17 代码不依赖它们。

十一、延伸练习

参考资料