1. 字符串作为“变量类型”里的特例,到底特殊在哪
1.1 字符、字符串与数组三者关系
学编程的人最开始都会接触字符和字符串,但多数人是在“能用就行”的阶段把这个概念滑过去的。字符串往往被定义成“一串字符”,但真到了语言实现层面,它并不是一个天生就该存在的简单标量类型。
整数、浮点数、布尔值这类变量类型,存的是一个可枚举的值域,内存占用通常是固定的。字符串则完全不同:一个字符串可能只有一个字母,也可能有上万字,它本质上是一个“序列”,需要一块连续或者逻辑上连续的内存来存放。C语言的处理方式最直白——没有原生字符串类型,只有“以\0结尾的字符数组”。C++、Java、Python、C#这些现代语言虽然做了封装,但你在调试器里展开一个字符串对象,还是能看到它内部持有一个或多或少的字符数组或指针。
之所以要在“变量类型”的章节里专门拿出5.6来写字符串,正是因为它是第一个让你跳脱“单值变量”思维、开始接触“复合数据”的类型。很多新手学到数组时觉得难,其实本质原因是还没建立起字符串与数组的关联意识。
1.2 为什么字符串常常被设计成“对象”,而不是基础类型
从Java开始,很多人有一个困惑:int、double是基础类型,String却是一个类,为什么它用起来跟基础类型一样方便?
java复制String name = "ethan";
System.out.println(name.length());
把“一串字符”封装成类,不是故意增加复杂度,而是因为这层封装能长期收拢对字符序列的一整套操作:求长度、截取、查找、分割、替换、比较。你看看C语言里实现一次字符串拼接需要调用多少次函数、处理多少次手动分配,就能理解类封装的价值。Python和JS的字符串更是把对象特性发挥得淋漓尽致,几乎所有操作都变成方法或运算符。
此外,字符串一旦被设计成一个类型,就牵涉到“传值还是传引用”“能否修改”“如何比较”这些基础而致命的问题。这些问题在整数上简单清晰,在字符串上却常常成为线上事故的诱因。所以这一节我直接按“存储——行为——比较——转换”四条主线展开,把字符串的底裤彻底翻一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 存储与编码:乱码、下标和长度为什么会出问题
2.1 内存里的字节序列与字符集的选择
字符串存储在内存中,本质就是一段字节数据,再配合上解释这些字节的编码规则。同一个字节序列,用GBK解码和用UTF-8解码,结果截然不同。中文互联网上的乱码,十有八九不是数据丢了,而是解码规则用错了。
ASCII时代,每个字符一个字节,一切都很简单。到了中文、日文、韩文这类表意文字,一个字节存不下,于是各搞各的编码,GBK、Shift-JIS、Big5满地走。Unicode出现后,给所有字符统一分配了一个码点,但码点不等于存储字节,还需要一个编码方案把码点变成字节序列。UTF-8是变长编码,英文字符占1字节,中文大多占3字节;UTF-16则用2字节或4字节表示一个字符。
这个基础的差异造成了字符串在内存中的实际占地千差万别。例如“abc你好”这个看起来只有5个“字符”的字符串,如果在Python里用UTF-8编码存储,你会得到:
python复制s = "abc你好"
print(len(s)) # 5,这是Python层面看到的字符个数
print(len(s.encode("utf-8"))) # 11,这才是UTF-8编码后的字节数
很多人调用外部接口、往Redis里写数据或者估算数据库字段长度时,从来不区分“字符长度”和“字节长度”,结果做了长度校验依然超限或者截断,根源就在这里。
2.2 下标与长度单位差异:字节、代码单元还是字符
不同语言对字符串的下标语义并不统一,这是我见过初学者踩坑最多的地方。
C语言没有真正的字符串下标概念,str[0]取到的是一个char,是实实在在的一个字节。如果你用C语言处理中文串并直接按下标操作,一个汉字就占两三个字节,按字节移动极容易切碎字符。
Java相对做了一层隔离,String内部使用UTF-16编码,一个char是一个16位代码单元。英文和常用中文都落在单代码单元内,但像Emoji这种增补平面字符就会占两个char,于是你看到的现象是:
java复制String emoji = "a💡b";
System.out.println(emoji.length()); // 3
System.out.println(emoji.charAt(1)); // 这里输出的是一个代理项,不是一个完整的Emoji
这个例子想说明什么呢?不同语言对“字符串长度”的定义,可能是字符数量、代码单元数量或者字节数量,没有统一答案。真正需要拿到“用户视觉上的字符个数”时,要做归一
