ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

字符串处理实战:长度、编码、分割与转换的坑与解法

字符串处理实战:长度、编码、分割与转换的坑与解法 字符串 | part02这个系列写到现在我越来越觉得字符串才是编程里最“阴魂不散”的东西。你以为自己在写业务逻辑实际上大部分时间都在跟长度、编码、分隔符、隐式转换打交道。这一篇不打算讲什么高深理论全部是实际开发里反复踩过的坑和沉淀下来的解法。内容包括字符计数的各种规则、字符串与数字互转的边界条件、分割截取逆序的常见写法、比较包含替换的细节以及 CLOB/BLOB、JSON、内存泄漏这类偏底层的实战场景。适合正在用 C/C、Java、Python、SQL 处理字符串问题的朋友也适合准备笔试面试、刷 OJ 题的学生党。1. 字符串长度与字符计数远没有你想的那么简单1.1 “每分10个字符汉字算一个英文数字算两个”到底是什么逻辑热词列表里有一条很典型的描述“每分10个字符汉字算一个字符英文字母和数字两个算一个字符”。这不是算法题而是真实业务里常见的按显示宽度计费或截断场景。短信、电子广告屏、票据打印、终端对齐都会用这种规则。这里的核心逻辑是一个字符的存储长度和显示宽度是两码事。在 ASCII 体系里一个英文字母或数字占一个字节显示宽度也是 1而汉字在 GBK 编码下占两个字节在 UTF-8 下占三个字节显示宽度通常按两个 ASCII 字符算。所以业务上经常约定“汉字算 1 个字符英文字母和数字算 0.5 个字符”为了不出现小数就翻倍成“汉字算 2英文数字算 1”最后除以 2 判断是否超长。实际代码里要按这个规则计算宽度我一般先做码点判断再累加宽度。以 Java 为例public static int displayWidth(String str) { int width 0; for (int i 0; i str.length(); i) { char c str.charAt(i); if (c \u4e00 c \u9fff) { width 2; } else if (c \u3000 c \u303f) { width 2; // 中文标点也算全角 } else { width 1; } } return width; }但这里有个坑代理对surrogate pair。如果字符串里混入了 Emoji比如‍它在 Java 的char数组里占两个char直接length()会算成 2遍历时还会拆成两个无意义的 char。我处理这类需求时通常会先过滤掉 Emoji或者用码点遍历int codePointCount str.codePointCount(0, str.length());在 Python 里相对简单len(中国a)是 3但你要按显示宽度算依然要额外写映射逻辑。面试里出现这种题千万不要傻乎乎只调length()要把“全角/半角”“代理对”这两个点说出来基本就稳了。1.2 不同语言和数据库里“长度”的定义完全不同很多人写 SQL 时栽过跟头varchar(20)到底能存 20 个汉字还是 20 个字节这取决于数据库和字符集。MySQL 的VARCHAR(n)里的 n 是字符数不是字节数。在utf8mb4下一个汉字占 3 字节一个 Emoji 占 4 字节但 varchar(20) 都能存 20 个这种字符。LENGTH()返回的是字节数CHAR_LENGTH()返回的是字符数这是最经典的区分。SELECT LENGTH(中国a), CHAR_LENGTH(中国a); -- utf8mb4 下LENGTH 7331CHAR_LENGTH 3Oracle 的VARCHAR2(n)默认 n 是字节数除非你用了VARCHAR2(n CHAR)显式声明字符语义。一个VARCHAR2(10)在 ZHS16GBK 字符集下最多存 5 个汉字而在 AL32UTF8 下最多只能存 3 个汉字因为 10 字节 / 3 字节每个汉字。这也是为什么很多 Oracle 迁移到 MySQL 时字段长度总感觉“变大了”的原因——两种数据库对 n 的解释根本不是一个东西。C 语言的strlen返回的是字节数不关心字符编码。如果字符串是 UTF-8 编码strlen数出来的“长度”和用户感知的字符数完全是两回事。C 的std::string::length()同样返回字节数Java 的String.length()返回 UTF-16 的char数量JavaScript 的.length和 Java 一样遇到 Emoji 也会出现长度为 2 的问题Python 的len()返回 Unicode 码点数量最符合直觉但如果你用的是bytes类型len()返回的还是字节数。经验之谈要处理跨语言、跨系统的字符串长度问题时永远先明确这个“长度”是字节数、字符数还是显示宽度否则后续的截断、校验、加密签名全都会出问题。1.3 C 语言字符串数组初始化和 strnlen 的边界C 语言里字符串数组初始化也是个容易出细节问题的点。比如这两种写法char str1[] hello; char str2[6] hello;str1的大小是 6因为编译器会自动在末尾补一个\0str2显式指定 6 也是能装下hello加终止符的。但如果你写char str2[5] hello看起来好像刚好实际上它是 UB未定义行为因为放不下终止符后续strlen和printf(%s)会一路读到野内存。我习惯在涉及外部输入的场景里用strnlen而不是strlensize_t len strnlen(buf, sizeof(buf));它可以避免越界探测。配合snprintf和strncpy这类带边界控制的函数能把不少内存问题挡在门外。CodeBlocks 里常见的一个问题是宽字符比如L中文报错。这不是 CodeBlocks 的锅而是项目没有设置宽字符支持。Windows 下用 MinGW 编译器时文件编码如果不是 UTF-8或者没有包含wchar.hL前缀就会出各种问题。我通常先确认源文件保存为 UTF-8再在编译器选项里加上-finput-charsetUTF-8 -fexec-charsetUTF-8大部分乱码和警告都能消掉。2. 字符串转数字判断逻辑远比想象中复杂2.1 Oracle 中“过滤不可转为数字的字符串”的正确姿势热词里有“oracle 过滤不可转为数字的字符串”这句话背后是一个很常见的报表需求某张表的某个字段是 VARCHAR2但里面混着数字、字母、特殊符号现在要把能转成数字的行筛出来做统计。最朴素的思路可能是SELECT * FROM t WHERE TO_NUMBER(col) IS NOT NULL;这个写法会直接报ORA-01722: invalid number因为 Oracle 的优化器可能在过滤前就对全表做了隐式转换。正确做法是用函数或者正则去判断。Oracle 11g 提供了VALIDATE_CONVERSION函数这是我目前最推荐的方式SELECT * FROM t WHERE VALIDATE_CONVERSION(col AS NUMBER) 1;如果数据库版本比较老10g/11g 早期可以用REGEXP_LIKE做匹配。数字字符串的规则要覆盖整数、小数、正负号、科学计数法。SELECT * FROM t WHERE REGEXP_LIKE(col, ^[-]?(\d\.?\d*|\.\d)([eE][-]?\d)?$);这里有几个细节特别容易漏空字符串在 Oracle 里等价于 NULLTO_NUMBER(NULL)返回 NULL不算错但业务上可能希望过滤掉字符串里的前后空格TO_NUMBER是能容忍并自动去除的但REGEXP_LIKE不会容忍。所以在做正则过滤前最好先TRIM一下数字字段如果是从 Excel 或网页采集的很可能带千分位逗号比如1,234.56直接转会失败。这种要先REPLACE(col, ,, )再判断。我第一次做这种需求时没考虑千分位结果报表少了三分之一的数据后来加了REPLACE才恢复正常。在写过滤逻辑时一定要先问一句这个字段的数据来源靠谱吗里面可能有哪些奇怪格式2.2 SQL Server 的 ISNUMERIC 和 TRY_CAST 谁更靠谱SQL Server 里旧代码常看到ISNUMERIC但它是个出了名的“宽松”判断函数。ISNUMERIC(.)返回 1ISNUMERIC(1e3)返回 1ISNUMERIC($12.5)也返回 1。对于纯粹想判断“是不是普通数字”的需求它会造成大量误放行。SQL Server 2012 及以上版本提供了TRY_CAST和TRY_CONVERT这是处理这个问题最优雅的方案SELECT * FROM t WHERE TRY_CAST(col AS DECIMAL(18, 6)) IS NOT NULL;TRY_CAST返回 NULL 表示转换失败不会抛异常。它的语义很清晰而且能同时兼顾精度和格式要求。判断结果为真的数据一定可以安全地参与数值运算不会像ISNUMERIC那样放进来的东西一算就炸。如果一定要兼容旧版本可以用老办法排除ISNUMERIC的误判结果加上LIKE模式去进一步过滤。但那样写出来的 SQL 非常啰嗦而且边界条件无穷无尽。我的原则是能用TRY_CAST就不用ISNUMERIC。顺便提醒一句变量名别取numeric、decimal这类和数据类型同名的东西容易误导调试起来很痛苦。2.3 MySQL 的字符串转日期和数字顺带提一下 DB2 和 ABAPMySQL 里字符串转数字最直接的是CAST(123 AS SIGNED)。但有一个细节CAST(123abc AS SIGNED)在 MySQL 里不会报错而是返回 123它做了非常“宽容”的前缀解析。这和 Oracle 的严格行为截然相反。所以在 MySQL 里做数字判断我很少依赖CAST的返回结果而是用正则先过滤SELECT * FROM t WHERE col REGEXP ^[-]?[0-9](\.[0-9])?$;字符串转日期则是另一个高频需求。MySQL 里STR_TO_DATE负责把字符串解析成日期SELECT STR_TO_DATE(2025-06-15 23:18:00, %Y-%m-%d %H:%i:%s);注意格式符和 Java 的SimpleDateFormat完全不同分钟是%i不是%m%m表示月份。我见过有人把%i写成%M结果返回 NULL排查半天才发现是格式符错了。反过来把日期转字符串用DATE_FORMAT这个比较直观但同样要小心格式符SELECT DATE_FORMAT(NOW(), %Y-%m-%d %H:%i:%s);DB2 里判断字符串是否是数字可以用REGEXP_LIKE配合类似的正则也可以尝试CAST(col AS DECIMAL)放进CASE里捕获异常但捕获异常的方式在复杂的 SQL 里不好维护。我更喜欢用TRANSLATE函数把数字字符替换掉再判断剩余是否为空SELECT * FROM t WHERE TRANSLATE(col, , 0123456789) IS NULL;这个思路的原理是如果只含数字把所有数字翻译成空串后结果就是空串。但它没法处理小数点、正负号要按业务需求扩展字符集。还有 ABAP 里判断字符串是否为数字可以用CO仅包含比较IF str CO 0123456789.或者condense后转N类型。这些都是老 SAP 开发常用的土办法但胜在简洁实用。2.4 各种语言里字符串转数字的边界情况C/C 里atoi是不推荐使用的因为它无法区分“转换结果是 0”和“转换失败”而且对越界输入的行为未定义。C 里推荐strtolC 里推荐std::stoi#include cstdlib const char* num 123abc; char* end; long val strtol(num, end, 10); // end 指向 a说明解析到 a 就停了strtol的end参数能告诉我们到底解析到哪个位置停止这是判断字符串是否完全合法的重要手段。Java 里对应的是Integer.parseInt它比 C 的atoi严格得多遇到非数字会直接抛NumberFormatException。但在用之前要想清楚异常处理是有开销的如果是在循环里批量转换大数据集极端情况下可能成为性能瓶颈。可以考虑正则预过滤或者直接用Integer.valueOf并捕获异常。JavaScript 的parseInt(123abc)返回 123Number(123abc)返回 NaN两者的行为完全不同。123这种一元加写法等价于Number(123)。在表单校验里我一般用Number因为它更严格在解析带单位的输入比如12px时反而要用parseInt。Python 的int(123)严格float(1e3)认识科学计数法int(12, 16)还能指定进制但要注意int(1_000)在 Python 3.6 会把下划线当作分隔符合法字符这在某些从外部读入的数据里可能产生意外结果。3. 分割、截取与逆序操作符背后藏着许多边界问题3.1 C 语言按空格分割字符串别直接无脑用 strtok热词里有“c语言将一个字符串按照里面的空格分开成”和“指针数组存放字符串”这是一道很经典的 C 语言题目。标准库的strtok用起来最省事char str[] hello world test; const char* delim ; char* token strtok(str, delim); while (token ! NULL) { printf(%s\n, token); token strtok(NULL, delim); }但strtok有几个明显的局限第一它会修改原字符串把分隔符位置改成\0原内容被破坏第二它不是线程安全的因为它用静态缓冲区记录上次位置。在多线程环境或者需要保留原文的场景我会用strsepGNU 扩展Linux/macOS 常用或者自己手写解析逻辑。手写按空格分割最稳妥的方式是用双指针扫描char str[] hello world test; char* result[16]; int count 0; char* p str; while (*p) { while (*p ) p; if (*p \0) break; result[count] p; while (*p *p ! ) p; if (*p) { *p \0; p; } }这个写法同样会修改原串但逻辑完全可控。如果你不想破坏原串那就只能逐字符拷贝到新分配的内存里或者用strdup复制一份再分割。指针数组存结果时一定记得先确认count不会超过数组容量否则就是缓冲区溢出。代码里声明char* result[16]扫描时必须加if (count 16) break;的防御这个习惯能救很多人一命。3.2 字符串逆序中文场景和经典双指针写法字符串逆序是 PTA 和各类 OJ 最爱考的基础题。最简单的原地双指针法长这样void reverse_str(char* s) { int len strlen(s); for (int i 0, j len - 1; i j; i, j--) { char tmp s[i]; s[i] s[j]; s[j] tmp; } }单个 ASCII 字符的逆序这样处理没问题。但如果是中文事情就麻烦了。C 语言里一个汉字在 UTF-8 下占 3 个字节直接按字节逆序会把一个完整的汉字打碎成乱码。正确做法是按字符边界逆序要识别 UTF-8 的连续字节10xxxxxx开头的字节是续字节把完整字符作为一个整体移动到目标位置。或者干脆换用宽字符接口比如 Windows 下的_wcsrev前提是程序内部统一使用宽字符处理。热词里还有“倒置字符串”这道题更进阶给一个句子I love programming要求输出programming love I每个单词内部不反转。经典解法是两次反转先把整个字符串反转成gnimmargorp evol I再对每个单词单独反转变成programming love I。这个思路非常精妙代码写起来也不复杂。遇到 Java 和 Pythonsplit( )再重组也能做到但要注意连续空格和首尾空格split会留空串需要过滤。JavaScript 里a b c.split( )同样会保留空串很多新手在这里意外翻车。3.3 Java、JavaScript、Python 分割函数的差异对比Java 的split接收的是正则表达式而不是普通字符串。想让a.b.c按点分割写成a.b.c.split(.)会得到空数组因为.在正则里是“任意字符”。必须转义String[] parts a.b.c.split(\\.);同理按竖线|分割也要转义。这个知识点我说过无数次但每次都能钓到踩坑的人。另外split()在 Java 8 和 Java 17 里的行为还不完全一样涉及到首尾空串的去留尽量避免依赖这个行为。JavaScript 的split接收的是字符串或正则普通字符串不需要转义但也因此少了 Java 那种“正则能力”。abc.split()返回字符数组这个用起来挺方便。截取方法substring、substr、slice三者里substring的两个参数若出现 start 大于 end 会自动交换而slice不会且slice支持负数从尾部计数。我建议统一用slice语义最清晰不容易出错。Python 的split()和split( )也有区别。split()不传参数时会按任意空白字符分割且自动过滤连续空白和首尾空格split( )则严格按单个空格切连续空格会产生空串。如果解析配置文件或日志一般用split()如果格式严格按空格分隔必须明确用split( )并处理空串。还有一个容易忽略的点rsplit可以从右往左分割指定maxsplit1是拆分路径的利器。3.4 回文子串计数与字符串题目的竞赛思维热词里那句“alice得到了一个字符串s。她觉得回文子串十分的interesting”一看就是算法题。求回文子串数量最容易想到的是中心扩展法时间 O(n^2)int countSubstrings(char* s) { int n strlen(s), ans 0; for (int i 0; i n; i) { // 奇数长度回文中心 ans expand(s, i, i); // 偶数长度回文中心 ans expand(s, i, i 1); } return ans; }expand函数就是往两边扩展只要两边字符相同就继续。这个解法的好处是代码量极小笔试时最不容易写错。如果数据规模到 10^5 以上就得考虑 Manacher 算法O(n) 复杂度但实现细节多得多。我刷题时的习惯是先写中心扩展拿分再根据数据范围决定要不要抠 Manacher。热词里另一道是“拼数(number) 题目描述 小 r 正在学习字符串处理”这道题是典型的字符串排序问题。给定若干数字字符串要拼成一个最大的数。不能直接按字典序排序因为9和98比较时显然998大于989所以比较器要写成拼接后比较int cmp(const void* a, const void* b) { char ab[64], ba[64]; snprintf(ab, sizeof(ab), %s%s, (char*)a, (char*)b); snprintf(ba, sizeof(ba), %s%s, (char*)b, (char*)a); return strcmp(ba, ab); // 降序 }这个思路在 Java 里可以写成(a, b) - (b a).compareTo(a b)。它的本质是把“比较两个字符串谁应该排前面”转化为“拼接结果谁更大”非常典型面试考频很高。4. 比较、包含与替换细节决定成败4.1 字符串比较相等Java 的是个坑Oracle 的LIKE也不是万能Java 里String用比较引用只有用equals才比较内容。但即使是equals也要注意空指针str.equals(abc)在str为 null 时直接 NPE。反过来写成常量调用就安全abc.equals(str)。另外equalsIgnoreCase做大小写不敏感比较但要注意它对 Unicode 的处理并不完美比如德语ß和ss的关系它不会处理。如果要严谨得用Collator。Oracle 里判断字符串包含某个子串新手最常用LIKESELECT * FROM t WHERE col LIKE %abc%;但LIKE会出现转义问题。如果数据本身包含%或_就破坏了匹配语义。想按字面匹配这些特殊字符需要显式转义SELECT * FROM t WHERE col LIKE %\%% ESCAPE \;如果只是判断“是否包含”我更推荐INSTR它返回子串位置0 表示不包含纯粹且不会和通配符冲突SELECT * FROM t WHERE INSTR(col, abc) 0;数据库里还有一个潜在陷阱是字符集和排序规则。MySQL 的utf8mb4_general_ci是不区分大小写的和LIKE都会忽略大小写而utf8mb4_bin则区分。同样一条WHERE name abc在不同库表上查出来的结果可能不一样。遇到比较结果“诡异”时先检查表字段的 collation这是我最常排查的方向之一。SQL Server 里对应CHARINDEX用法是CHARINDEX(abc, col)返回位置 0 代表不含。DB2 则是POSSTR(col, abc)注意参数的顺序。每个数据库都不一样跨库迁移时这类函数容易成为隐患。4.2 大小写转换和模板字符串看着简单其实有暗坑热词里有“字符串字母大小写转换”和“模板字符串”。大小写转换的初级写法就是直接调库Java 用toLowerCase()/toUpperCase()Python 用.lower()/.upper()C 标准库有tolower/toupper。但有两个点要提醒。第一C 语言里tolower和toupper的参数是int必须是EOF或unsigned char范围内的值。如果直接传一个普通的char在有符号平台下遇到负数非 ASCII 字符的高位为 1会变成未定义行为。正确写法是char lower tolower((unsigned char)c);第二大小写转换有语言环境问题。Java 的toLowerCase()不带 Locale 时土耳其语环境下I.toLowerCase()会变成ı不带点的 i这是一个经典笑话级别的大坑。处理文件名、URL 等场景时我通常会显式指定 Localestr.toLowerCase(Locale.ROOT);模板字符串在 JavaScript 里最常用反引号加${}嵌入变量。但要警惕模板字符串里出现的$和{}不是普通文本如果数据来自用户输入并且要被模板拼接要防止模板注入。另外模板字符串里的换行会真实保留这在生成 SQL 或日志时可能意外引入换行符。有人说“模板字符串就是增强版拼接”这话没错但它的转义规则和普通字符串并不完全相同用的时候要注意。4.3 字符串替换全局替换和第一个替换的不同选择JavaScript 里字符串替换有两个函数replace默认只替换第一个匹配项要全部替换必须用正则加全局标志a-b-c.replace(-, ); // ab-c a-b-c.replace(/-/g, ); // abcJava 里String.replace是全部替换replaceFirst/replaceAll才涉及正则。很多人分不清replace和replaceAll前者参数是普通字符串后者参数是正则。把用户输入当正则传给replaceAll一旦输入里含有$或\结果就会失控。比如str.replaceAll(userInput, x)如果userInput是\\d它当成正则把所有数字替换掉而不是按字面替换。遇到“把用户输入当成普通字符串做全局替换”的需求最安全的做法是先用Pattern.quote(userInput)把输入转成正则字面量或者干脆用replace(CharSequence, CharSequence)的重载形式。这个细节我在代码评审里见过太多次值得单独拎出来讲。5. 底层与存储场景编码、JSON、CLOB/BLOB 和内存问题5.1 JSON 转字符串与 GzipInputStream 转字符串的字符集统一JSON 转字符串Java 生态里最常用的是 JacksonObjectMapper mapper new ObjectMapper(); String json mapper.writeValueAsString(obj);但序列化出来的字符串经常包含中文默认情况下 Jackson 输出的就是 UTF-8 的 JSON 字符串如果直接写ObjectMapper默认是 UTF-8但在控制台或日志里显示时可能乱码那是显示终端问题不是数据问题。把 JSON 直接写入文件时一定要指定编码否则 Windows 默认 GBK 会把中文写坏。另一个坑藏在字符串里JSON 中的特殊字符会被自动转义比如变成\换行变成\n。但如果你把 JSON 字符串再塞进另一层 JSON转义会嵌套叠加解出来的时候容易混乱。我处理这类多层 JSON 时习惯在关键节点打日志检查而不是靠肉眼猜。GzipInputStream转字符串是个偏门的场景通常用于读取压缩的 HTTP 响应或日志文件。核心代码如下ByteArrayOutputStream baos new ByteArrayOutputStream(); try (GZIPInputStream gis new GZIPInputStream(new ByteArrayInputStream(compressedBytes))) { byte[] buffer new byte[4096]; int len; while ((len gis.read(buffer)) ! -1) { baos.write(buffer, 0, len); } } String result baos.toString(UTF-8);这里最关键的坑是最后一步baos.toString(UTF-8)如果压缩前的原始字节不是 UTF-8 编码这步就会产生乱码。有些人写baos.toString()不传字符集这会用平台默认字符集在 Windows 和 Linux 上结果可能不同。从这个角度说只要涉及字节和字符串互转永远显式指定字符集这是我反复强调的一条底线。5.2 Oracle 的 CLOB/BLOB 转字符串Java 代码与 SQL 的配合Oracle 里CLOB存储大文本BLOB存储二进制。报表和接口联调时经常需要把 CLOB 转成普通字符串。SQL 层面比较直接的做法SELECT TO_CHAR(SUBSTR(clob_col, 1, 4000)) FROM t;但TO_CHAR对 CLOB 直接转换有限制超过一定长度会报错。如果 CLOB 内容超过 4000 字符SUBSTR也只能截取一部分。长文本场景下更稳妥的方式是在 Java 里用流读取Clob clob rs.getClob(content); String content clob.getSubString(1, (int) clob.length());Java 的Clob.getSubString的第二个参数是int但 CLOB 长度上限远大于int所以当 CLOB 特别大时这种写法会溢出。正确处理方式是分段读取Reader reader clob.getCharacterStream(); BufferedReader br new BufferedReader(reader); StringBuilder sb new StringBuilder(); char[] buf new char[4096]; int len; while ((len br.read(buf)) ! -1) { sb.append(buf, 0, len); }热词里还有“oracle long转字符串”。Oracle 的LONG类型早已不被推荐但遗留系统里偶尔还能见到。LONG不能直接在 SQL 里用TO_CHAR转成大字符串也不能用在WHERE条件里只能一条条读出来在应用层处理。遇到这种老字段我的建议是尽早让 DBA 帮忙迁移成 CLOB否则后面每次查询都要小心翼翼。BLOB 转字符串更绕一层它先把二进制按字节读出来再按字符集解码。BLOB 存储的到底是什么取决于写进去的时候用的编码。如果 BLOB 存放的是 UTF-8 文本Java 侧可以byte[] bytes blob.getBytes(1, (int) blob.length()); String text new String(bytes, StandardCharsets.UTF_8);如果 BLOB 里存的是 GBK 文本却用 UTF-8 解码就会出现经典的“锟斤拷”乱码。这里没有通用解法只能先确认写入端的编码再做对应解码。和上面的 JSON、Gzip 场景一样字符集问题永远是二进制转字符串的第一道坎。5.3 MFC 的 CString 内存泄漏与宽窄字符转换热词里有一条“mfc字符串内存泄漏”看着就很有年代感但 MFC 项目至今仍在大量运行。CString 本身有引用计数机制正常情况下会自动释放内存。出现内存泄漏最常见的原因不是 CString 本身而是GetBuffer和ReleaseBuffer没有成对调用。CString str; LPTSTR pBuf str.GetBuffer(100); // 对 pBuf 做操作 // 如果没有调用 ReleaseBufferCString 内部状态不一致可能导致内存泄漏或崩溃 str.ReleaseBuffer();更隐蔽的坑是 CString 与std::string混用。CString 在 Unicode 编译选项下是宽字符wchar_t而std::string是窄字符char。直接赋值或拼接会导致编译错误或乱码。正确的转换方式// CString - std::string CStringW cs L中文; std::string str CW2A(cs.GetString(), CP_UTF8); // std::string - CString std::string s 中文; CStringW cs CA2W(s.c_str(), CP_UTF8);用CW2A/CA2W比CString的GetBuffer更安全因为它们内部管理临时内存。但注意CW2A的结果是临时对象不能长期持有指针。我见过有人把CW2A的返回值赋给一个const char*存到类成员里程序跑一会儿指针悬空崩溃无从查起。这种问题说到底是对临时对象生命周期不敏感造成的。从这些 MFC 老坑里能学到的一个通用教训C 里任何返回指针的转换宏或函数都要去确认返回指针是否指向临时缓冲。5.4 枚举类型转字符串别硬写 to_string热词里有“枚举类型转换为字符串”这本身不是热门话题但实际需求很多。日志打印、接口返回、配置映射都需要把枚举转成可读字符串。很多人第一反应是std::to_string(enumValue)结果是数字不是语义化字符串。C 里枚举转字符串最简单的方案是维护映射表const char* colorToString(Color c) { switch (c) { case RED: return RED; case GREEN: return GREEN; default: return UNKNOWN; } }Java 里可以直接用name()但更推荐toString()重载因为name()是编译期固定的如果枚举定义重构调用方拿到的字符串可能不是业务想要的。Spring 项目里经常用 JsonValue 注解输出自定义字符串既保持代码可读又对前端友好。Python 的枚举转字符串更自然Color.RED.name是RED而str(Color.RED)在 Python 3.11 返回Color.RED这种限定名。不同版本的 Python 行为有差异千万别在跨版本环境里依赖默认字符串。我的通用建议是对外的字符串转换逻辑永远显式实现不要依赖默认行为。6. 常见问题与排查技巧实录6.1 字符串相关高频问题的速查表我整理了自己平时排查字符串问题的心得做成一张速查表基本覆盖热词里提到的绝大多数场景。遇到类似的症状先对照这个表定位方向能省不少时间。症状可能原因排查/解决方案长度总是和预期不一致混淆了字符数、字节数、显示宽度确认业务需求选择length()/LENGTH()/CHAR_LENGTH()或码点计数中文截断后出现乱码按字节截断切碎了多字节字符先按字符边界截取再用码点或substring类函数C 语言要识别 UTF-8 续字节字符串数字无法相互转换隐式转换失败、字符集问题用TRY_CAST/VALIDATE_CONVERSION/strtol并检查边界正则匹配不到预期内容特殊字符未转义、正则语义不同Javasplit(\\.)、SQLLIKE的ESCAPE、JavaScript 正则全局标志数据库判断数字结果异常空格、千分位、科学计数法先TRIM、REPLACE清格式再用严格函数判断字符串比较不相等排序规则大小写敏感、含不可见字符检查 collation、trim、char 编码值JSON 或日志输出乱码字符集不一致写入端与读取端编码不同统一 UTF-8字节与字符串转换时显式传字符集替换只生效了一次用了 replace 而不是 replaceAll / 全局正则按语言选择全局替换方法C 内存异常或泄漏GetBuffer未ReleaseBuffer、临时对象悬空成对调用、避免长期持有转换临时指针Emoji 长度算错UTF-16 代理对按两个 char 计算Java/JS 用码点计数这张表不是万能药但能作为排查的起点。很多问题实际是交叉叠加的比如“JSON 解析乱码 长度不对”本质可能只是同一个字符集问题。6.2 一个真实的排查案例分割字符串后比较为什么始终失败有一次业务反馈某个接口返回的数据不对日志里看到一段“按逗号分割后第一个字段等于 abc 但就是匹配不上”。排查过程是这样的先打印两侧字符串加引号的效果左边: abc 右边: abc 右边末尾多了个空格。这就是典型的数据源前后空格问题。解决方式是分割后统一trim或者 SQL 查询时用TRIM处理字段。很多人会把锅甩给排序规则实际上先做trim再比较能解决一大批“明明应该相等”的怪问题。第二个案例是 Java 的split分割后数组长度和预期不符。业务方说“我用逗号分割为什么最后一个逗号后面没有元素数组还能有内容”。实际上a,b,.split(,)得到的是[a,b]尾部的空字符串会被丢弃而a,,b.split(,)得到的是[a,,b]中间的空串会保留。如果业务需要保留尾部空串用split(,, -1)。这个限制文档里有写但太容易忽略值得作为案例记住。第三个案例是 MFC 程序里字符串在 Debug 下没毛病Release 下随机崩。最终定位到GetBuffer和ReleaseBuffer中间有个return提前退出导致释放状态没有恢复正常。这种“异常路径绕过资源清理”的问题任何语言里都是隐患。经验教训只有一条收了资源任何路径都要还回去最好用 RAII 或 try-finally。7. 最后再补充两个建议字符串处理的经验说到底是“编码、边界、资源”三个关键词的排列组合。热词里提到的“oracle 过滤不可转为数字的字符串”“c语言按空格分割”“MFC字符串内存泄漏”其实都是这三个问题的具体形态。我个人处理字符串问题时有几个稳定的习惯分享给各位第一所有从外部拿到的字符串先明确它的编码。HTTP 接口、文件流、数据库字段只要涉及字节和字符互转我总会显式指定 UTF-8绝不依赖平台默认值。这个习惯解决了我 70% 以上的乱码问题。第二写分割、截取、比较逻辑之前先画一张边界表。空字符串、null、连续分隔符、首尾空白、超长截断这些边界各是什么行为先想清楚再写代码。与其上线后被用户拿奇怪数据打脸不如在代码审查阶段就把边界测一遍。第三在日志里给字符串加引号再输出。不要输出裸字符串加上函数名末尾标记比如[值]。这样前后空格、不可见字符、换行符都会更容易暴露出来。排查效率高很多。字符串这块内容part03 我打算专门整理一套跨语言的定时任务与字符集处理实战到时候把 Java、Python、Shell 和数据库的配合细节展开写。如果你们在项目里遇到过特别坑的字符串问题也欢迎拿出来聊聊。
返回列表