news 2026/9/30 4:23:11

Java String深度解析:从内存模型到常量池,一文搞定字符串高频考点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java String深度解析:从内存模型到常量池,一文搞定字符串高频考点

每次面试,只要候选人简历上敢写“熟悉 Java”,我几乎必问的一个问题就是:String 到底存在哪里?字符串常量池还是堆?不少人能答上“字面量在常量池,new 出来的在堆”。我一追问“那String str = new String("abc")到底创建了几个对象”,就开始翻车了。今天这篇就把 String 这个“最熟悉的陌生人”拆开揉碎,从内存模型、常用方法、拼接性能到高频易错点,一次讲清楚。适合刚学 Java 的基础党,也适合正在准备面试,或者写代码被字符串坑过无数次的同学。

1. String 为什么“不可变”?这背后是内存模型和常量池的设计

1.1 先搞清楚字符串在 JVM 里怎么放

String 在 JDK 8 之前底层是char[],JDK 9 开始优化成byte[]并引入coder字段,用来区分 Latin-1 和 UTF-16 编码。这是为了节省内存:纯英文场景下,一个 char 占 2 字节,一个 byte 只占 1 字节。但真正让 String 特别的是它被设计成了final class,内部核心字段private final char value[]或private final byte[] value,也就是说字符串对象创建之后,里面的字符序列就不能再被修改了。

JVM 为了性能和内存考虑,专门划了一块“字符串常量池”,存放字符串常量。当你写出"abc"这种字面量时,JVM 会先去常量池里查,有没有相同内容的字符串。有就直接复用,没有就创一个放进去。JDK 7 之后,字符串常量池从永久代移到了堆中。这个细节经常被拿来考,记忆点就是:常量池不是 JVM 里一个单独的神秘区域,它本质上是堆管理的一部分,只是逻辑上独立。

这里有个很常见的理解偏差:常量池里的字符串和普通的new String()对象,虽然内容可能完全一样,但在内存里是完全不同的对象。"abc"是常量的规范化存储,而new String("abc")是在堆上额外给你造了一个新对象。理解不了这点,后面的==和equals问题一定绕晕。

1.2 字面量和 new String() 的区别

直接看代码,这是面试里出现频率最高的代码片段之一:

String s1 = "abc"; String s2 = "abc"; String s3 = new String("abc"); System.out.println(s1 == s2); // true System.out.println(s1 == s3); // false System.out.println(s1.equals(s3)); // true

运行结果先记下来。s1和s2都指向常量池里的同一个对象,所以==为true。s3不是常量池里那个对象,而是在堆上 new 出来的另一个 String 对象,所以s1 == s3是false。但内容一样,所以equals是true。

这个例子背后藏着一个非常重要的原则:在 Java 里比较字符串内容,永远用equals,不要用==。==只比较引用地址,不比较字符串是否相等。除非你能百分之百确定两个引用指向同一个常量池对象,否则==就是给自己埋雷。

还有一个经典衍生题:String s3 = new String("abc")到底创建了几个对象?标准答案分两种情况:

  • 如果常量池里已经有"abc",那么只在堆上创建了 1 个对象。
  • 如果常量池里还没有"abc",那么会先在常量池创建 1 个 String 对象,再在堆上创建 1 个 String 对象,一共 2 个对象。

注意,日常面试讨论一般不算内部char[]/byte[]数组对象,否则可以追问到 3 个甚至更多。先把这个常规答案记牢,再深入一点就有优势。

1.3 为什么设计成不可变

很多初学者不理解:为什么要让 String 不可变?改成可变的多方便?实际上不可变背后有四个实打实的好处。

一是字符串常量池安全。既然多个引用可能指向同一个字符串对象,如果这个对象可以被修改,那一个地方改了,其他引用全部会被影响,程序瞬间乱套。不可变让常量池共享变得安全。

二是线程安全。不可变对象天然线程安全,不需要加锁,可以在多线程环境里放心共享。String 作为最常见的参数类型、日志信息、网络数据载体,这一条价值极大。

三是哈希缓存。String 重写了hashCode(),而且因为内容不可变,哈希值可以缓存在对象内部。这也是为什么 String 特别适合做 HashMap 的 key。如果字符串可变,hashCode 一变,HashMap 里的数据就找不到了。

四是安全性。比如我们读取一个文件名、路径、类名,如果拿到的是可变字符串,调用过程中可能被其他代码偷偷改掉,造成不可预料的后果。final class 还避免了被子类重写行为。

看似简单的不可变设计,其实是为了整个 JVM 和 Java 生态的稳定。这个知识点不光是面试用来背的,写代码时也应该有这种意识。

2. String 常用方法实战,附易错点

2.1 equals 与 ==:字符串比较的生死线

前面已经提到==和equals,这里再往深挖一层。Object.equals()默认就是==比较引用地址,而String重写了它,改成逐个字符比较内容。所以两个 String 内容相同,equals一定返回true。

看几个容易出错的变体:

String s = "abc"; StringBuilder sb = new StringBuilder("abc"); System.out.println(s.equals(sb)); // false System.out.println(sb.equals(s)); // false

为什么两个都是 false?因为String.equals()里做了一个instanceof String判断,StringBuilder 不是 String 的实例,直接返回 false;而 StringBuilder 没有重写equals(),用的是Object.equals()的引用比较,s 和 sb 根本就是两个对象,所以也是 false。如果想把 String 和 StringBuilder 内容比较,得先sb.toString()再 equals。

还有 null 的问题。很多新手喜欢写:

String str = getFromDb(); // 可能返回 null if (str.equals("success")) { ... }

一旦 str 是 null,这里直接抛 NullPointerException。所以更稳妥的写法是:

if ("success".equals(str)) { ... }

把常量放前面,变量为 null 也不会报错,就是 false。这个习惯养成之后,能帮你少踩无数坑。

2.2 intern():理解字符串拘留的妙用

String.intern()是一个容易被忽略,但面试很喜欢问的方法。它的作用一句话说:如果常量池里有内容相同的字符串,直接返回池中的引用;如果没有,就把当前字符串加入常量池并返回引用。

看这个例子:

String a = "abc"; String b = new String("abc"); System.out.println(a == b); // false System.out.println(a == b.intern()); // true

b.intern()返回的是常量池里那个"abc",所以和a是同一个引用。实际开发中,如果你的系统里有大量内容重复的字符串,比如从数据库读出来的状态值、编码值,可以考虑用 intern 去重,减少内存占用。

但 intern 不是万能的。频繁调用 intern 本身有额外的字符串查找和哈希操作,如果池里根本没有这个字符串,还可能引起常量池膨胀。尤其是 JDK 7 之前,字符串常量池在永久代,intern 用多了很容易 OOM。所以它更适合用在内容有限且重复度高的场景,不适合无脑乱用。

2.3 常用方法:split、replace、substring 的隐藏坑

这几个方法每个单独拿出来都看起来很简单,组合起来就变成面试易错点密集区。

先说split。split的参数是正则表达式,不是普通字符串。这意味着你想按.分割字符串,不能写str.split("."),因为正则里.代表任意字符,这样切割出来的结果往往不是你要的。必须转义:

String str = "a.b.c"; String[] arr = str.split("\\."); // 正确

同理,竖线|要写成str.split("\\|"),反斜杠本身要写成str.split("\\\\")。还有一个隐藏坑:split会丢弃尾部空字符串。看代码:

String str = "a,b,,"; String[] arr = str.split(","); System.out.println(arr.length); // 2,而不是 4

结果只有["a", "b"],因为默认 limit 为 0,尾部空串被丢弃。如果你需要保留尾部空字符串,要传负数:

String[] arr = str.split(",", -1); // ["a", "b", "", ""]

再说replace和replaceAll。replace是普通字符匹配,replaceAll是正则匹配。新人最容易犯的错误是用replaceAll替换字符串里的$或\。比如想把"price=100$"中的$替换成空,直接写str.replaceAll("$", "")会发现在末尾多了一个空替换位置,因为$在正则里是字符串结尾的锚点。正确做法是转义"\\$",或者干脆用replace("$", ""),因为 replace 不涉及正则,匹配的是字面字符。

最后说substring。在 JDK 6 时代,substring返回的新 String 并没有复制字符数组,而是继续引用原来的 String 底层数组。假如你截取了一个超大字符串的一小段,并长期持有,那整个大数组就一直被引用着没法回收,内存就泄漏了。JDK 7 之后改成复制字符数组,这个问题基本解决。如果你还在维护老 JDK 6/7 的项目,就需要谨慎,截取完后主动new String(str.substring(...))这种操作在一些老系统里确实有人这么干。

2.4 判空、空串与 null 的处理

很多 Java 老手也会在这里翻车。空串""是长度为 0 的字符串对象,而null表示引用不指向任何对象,两者有天壤之别。

String a = ""; String b = null; System.out.println(a.isEmpty()); // true System.out.println(b.isEmpty()); // NPE

最稳妥的判断写法是:

public static boolean isEmpty(String str) { return str == null || str.isEmpty(); }

如果你用的是 JDK 11 及以上,还可以考虑isBlank(),它不仅能判断空串,还能判断只包含空白字符的字符串,比如空格、Tab。" "用isEmpty()判断是 false,但用isBlank()判断是 true。这对处理用户输入的校验场景非常有用。

String.valueOf(null)这个也要注意。虽然String.valueOf(Object obj)在传 null 时返回字符串"null",但如果你直接调用String.valueOf(char[] data)并传 null,行为又不一样。实际开发里最常见的坑是字符串拼接:任何字符串和 null 用+拼接,null 会被输出成"null"。比如"结果:" + null的结果是"结果:null"。代码审查时经常看到有人因为这个把脏数据写进了日志,排查的时候一脸懵。

3. 字符串拼接:性能、原理与最佳实践

3.1 + 号拼接的背后:StringBuilder

很多人以为"a" + "b"就是简单字符串拼一起,没什么技术含量。但放到 JVM 层面,编译器会做很多手脚。

如果是两个字面量相加:

String s = "a" + "b";

编译期间就会直接优化成"ab",不会有任何运行时拼接。如果是变量相加:

String a = "a"; String b = "b"; String s = a + b;

编译器会生成类似这样的代码:

String s = new StringBuilder() .append(a) .append(b) .toString();

也就是说,+的本质是创建 StringBuilder 然后 append。用起来省事,但要小心循环里的情况。如果在一个循环里反复写s += item,每次循环都会 new 一个 StringBuilder,再调 toString,循环一万次就创建一万个临时对象,GC 压力巨大。

我自己写代码时确实见过这种“性能杀手”,在一个批量拼接邮件内容的场景里,循环里直接content += line,数据量上来后接口直接超时。改成外面放一个 StringBuilder,循环里只 append,性能立刻回暖。这不是玄学,是明摆着的对象创建和复制开销。

3.2 StringBuilder 与 StringBuffer 怎么选

两者的 API 几乎一模一样,区别就在线程安全和性能。StringBuffer 的公开方法基本都加了synchronized,所以是线程安全的;StringBuilder 没有加锁,线程不安全,但单线程下更快。

日常开发中,绝大多数拼接场景都在方法内部,变量不会逃逸到其他线程,所以优先用 StringBuilder。只有明确需要多线程共享同一个可变字符串缓冲区的时候,才考虑 StringBuffer。另外,StringBuffer 因为同步带来的额外开销,在并发竞争激烈时会放大,尽量不要为了“保险”而无脑使用。

还有一个容量问题。StringBuilder 和 StringBuffer 初始容量默认是 16。当内容超过容量时,底层数组会扩容,扩容规则大约是(旧容量 << 1) + 2,也就是变成原来的两倍再加 2。扩容意味着要申请新数组、复制旧数据,对象一大就很浪费。所以如果你能预估字符串最终长度,最好在创建时指定容量:

StringBuilder sb = new StringBuilder(1024);

这个习惯在小字符串上看不出差别,但当你拼接几 KB 甚至几十 KB 的内容时,提前设置容量能省下好几轮数组复制,实测对性能有不小帮助。

3.3 循环拼接的优化方案

很多场景其实没必要自己 new StringBuilder,JDK 已经提供了更高级的工具。例如把集合元素用逗号连接:

List<String> list = Arrays.asList("A", "B", "C"); String result = String.join(",", list);

或者用 Stream:

String result = list.stream() .collect(Collectors.joining(","));

这些方式内部都用了 StringBuilder,但帮你处理了分隔符边界,代码更简洁,也不容易出错。

如果一定要用循环拼接,记住这个模板:

StringBuilder sb = new StringBuilder(list.size() * 10); for (String item : list) { sb.append(item).append(','); } sb.deleteCharAt(sb.length() - 1); String result = sb.toString();

这套写法的关键是提前估算容量,避免扩容;同时把 append 链式调用,减少临时对象。虽然看起来比一行+啰嗦,但在高并发、大数据量场景下,差别非常明显。我的建议是:字符串长度小于几十个字符的简单拼接,直接+没问题,人类可读性最高;数据量大、循环里拼接,老老实实用 StringBuilder。

4. 面试高频题与八股串联

4.1 String、StringBuilder、StringBuffer 的区别

这是 Java 面试必考题,可以说覆盖了字符串核心知识点。我建议用一个表格来记忆:

对比项StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全安全(不可变)不安全安全(方法同步)
性能拼接时产生大量对象高略低
使用场景常量、少量拼接单线程大量拼接多线程共享缓冲区

面试官问这个问题时,不要只背结论,要能补充一句“String 的不可变带来了常量池共享、哈希缓存、线程安全等好处,所以它适合做 key 和通用参数;StringBuilder 和 StringBuffer 是为了解决字符串频繁修改时的性能和可变性问题出现的,单线程用 Builder,多线程用 Buffer”。这样回答既有深度,又显得你理解底层,而不是背了八股。

另外要提醒一点:StringBuffer 的线程安全和 String 的线程安全不是一回事。String 是因为不可变,所以多个线程读同一个引用不会出问题;StringBuffer 是通过锁保证每次方法调用原子性。但多个线程同时调用 append 后再做更复杂的操作,StringBuffer 也不保证复合操作的原子性。很多人拿这个概念糊弄面试官,一旦被追问就露馅。

4.2 new String("abc") 创建了几个对象?

前面已经提到大部分答案,这里补充一些容易忽略的上下文。你要先分清楚:

  • 如果常量池里已经有"abc",此时new String("abc")只是在堆上多创建一个 String 对象,重点在于这个对象的底层 value 数组也是新建的。面试的标准答案:创建了 1 个 String 对象。
  • 如果常量池里没有"abc",那么加载类的时候会先把"abc"作为常量放进常量池,然后new String("abc")又在堆上创建 1 个新对象。面试的标准答案:创建了 2 个 String 对象。

但这个题经常有变体:构造函数里的"abc"算不算?底层char[]算不算?如果面试官深挖底层,你可以说“从语言层面看是两个字符串对象;如果再算内部数组,会有更多对象”。这种回答显得你懂边界,而不是死记答案。

还有一道类似的题:String s = new String("a") + new String("b")创建了几个对象?这就更复杂了,涉及"a"、"b"、StringBuilder、"ab"等。面试里遇到这种问题,不要慌,先分析每一步创建了哪些对象,再给出结论。实际上,懂原理的人不会刻意去背数字,而是能现场推出来。

4.3 String 为什么用 final 修饰?

String 类被声明为final,也就是不能被继承。这是保证不可变设计不被破坏的关键。如果 String 可以被继承,那子类就能覆盖方法,甚至修改内部的 value 字段,整个字符串安全性就崩塌了。

除了整个类final,内部的value数组也用了final修饰,但这只能保证引用不被重新赋值,不能保证数组内容不被修改。懂反射的人可能会说可以用反射去改 value 数组里的字符,确实能改,但这是暴力破坏了 Java 的封装语义,实际代码里没人会这么干。面试时主动提这个点,能让面试官觉得你不是机械背概念。

final 还有一层含义是编译期优化。字符串常量在编译时就确定下来了,"a" + "b"可以变成"ab",大量依赖 final 常量的场景也能在编译期直接计算。String 作为 Java 里使用量最大的类,这种优化价值很高。

4.4 switch 用 String 的底层原理

Java 7 开始,switch 支持 String。很多人不知道底层的实现方式,面试容易被问倒。

本质上,编译器对 String 的 switch 不是直接比较字符串,而是先用字符串的hashCode()做整型 switch,然后在每个 case 分支里再用equals()做二次确认。为什么要这样做?因为 hashCode 可能碰撞,只有二次 equals 才能保证结果准确。

这里藏着一个易错点:switch的 case 不能为 null,switch 的变量也不能为 null,否则会在内部调用hashCode()时抛出 NullPointerException。所以如果你想对可能为 null 的字符串做 switch,一定先判空,或者把它转成默认值:

String result = getResult(); switch (result == null ? "" : result) { case "SUCCESS": ... break; default: ... }

这个处理在实际代码里非常实用,很多人接口返回 null 时直接在 switch 里炸了,就是因为没这个意识。

5. 实践中的坑与排查技巧

5.1 split 和正则容易踩的雷

除了前文提到的.、|、$,还有一个每过一阵子就有人踩的坑:用split("\\")切反斜杠。如果你想按单个反斜杠切分 Windows 路径C:\a\b\c,正则表达式需要一个正斜杠表示转义,所以代码得写"\\\\":

String path = "C:\\a\\b\\c"; String[] parts = path.split("\\\\");

每多一层转义,代码就越难看懂,但这就是正则的规则。如果不确定某个字符在正则里有没有特殊含义,最稳妥的办法是用Pattern.quote()包裹待分隔符,这样它会把输入当作字面量处理:

String[] parts = str.split(Pattern.quote("."));

这个方法虽然有点绕,但不会再为特殊字符转义头疼。另一个习惯是加入团队后,把这类正则字符转义写进团队的代码规范,避免反复踩坑。

5.2 substring 曾经的内存泄漏

这里再展开说一下。JDK 6 里的String内部是char[] value,substring(beginIndex, endIndex)返回的新 String 会通过调整偏移量和长度,直接复用同一个char[]。看起来高效,但隐藏一个致命问题:如果你截取一个 1 GB 大字符串的其中 10 个字符,并把截取结果长期持有,那 1 GB 的 char[] 因为被新对象引用着,永远不会被回收。这就是典型的内存泄漏。

JDK 7 改成了复制底层数组,新字符串独立持有自己的 char[],旧的大数组只要没有被原字符串引用就能回收。现在主流 JDK 已经不存在这个问题,但如果你在维护一些老项目,遇到“内存只增不减”的现象,可以怀疑是不是有老版本的 JVM 和 substring 在搞事。另外,就算现在不会有这个问题,如果你把一个超大字符串本身长期保存在变量里,只用了其中很小一段,这仍然是在浪费内存。尽早让大字符串变量失去引用,或者显式处理成小字符串,永远是好的习惯。

5.3 其他容易被忽略的坑

这里把我实际审查代码时经常看到的问题集中列一下,每个都有人中招过。

第一个是String.replaceAll替换内容里有$。比如你要把模板里的{var}替换成一个包含$的值,replaceAll的第二个参数里$有特殊含义,它会引用正则捕获组,结果跟预期完全不一样。遇到这种场景,优先用replace,或者用Matcher.quoteReplacement()包裹替换内容。

第二个是字符串常量拼接和变量拼接结果不一致。常量拼接在编译期就完成了,变量拼接走 StringBuilder。面试题常问:

String a = "hello"; String b = "hello"; String c = "he"; String d = c + "llo"; System.out.println(b == d); // false

因为 d 是运行时拼接出来的新对象,不在常量池里。但如果你用final String c = "he",那 c 也是编译期常量,d 的结果就会是 true。这个点极其容易混淆。

第三个是String.valueOf(null)的重载问题:

String.valueOf(null);

这行代码在 Java 里会调用String.valueOf(char[] data)这个重载方法,因为 null 可以匹配多个重载版本,编译器会选择最具体的类型。结果是什么?很遗憾,它并不是返回"null",而是直接抛 NullPointerException。因为这版 valueOf 里访问了参数长度。这种重载陷阱非常冷门,但一旦在面试里遇到就很容易让候选人措手不及。实际开发中尽量避免这种模糊调用。

第四个是字符串在 equals 比较时大小写不一致。比如状态码比对,有人写成"SUCCESS".equals(result),但 result 可能是"success",永远比较不过。需要忽略大小写时用equalsIgnoreCase()。

6. 个人实操心得与建议

折腾了这么多年 Java,我的体会是 String 的坑不在于 API 少,而在于 API 太常用,反而没人认真看文档。很多问题只要记住一个核心原则就能避免一大半:内容比较用 equals,不要用 ==;正则相关先查转义;拼接量大用 StringBuilder;判空前先考虑 null。

还有一个工程上的建议:实体类里的字符串字段,默认初始化成空串"",而不是 null。比如 Java Bean 的属性,如果从数据库读出来可能是 null,你返回给前端时就不确定字段是"null"还是不返回,很容易制造前后端联调的问题。与其到处写str == null ? "" : str,不如在某些明确允许为空的场景里直接用空串,让代码少一层判断。

另外,新 JDK 的 API 值得花时间熟悉。JDK 11 的isBlank()、strip()、repeat(),JDK 12 的indent(),JDK 15 的formatted(),这些都能让字符串处理代码更简洁。尤其是repeat(),以前要实现字符串重复 N 遍,得写循环拼接,现在一行搞定,性能还更稳。很多人还在用老 JDK 写旧代码,能接触新特性的时候没必要排斥。

最后分享一个小技巧,也是在代码 review 时我经常教新人的:凡是涉及字符串拼接后又做 equals 判断的场景,尽量先把拼接结果规范化,不要两边都拿+现拼现比。比如if (a + b).equals(c + d),中间产生的临时字符串既浪费内存,又容易让逻辑混乱。先定义好拼接结果,可读性和性能都会好很多。字符串没有特别多高深技巧,但把细节做到位,写出来的代码会稳很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 4:22:38

从零搭建AI工程体系:RAG检索增强与模型调用实战指南

1. 从零搭建AI工程体系&#xff0c;为什么我劝你别一上来就啃框架这两年AI应用开发的门槛肉眼可见地降低了&#xff0c;随便拉个前端都能用现成的API搭出一个能跑通的对话Demo。但真到了要把这套东西塞进生产环境、扛住真实流量、控制住成本、还得让模型输出稳定可控的时候&…

作者头像 李华
网站建设 2026/9/30 4:21:52

深度学习在物理层无线通信中的应用:原理、实现与工程避坑

简介&#xff1a;这是一份《通信学报》2019年发表的学术论文PDF&#xff0c;主题聚焦深度学习在物理层无线通信中的应用&#xff0c;适合通信工程、机器学习交叉领域的研究者与高年级学生阅读。论文从5G高可靠与高容量需求出发&#xff0c;分析现有物理层技术面临的模块化设计与…

作者头像 李华
网站建设 2026/9/30 4:21:52

二体问题相对运动方程完整推导:从质心系到约化质量

二体问题相对运动方程的完整推导二体问题&#xff08;two-body problem&#xff09;几乎是所有做轨道力学、天体物理、甚至分子动力学模拟的人绕不开的第一道关。我刚接触数值模拟那会儿&#xff0c;总习惯直接拿起牛顿第二定律&#xff0c;把两个天体分别写成两个独立的加速度…

作者头像 李华
网站建设 2026/9/30 4:21:38

SSRF服务端请求伪造完全指南:从原理到实战的漏洞利用手册

“SSRF是什么&#xff1f;”“SSRF能做什么&#xff1f;”“怎么绕过SSRF限制&#xff1f;” SSRF&#xff08;Server-Side Request Forgery&#xff09;是Web安全中最危险的漏洞之一。它允许攻击者让服务器代替自己去发起请求&#xff0c;从而访问内网资源、读取文件、甚至攻击…

作者头像 李华
网站建设 2026/9/30 4:21:01

基于Matlab的风-水电联合优化调度EI论文复现全流程解析

最近要做一篇EI论文的复现&#xff0c;方向是风-水电联合优化运行分析&#xff0c;平台选Matlab。论文前前后后读了三遍&#xff0c;代码写了五个晚上&#xff0c;中间推翻过一次建模思路&#xff0c;最后总算把输出曲线和论文图表对上了。这篇就把完整的复现过程摊开讲一讲&am…

作者头像 李华
网站建设 2026/9/30 4:20:41

C++自定义字面量实战:从单位换算到编译期校验

C 的每个版本都会带来一些让代码读起来舒服的小特性&#xff0c;自定义字面量&#xff08;user-defined literals&#xff09;绝对算其中之一。它最早在 C11 里出现&#xff0c;之后我在自己的工程里几乎一直在用&#xff0c;尤其是在写单位换算、时间处理和测试数据构造的时候…

作者头像 李华