说实话,字符串这章是我见过最容易被低估的内容。不少开发者觉得字符串函数就是查查手册背背 API,可真到了项目里,绕来绕去翻车的全是字符串。前几天我刷到一个热搜问题:博途里字符串的值已经置成 0 了,触摸屏上却还显示原来的字符。评论区一堆人猜来猜去,但真正的原因其实藏在字符串的内存结构里。再翻热搜里的“C 语言字符串逆序”“SQLServer 字符串转数字”“Java 字符串转 JSON”这些提问,你会发现大家对字符串的理解是碎片化的,只知道某个函数怎么调用,却不知道这个函数背后做了什么。
这篇文章我打算换个讲法,不讲教科书上的函数列表,而是从“字符串到底是什么”出发,把热搜里那些高频问题拆开揉碎。你写代码时会踩的坑,面试时会被问的原理,调接口时碰到的编码乱码,基本都逃不出这几个主题。适合刚入门的初学者,也适合写了好几年业务代码、想回头补基础的朋友。
1. 字符串的本质:先把“字符”和“字节”这两件事掰扯清楚
1.1 为什么几乎所有字符串问题都出在“编码”上
字符串在高层的抽象里是一串字符,但在计算机内存里,一切都只是字节。字符和字节之间靠编码规则互相转换。最常见的编码是 ASCII、UTF-8、UTF-16、GBK 这几种,它们把“字”翻译成“字节”的方式完全不同。
这里有一个非常经典的认知盲区:一个英文字母在几乎所有编码里都占 1 个字节,但一个汉字在 UTF-8 下占 3 个字节,在 GBK 下占 2 个字节,在 UTF-16 下可能占 2 或 4 个字节。所以“字符串长度”这个看似最简单的概念,在不同语言、不同编码下会有不同答案。
Python 里的 len("你好") 返回 2,因为 Python 的字符串对象存储的是 Unicode 字符;Java 里 "你好".length() 返回 2;但如果你取 "你好".getBytes("UTF-8").length,得到的是 6。C 语言的 strlen("你好") 的结果取决于源文件编码和运行环境,如果是 UTF-8 编码且终端按 UTF-8 输出,得到的很可能是 6。不是任何一个函数算错了,而是它们衡量的对象本身就不一样。
热搜词里有一个非常典型的问题:python十六进制和字符串互转。很多人第一次接触这个概念时一头雾水:字符串就是字符串,十六进制不就是数字吗?其实这里面隐藏的同样是“字符到字节”的映射问题。一个字符串“Hello”如果按 UTF-8 编码成字节,就是 48 65 6C 6C 6F,这串十六进制就是它内存中的真正形态。反过来,拿到十六进制字节流,按指定编码解码,才能还原成可读的字符串。
python复制# 字符串转十六进制
"Hello".encode("utf-8").hex()
# 输出:48656c6c6f
# 十六进制转字符串
bytes.fromhex("48656c6c6f").decode("utf-8")
# 输出:Hello
我见过太多项目里的乱码问题,最后定位下来都是编码不一致。上游接口用 GBK 编码发过来,下游消费者按 UTF-8 解析,中文就变成一堆问号或乱码。处理这类问题的第一原则不是到处转码,而是先明确数据链路每一环的编码,再在系统边界处统一转码。在 2024 年的今天,新项目一律 UTF-8,旧系统对接才需要额外处理 GBK 这类兼容问题。
还有热搜里的 urldecoder.decode处理有%的字符串,这属于 URL 编码范畴。URL 里的 %20 表示空格、%E4%B8%AD 是“中”字的 UTF-8 编码。URLDecoder.decode 做的事就是把这种百分号编码还原成普通字符串。很多人在处理带 % 的原始数据时踩坑,是因为没搞清楚:数据里的 % 到底是 URL 编码产生的,还是原本就有的字符。如果你只是单纯想把一个已经编码的 URL 还原,直接 decode 没问题;如果数据里本身就有字面意义上的 %,就要先转义成 %25,否则解出来的内容和你期望的会差很远。
1.2 不可变与可变:一道所有语言都绕不开的分岔口
字符串在编程语言里有两种截然不同的设计:不可变与可变。
Java、Python、C#、JavaScript、Lua 这些语言的字符串默认不可变。你看起来“修改”了一个字符串变量,实际上是在内存里创建了一个新的字符串对象,然后把变量指向了新的对象。而 C、C++、Go 的字符串底层是字节数组,可以在原地址上改内容。
不可变设计带来几个实际好处:线程安全,多个线程共享同一个字符串实例不会互相干扰;哈希值可以缓存,同一个字符串只在第一次计算哈希,后面直接用;安全性更好,不会因为一个引用的修改污染其他引用。但代价也很明显——频繁拼接字符串时会产生大量中间对象,影响性能。
可变字符串适合需要就地修改的场景,但会引入别名问题。两个变量指向同一块内存,你在一个变量上改了内容,另一个变量看到的内容也跟着变了。C 语言里传指针给函数,函数里 strcpy 一下,外部变量的内容就悄无声息地被覆盖,这是无数 C 语言新手崩溃的根源。
热搜里有两个问题正好对应这个话题。一个是 lua 字符串如何改变其中某个字符的值,Lua 的字符串是不可变对象,你不能像 C 语言那样直接 s[2] = 'x'。惯用做法是用 gsub 或手工构造新字符串:
lua复制local s = "hello"
-- 把第 2 个字符(从 1 开始计数)改成 'a'
s = s:sub(1, 1) .. "a" .. s:sub(3)
另一个是 c语言字符串拼接。C 语言里字符串本质是字符数组,拼接要自己保证目标缓冲区足够大,否则就是缓冲区溢出。我早年写 C 的时候,因为 strcat 目标数组开小了,
