第10章:字符串大小写转换:手写 toUpper、toLower 函数
各位同学,今天我们来聊聊字符串处理里最基础、也最容易被忽视的一个操作——大小写转换。
你可能会想:「这有什么好讲的?直接用库函数不就行了?」
嗯,我以前也这么想。直到有一次,我在一个嵌入式项目里,发现库函数的 toupper() 在特定字符集下居然会崩溃……从那以后,我养成了一个习惯:核心功能,自己手写一份。
10.1 大小写转换的本质
说白了,大小写转换就是利用 ASCII 码的规律。
大写字母 A-Z 的 ASCII 范围是 65-90,小写字母 a-z 是 97-122。你发现没有?大小写之间正好差 32。
举个例子:'A' 是 65,'a' 是 97。97 - 65 = 32。反过来也一样。
所以,toUpper 就是把小写字母减 32,toLower 就是把大写字母加 32。
核心公式:
- 小写转大写:ch - 32
- 大写转小写:ch + 32
- 判断大写:ch >= 'A' && ch <= 'Z'
- 判断小写:ch >= 'a' && ch <= 'z'
10.2 手写 toLower 函数
我们先写一个最简单的版本。我个人习惯,先写单字符版本,再写字符串版本。这样逻辑清晰,不容易出错。
// 单字符转小写
char my_toLower(char ch) {
if (ch >= 'A' && ch <= 'Z') {
return ch + 32;
}
return ch; // 非大写字母,原样返回
}
你看,就这么几行。但这里有个坑——我建议你永远不要直接写数字 65、97。为什么?
因为可读性太差了。三个月后你自己回来看代码,还得去查 ASCII 表。用字符常量 'A'、'a',一目了然。
接下来是字符串版本:
// 字符串转小写(原地修改)
void str_toLower(char *str) {
if (str == NULL) return; // 防御性编程
while (*str != '\0') {
if (*str >= 'A' && *str <= 'Z') {
*str = *str + 32;
}
str++;
}
}
注意:这个函数会直接修改原字符串。如果你需要保留原串,记得先拷贝一份。
我曾经在项目中因为没注意这一点,把一个配置字符串给改了,导致后续解析全乱套……排查了整整一下午。
10.3 手写 toUpper 函数
toUpper 和 toLower 是对称的。逻辑一模一样,只是判断条件和偏移方向相反。
// 单字符转大写
char my_toUpper(char ch) {
if (ch >= 'a' && ch <= 'z') {
return ch - 32;
}
return ch;
}
// 字符串转大写(原地修改)
void str_toUpper(char *str) {
if (str == NULL) return;
while (*str != '\0') {
if (*str >= 'a' && *str <= 'z') {
*str = *str - 32;
}
str++;
}
}
你可能会问:「能不能把两个函数合并成一个?」
可以,但我不建议。为什么呢?单一职责原则。一个函数只做一件事,代码更清晰,测试也更方便。
10.4 进阶:不修改原字符串的版本
有时候,我们不想破坏原始数据。那就需要返回一个新字符串。
#include <stdlib.h>
#include <string.h>
// 返回新字符串(大写版本)
char* str_toUpper_copy(const char *src) {
if (src == NULL) return NULL;
size_t len = strlen(src);
char *dest = (char*)malloc(len + 1);
if (dest == NULL) return NULL; // 内存分配失败
for (size_t i = 0; i < len; i++) {
if (src[i] >= 'a' && src[i] <= 'z') {
dest[i] = src[i] - 32;
} else {
dest[i] = src[i];
}
}
dest[len] = '\0'; // 别忘了结束符
return dest;
}
避坑指南:
- 调用者记得
free()释放内存,否则会内存泄漏 - 我习惯在函数名上加
_copy后缀,明确告诉使用者「我会分配新内存」 - 如果源字符串很长(比如几 MB),频繁 malloc/free 会影响性能。这时候原地修改更合适
10.5 性能优化:查表法
如果你的程序需要频繁做大小写转换(比如每秒处理百万级字符),那上面的 if 判断可能成为瓶颈。
这时候,查表法就派上用场了。
// 预计算转换表
static const char toUpper_table[256] = {
// 0-127 标准 ASCII
0, 1, 2, ... , // 省略中间
'A', 'B', 'C', ... , 'Z', // 97-122 位置放对应大写
// 128-255 原样保留
};
char fast_toUpper(char ch) {
return toUpper_table[(unsigned char)ch];
}
你看,一次查表就搞定,没有分支判断。CPU 分支预测失败的开销也省了。
我在一个日志解析模块里用过这个方案,性能提升了大约 30%。当然,如果你的场景没那么极端,用 if 版本完全够用。
10.6 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
10.7 避坑总结
最后,我把这些年踩过的坑整理一下:
| 坑点 | 后果 | 解决方案 |
|---|---|---|
| 直接写数字 65、97 | 代码可读性差,容易写错 | 用字符常量 'A'、'a' |
| 忘记处理 NULL 指针 | 程序崩溃 | 函数开头加 if (str == NULL) return |
| 原地修改导致数据丢失 | 后续逻辑出错 | 明确函数语义,需要拷贝时用 _copy 版本 |
| 查表法忽略 128-255 范围 | 扩展 ASCII 字符被错误转换 | 表大小设为 256,非字母原样保留 |
| 忘记释放 malloc 的内存 | 内存泄漏 | 调用者负责 free,或使用栈缓冲区 |
好了,关于大小写转换,该讲的都讲了。从最简单的 if 判断,到高性能的查表法,再到各种边界情况处理。你想想看,是不是比直接用库函数心里踏实多了?
下次面试官问你「手写一个 toUpper」,你就可以自信地告诉他:这题我熟,还能给你讲出三种写法。
公众号:蓝海资料掘金营,微信deep3321