第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 知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

字符串大小写转换知识体系 核心原理:ASCII 差 32 toLower:大写 + 32 toUpper:小写 - 32 方式一:if 判断(简单直观) 方式二:查表法(高性能) ⚠ 注意事项:原地修改 vs 拷贝、NULL 检查、内存释放

10.7 避坑总结

最后,我把这些年踩过的坑整理一下:

坑点 后果 解决方案
直接写数字 65、97 代码可读性差,容易写错 用字符常量 'A'、'a'
忘记处理 NULL 指针 程序崩溃 函数开头加 if (str == NULL) return
原地修改导致数据丢失 后续逻辑出错 明确函数语义,需要拷贝时用 _copy 版本
查表法忽略 128-255 范围 扩展 ASCII 字符被错误转换 表大小设为 256,非字母原样保留
忘记释放 malloc 的内存 内存泄漏 调用者负责 free,或使用栈缓冲区

好了,关于大小写转换,该讲的都讲了。从最简单的 if 判断,到高性能的查表法,再到各种边界情况处理。你想想看,是不是比直接用库函数心里踏实多了?

下次面试官问你「手写一个 toUpper」,你就可以自信地告诉他:这题我熟,还能给你讲出三种写法


公众号:蓝海资料掘金营,微信deep3321