说实话,字符串这门“基本功”在开发圈子里经常被低估。很多刚入行的朋友以为字符串就是“一串字符”,查个长度、拼一下、转个数字就完事了。可真到项目里跑起来,SQL Server里转数字报错、Oracle里小数点的0不见、C语言里截个字符串结果乱码、JavaScript里判断包含还踩了正则的坑——这些热搜词背后,全是真实的开发事故。这篇内容我会围绕字符串的底层逻辑和跨语言实操展开,把最常踩的坑、最常用的函数、最实用的写法一次讲透。不管你写C、C++、Java、JavaScript、Python,还是天天跟SQL Server、Oracle打交道,这篇都值得存一份当工具书翻。
1. 字符串到底是个什么“东西”?先把这个底层逻辑搞透
1.1 C语言:字符串就是字符数组的“某种习惯”
很多人学C语言时,对字符串的第一印象是 char *s = "hello",然后下意识觉得字符串是一种“内置类型”。实际上C语言根本没有字符串类型,所谓字符串不过是一段连续的字符数组,用 \0 作为结束标志。这个设计在上世纪70年代很实用,因为内存极其金贵,一个结束符就能判断边界,不需要额外存长度。但放到今天,它带来的问题非常多:数组越界、字符串拼接需要手动管理内存、中文字符可能因为编码问题被截断成半个字符。
你需要记住的核心结论是:C字符串的长度不是由数组大小决定的,而是由第一个 \0 决定的。strlen 函数就是从头数到 \0 为止,它不会管你分配了多少字节。所以如果你把一个没有 \0 的字符数组交给 strlen,它会一路数到你内存里的未知区域,返回一个“惊喜”数字。这也就是为什么 char buf[10] = {'h','e','l','l','o'} 里 strlen(buf) 输出是5,而 char buf[10] = {'h','e','l','l','o','\0'} 你才能确定它一定是5。
理解了这一点,很多热搜问题就迎刃而解。比如“字符串逆序输出c”“c语言字符串函数”,本质上都是在操作字符数组和指针,不是在操作一个“高级类型”。我在给新人讲的时候常说:C语言的字符串就是一个数组+终止符的约定,谁忘记 \0,谁就要为内存越界买单。
1.2 C++ string与Java String:可变与不可变的分水岭
到了C++,标准库提供了 std::string,它替你管理了字符数组的内存,支持自动扩容、拼接、查找,用起来比C的 char[] 舒服得多。但C++的 std::string 是可变的,也就是说你可以直接修改某个位置的字符,比如 str[0] = 'a' 完全合法。这在性能上是优势,但在多线程环境下需要留意同步问题。
Java的 String 则走了另一个极端:它是不可变的。String s = "abc" 之后,你没有任何办法去修改s内部的字符,所有看起来像修改的操作(比如 replace、concat),底层都是新建了一个新的字符串对象。这个设计的核心原因是安全性和缓存:字符串常量可以被安全地共享,哈希值可以提前缓存而不用担心失效,作为HashMap的key时也不会因为内容变化导致查找错乱。
但不可变带来的缺点就是字符串拼接性能差。如果你在循环里写 s += "a",Java会反复创建新对象,循环一万次就有一万个中间字符串产生。正确做法是用 StringBuilder 或 StringBuffer。这也是为什么很多Java新人面评时,面试官总会问“String、StringBuilder、StringBuffer的区别”的原因。
JavaScript和Python里的字符串也是不可变的。Python中 str.replace() 返回新字符串,原字符串纹丝不动;JS中 str.toUpperCase() 同样返回新值。所以在动态语言里,“修改字符串”这个说法严格来讲不成立,你只是把变量重新指向了一个新字符串。
1.3 编码问题:字符串背后是字节,不是字符
如果只记“字符串是字符数组”,你会在中文处理上吃大亏。因为计算机存储的永远只有字节,字符是字节按照一定编码规则解析后的结果。早期美国程序员用ASCII,一个字符占一个字节,刚好128个符号。但中文至少需要两个字节,于是出现了GBK、GB2312、UTF-8、UTF-16等一堆编码方案。
UTF-8是目前事实标准,它的聪明之处在于:ASCII字符仍占1字节,和英文文档完全兼容;中文、日文等字符占3到4字节,靠高位字节区分。这意味着你在C语言里“数到第5个字符”时,如果直接用下标访问 char 数组,遇到中文就会把多字节字符拦腰截断,得到一堆乱码。很多帖子问“c 取字符串中的连续几个字母”,在纯英文环境下 strncpy 没问题,一旦出现中文,这个函数就不够用了,因为它按字节复制,不关心多字节字符的边界。
我个人的项目经验是:内存和网络传输阶段默认按UTF-8字节流处理,显示和业务逻辑阶段再交由库函数转码。C/C++用UTF-8字符串字面量处理中文,Java/Python内部字符串统一是Unicode(UTF-16或Unicode码点),外部文件读写时显式指定编码,能避开90%的乱码问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字符串转数字:从SQL Server到Oracle,一路都是坑
2.1 SQL Server:CAST、CONVERT、TRY_CAST三件套
热搜里“sqlserver 字符串转数字”是个经典需求。你可能遇到的情况是:某个字段在数据库里存的是 varchar,里面全是数字,但你要和 int 类型的列做关联或计算。这时候你可能会直接写:
sql复制SELECT CAST(age AS INT) FROM users WHERE age IS NOT NULL
如果 age 字段里所有值都是纯数字,这样没问题。但只要有一行是 '12a' 或 '',整个查询就会报“将varchar转换为数据类型int时失败”。这是SQL Server的严格转换规则:CAST 和 CONVERT 遇到非法字符直接抛错,整个批处理回滚,不会像某些语言那样给你一个 NaN。
所以更稳妥的写法是使用 TRY_CAST 或 TRY_CONVERT:
sql复制SELECT
TRY_CAST(age AS INT) AS age_int,
CASE WHEN TRY_CAST(age AS INT) IS NULL THEN '非法数字' ELSE '正常' END AS check_result
FROM users
TRY_CAST 在转换失败时返回NULL,而不是报错。这个特性在你清理数据、匹配旧系统数据时特别有用。同理,如果你在 WHERE 条件里需要过滤出“能转成数字”的行,可以写成:
sql复制WHERE TRY_CAST(age AS INT) IS NOT NULL
这就避免了早年大家用 ISNUMERIC 的坑。ISNUMERIC 的判断标准非常宽泛,它会把 '+'、'-'、'.'、'$' 都当成数字。你用它过滤完再转,照样报错。所以记住:SQL Server里做字符串转数字,无脑优先考虑 TRY_CAST,需要精度再考虑 TRY_CONVERT(decimal(18,2), column)。
2.2 Oracle:TO_NUMBER和那个“小数点前0丢失”
Oracle里字符串转数字用 TO_NUMBER:
sql复制SELECT TO_NUMBER('123.45') FROM dual;
但搜索引擎里有个高频词“oracle数字转字符串小数点前0丢失”,这个场景非常典型。比如你有一个数字 0.50,想转成字符串 '0.50',直接写 TO_CHAR(0.50) 的结果是 '.5',前面的0不见了,后面的0也没了。
原因在于Oracle的 TO_CHAR 默认用的是“最短有效数字”格式,小数点前如果只有0,它会把0省掉。解决办法是指定格式模型:
sql复制SELECT TO_CHAR(0.50, 'FM0.00') FROM dual; -- 输出:0.50
SELECT TO_CHAR(0.50, '0.00') FROM dual; -- 输出: 0.50(注意前面有个空格,FM作用就是去掉空格)
FM 是“Fill Mode”,去掉前导和后缀空格,0.00 表示小数点前后至少保留一位整数和两位小数。如果你处理的是金额,强烈建议显式写 TO_CHAR(amount, 'FM999990.00'),其中9表示“有数字就显示数字,没有就空着”,0表示“没有数字也要显示0”。搞清楚9和0在格式模型里的区别,你就再也不会被Oracle转字符串的0弄晕。
2.3 Java/C/C++/Python:四门语言,四个脾气
Java里字符串转数字最常用的是 Integer.parseInt(String) 和 Double.parseDouble(String)。但它们对格式极敏感:前面有空格会失败(不过 trim() 一下就好),有千分位逗号会失败,有单位后缀会失败。Integer.valueOf 和 parseInt 的区别在于返回值,前者返回包装类型,后者返回基本类型。如果你需要容错,可以用 NumberUtils.toInt(Apache Commons)或在catch块里兜底。
C语言的老牌函数是 atoi 和 strtol。atoi 不检查错误,传给它 "abc" 它返回0,你根本区分不了是“转换失败”还是“字符串本来就是0”。所以项目里我强烈建议用 strtol 或 strtod:
c复制char *end;
long val = strtol("123abc", &end, 10);
if (*end != '\0') {
printf("转换不完全,剩余部分: %s\n", end);
}
strtol 的第二个参数会告诉你转换停在哪个位置,这样你能识别出 "123abc" 这种混合字符串,而不是默默得到123就完事。C++里则推荐 std::stoi、std::stod,它们会抛出 std::invalid_argument 和 std::out_of_range 异常,比C风格函数安全得多。
Python的 int("123") 和 float("123.45") 语法简洁,但同样会抛 ValueError。可以这么兜底:
python复制def to_int(s, default=0):
try:
return int(s)
except (ValueError, TypeError):
return default
JavaScript则是最“佛系”的语言:parseInt("123") 返回123,parseInt("123abc") 也返回123,因为它会解析到第一个非法字符为止。而 Number("123abc") 返回NaN,+"123" 返回123。很多人在这上面栽跟头,就是因为没搞清 parseInt、Number、一元加号三者的行为差异。
2.4 判断一个字符串是不是数字:正则与异常捕获
和“字符串转数字”强相关的是“java 判断字符串是否数字”。我见过太多人一上来就写正则 \\d+,结果没考虑负数、小数、科学计数法。其实要根据业务场景来决定:
- 只允许非负整数:
^\d+$ - 允许整数(含负号):
^-?\d+$ - 允许小数:
^-?\d+(\.\d+)?$ - 允许科学计数法:
^-?\d+(\.\d+)?([eE][+-]?\d+)?$
但正则也有风险,比如 " 123 " 这种带首尾空格的字符串,正则可能判断为false,实际业务中你又希望它是true。更通用的方案还是“先尝试转换,捕获异常”。这里我不想给一个万能答案,给个处理思路:
java复制public static boolean isNumeric(String str) {
if (str == null || str.trim().isEmpty()) return false;
try {
new java.math.BigDecimal(str.trim());
return true;
} catch (NumberFormatException e) {
return false;
}
}
BigDecimal 能覆盖整数、小数、科学计数法,兼容性极好。唯一的代价是性能不如手写正则,但对大多数业务系统来说完全够用。
3. 截取、分割、拼接:字符串的“切菜功夫”
3.1 SQL Server截取到某个字符:CHARINDEX + SUBSTRING 的组合拳
“sqlserver 截取字符串到某个字符”这个需求很常见。比如你有一个字段存的是“姓名:张三,年龄:18”,你想把冒号后面的姓名取出来。最直观的写法是用 CHARINDEX 找到冒号位置,再用 SUBSTRING 截取:
sql复制DECLARE @str NVARCHAR(100) = N'姓名:张三,年龄:18'
SELECT SUBSTRING(
@str,
CHARINDEX(':', @str) + 1,
CHARINDEX(',', @str) - CHARINDEX(':', @str) - 1
) AS name
逻辑不复杂:先找 : 的位置,再找 , 的位置,两者相减得到长度。这是SQL Server处理变长字符串的惯用套路。如果你想要的是“截取某个字符右边的所有内容”,可以写 RIGHT(@str, LEN(@str) - CHARINDEX(':', @str)),注意 RIGHT 的第二个参数是字符数,不是位置。
这里有个细节:CHARINDEX 找不到子串时会返回0,所以你要先判断位置是否大于0,否则 SUBSTRING 的计算会出现负数,直接报错。写成这样更稳:
sql复制IF CHARINDEX(':', @str) > 0
SELECT SUBSTRING(@str, CHARINDEX(':', @str) + 1, LEN(@str))
3.2 C#的Substring与IndexOf,以及“连续几个字母”
C#的截取方法和SQL Server类似,Substring(起始索引, 长度),配合 IndexOf 寻找位置。比如:
csharp复制string str = "hello world";
int pos = str.IndexOf(' ');
string first = str.Substring(0, pos); // "hello"
与Java不同的是,C#的 Substring 在索引越界时会抛 ArgumentOutOfRangeException,所以务必先判断 pos >= 0 再用。如果你需要“取字符串中的连续几个字母”,C#里就是 Substring(start, length),这是最正统的做法。
但有一个容易被忽略的点:C#的索引和大多数语言一样从0开始,而SQL Server的 SUBSTRING 居然从1开始。如果你在两个体系之间来回切换,很容易把位置计算错。我自己在这种跨语言开发时,会先在注释里写清楚“起始位置是0还是1”,避免半小时后自己都忘了。
3.3 字符串分割与拼接:从Split到join
“字符串分割”现在是各语言最基础的能力,但用法细节差异很大:
| 语言 | 分割写法 | 注意事项 |
|---|---|---|
| JavaScript | str.split(',') |
空字符串分割会返回字符数组 |
| Java | str.split(",") |
参数是正则,. 需要转义成 \\. |
| Python | str.split(",") |
不传参数时按空白字符分割 |
| C++ | 手写或用 getline |
标准库无直接split,需要配合 std::stringstream |
| C# | str.Split(',') |
参数是字符或字符串数组,不是正则 |
| SQL Server | STRING_SPLIT(str, ',') |
SQL Server 2016+,且结果行序不保证 |
其中最容易踩坑的是Java。String.split(".") 得到的结果是空数组,因为 . 在正则里表示“任意字符”。如果你要按.分割,必须写 "\\."。如果你要按 | 分割,也要写 "\\|",因为 | 是正则里的或操作符。
拼接方面,SQL Server和Oracle有差异。SQL Server 2017+提供了 STRING_AGG,Oracle则有 LISTAGG:
sql复制-- SQL Server
SELECT STRING_AGG(name, ',') FROM users;
-- Oracle
SELECT LISTAGG(name, ',') WITHIN GROUP (ORDER BY name) FROM users;
旧版SQL Server只能用 STUFF 配合 FOR XML PATH,那是另一段陈年血泪史,能不用就别用。
3.4 两个文本框里的字符串连接:初学者的第一个小项目
“两个文本框里的字符串连接在一起怎么弄”这个热搜词一看就是刚入门的朋友搜索的。这个需求本身很简单,但背后涉及字符串拼接和类型转换两个知识点。用JavaScript举例:
html复制<input id="a" type="text">
<input id="b" type="text">
<button onclick="join()">连接</button>
javascript复制function join() {
var a = document.getElementById('a').value;
var b = document.getElementById('b').value;
console.log(a + b); // 直接拼接
console.log(`${a}${b}`); // 模板字符串拼接
}
如果是数字相加,记得先转换:
javascript复制var sum = Number(a) + Number(b);
这里要提醒新手:input.value 拿到的永远是字符串,"1" + "2" 得到 "12",不是 3。学习字符串的时候,先建立“字符串和数字是不同的类型,不能靠运算符自动判定意图”这个意识,能减少很多bug。
4. 查找、包含、比较、判断:字符串的“体检”手段
4.1 JavaScript和Java怎么判断字符串包含
“js判断字符串是否包含”几乎是每个前端新人的第一课。ES6之后你可以直接写 str.includes("abc"),返回 true 或 false。这是最简洁的写法,推荐优先使用。
但你还会在老项目里看到 str.indexOf("abc") !== -1,这是ES6之前的标准写法。indexOf 找不到会返回 -1,所以判断条件就是“不等于-1”。还有一种写法是 str.search("abc") !== -1,search 接受正则,缺点是性能不如 indexOf、includes。
另一个高频需求是从字符串开头/结尾判断:
javascript复制str.startsWith("abc"); // 开头
str.endsWith("xyz"); // 结尾
如果目标浏览器比较老,也可以用 str.slice(-3) === "xyz" 来替代 endsWith。
Java 8及以下没有 contains 的简单版吗?其实有:str.contains("abc") 是Java字符串类自带的方法,返回布尔值。它底层调用的是 indexOf 逻辑。但如果你需要忽略大小写,Java里必须写 str.toLowerCase().contains("abc".toLowerCase()),没有JS那种 includes("abc", {ignoreCase: true}) 的原生参数。
4.2 SQL Server包含判断的三种写法:LIKE、CHARINDEX、PATINDEX
“sqlserver 字符串包含判断方法”在SQL Server里有多条路可以走:
sql复制-- 1. LIKE 模糊匹配
SELECT * FROM users WHERE name LIKE '%张%'
-- 2. CHARINDEX 位置判断
SELECT * FROM users WHERE CHARINDEX('张', name) > 0
-- 3. PATINDEX 模式匹配
SELECT * FROM users WHERE PATINDEX('%张%', name) > 0
三者的区别:LIKE 是SQL最直观的写法,性能也不错,适合简单的包含判断;CHARINDEX 是函数式判断,如果你后续还要截取子串,用它能直接拿到位置;PATINDEX 支持正则风格的 % 和 _ 模式匹配,适合处理“匹配数字开头”这种需求。
但要注意,SQL Server的 LIKE 默认不区分大小写,除非你用的是二进制排序规则。如果想区分大小写,可以给列指定 COLLATE Latin1_General_CS_AS。这是很多开发者在联调环境没发现、上了生产突然发现“大小写不区分了”的原因。
4.3 字符串比较:C的strcmp、Java的equals、JS的===
字符串比较看似简单,坑比想象中多。C语言里不能直接写 if (str1 == str2),因为那比较的是两个指针变量的地址,而不是内容。必须用 strcmp:
c复制if (strcmp(str1, str2) == 0) {
// 相等
}
strcmp 按字典序比较,返回值小于0、等于0、大于0分别表示第一个字符串小于、等于、大于第二个字符串。所以 strcmp 也是字符串排序时比较函数的底层基础。
Java里 == 比较的是引用地址,不是内容。字符串内容比较必须用 equals:
java复制if (str1.equals(str2)) { ... }
if (str1.equalsIgnoreCase(str2)) { ... } // 忽略大小写
要特别提出 String 的“池化”现象:String s1 = "abc"; String s2 = "abc"; 由于字符串常量池的存在,s1 == s2 可能返回 true,因为两个引用指向了同一个对象;但 String s3 = new String("abc"); 会强制创建一个新对象,s1 == s3 返回 false。很多初级程序员因此被搞得晕头转向。经验法则:永远用 equals 判断内容是否相等,不要依赖 == 的偶然行为。
JavaScript的比较规则就更“玄学”了。== 会做类型转换,"1" == 1 是 true;=== 不仅比较内容还比较类型,所以 "1" === 1 是 false。在项目里我建议一律使用 ===,因为Javascript这种隐式转换容易掩盖bug。
4.4 判断字符串是不是JSON:JSON.parse真的够吗
“判断字符串为json格式”这个话题在前后端联调、配置文件解析时经常遇到。最朴素的想法是:
javascript复制function isJson(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
这个写法对付90%的场景够了,但要注意两个边界:
JSON.parse("123")不会抛异常,返回数字123,它是合法JSON。JSON.parse('"hello"')返回字符串hello,也是合法JSON。JSON.parse("null")返回null,同样是合法JSON。
所以如果你要判断的是“是不是一个JSON对象/数组”,光靠 JSON.parse 不够,还需要再判断解析结果的类型:
javascript复制function isJsonObject(str) {
try {
const val = JSON.parse(str);
return val !== null && typeof val === 'object' && !Array.isArray(val);
} catch (e) {
return false;
}
}
Java里可以用 ObjectMapper(Jackson)或 Gson,也是同样的逻辑:解析成功 + 解析结果为合适类型,才叫合法。
4.5 iOS判断字符串尾部特定字符:hasSuffix的替代方案
“ios开发 判断字符串尾部几位是特定字符串方法”这个热搜词指向的是Objective-C或Swift里判断后缀的需求。Swift写起来很简单:
swift复制let str = "hello.txt"
if str.hasSuffix(".txt") {
print("是TXT文件")
}
OC里则是:
objc复制if ([str hasSuffix:@".txt"]) {
NSLog(@"是TXT文件");
}
但如果要判断的是“尾部几位字符是否等于某个特定字符串”,直接用 hasSuffix 就够了;若是需要裁剪,可以用 substringFromIndex 或 suffix(3):
swift复制let last3 = str.suffix(3) // 取末尾3个字符
iOS开发中另一个常见坑是字符串里有Emoji时,Swift的 count 返回的是“字形簇”数量,不是 NSString 的 length 字节数。你拿 NSString 的length去做截断,一个Emoji可能占2个UTF-16单元,导致截取出半个字符。所以做字符串处理时,尽量一致性使用Swift原生 String 的API,不要混用 NSString 的length做索引计算。
5. 逆序、排序、字符级操作:字符串里的“算法题”专题
5.1 字符串逆序:从C语言经典写法到PTA的注意事项
“字符串逆序输出c”和“字符串逆序c语言pta”这两个热搜词,说明很多人在刷题场景遇到了这个经典题目。C语言里最地道的是用双指针:
c复制#include <stdio.h>
#include <string.h>
void reverse(char *str) {
if (str == NULL) return;
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
char tmp = str[left];
str[left] = str[right];
str[right] = tmp;
left++;
right--;
}
}
int main() {
char s[] = "hello";
reverse(s);
printf("%s\n", s); // olleh
return 0;
}
关键点在于:char s[] = "hello" 是可修改的字符数组,它可以被逆序。但 char *s = "hello" 指向的是字符串字面量,字面量可能存放在只读区,你尝试修改它就会触发段错误。这是C初学者最容易踩的坑:第一眼看起来 char s[] 和 char *s 差别不大,但在字符串逆序这类需要原地修改的场景里,一个是“可以改的数组”,一个是“只读的内存”。
PTA(程序设计实验辅助教学平台)版本的“字符串逆序”通常要求不是原地逆序,而是输出逆序字符串,比如读入一行字符串后倒序输出。这时你只需要从尾部往前遍历打印即可,不涉及修改原字符串,代码更安全。
5.2 字符串排序:字典序在编程里的真正含义
“字符串排序”这个词包含两个层面:一是对一个字符串内部的字符排序,二是对多个字符串按字典序排序。
字符排序的实现比较简单。C语言可以用 qsort,配合一个比较单个字符的回调;C++可以用 std::sort 作用于 std::string 的迭代器:
cpp复制std::string s = "dcba";
std::sort(s.begin(), s.end());
// s 变成 "abcd"
JavaScript里字符串不可变,需要先转数组再排序再拼接:
javascript复制const sorted = str.split('').sort().join('');
多个字符串排序在各大语言都有现成的sort函数,默认按字典序排序。注意C语言中 strcmp 的返回值决定了比较结果,所以它是C排序比较函数的核心。Java的 Collections.sort(list) 对字符串默认也是字典序。Python同理,sorted(["banana", "apple", "cherry"]) 就是按Unicode码点排序。
这里要理解“字典序”的本质:不是看字符串长度,而是从第一个字符开始比较,字符的数值(编码值)决定顺序。比如 "abc" < "abd","abc" < "abcd",因为先比完abc相等,较短的字符串“更小”。全角字符数字和半角字符数字的排序结果不同,因为它们编码值不同——这在大全中文业务排序时很容易困惑。
5.3 逐字符读取字符串:C++与C的区别
“c++逐字符读取字符串”经常出现在处理输入或加密解密的场景里。C里最朴素的是用下标循环:
c复制char str[] = "hello";
for (int i = 0; i < strlen(str); i++) {
printf("%c\n", str[i]);
}
但这种写法每次循环都调用一次 strlen,效率是O(n²)。更专业的是先存长度:
c复制int len = strlen(str);
for (int i = 0; i < len; i++) { ... }
C++则会用迭代器或范围for:
cpp复制std::string s = "hello";
for (char c : s) {
std::cout << c << std::endl;
}
范围for的底层就是迭代器,它不会越界,也不依赖字符串长度,安全性和可读性都比C语言下标好。如果想同时拿到字符和下标,可以用传统的索引循环或用 std::distance。补充一个小技巧:C++迭代器循环中想“修改当前字符”,需要把 char c 改成 char& c。
5.4 大小写转换、数组交集、字符数组初始化:字符串的“精细化处理”
大小写转换在不同语言里都有现成API,但要注意不是所有字符都有“大小写”概念,比如中文、数字、符号在转换后不变。
cpp复制// C++
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
// Java
String upper = str.toUpperCase();
// JavaScript
const upper = str.toUpperCase();
// SQL Server
SELECT UPPER(name) FROM users;
“js 字符串数组取交集”这个需求会用到数组的 filter 和 includes:
javascript复制const arr1 = ["a", "b", "c"];
const arr2 = ["b", "c", "d"];
const intersection = arr1.filter(item => arr2.includes(item));
// ["b", "c"]
如果需要去重,再用 Set 包一层。
“c++字符串数组初始化”这个热搜词,其实有个经典陷阱:string arr[3] = {"a", "b"} 是合法的,编译器将第三个元素默认构造为空字符串 ""。但如果数组类型是 char* 而不是 std::string,char* arr[3] = {"a", "b"}; 则是初始化了三个指针,其中 arr[2] 是空指针,你没有初始化它——访问它会导致未定义行为。所以用 std::string 数组比 char* 数组安全得多。
6. 工程实践中的字符串:格式化、编码、工具链与冷门场景
6.1 格式化字符串:模板字符串、printf以及pwn题思路
“模板字符串”在JavaScript里是反引号:
javascript复制const name = "张三";
const greeting = `你好,${name}`;
重点是它支持换行和嵌入任意表达式,用起来比字符串拼接清晰很多。在Java 15+也有 String.formatted 或 String.format:
java复制String s = String.format("你好,%s,今年%d岁", name, age);
C语言的printf就不多说了,%s 代表字符串,%d 代表整数。但“pwn题之格式化字符串”这个热词非常有意思——它指的是安全领域的格式化字符串漏洞。简单来说,如果程序把用户输入直接当成 printf 的第一个参数,攻击者就可以用 %x、%n 这类格式符读取或改写栈上的数据。
举个最简单的例子:
c复制printf(user_input); // 危险写法
printf("%s", user_input); // 安全写法
当 user_input 为 "%x %x %x" 时,printf会按格式符去栈上取参数,从而泄露内存数据。这个问题在CTF和真实漏洞中都很经典。核心教训是:格式化函数第一个参数必须是格式字符串,永远不要让不可信输入直接作为格式字符串使用。这不仅是安全要求,也是代码规范要求。
6.2 编码与类型转换:MFC TCHAR、VC字节数组、Delphi字典Key
“mfc tchar字符串操作”是老Windows/MFC开发者的老朋友了。TCHAR是一个“自适应”类型:在ANSI编译环境下它是 char,在Unicode环境下它是 wchar_t。为了兼容两种编码,MFC提供了一组宏:_T("字符串") 会根据编译选项自动加宽字符前缀 L。
但现代开发其实已经不用纠结这个了,新项目全部走Unicode,直接用 std::wstring 或 CString 的宽字符版本即可。如果你还在维护老项目,记住一条:文件读写和网络传输尽量转成UTF-8,内部内存处理用宽字符,可以省去大量转换bug。
“vc 字节数组转换成字符串”是一个很典型的“想当然”问题。字节数组 BYTE data[] = {0x48, 0x65, 0x6C, 0x6C, 0x6F} 可以在ASCII编码下解释成 "Hello"。但你要让程序知道“这些字节就是UTF-8/GBK/ASCII的编码结果”,才能正确显示。C++里的做法是:
cpp复制std::string str(reinterpret_cast<char*>(data), sizeof(data));
如果字节数组含有中间的空值(比如 {0x48, 0x00, 0x65}),直接用 (char*)data 会让字符串在 0x00 处截断。必须用双参数构造,显式指定长度。这正是初学字节数组转字符串最容易踩的坑。
“delphi 字符串作字典key”这个热词其实牵扯的是哈希表里字符串作为键的用法。Delphi的 TDictionary<string, Integer> 直接支持字符串键,默认的哈希算法是大小写不敏感的(取决于编译器版本和比较器)。如果你要求大小写敏感,需要传入自定义的 IComparer<string>。这里说一句通用结论:在几乎所有语言的哈希表里,字符串作为key都是最佳实践,因为它值类型不可变,哈希值稳定,这也是Java String不可变设计的原因之一。
6.3 连接数据库时的字符串陷阱:带引号字符串与SQL语句
“将带引号的字符串保存到数据库的sql语句”也是一个高频开发痛点。假设你想插入一条记录,字符串内容是 O'Brien,如果直接拼SQL:
sql复制INSERT INTO users (name) VALUES ('O'Brien')
这行SQL会因为单引号不配对而报错。解决办法有两种:第一是转义单引号,SQL Server里单引号的转义方式是重复两个单引号,即 'O''Brien';第二是使用参数化查询,这才是项目里必须采用的做法:
csharp复制string sql = "INSERT INTO users (name) VALUES (@name)";
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@name", "O'Brien");
cmd.ExecuteNonQuery();
参数化查询不仅彻底解决了引号问题,还避免SQL注入。同理,Java的 PreparedStatement 用 ?,Python的 sqlite3 和 psycopg2 用 %s,概念完全一致。永远不要用字符串拼接去拼SQL语句,这是写进我代码规范的第一条红线。
6.4 Excel、WKT、虚拟文本:几个冷门但高频的字符串场景
“python查找excel中字符串”这个需求多出现在数据处理中。用 pandas 读Excel后,查找某个字符串可以用 str.contains:
python复制import pandas as pd
df = pd.read_excel("data.xlsx")
result = df[df["列名"].astype(str).str.contains("关键字", na=False)]
na=False 表示缺失值不算匹配,否则返回NaN会干扰筛选。
“wkt字符串 转 多边形 shp”是GIS领域的经典需求。WKT(Well-Known Text)是一种用文本描述几何对象的格式,例如 POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))。读取WKT后转为shp,你需要借助GIS库(如Python的 shapely 和 geopandas):
python复制from shapely.wkt import loads
import geopandas as gpd
from shapely.geometry import mapping
polygon = loads("POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))")
gdf = gpd.GeoDataFrame(geometry=[polygon])
gdf.to_file("output.shp")
WKT里的坐标对是英文逗号分隔,各坐标点之间也是逗号分隔,外层用括号包裹。这个格式看着简单,但手写很容易漏括号或逗号,建议用库解析而不是自己做字符串拆分。
“虚空之花字符串”“元气骑士字符串命令名称”这类热词比较特殊,它们多出现在游戏编辑器和特定指令系统里。核心思路是:游戏会识别特定格式的字符串指令或物品ID,你需要按项目文档组合成合法字符串。这类场景的通用经验是,先找到权威的格式定义,再用预处理函数校验你拼出来的字符串是否合法,避免直接把运行时错误抛给玩家。
“小明和字符串”这类题目型热搜词,大概率是竞赛题或作业题,通常要求从某字符串中按规则提取子串或统计字符频次,属于字符串基本功的综合应用。做题时先画状态流程图,再动手写代码,比边写边改高效得多。
我在实际项目中还有个习惯:任何涉及字符串格式转换的地方,都先用一个小的测试脚本验证边界情况,比如空字符串、超长字符串、含Unicode的字符串、含特殊符号的字符串。这样能挡掉大部分线上事故。字符串的东西看起来基础,但组合起来可以衍生出无穷的bug,多花十分钟写边界测试,比上线后加班排查强得多。
