ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

字符串底层原理与工程实践:编码、比较与常用操作深度解析

字符串底层原理与工程实践:编码、比较与常用操作深度解析 字符串看着人畜无害背地里全是坑。写过几年代码的人基本都有这种体会字符串是最常用的数据类型但也是出问题最多的地方。不管是C语言的char[]数组和\0结束符还是Python的不可变对象又或者是Java里和equals的经典迷惑行为归根结底都是因为没有真正吃透“字符串到底是什么”这个问题。这篇不打算按某一种语言来讲而是把字符串这个硬核主题拆开揉碎从底层原理、常用操作、转换机制、排序规则到实际开发里的疑难杂症一次说清楚。无论你写C、C、Python、Java还是C#很多概念都是相通的理解了底层逻辑换个语言只是换个API的问题。1. 字符串的本质从底层视角看它到底是什么1.1 数组、指针和结束符C语言的字符串真相C语言里没有真正的字符串类型这是所有字符串问题的根源。所谓的字符串本质上就是一个char数组或者指向char的指针。比如char str[] hello; char *p world;这两种写法有本质区别。str[]会在栈上分配一块连续内存内容是h e l l o \0而p指向的是只读的字符串常量区如果你试图通过p去修改字符大概率直接段错误。很多初学者在这个地方栽过跟头——字符串字面量是存放在只读区域的char *p abc; p[0] x;在绝大多数平台上是非法的。更要命的是结束符\0。C语言判断字符串长度就是一路数到\0为止所以\0既是边界又是隐患忘记留结束符位置char buf[5] hello;直接越界写入行为未定义。手动拷贝字符串后忘记加\0strlen就会一直读到随机内存直到碰上一个\0才停表现就是乱码加莫名奇妙的长度。我自己调试过最诡异的一次崩溃就是字符串数组差一个结束符的位置printf打印正常但strlen结果忽大忽小。排查半天才发现是声明数组时长度少算了1。说到底C语言里操作字符串最核心的就是两件事内存边界和结束符状态。1.2 不可变与可变不同语言的设计哲学到了高级语言字符串不再裸奔但不同语言选择了完全不同的路。Python和Java的字符串是不可变对象。str.replace()、str.upper()这些操作并不是在原字符串上修改而是新创建一个字符串对象。这个设计带来两个直接影响一是字符串可以作为哈希表的键而不用担心被修改导致哈希值变化二是线程安全天然有优势。代价是频繁拼接字符串会不断创建新对象所以Python里有个经典优化——.join(list)比循环拼接快得多Java里则用StringBuilder。C的std::string则是可变但封装良好的内部管理内存自动处理\0你不需要关心结束符问题但std::string和C风格字符串const char*互相转换时老问题还是会冒出来。比如c_str()返回的指针在后续修改字符串之后就会失效如果还持有这个指针就会读到野数据。C#的字符串也是不可变的但是提供了StringBuilder来应对大量拼接场景。JavaScript虽然字符串不可变但模板字符串反引号让动态拼接舒服了很多。理解设计哲学之后很多“为什么”就不难解释了为什么Python字符串直接赋值更改会报错——因为你尝试修改的是不可变对象只能重新绑定新值为什么C函数返回std::string很安全但返回const char*就很危险——因为返回的指针指向的对象生命周期结束后就成悬垂指针了。2. 字符串长度、比较、查找最基础也最见功力2.1 length不是你想的那个length字符串长度这个操作看起来人畜无害实际上一堆坑。C语言的strlen()是O(n)复杂度因为它要逐字符扫描到\0。如果你在循环里反复调用strlen(str)作为循环终止条件那复杂度直接变成O(n²)性能杀手。正确的做法是一开始就把长度算好存起来。C的std::string::length()和size()都是O(1)因为长度是内部成员变量不用扫描。Python的len()也是O(1)同理内部存了长度。Java的String.length()是O(1)但注意它数的是UTF-16的code unit数量不是用户感知的“字符数”。一个emoji表情在Java里长度是2这就导致了截取字符串时可能截出半个字符——半代理项surrogate pair劈开之后就是一个乱码字符。实际开发中处理用户输入的emoji用codePointCount更靠谱。JavaScript的length和Java是同一个问题也是UTF-16 code unit数量。中文是1但这种扩展区汉字就是2。截取时同样要小心。还有SQL Server里的LEN()它返回的是字符数且忽略末尾空格而DATALENGTH()返回的是字节数。如果存储的是中文NVARCHAR下2字节/字符两个值差一半新手经常搞混导致判断出错。2.2 相等比较为什么有的语言用有的用equals比较字符串相等是新手最容易踩的雷。C语言里比较的是指针地址不是内容。想比较内容得用strcmp(a, b) 0或者strncmp(a, b, n)限制长度。很多新人写出if (char1 char2)然后发现永远不相等就是因为两个不同的字符数组地址天然不同。Java里比较的是引用只有字符串常量池里的字符串才能用碰巧相等。绝大多数字符串是运行时创建的新对象所以new String(abc) abc结果是false。正确的姿势是equals()要忽略大小写用equalsIgnoreCase()。Python里的比较的是内容因为Python的默认调用了__eq__而is才比较内存地址。所以Python用户很少踩这个坑但要注意is和的区别在判断None时必须用is。C的std::string重载了operator直接比较内容std::string和const char*之间也可以直接用比较会隐式转换但要小心隐式转换可能带来的性能损耗和意外匹配。C#的对于字符串也是比较内容的因为C#对string做了运算符重载。一句话总结高级语言里比较内容底层语言里比较地址Java夹在中间只有常量池特例。凡是看到字符串比较第一反应先确认当前语言的语义再去写代码。2.3 查找与包含正则还是普通匹配判断一个字符串是否包含另一个子串几乎每种语言都有现成APIPythonin运算符、str.find()、str.index()Javacontains()、indexOf()JavaScriptincludes()、indexOf()Cstd::string::find()Cstrstr()C#Contains()看起来简单但有个经典性能坑如果在一个长字符串上反复查找同一个子串每次调用都是O(n*m)的暴力扫描。你会觉得“也不是不能用”但数据量上去之后就是灾难。我自己遇到过一次线上事故就是在一个几MB的日志字符串上反复调用indexOf几十次单请求耗时飙到几秒。后来改成一次扫描解析或者用KMP预处理问题立刻消失。另外一个高频场景是判断字符串是否只包含字母数字。热搜里有“java 判断字符串中是否不是字母和数字”这个场景一般用正则if (str.matches([a-zA-Z0-9]*)) { // 全是字母或数字 }但注意matches在Java里是全匹配不是部分匹配。Python里则是re.fullmatch或者用^[a-zA-Z0-9]$做re.match。还有更精确的写法是用Character.isLetterOrDigit()遍历判断这个写法比正则更可控也方便自定义“什么是合法字符”。比如你只允许ASCII字母而不是Unicode字母正则就要写成[a-zA-Z]而不是\w或\p{L}\w在不少语言里会匹配中文、日文假名等Unicode单词字符。判断数字有个经典陷阱isNaN()在JavaScript里对空字符串、空白字符串、null都会返回false导致isNaN()认为空字符串是数字。这类问题归根结底是“类型转换规则”和“正则/API默认行为”之间的差异写任何判断之前先想清楚边界条件。3. 分割、拼接、反转字符串三件套的底层逻辑3.1 split的隐藏规则和常见误解字符串分割是使用频率极高的操作。热搜里的“字符串分割”相关词有C的strtok()、C#基于指定字符分割成数组、Python的split()、SQL Server的字符串拼接拆分等这里面的坑也不少。Python的split()有几个容易忽略的点a,b,.split(,)返回[a, b, ]末尾空字符串保留。但a,,b.split(,)返回[a, , b]连续分隔符产生的空串是保留的。如果传的参数是空字符串直接抛ValueError。不加参数时split()会按任意空白字符分割并自动去掉空串这个和split( )行为不同——很多人没注意到这一点。Java的split()接收的是正则表达式不是普通字符。这意味着如果要按.分割必须写split(\\.)写split(.)会把每个字符都切开。同理按|分割要写split(\\|)。这个坑几乎每个Java新手都会踩到一次。C语言的strtok()是个特立独行的函数char *token strtok(str, ,); while (token ! NULL) { printf(%s\n, token); token strtok(NULL, ,); }它有两个大坑一是会把原字符串里的分隔符改成\0即破坏原字符串二是用静态内部变量保存状态因此不可重入、不是线程安全的。多线程环境下请用strtok_r()。还有一个细节是strtok会跳过连续的分隔符也就是说a,,b分割出来只有a和b空串被丢弃了和Python行为不一样。C里一般用getline配合stringstream实现分割或者手写find循环。C17之后可以用std::string_view避免拷贝。C#的分割很有意思Split(new char[]{,})和Split(,)在较新版本里都能用了而且Split默认行为在.NET Core 3.0 以后发生了变化——StringSplitOptions.None不再保留空条目的问题需要通过参数StringSplitOptions.RemoveEmptyEntries来控制。3.2 反转字符串的三层境界“字符串逆序输出”在热搜里出现了很多次C语言、C、Python都有涉及看起来是基础题但做起来能分出好几个层次。境界一最简单的——倒着打印for (int i strlen(s) - 1; i 0; i--) { putchar(s[i]); }这不算真正的反转只是逆序输出原字符串没动。很多初学者以为这就是“逆序”但在后续处理中往往需要真的把字符串反转过来。境界二原地反转void reverse(char *s) { int len strlen(s); for (int i 0; i len / 2; i) { char tmp s[i]; s[i] s[len - 1 - i]; s[len - 1 - i] tmp; } }头尾交换即可注意边界是len/2奇数长度时中间字符不动。如果忘记\0比如用sizeof而不是strlen会把结束符也反转进去结果就是字符串里混入了\0打印出来只有一半。境界三处理Unicode的“真正”反转对于Python直接s[::-1]一步反转非常优雅。但如果你在Java里做同样的事要注意代理对问题。一个emoji被劈成两个code unit之后反转会变成乱码。正确的做法是String reversed new StringBuilder(s).reverse().toString();等等StringBuilder.reverse()其实也处理不好代理对——它只是反转UTF-16 code unit数组代理对一样会被劈开。在Java里要正确处理得先按codePoint拆开再反转int[] codePoints s.codePoints().toArray(); StringBuilder sb new StringBuilder(); for (int i codePoints.length - 1; i 0; i--) { sb.appendCodePoint(codePoints[i]); } String reversed sb.toString();这个问题在实际开发中不算罕见用户昵称里带个emoji很常见凡是做文本处理的地方都要想想“字符”和“code unit”是不是同一回事。3.3 拼接与替换的性能真相拼接字符串的性能不同语言差异很大。Python的循环拼接是大忌result for s in many_strings: result s # 每次循环都创建一个新字符串复杂度是O(n²)正确的写法result .join(many_strings)join一次遍历完成底层在计算好总长度后一次性分配内存。Java同理循环里str x在编译后其实就是StringBuilder的append但如果在循环内部反复创建StringBuilder也是浪费最好自己在循环外创建StringBuilder sb new StringBuilder(); for (String s : list) { sb.append(s); } String result sb.toString();JavaScript因为引擎优化在多数情况下还能忍但大量拼接时用数组push加join依然是可靠的选择。替换操作也有细节。Python的str.replace()默认替换所有匹配项Java的replace()和replaceAll()有区别——replaceAll用的是正则。最经典的坑是str.replaceAll(., x)会把每个字符都替换成x因为.在正则里匹配任意字符而replace(., x)才按字面量只替换点号。我在Code Review里见过好几次这类Bug。C里std::string::replace的参数是位置和长度不是像Python那样传旧子串找新子串做“find再replace”通常要自己写循环。C23终于有了string::contains和starts_with/ends_with算是补上了标准库的空白。4. 字符串与数字、编码的双向奔赴4.1 字符串转数字的十八般坑字符串转数字这个需求几乎天天遇到SQL Server的CAST/CONVERT、C#的Parse/TryParse、Python的int()/float()、Java的Integer.parseInt()、C语言的atoi/strtol。核心坑有两个维度非法输入处理和边界值。C语言的atoi最坑遇到非数字字符不会报错而是直接返回0atoi(abc)返回0atoi(123abc)返回123atoi()返回0——你根本分不清输入到底是0还是abc还是空串。生产环境请使用strtol并检查errno和尾指针char *end; errno 0; long val strtol(str, end, 10); if (errno ERANGE || end str) { // 溢出或没有有效数字 }Python的int()遇到非法字符串会抛ValueError这是好事。但注意int(1.5)会报错浮点字符串要转float再转int。int(0x10, 16)可以指定进制但int(10, 2)只能转二进制字符串必须是合法的目标进制数这个比parseInt(0x10)严格得多。JavaScript的parseInt非常包容但也因此非常坑parseInt(123abc)返回123parseInt(abc)返回NaN。如果用户输入是表单字段这种宽容会导致数据悄悄被截断。推荐用严格校验if (/^\d$/.test(str)) { let num parseInt(str, 10); }同时注意parseInt(08)在老版本浏览器IE里会被当成八进制解析返回0所以永远传第二个参数10。C#的int.Parse和Java的Integer.parseInt都是严格解析不合法就抛异常。C#更推荐int.TryParse(str, out var result)可以免去异常处理的性能开销。SQL Server里字符串转数字常用CAST(123 AS INT)但如果字符串是abc会直接报错中断所以转换之前要先用ISNUMERIC判断然而ISNUMERIC(1e2)返回1但CAST成INT还是会失败——因为它是科学计数法。这种边界情况非常多稳妥做法是先做数据清洗再转。4.2 数字转字符串的格式化陷阱数字转字符串看起来是反方向坑也不少。C里std::to_string很方便但注意它对于浮点数的转换精度可能是最短表示也可能不是不同编译器实现不同。如果要做固定小数位推荐用std::ostringstream配合std::fixed和std::setprecisionstd::ostringstream oss; oss std::fixed std::setprecision(2) 3.14159; std::string s oss.str(); // 3.14C#里double.ToString()默认返回最短往返字符串R要固定格式必须用ToString(F2)或ToString(0.00)。Python里str(3.14159)和repr(3.14159)在现代Python 3里结果基本一致都是最短表示但如果你要做格式化s f{value:.2f} # 或 format(value, .2f)而且round()返回的是float而不是str很多人把round(3.14159, 2)当成3.14来用结果发现是个3.14的浮点后续拼字符串的时候还要再str()。还有一个大坑是大数的字符串转换。C#里int转字符串很容易但如果数字超过long范围得用BigInteger.ToString()Python的int是任意精度的str(10**100)可以输出完整的100位数字这一点在跨语言对接的时候要小心——如果对方系统用long你的“超大整数”转成字符串过去后对方解析会溢出。4.3 字符与ASCII隐藏在编码背后的约定C#字符串转ASCII码这个场景一般是把字符变成对应的数字编码string s ABC; byte[] ascii Encoding.ASCII.GetBytes(s);输出的就是65、66、67。单字符可以用(int)A得到65。这个操作本身简单但背后的“ASCII / Unicode / UTF-8 / UTF-16”概念很容易混。简单说ASCII只有128个字符只覆盖英文字母、数字、标点和控制符。中文等非英文字符必须用Unicode字符集去编码。UTF-8是变长的Unicode编码方案英文1字节、中文3字节、emoji 4字节。UTF-16是Java和C#内部使用的编码方案常用汉字2字节扩展字符4字节。所以C#里Encoding.ASCII.GetBytes(你好)会把中文变成63问号因为ASCII编码不了中文。正确做法是Encoding.UTF8.GetBytes(你好)。Python里字符串直接赋值更改会报错因为str不可变但如果要对字符串做编码转换通常用encode/decodes 你好 b s.encode(utf-8) # bytes print(b.decode(utf-8)) # 转回来QT里QString和double转换也有编码细节。QString::number(3.14)可以转成字符串QString(3.14).toDouble()转回数字。但要注意toDouble在失败时返回0.0无法区分0.0和abc需要配合bool *ok参数判断bool ok; double d str.toDouble(ok); if (ok) { // 转换成功 }这类“失败返回0”的API设计本质上是一把双刃剑——方便但危险使用前先想清楚“0是合法输入还是转换失败”。5. 字符串排序与竞赛题实战思路5.1 字符串排序常见的三种维度字符串排序在热搜里多次出现也是面试和竞赛的常客。排序本身不难难的是“按什么规则排”。最基本的字典序排序C里直接用std::sort加默认比较std::vectorstd::string v {banana, apple, cherry}; std::sort(v.begin(), v.end());按长度排序std::sort(v.begin(), v.end(), [](const std::string a, const std::string b) { return a.length() b.length(); });Java里注意Arrays.sort对字符串数组默认也是字典序但Collections.sort对ListString一样是字典序。compareTo按比较顺序ListString list new ArrayList(); Collections.sort(list, (a, b) - a.length() - b.length());Python的sorted有key参数sorted(strs, keylen) # 按长度 sorted(strs) # 字典序 sorted(strs, keylambda s: s.lower()) # 不区分大小写的字典序有一种容易被忽略的排序规则是数字字符串的排序。[10, 9, 100]按字典序排出来是[10, 100, 9]因为字符1在9前面。如果想让数字字符串按数值排就得转成数字再排序或者用自然排序比较器。竞赛和实际开发中还有一种特殊排序“拼接后最大/最小”。这就是经典的**“拼数”问题**热搜里的c. 拼数(number)就是这个类型。5.2 拼数问题贪心排序的经典模型拼数问题一般长这样给出一组非负整数把它们拼接成一个最大的数或最小的数。比如[3, 30, 34, 5, 9]能拼出的最大数是9534330。最简单也最容易想到的方法是“按字符串字典序降序排”但这样往往不对——3和30按字典序降序是30在前3在后拼出来是303但330才是更大。这里的关键是比较规则要自定义bool cmp(const string a, const string b) { return a b b a; }这个比较器的含义是如果a放在b前面拼出来的字符串比b放在a前面更大那么a应该排在b前面。这个规则不是直观的字典序而是“通过拼接结果来决定排列顺序”数学原理是传递性和最优性可以证明。用这个规则对[3, 30, 34, 5, 9]排序得到9, 5, 34, 3, 30拼接出9534330正确。这类题目在GESP等竞赛中也屡屡出现比如“小杨有n个仅包含小写字母的字符串小杨想将这些字符串排序”“b4578 [gesp202609 三级] 分割字符串”等。GESP三级的题目一般不会太刁钻但考察的知识点很明确熟练掌握字符串分割、排序、比较、拼接的能力。三级题目里出现过的模式主要就是按分隔符切分字符串、按某种自定义规则排序、字符串转数字再处理、输出格式控制。竞赛刷题时我有个心得字符串题的难点永远不在于语言本身而在于边界条件和比较规则的确认。比如“分割字符串”这类题要先把所有边界情况列出来——开头有分隔符吗结尾有分隔符吗连续分隔符怎么处理空字符串保留吗——再去写代码。很多人在竞赛里丢分不是不会写分割而是没考虑到空串和边界。5.3 从竞赛到工程字符串题的经验迁移竞赛里的字符串题看起来“没用”但实际上和工程里的字符串处理是一脉相承的。拼数问题的自定义比较器思想移植到业务里就是“多个字段拼接后需要按结果排序”的场景分割字符串的边界处理就是CSV解析、日志切分时的核心逻辑字符串转数字的陷阱就是接口数据校验的日常。我遇到过这样一个真实业务需要把一批用户ID按“拼接后的字符串最小”输出给下游系统因为下游按字符串字典序排序后需要保证某种一致性。当时在代码里写了一个自定义比较器用例跑起来没问题但到了边界数据空字符串、超长字符串、前导0就出问题。后来才发现这个问题本质上就是拼数问题加上按值排序的双重变体处理的时候必须注意如果结果字符串开头是0应该去掉前导0否则会输出0还是000完全不同。数字可能很大转字符串时用语言原生的任意精度转换别手动取模。比较器的返回值必须是严格的偏序关系否则std::sort行为未定义。写a b b a时相等的情况要返回false避免cmp(a,b)和cmp(b,a)同时为真。这类细节只有真正被坑过才会长记性。6. 实际开发中的疑难杂症我踩过的那些坑6.1 宽字符与编码混乱中国程序员永远绕不开的坎热搜里有“codeblock 字符串 宽字符l 表示 出错”还有“ida显示中文字符串”。这类问题看着零散本质上都是编码不匹配。CodeBlocksMinGW GCC里的L宽字符串是wchar_t*类型在Windows上是2字节UTF-16在Linux上是4字节UTF-32。同一个L中文在不同平台上内存布局完全不同跨平台代码如果直接序列化这个字符串写出来的二进制格式都不一样。更常见的问题是源码文件用UTF-8保存但编译器默认按本地编码GBK解析导致中文字符串字面量变成乱码。解决办法是让源码文件编码和编译器默认编码一致VS里可以设置/utf-8编译选项GCC可以用-finput-charsetUTF-8 -fexec-charsetUTF-8。或者字符串统一用转义序列表示比如\xe4\xbd\xa0\xe5\xa5\xbd就是UTF-8编码的“你好”。IDA显示中文字符串乱码也是同一类问题ELF文件里的字符串如果是UTF-8但IDA的默认字符编码是ASCII或本地编码就会把中文字节按单字节显示成乱码。把IDA的编码设置改成UTF-8即可。我在实际开发中的一个习惯是所有涉及字符串的接口统一声明编码格式并在代码注释里写清楚。比如“入参utf8”、“出参utf8”、“配置文件必须是utf8无BOM”等。编码问题90%以上是因为双方默认不同说清楚约定就能消灭绝大多数Bug。6.2 C函数返回字符串的几种方式C函数返回字符串是热搜词因为这个看似简单的操作背后有三种选择每一种都有自己的适用场景方式一返回std::stringstd::string getName() { return Alice; }这是最推荐的方式。现代C有返回值优化RVO/复制省略返回std::string几乎不会发生额外拷贝。代价是函数每次被调用都会构造一个新字符串如果被频繁调用且只是只读使用会有些浪费。方式二返回const char*const char* getName() { return name_.c_str(); }如果name_是成员变量这个返回的指针在下次修改name_前是有效的。但如果函数里的字符串是局部变量返回局部std::string的c_str()就成了悬垂指针const char* getBad() { std::string s hello; return s.c_str(); // 悬垂指针 }函数结束、s析构指针指向的内存被释放外面用这个指针就是未定义行为。方式三通过出参返回void getName(std::string out) { out Alice; }这种方式适合“复用已有字符串对象”的场景可以在循环里反复调用而不产生新的分配。实际编码中我的原则优先返回std::string其次返回成员变量的const char*绝不返回局部变量的const char*。6.3 判断字母数字、驼峰命名和模板字符串日常小场景的高频写法最后聊几个热搜里的高频率小场景。Java判断字符串是否不是字母和数字也就是“如果包含非法字符就报错”的常见写法if (!str.matches([a-zA-Z0-9])) { throw new IllegalArgumentException(只允许字母和数字); }注意matches要求全匹配所以这里的表达式用表示至少一个字符。如果字符串可以为空改*。更好的写法是逐字符判断for (char c : str.toCharArray()) { if (!Character.isLetterOrDigit(c)) { // 非法 } }Character.isLetterOrDigit和[a-zA-Z0-9]并不完全等价前者接受Unicode字母比如中文字符也会被当作字母。如果业务上只要ASCII请用后者的正则。Python判断字符串是否驼峰式命名这个需求典型出现在解析XML属性名或参数名字段时。驼峰式判断的核心逻辑是首字母小写、后续单词首字母大写、不能有下划线或空格。一个简单的判断def is_camel_case(s: str) - bool: if not s or s[0].isupper() or _ in s: return False return all(c.isalnum() for c in s)实际上很多“驼峰判断”的难点在“什么是驼峰”的约定上——有的要求首字母小写lowerCamelCase有的要求首字母大写UpperCamelCase有的允许数字连缀且数字后首字母大写。先定义清楚再动手是这类小工具的核心。模板字符串JavaScript的反引号let name Alice; let msg Hello, ${name}!;模板字符串不只是拼接语法糖它支持嵌入表达式、多行文本还能配合String.raw处理转义。我在实际写代码时遇到涉及变量拼接的字符串一律用模板字符串比可读性好太多还不会写错引号嵌套。Python 3.6的f-string、C#的$插值字符串是同一类进化。C语言指针数组存放字符串这个其实是C语言的经典存储模型声明一个char *strs[]每个元素指向字符串常量。方便是方便但字符串本身只读且长度固定业务需要改字符串时就得换char strs[][MAX_LEN]二维数组或者char **动态分配。理解这些场景的本质就理解了为什么几乎所有现代语言都选择做“字符串是对象”而不让程序员手工管理。写在最后的经验之谈字符串处理写得多了我的体会是大多数字符串Bug不是出在“调用了错误的API”而是出在没有弄清楚当前语言的字符串模型——它是值类型还是引用类型可变还是不可变长度是按字节、字符还是code unit比较是按内容还是按引用\0是否存在转换失败时是抛异常还是返回默认值这些问题的答案在不同语言里完全不同但只要你肯花时间把某一个语言彻底弄清楚一次再看其他语言时只需要对照着看差异就够了。字符串就是一栋地基不太一样但结构相同的房子——数组、编码、不可变性、比较语义这四根柱子撑起了一切。如果以后遇到“字符串怎么表示”“为什么这个字符串相等判断不对”这类问题别急着上网搜先把这四根柱子验一遍多半就能定位到问题。字符串本质上就是一个字节序列加上一个解释规则规则对了一切都顺了。
返回列表