菜单

StringUtil.h说明

下载

StringUtil.h 详细使用手册

本文档依据当前工程中的以下实现编写:

  • src/util/StringUtil.h

文档以 .cpp 的实际行为为准。StringUtil.h 中包含两个工具类:

  • StringUtil:字符串判断、格式化、分割、编码转换、字节转换、JSON 拼接和数值转换。
  • TransPY:中文字符串转拼音。

1. 使用前准备

1.1 引入头文件

cpp 复制代码
#include "util/StringUtil.h"

#include <cstdint>
#include <iostream>
#include <map>
#include <regex>
#include <string>
#include <vector>

using stt::KV_Str_Str;
using stt::util::StringUtil;
using stt::util::TransPY;

两个类的方法都是静态方法,不需要创建对象:

cpp 复制代码
std::string upper = StringUtil::toUpperCase("alarm");
std::string pinyin = TransPY::getPinyin("中国");

也可以使用完整名称:

cpp 复制代码
bool ok = stt::util::StringUtil::endWith("report.csv", ".csv");

1.2 字符编码约定

  • 工程中的普通中文字符串通常使用 UTF-8。
  • std::string::size()、splitByLength、charsToString 等接口按字节处理,不按 Unicode 字符处理。
  • toLowerCase 和 toUpperCase 只适合 ASCII 英文字母,不会正确执行 Unicode 大小写转换。
  • TransPY::getPinyin 先把 UTF-8 转为 GBK,再查询内置拼音表。
  • trancoding 依赖 iconv,编码名称必须是当前平台 iconv 支持的名称,例如 UTF-8、GBK、UTF-16LE。

1.3 异常、错误和线程注意事项

  • match、rreplace 在正则表达式非法时可能抛出 std::regex_error,接口内部不捕获。
  • formatDate 会访问全局 HMI Runtime;必须在 Runtime 已初始化后调用。
  • formatDate 使用 localtime/gmtime 返回的静态缓冲区,不适合多个线程同时调用。
  • trancoding 失败时向 std::cerr 输出系统错误,然后返回空字符串;空字符串也可能表示输入本来为空。
  • 数值转换的单返回值重载失败时返回 0,无法区分“转换失败”和合法数字 0。关键业务应使用带输出参数的 bool 重载。

1.4 当前实现中的高风险行为

以下行为不是通用 C++ 字符串库的常规语义,调用前必须注意:

接口 当前实现行为 建议
formatFloat(..., true, ...) 千分位逗号插入位置错误,可能生成 1234,.50 当前版本不要启用千分位
formatDate deli1、deli2 的普通分隔符值没有真正使用,实际固定为 - 和 : 不要依赖自定义 / 等分隔符
splitByLength 循环条件和补齐长度写死为 8,且会修改原字符串 只按 8 字节使用,传 len=8
replace 连续或相邻的多个匹配项可能漏替换;from 为空会死循环 调用前保证 from 非空;多次替换优先用 rreplace
safeStoi、safeStoll 没有检查是否消费完整字符串,"12abc" 也会成功并得到 12 严格输入先用 match,或自行检查
vectorToJsonc、mapToJson 不转义引号、反斜杠、换行等 JSON 特殊字符 外部输入应使用 json-c 等正式 JSON API
StringToChars 不检查输出指针和容量,超长输入还有循环计数风险 预分配足够空间,限制十六进制文本不超过 510 个字符

2. 接口速查

分类 方法
正则匹配 match
前后缀判断 startWith、startWithNoCase、endWith、endWithNoCase
数字和日期格式化 formatFloat、formatDate、formatScience、snprintf
分割和拼接 split、join、splitByLength
大小写和替换 toLowerCase、toUpperCase、replace、rreplace
SQL sqlEsc
编码转换 trancoding
十六进制和字节 hexToUInt、charsToString、StringToChars、charArrayToString、stringToCharArray
JSON 文本拼接 vectorToJsonc、mapToJson、vertInt
安全数值转换 safeStoi、safeStof、safeStoll、safeStod
去除首尾字符 trim
中文转拼音 TransPY::getPinyinByCode、TransPY::getPinyin

3. 正则匹配

3.1 match

cpp 复制代码
static bool match(const string& reg, const string& str);

判断整个 str 是否符合正则表达式 reg。实现使用:

cpp 复制代码
std::regex(reg, std::regex_constants::extended)

因此它采用 POSIX Extended 语法,并通过 regex_match 匹配整个字符串,而不是搜索其中一段。

参数:

  • reg:正则表达式。
  • str:待检查字符串。

返回值:整个字符串匹配时返回 true,否则返回 false。

示例:

cpp 复制代码
bool digits = StringUtil::match("^[0-9]+$", "20261020"); // true
bool code = StringUtil::match("^[A-Z]{2}[0-9]{4}$", "AB0123"); // true
bool mobile = StringUtil::match("^1[3-9][0-9]{9}$", "13800138000"); // true
bool bad = StringUtil::match("^[0-9]+$", "12A"); // false

常用兼容写法:

需求 表达式
非空十进制数字 ^[0-9]+$
可选负号的整数 ^-?[0-9]+$
最多两位小数 ^-?[0-9]+(\.[0-9]{1,2})?$
英文、数字、下划线 ^[A-Za-z0-9_]+$
日期 yyyy-MM-dd ^[0-9]{4}-[0-9]{2}-[0-9]{2}$

注意事项:

  • 不要直接照搬 ECMAScript/JavaScript 正则。当前语法不保证支持 \d、\w、(?=...) 等写法。
  • 头文件注释中的部分示例使用了 \d、\w 或前瞻,和当前 extended 语法不一致;优先改写为 [0-9]、[A-Za-z0-9_] 等形式。
  • {m,n} 中的 m、n 必须替换成真实数字,例如 {2,5}。
  • 非法正则会抛出 std::regex_error。正则来自外部配置时应捕获异常。
cpp 复制代码
try {
    bool ok = StringUtil::match(userPattern, input);
    // 使用 ok
} catch (const std::regex_error& e) {
    std::cerr << "正则表达式错误: " << e.what() << std::endl;
}

4. 前缀和后缀判断

4.1 startWith

cpp 复制代码
static bool startWith(const string& s, const string& sub);

区分大小写地判断 s 是否以 sub 开始。

cpp 复制代码
StringUtil::startWith("SAMPLE_4.db", "SAMPLE_"); // true
StringUtil::startWith("SAMPLE_4.db", "sample_"); // false
StringUtil::startWith("abc", "");                // true
StringUtil::startWith("ab", "abc");             // false

空前缀按 C++ 字符串规则视为匹配。

4.2 startWithNoCase

cpp 复制代码
static bool startWithNoCase(const string& s, const string& sub);

先把两个字符串转成小写,再判断前缀。适合 ASCII 英文:

cpp 复制代码
bool ok = StringUtil::startWithNoCase("Report.CSV", "report"); // true

该接口不是 Unicode 大小写比较;UTF-8 中文不受影响,其他非 ASCII 字符不要依赖其结果。

4.3 endWith

cpp 复制代码
static bool endWith(const string& s, const string& sub);

区分大小写地判断后缀:

cpp 复制代码
StringUtil::endWith("report.csv", ".csv"); // true
StringUtil::endWith("report.CSV", ".csv"); // false
StringUtil::endWith("abc", "");            // true

4.4 endWithNoCase

cpp 复制代码
static bool endWithNoCase(const string& s, const string& sub);

忽略 ASCII 大小写判断后缀:

cpp 复制代码
bool isCsv = StringUtil::endWithNoCase("REPORT.CSV", ".csv"); // true

5. 数字格式化

5.1 formatFloat

cpp 复制代码
static string formatFloat(
    double fvalue,
    int fraclen,
    bool thousandth,
    int intnum = 0,
    bool intbl = false,
    const string& prefix = "",
    const string& suffix = "");

参数:

  • fvalue:待格式化数值。
  • fraclen:小数位数。大于等于 0 时使用定点格式并四舍五入;小于 0 时不固定小数位。
  • thousandth:是否添加千分位。
  • intnum:启用补零时传给 std::setw 的总字段宽度。
  • intbl:是否使用字符 0 补足字段宽度。
  • prefix:结果前缀。
  • suffix:结果后缀。

普通格式化示例:

cpp 复制代码
std::string a = StringUtil::formatFloat(12.345, 2, false); // "12.35"
std::string b = StringUtil::formatFloat(42, 0, false, 5, true); // "00042"
std::string c = StringUtil::formatFloat(9.5, 1, false, 0, false, "$", " V");
// "$9.5 V"

注意事项:

  • 当前千分位实现存在位置错误。formatFloat(1234.5, 2, true) 会产生类似 1234,.50 的错误结果,当前版本应传 false。
  • intnum 是整个输出字段的最小宽度,不是严格的“整数位数”。小数点、小数位和负号都会占宽度。
  • 当 fraclen < 0 且输入正好是整数时,输出不带小数;非整数使用流的默认精度,可能出现精度截断或科学计数法。
  • NaN 和无穷值没有专门拦截,会按标准流规则输出 nan、inf 等文本。

5.2 formatScience

cpp 复制代码
static string formatScience(double fvalue, int fraclen);

把数值转换为小写 e 的科学计数法:

cpp 复制代码
std::string value = StringUtil::formatScience(12345.0, 2);
// "1.23e+04"

注意事项:

  • fraclen 应使用非负、合理的小数位数,例如 0..15。
  • 实现通过 sprintf 动态拼接格式串,没有对极端的负数或超大 fraclen 做边界保护。

5.3 snprintf

cpp 复制代码
static string snprintf(const char* pszFmt, ...);

按 printf 格式生成长度自动扩展的 std::string:

cpp 复制代码
std::string date = StringUtil::snprintf("%04d-%02d-%02d", 2026, 10, 20);
// "2026-10-20"

std::string message = StringUtil::snprintf(
    "设备=%s, 值=%.2f",
    "M1",
    12.345);
// "设备=M1, 值=12.35"

注意事项:

  • 头文件注释写着“最后有 \n”,但当前实现不会自动添加换行。
  • %s 必须传 const char*,传 std::string 时要调用 .c_str()。
  • 格式符和参数类型不一致属于未定义行为,例如 %d 不能直接接收 long long。
  • pszFmt 不能为 nullptr。
  • 需要输出字面量 % 时写 %%。

6. 日期时间格式化

6.1 formatDate

cpp 复制代码
static string formatDate(
    unsigned long long lvalue,
    const string& deli1 = "-",
    const string& deli2 = ":",
    bool showYear = true,
    bool showMonth = true,
    bool showDay = true,
    bool showHour = true,
    bool showMinute = true,
    bool showSecond = true,
    bool showWeek = false,
    bool isLocal = true);

lvalue 是从 Unix Epoch(1970-01-01 00:00:00 UTC)开始的秒数,不是毫秒数。

参数:

  • deli1:日期格式控制。当前实现只有特殊值 "中文" 会产生中文年月日;其他值均使用 -。
  • deli2:头文件设计为时间分隔符,但当前实现没有读取该参数,时间固定使用 :。
  • showYear 到 showSecond:分别控制每个时间分量是否显示。
  • showWeek:是否追加本地化的星期文本。
  • isLocal:true 使用本地时区,false 使用 UTC。

完整时间示例:

cpp 复制代码
unsigned long long seconds = 1792425600ULL;

std::string local = StringUtil::formatDate(
    seconds,
    "-", ":",
    true, true, true,
    true, true, true,
    false,
    true);

只显示日期:

cpp 复制代码
std::string date = StringUtil::formatDate(
    seconds,
    "-", ":",
    true, true, true,
    false, false, false,
    false,
    true);
// 形如 "2026-10-20",具体日期受秒值和时区影响

显示中文年月日和星期:

cpp 复制代码
std::string text = StringUtil::formatDate(
    seconds,
    "中文", "中文",
    true, true, true,
    true, true, true,
    true,
    true);

只有当 Runtime 当前语言为 zh_CN 且 deli1 == "中文" 时,结果才使用 年、月、日、时、分、秒。否则仍使用 - 和 :。

注意事项:

  • 毫秒时间戳必须先除以 1000:formatDate(ms / 1000, ...)。
  • 当前实现忽略普通的 deli1/deli2 内容。传入 / 仍会得到 -,传入 . 仍会得到 :。
  • 函数一开始就调用 getRuntime()->getLanguage(),即使不显示星期也要求 Runtime 有效。
  • showWeek=true 时还会调用语言管理器取得本地化文本。
  • 本地时间结果依赖设备的 TZ 配置。
  • 同时省略日期和时间但显示星期时,只返回星期文本。

7. 十六进制数值转换

7.1 hexToUInt

cpp 复制代码
static uint32_t hexToUInt(const string& str);

使用 %x 把十六进制文本转换为 uint32_t:

cpp 复制代码
uint32_t a = StringUtil::hexToUInt("FF");   // 255
uint32_t b = StringUtil::hexToUInt("0x10"); // 16
uint32_t c = StringUtil::hexToUInt("7b");   // 123

注意事项:

  • 不返回转换状态;空字符串或完全非法的字符串得到 0,与合法的 "0" 无法区分。
  • %x 可以在遇到非法尾随字符时停止,因此 "FFxyz" 可能仍得到 255。
  • 不适合校验外部输入。需要严格校验时先调用 match("^(0[xX])?[0-9A-Fa-f]+$", text)。

8. 分割和拼接

8.1 split

cpp 复制代码
static void split(
    const string& str,
    const string& split,
    vector<string>& res);

按字符串分隔符拆分,调用时会先清空 res。

cpp 复制代码
std::vector<std::string> fields;
StringUtil::split("A,B,,D", ",", fields);
// fields = {"A", "B", "", "D"}

StringUtil::split("key--value--tail", "--", fields);
// fields = {"key", "value", "tail"}

边界行为:

cpp 复制代码
StringUtil::split("", ",", fields);    // fields 为空
StringUtil::split("abc", "", fields); // fields = {"abc"}
StringUtil::split(",a,", ",", fields); // fields = {"", "a", ""}

分隔符可以包含多个字节。函数只做字节序列匹配,不理解转义、引号或 CSV 规则,因此不能用于可靠解析 CSV。

8.2 join

cpp 复制代码
static string join(const vector<string>& src, const string& sub);

用 sub 连接字符串列表:

cpp 复制代码
std::vector<std::string> names = {"A", "B", "C"};
std::string result = StringUtil::join(names, ","); // "A,B,C"

std::vector<std::string> empty;
std::string result2 = StringUtil::join(empty, ","); // ""

空元素会保留:

cpp 复制代码
std::vector<std::string> values = {"A", "", "C"};
std::string result = StringUtil::join(values, "|"); // "A||C"

8.3 splitByLength

cpp 复制代码
static vector<string> splitByLength(int len, char c, string& src);

当前实现按固定 8 字节块工作,并且会从 src 中持续删除已经取出的内容。虽然有 len 参数,循环条件和最后补齐长度仍写死为 8。

推荐的唯一用法:

cpp 复制代码
std::string source = "ABCDEFGHIJK";
std::vector<std::string> blocks =
    StringUtil::splitByLength(8, '0', source);

// blocks[0] == "ABCDEFGH"
// blocks[1] == "IJK00000"
// source == ""

注意事项:

  • 必须传 len=8;其他长度可能分段错误或补齐错误。
  • len<=0 且源字符串至少 8 字节时可能死循环。
  • src 是非常量引用,调用后内容会被清空或改变。
  • 中文 UTF-8 一个字符通常占 3 字节,可能被从中间截断,产生非法 UTF-8。

9. 大小写转换

9.1 toLowerCase

cpp 复制代码
static string toLowerCase(const string& src);
cpp 复制代码
std::string value = StringUtil::toLowerCase("Alarm_A1");
// "alarm_a1"

9.2 toUpperCase

cpp 复制代码
static string toUpperCase(const string& src);
cpp 复制代码
std::string value = StringUtil::toUpperCase("Alarm_A1");
// "ALARM_A1"

两个方法都返回新字符串,不修改输入。它们逐字节调用 C 字符分类函数,只应对 ASCII 文本使用。UTF-8 中文会保持原始字节,但非 ASCII 字节传入 tolower/toupper 在不同平台上存在可移植性风险。

10. 字符串替换

10.1 replace

cpp 复制代码
static string replace(
    string str,
    const string& from,
    const string& to);

执行普通文本替换,不把 from 当正则表达式:

cpp 复制代码
std::string path = StringUtil::replace("data/file.db", ".db", ".csv");
// "data/file.csv"

当前实现有两个重要限制:

  • from.empty() 时会无限循环,调用前必须拦截。
  • 连续或相邻的多次匹配可能漏掉。例如替换 "aa" 中的每个 "a",当前实现不会可靠地全部替换。

安全使用方式:只在确认目标只出现一次时使用。

cpp 复制代码
if (!from.empty() && source.find(from) != std::string::npos) {
    std::string result = StringUtil::replace(source, from, to);
}

需要可靠替换全部匹配项时,可使用 rreplace 并对查找文本进行正则转义,或者在业务代码中使用标准的循环替换实现。

10.2 rreplace

cpp 复制代码
static string rreplace(
    string str,
    const string& pattern,
    const string& to);

使用 std::regex_replace 替换所有正则匹配项:

cpp 复制代码
std::string a = StringUtil::rreplace(
    "Hello world, WORLD!",
    "[Ww][Oo][Rr][Ll][Dd]",
    "C++");
// "Hello C++, C++!"

std::string b = StringUtil::rreplace("A  B   C", " +", " ");
// "A B C"

捕获组可在替换串中用 $1、$2 引用:

cpp 复制代码
std::string date = StringUtil::rreplace(
    "2026-10-20",
    "([0-9]{4})-([0-9]{2})-([0-9]{2})",
    "$1/$2/$3");
// "2026/10/20"

注意事项:

  • rreplace 使用 std::regex 默认 ECMAScript 语法,而 match 使用 POSIX Extended 语法,两者语法并不完全相同。
  • 非法表达式会抛出 std::regex_error。
  • 用户输入如果只是普通查找文本,必须先转义正则特殊字符,否则 .、*、[ 等会改变匹配含义。

11. SQL 字符串转义

11.1 sqlEsc

cpp 复制代码
static string sqlEsc(const string& src);

把每个单引号 ' 替换为两个单引号 '',用于 SQLite 单引号字符串字面量:

cpp 复制代码
std::string name = "O'Reilly";
std::string sql =
    "SELECT * FROM S_USER WHERE UNAME='" +
    StringUtil::sqlEsc(name) +
    "'";

// SELECT * FROM S_USER WHERE UNAME='O''Reilly'

注意事项:

  • 该方法只适合已经放在单引号中的“值”,不能用于表名、字段名、排序方向或整段 SQL。
  • 它不验证 SQL,也不提供参数绑定。能够使用数据库绑定参数时,应优先使用绑定参数。
  • 不要忘记调用方自己的外层单引号。

12. 字符编码转换

12.1 trancoding

cpp 复制代码
static string trancoding(
    const string& from,
    const string& to,
    const string& str);

通过 iconv 把 str 从 from 编码转换到 to 编码。

UTF-8 转 GBK:

cpp 复制代码
std::string utf8 = "中文";
std::string gbk = StringUtil::trancoding("UTF-8", "GBK", utf8);

if (gbk.empty() && !utf8.empty()) {
    std::cerr << "编码转换失败" << std::endl;
}

GBK 转 UTF-8:

cpp 复制代码
std::string utf8Again = StringUtil::trancoding("GBK", "UTF-8", gbk);

注意事项:

  • 空输入直接返回空字符串。
  • 编码名称无效、输入字节不完整或包含目标编码不能表示的字符时,返回空字符串并向 std::cerr 输出错误。
  • 输出缓冲区固定预分配为输入字节数的 4 倍;如果目标编码需要更多空间,转换会失败,不会自动扩容重试。
  • 接口不会追加字符串终止符;返回的 std::string 自己保存准确长度。
  • 转换结果可能包含 \0,后续不要只用 C 字符串函数处理。

13. 字节数组和十六进制文本

13.1 charsToString

cpp 复制代码
static string charsToString(unsigned char* data, int len);

把二进制字节转换为不带空格、不带 0x 前缀的大写十六进制文本:

cpp 复制代码
unsigned char data[] = {0x5A, 0x00, 0xFF};
std::string hex = StringUtil::charsToString(data, 3);
// "5A00FF"

data == nullptr 或 len <= 0 时返回空字符串。输出长度始终是有效输入字节数的两倍。

13.2 StringToChars

cpp 复制代码
static int StringToChars(
    const string& str,
    unsigned char* data);

把十六进制文本转换为二进制字节,支持大小写字母:

cpp 复制代码
std::string hex = "5A00ff";
std::vector<unsigned char> data(hex.size() / 2);

int count = StringUtil::StringToChars(hex, data.data());
if (count > 0) {
    // data = {0x5A, 0x00, 0xFF}
}

返回值:

返回值 含义
大于 0 成功写出的字节数
0 遇到非法十六进制字符,或输入为空
-1 输入字符数为奇数

注意事项:

  • 调用方必须保证 data 至少有 str.size()/2 字节空间。
  • 非空输入时 data 不能为 nullptr,否则会崩溃。
  • 发生非法字符时,缓冲区前半部分可能已经被写入,不能继续使用其中的旧结果。
  • 实现的循环变量是 uint8_t,输出超过 255 字节时有回绕风险。应限制十六进制字符串最多 510 个字符。
  • 该接口不接受空格、冒号或 0x 前缀。"AA BB"、"0xAA" 都不是有效输入。

推荐封装:

cpp 复制代码
bool parseHex(const std::string& text, std::vector<unsigned char>& output) {
    output.clear();
    if (text.empty() || text.size() % 2 != 0 || text.size() > 510) {
        return false;
    }

    output.resize(text.size() / 2);
    int count = StringUtil::StringToChars(text, output.data());
    if (count != static_cast<int>(output.size())) {
        output.clear();
        return false;
    }
    return true;
}

13.3 charArrayToString

cpp 复制代码
static string charArrayToString(
    vector<uint8_t>& arry,
    const string& coding = "");

把字节向量原样复制到 std::string:

cpp 复制代码
std::vector<uint8_t> bytes = {'A', 0x00, 'B'};
std::string value = StringUtil::charArrayToString(bytes);

// value.size() == 3
// value[0] == 'A', value[1] == '\0', value[2] == 'B'

注意事项:

  • 头文件注释称遇到 0 会截断,但当前实现用显式长度构造字符串,嵌入的 0 不会截断。
  • coding 参数当前没有使用,不会执行字符编码转换。
  • 参数不是 const 引用,但函数不会修改向量。
  • 实现会在栈上创建与输入等长的临时数组,大数据不适合通过此接口转换。

13.4 stringToCharArray

cpp 复制代码
static vector<uint8_t> stringToCharArray(
    const string& src,
    const string& coding = "");

把字符串中的全部字节原样复制到 vector<uint8_t>:

cpp 复制代码
std::string source("A\0B", 3);
std::vector<uint8_t> bytes = StringUtil::stringToCharArray(source);

// bytes.size() == 3
// bytes = {'A', 0x00, 'B'}

coding 参数当前没有使用。该接口不附加末尾 \0;需要 C 字符串结尾时由调用方自行添加。

14. JSON 文本拼接

14.1 vectorToJsonc

cpp 复制代码
static string vectorToJsonc(vector<KV_Str_Str>& arry);

把键值对向量拼成 JSON 对象文本。普通值加双引号;外观为 {...} 或 [...] 的值直接作为嵌套 JSON 写入。

cpp 复制代码
std::vector<KV_Str_Str> values = {
    {"cmd", "start"},
    {"device", "M1"},
    {"options", "{\"speed\":10}"},
    {"items", "[1,2,3]"}
};

std::string json = StringUtil::vectorToJsonc(values);
// {"cmd":"start","device":"M1","options":{"speed":10},"items":[1,2,3]}

空向量返回 {}。

注意事项:

  • 普通值总是字符串;"123" 会输出为 JSON 字符串 "123",不是数字。
  • 仅按首尾字符判断对象和数组,不验证内部 JSON 是否有效。
  • 键和值都不进行 JSON 转义。包含 "、\、换行或控制字符时,结果可能是非法 JSON。
  • 向量允许重复键,方法不会检查。
  • 函数参数不是 const 引用,但当前实现不会修改向量。

14.2 mapToJson

cpp 复制代码
static string mapToJson(map<string, string>& jsonMap);

和 vectorToJsonc 类似,但输入是 std::map,并额外识别 vertInt 产生的 _I_ 标记。

cpp 复制代码
std::map<std::string, std::string> values;
values["name"] = "Pump-1";
values["count"] = StringUtil::vertInt("3");
values["enabled"] = "true"; // 普通值,仍会加引号

std::string json = StringUtil::mapToJson(values);
// 按 map 键顺序输出,count 不带引号

因为 std::map 按键排序,输出顺序不是插入顺序。

14.3 vertInt

cpp 复制代码
static string vertInt(const string& str);

给文本添加 _I_ 标记,供 mapToJson 把它作为不带引号的值写出:

cpp 复制代码
std::string marked = StringUtil::vertInt("42"); // "_I_42"

std::map<std::string, std::string> values;
values["count"] = marked;

std::string json = StringUtil::mapToJson(values);
// {"count":42}

注意事项:

  • vertInt 不验证内容是否真的是整数。vertInt("abc") 会生成非法 JSON 值 abc。
  • mapToJson 删除标记时使用字符集合查找;标记后的值如果以额外的 _ 或 I 开始,可能被多删字符。
  • mapToJson 同样不执行 JSON 转义,不应直接处理不可信文本。
  • 正式业务 JSON 推荐使用工程已有的 json-c,而不是手工拼接。

15. 字符串转数值

每种数值类型都有两个重载:

  • 单返回值版本:失败返回 0,写法简短,但无法判断失败。
  • bool 版本:返回是否成功,并通过引用输出结果,适合需要输入校验的业务。

15.1 safeStoi

cpp 复制代码
static int safeStoi(const string& str);
static bool safeStoi(const string& str, int& result) noexcept;

简化用法:

cpp 复制代码
int count = StringUtil::safeStoi("123"); // 123
int bad = StringUtil::safeStoi("abc");   // 0

推荐用法:

cpp 复制代码
int count = 0;
if (!StringUtil::safeStoi(input, count)) {
    std::cerr << "不是有效整数" << std::endl;
}

当前实现基于 strtoll,但没有检查 *endptr == '\0':

cpp 复制代码
int value = 0;
bool ok = StringUtil::safeStoi("12abc", value);
// 当前实现:ok == true, value == 12

其他注意事项:

  • 会接受前导空格和正负号。
  • 只检查 long long 是否溢出,不检查结果是否超出 int 范围;缩窄到 int 时结果依赖平台。
  • 失败时 bool 重载不会主动清零 result,应先初始化。
  • 实现用数字 34 判断范围错误,而不是符号常量 ERANGE,存在跨平台风险。

严格整数建议先匹配:

cpp 复制代码
int value = 0;
bool ok = StringUtil::match("^-?[0-9]+$", input) &&
          StringUtil::safeStoi(input, value);

15.2 safeStoll

cpp 复制代码
static long long safeStoll(const string& str);
static bool safeStoll(const string& str, long long& result) noexcept;
cpp 复制代码
long long timestamp = 0;
if (StringUtil::safeStoll("1792425600000", timestamp)) {
    // 使用 timestamp
}

它和 safeStoi 一样没有检查尾随字符,因此 "123ms" 会成功并得到 123。前导空格和正负号可被接受。溢出或完全没有数字时返回 false。

15.3 safeStof

cpp 复制代码
static float safeStof(const string& str);
static bool safeStof(const string& str, float& result) noexcept;
cpp 复制代码
float pressure = 0.0f;
bool ok = StringUtil::safeStof("12.75", pressure);
// ok == true, pressure == 12.75f

浮点版本会检查是否消费完整字符串,并拒绝无穷和 NaN:

cpp 复制代码
float value = 0.0f;
StringUtil::safeStof("12.5abc", value); // false
StringUtil::safeStof("nan", value);     // false
StringUtil::safeStof("inf", value);     // false

注意事项:

  • 前导空格通常被接受,但尾随空格会因为没有完全消费而失败。
  • 上溢、下溢或非有限值返回 false。
  • 失败时输出参数保持调用前的值。

15.4 safeStod

cpp 复制代码
static double safeStod(const string& str);
static bool safeStod(const string& str, double& result) noexcept;
cpp 复制代码
double value = 0.0;
if (!StringUtil::safeStod("-0.00125", value)) {
    std::cerr << "数值格式错误" << std::endl;
}

规则与 safeStof 相同:要求完整消费输入,拒绝 NaN、无穷、上溢和下溢。

15.5 四种转换的对比

输入 safeStoi/safeStoll safeStof/safeStod
"123" 成功 成功
"-12" 成功 成功
" 12" 成功 成功
"12 " 当前实现成功并取 12 失败
"12abc" 当前实现成功并取 12 失败
"abc" 失败 失败
"nan" 失败 失败
空字符串 失败 失败

16. 去除首尾字符

16.1 trim(string)

cpp 复制代码
static string trim(string s);

只删除首尾 ASCII 空格字符 ' ':

cpp 复制代码
std::string value = StringUtil::trim("   alarm text   ");
// "alarm text"

std::string tabs = StringUtil::trim("\talarm\t");
// 制表符不会被删除

中间的空格不会删除,输入字符串本身也不会被修改,因为参数按值传递。

16.2 trim(string, char)

cpp 复制代码
static string trim(string s, char c);

删除首尾连续出现的指定单字节字符:

cpp 复制代码
std::string value = StringUtil::trim("---ABC---", '-');
// "ABC"

std::string path = StringUtil::trim("///data///", '/');
// "data"

该方法一次只能指定一个字节,不能直接传字符串或多字节中文字符。

17. 中文转拼音

17.1 TransPY::getPinyin

cpp 复制代码
static string getPinyin(
    const std::string& str,
    int mode = 1);

输入应为 UTF-8 中文字符串。返回结果不包含汉字之间的空格或分隔符。

mode 含义:

mode 中文拼音输出 示例形态
1 全部大写 ZHONGGUO
2 全部小写 zhongguo
3 每个汉字拼音首字母大写 ZhongGuo
4 每个汉字只保留一个小写首字母 zg

示例:

cpp 复制代码
std::string upper = TransPY::getPinyin("中国", 1);   // "ZHONGGUO"
std::string lower = TransPY::getPinyin("中国", 2);   // "zhongguo"
std::string title = TransPY::getPinyin("中国", 3);   // "ZhongGuo"
std::string initials = TransPY::getPinyin("中国", 4); // "zg"

ASCII 字符会原样保留,不受 mode 大小写规则影响:

cpp 复制代码
std::string value = TransPY::getPinyin("中国-A1", 2);
// 形如 "zhongguo-A1"

注意事项:

  • 内部先执行 UTF-8 -> GBK。编码转换失败时通常返回空字符串。
  • 非法 mode 没有默认处理分支,中文部分不会追加,ASCII 部分仍可能保留。
  • 多音字只能使用内置表中的固定读音,不能结合上下文判断。
  • 内置表未覆盖或无法编码为 GBK 的字符可能被跳过。
  • 结果不保证文件名唯一;用于配方名或导出文件名时仍应处理重名。

17.2 TransPY::getPinyinByCode

cpp 复制代码
static const char* getPinyinByCode(uint32_t code);

这是底层查表接口。code 不是 Unicode 码点;当前 getPinyin 使用 GBK 两字节计算内部索引:

cpp 复制代码
uint32_t code = (highByte - 0xA0) * 100 + lowByte - 0xA0;
const char* pinyin = TransPY::getPinyinByCode(code);

普通业务应直接调用 getPinyin。返回的是只读 const char*,调用方不要释放或修改它。

18. 综合示例

18.1 校验并解析用户输入

cpp 复制代码
std::string input;
vm->getChar("数量", input);
input = StringUtil::trim(input);

int count = 0;
if (!StringUtil::match("^[0-9]+$", input) ||
    !StringUtil::safeStoi(input, count) ||
    count < 1 || count > 1000) {
    vm->setChar("提示", "数量必须是 1 到 1000 的整数");
    return;
}

vm->setChar("提示", StringUtil::snprintf("数量=%d", count));

18.2 安全拼接 SQL 字符串值

cpp 复制代码
std::string userName;
vm->getChar("用户名", userName);

std::string sql =
    "SELECT UID, UNAME FROM S_USER WHERE UNAME='" +
    StringUtil::sqlEsc(userName) +
    "'";

这里 sqlEsc 只处理字符串值。如果数据库接口支持参数绑定,应改用参数绑定。

18.3 解析十六进制通讯帧

cpp 复制代码
std::string frameText;
vm->getChar("发送内容", frameText);
frameText = StringUtil::trim(frameText);
frameText = StringUtil::toUpperCase(frameText);

if (!StringUtil::match("^[0-9A-F]+$", frameText) ||
    frameText.size() % 2 != 0 ||
    frameText.size() > 510) {
    vm->setChar("提示", "十六进制格式错误");
    return;
}

std::vector<unsigned char> frame(frameText.size() / 2);
int count = StringUtil::StringToChars(frameText, frame.data());
if (count != static_cast<int>(frame.size())) {
    vm->setChar("提示", "十六进制转换失败");
    return;
}

std::string echo = StringUtil::charsToString(frame.data(), count);

18.4 生成可读的导出文件名

cpp 复制代码
std::string recipeName;
vm->getChar("配方名称", recipeName);
std::string asciiName = TransPY::getPinyin(recipeName, 4);

// 只保留文件名中容易处理的字符
asciiName = StringUtil::rreplace(asciiName, "[^A-Za-z0-9_-]", "_");

std::string fileName = asciiName + ".csv";

仍应在生成后检查空文件名、重名和目标路径权限。

19. 使用建议总结

  • 普通前后缀判断优先使用 startWith、endWith,文件扩展名判断使用 endWithNoCase。
  • 严格整数输入不能只调用 safeStoi/safeStoll,应先用兼容的正则限制格式和范围。
  • 数值转换需要判断错误时使用 bool 重载,不要依赖失败时返回的 0。
  • 正则匹配和正则替换的语法模式不同,表达式不要未经测试就在两者之间复用。
  • 当前不要使用 formatFloat 的千分位功能。
  • splitByLength 当前只按 len=8 使用,并记住它会修改源字符串。
  • 十六进制转字节前检查偶数长度、字符集、最大长度和缓冲区容量。
  • JSON 和 SQL 面向外部输入时,优先使用结构化 JSON API和数据库参数绑定。
  • 拼音转换适合生成搜索键或辅助文件名,不能用于需要准确多音字语义的场景。
上一个
Util.h说明
下一个
DateUtil.h说明
最近修改: 2026-10-20Powered by