HOWTO · C++
如何在 C++ 中把字符串转换为十六进制
本文介绍了如何在 C++ 中把字符串转换为十六进制值。
本页内容
在 C++ 中把 std::string 转换为十六进制,就是生成字符串各字节的文本表示:每个 char 变成一个两位的十六进制(基数为 16)值,因此 "Hello" 会变成 48656c6c6f。调试二进制数据、生成校验和或指纹,以及向需要十六进制编码负载的协议提供数据时,都需要这种转换。
本文介绍如何直接打印十六进制输出、如何构建可复用的十六进制 std::string、如何让每个字节保持两位数,以及结果与文本编码的关系,最后比较这几种方法,方便你选择合适的方案。
把字符串转换为十六进制字节
最直接的方法是逐字节遍历输入,并把每个值以十六进制写入 std::cout。流操作符 std::hex 会把整数输出切换为十六进制,std::setw(2) 配合 std::setfill('0') 能让每个字节保持两位数字。用 unsigned char 迭代可以把每个字节保持在 0–255 范围内,这样即使在普通 char 为有符号的平台上,大于 0x7f 的字节也能正确格式化。运行下面的示例:
本节代码中的字符串字面量以及输出仍为英文。
#include <iomanip>
#include <iostream>
#include <string>
int main() {
std::string input = "Hello";
std::cout << std::hex << std::setfill('0');
for (unsigned char c : input) {
std::cout << std::setw(2) << static_cast<int>(c);
}
std::cout << '\n';
}
48656c6c6f
std::hex 一旦设置就会持续生效,直到你改变它,所以在循环之前设置一次即可。std::setw(2) 只对下一个插入的值生效,这就是它放在循环内的原因,而 std::setfill('0') 会一直保留。打印到 std::cout 适合快速查看,但转换后的文本滚过屏幕之后就消失了——下一节会把它保存下来。
构建并存储十六进制字符串
当转换后的文本稍后还要用到时——比如从函数返回、追加到日志消息或写入文件——就把它累积在 std::stringstream 中,而不是输出到 std::cout。同样的格式化操作符依然适用,str() 会产生一个保存结果的 std::string:
示例中显示的标签(string: 和 hexval:)以及输出仍为英文。
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
std::string toHex(const std::string& input) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (unsigned char c : input) {
ss << std::setw(2) << static_cast<int>(c);
}
return ss.str();
}
int main() {
std::string text = "C++ Programming";
std::string hexText = toHex(text);
std::cout << "string: " << text << '\n';
std::cout << "hexval: " << hexText << '\n';
}
string: C++ Programming
hexval: 432b2b2050726f6772616d6d696e67
由于 toHex 返回的是 std::string,由调用者决定如何处理它:打印、存储,或者传给文件流。这个示例只打印该值——把它写入文件是通过 std::ofstream 单独完成的一步,而 std::ofstream 同样接受这段十六进制文本。
把每个字节格式化为两位十六进制数字
一个字节的取值范围是 0 到 255,也就是 0x00 到 0xff,所以两位数字总是够用。如果不做填充,小于 0x10 的值只会打印成一位,输出就会产生歧义:字节对 0x01, 0x0a 和单个字节 0x1a 都会得到 1a。std::setw(2) 设置最小字段宽度,std::setfill('0') 提供填充字符:
本节代码中的字符串字面量以及输出仍为英文。
#include <iomanip>
#include <iostream>
#include <string>
int main() {
std::string input = "A\nB";
std::cout << std::hex << std::setfill('0');
for (unsigned char c : input) {
std::cout << std::setw(2) << static_cast<int>(c);
}
std::cout << '\n';
}
410a42
中间的字节是 0x0a,仍然打印成两位。去掉 std::setw 和 std::setfill,同样的输入会打印成 41a42,你再也分不清一个字节在哪里结束、下一个从哪里开始。std::setw 要放在循环内——它在每个格式化值之后都会重置——而 std::setfill 只需设置一次。
理解字节值与文本编码
到目前为止,每个示例转换的都是字节,而不是字符。std::string 存储的是一串 char,循环格式化的是每个 char 的数值。对 ASCII 输入来说,这个区别看不出来——H 就是单个字节 0x48——但对其他文本就很重要。在 UTF-8 中,é(码位 U+00E9)存储为两个字节 0xc3 0xa9,所以同样的循环会产生 c3a9 而不是 e9:
本节代码中的注释以及输出仍为英文。
#include <iomanip>
#include <iostream>
#include <string>
int main() {
std::string input = "\xc3\xa9"; // UTF-8 bytes for U+00E9
std::cout << std::hex << std::setfill('0');
for (unsigned char c : input) {
std::cout << std::setw(2) << static_cast<int>(c);
}
std::cout << '\n';
}
c3a9
因此,本文中的函数描述的是字符串恰好存储的编码字节。如果你需要的是十六进制码位,就先解码文本,再格式化解码后的值。无符号字节处理也同样重要:在普通 char 为有符号的平台上,像 0xc3 这样的字节会转换为小于零的值,输出为 ffffffc3。用 unsigned char 迭代——或者在 static_cast<int> 之前先经过它转换——可以让该值保持为 0xc3。
选择转换方法
只需要查看一次数据时,使用直接输出到 std::cout 的循环。当结果必须在调用之后继续存在时,返回一个用 std::stringstream 构建的 std::string——这是可读性最好的通用方案,并把 std::hex、std::setw 和 std::setfill 的格式化集中在同一个地方。
第三种选择是把每个字节拆成两个 4 位半字节(nibble),再通过查找表分别映射:
本节代码中的字符串字面量以及输出仍为英文。
#include <iostream>
#include <string>
std::string toHexLookup(const std::string& input) {
static const char digits[] = "0123456789abcdef";
std::string result;
result.reserve(input.size() * 2);
for (unsigned char c : input) {
result += digits[c >> 4];
result += digits[c & 0x0f];
}
return result;
}
int main() {
std::cout << toHexLookup("World") << '\n';
}
576f726c64
查找表版本总是为每个字节输出恰好两位数字,而且不做任何流格式化,有些代码库出于一致性考虑更偏好这种方式。它是否运行得更快取决于你的标准库实现,如果这一点对你很重要,请在自己的数据上测量。无论选择哪种方法,结果描述的都是字节:对 ASCII 文本来说,读作每个字符对应一对数字;对 UTF-8 或二进制数据来说,读作每个编码字节对应一对数字。