实战:字符串工具类——把string玩出花来

说实话,C++标准库里的std::string已经挺好用了。但我在实际项目中待久了,发现总有一些操作是重复写的——分割字符串、去掉首尾空格、大小写转换、判断是否以某串开头……每次都要重新写一遍,烦不烦?

所以,我习惯把这些常用操作封装成一个工具类。今天我们就来手写一个StringUtil,把字符串处理那些事儿一网打尽。

为什么要自己封装?

你想想看,标准库给了你findsubstrerase这些基础方法,但实际业务里我们要的是“把逗号分隔的字符串拆成列表”、“把字符串里的所有空格去掉”、“判断一个字符串是不是纯数字”。

这些需求,标准库没有直接提供。每次写业务代码时都去重复实现一遍,代码里到处都是for循环和find调用,看着就乱。

我在一个日志解析项目里,就因为字符串分割的代码散落在各处,后来要改分隔符,改了七八个地方才改完。从那以后,我就铁了心要做一个统一的字符串工具类。

StringUtil 的核心设计

我们先定个基调:这个工具类只包含静态方法,不保存状态。说白了,就是一堆函数,你传字符串进去,它给你处理完吐出来。

设计原则:

  • 所有方法都是静态的,不需要实例化
  • 输入参数尽量用 const std::string&,避免拷贝
  • 返回值按需选择:返回新字符串,或通过引用参数传出
  • 异常安全:不抛异常,出错返回空或false

常用方法一览

方法名 功能说明 使用场景
Trim 去掉字符串首尾的空白字符 用户输入清洗
Split 按指定分隔符拆分成字符串数组 解析CSV、配置文件
ToUpper / ToLower 大小写转换 不区分大小写的比较
StartsWith / EndsWith 判断前缀/后缀 文件扩展名判断、URL匹配
Replace 替换所有匹配的子串 模板字符串填充
IsNumber 判断字符串是否为纯数字 输入合法性校验

代码实现:一步一步来

嗯,这里我挑几个最常用的方法,把代码贴出来。你看着学,别光复制,要理解里面的细节。

1. Trim——去掉首尾空白

#include <string>
#include <algorithm>
#include <cctype>

class StringUtil {
public:
    // 去掉字符串首尾的空白字符(空格、制表符、换行等)
    static std::string Trim(const std::string& str) {
        if (str.empty()) return str;

        // 找到第一个非空白字符
        size_t start = 0;
        while (start < str.size() && std::isspace(str[start])) {
            ++start;
        }

        // 找到最后一个非空白字符
        size_t end = str.size();
        while (end > start && std::isspace(str[end - 1])) {
            --end;
        }

        // 如果全是空白,返回空字符串
        if (start >= end) return "";

        return str.substr(start, end - start);
    }
};

小技巧: 我习惯用 std::isspace 而不是手动判断空格,因为它能处理制表符、换行符等所有空白字符。曾经有个bug就是因为只判断了空格,没处理制表符,结果配置文件解析一直出错。

2. Split——字符串分割

#include <vector>
#include <sstream>

class StringUtil {
public:
    // 按指定分隔符分割字符串
    static std::vector<std::string> Split(const std::string& str, char delimiter) {
        std::vector<std::string> result;
        std::stringstream ss(str);
        std::string token;

        while (std::getline(ss, token, delimiter)) {
            result.push_back(token);
        }

        return result;
    }

    // 重载:支持字符串作为分隔符(更灵活)
    static std::vector<std::string> Split(const std::string& str, const std::string& delimiter) {
        std::vector<std::string> result;
        size_t start = 0;
        size_t end = str.find(delimiter);

        while (end != std::string::npos) {
            result.push_back(str.substr(start, end - start));
            start = end + delimiter.length();
            end = str.find(delimiter, start);
        }

        // 添加最后一段
        result.push_back(str.substr(start));

        return result;
    }
};

注意:std::getline 分割时,如果字符串末尾有分隔符,会多出一个空字符串。比如 "a,b," 会得到 ["a", "b", ""]。要不要保留这个空串,看你的业务需求。我在解析CSV时一般会过滤掉空串。

3. StartsWith / EndsWith——判断前缀后缀

class StringUtil {
public:
    static bool StartsWith(const std::string& str, const std::string& prefix) {
        if (prefix.length() > str.length()) return false;
        return str.compare(0, prefix.length(), prefix) == 0;
    }

    static bool EndsWith(const std::string& str, const std::string& suffix) {
        if (suffix.length() > str.length()) return false;
        return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
    }
};

这里用 compare 而不是 find,是因为 find 会从头搜索整个字符串,效率低。而 compare 只比较指定位置,性能好很多。我以前在一个高频调用的地方用了 find,结果性能分析一跑,发现这里成了热点。换成 compare 后,速度提升了好几倍。

4. Replace——替换所有匹配

class StringUtil {
public:
    static std::string Replace(const std::string& str, 
                               const std::string& from, 
                               const std::string& to) {
        if (from.empty()) return str;

        std::string result = str;
        size_t pos = 0;

        while ((pos = result.find(from, pos)) != std::string::npos) {
            result.replace(pos, from.length(), to);
            pos += to.length();  // 跳过刚替换的内容,避免死循环
        }

        return result;
    }
};

避坑指南: 我曾经在替换时忘了更新 pos,结果 find 一直找到同一个位置,陷入了死循环。所以替换后一定要把 pos 移到替换内容的后面,而不是 pos + 1,否则如果 to 里包含 from,又会出问题。

知识体系总览

下面这张图,把字符串工具类的核心方法、设计思路和常见应用场景串起来了。你看一遍,心里就有谱了。

字符串工具类 StringUtil 知识体系 StringUtil 核心方法(静态函数) Trim Split ToUpper/ToLower StartsWith/EndsWith Replace 设计要点 静态方法,无状态 const & 传参,避免拷贝 异常安全,返回空或false 应用场景:日志解析 / 配置文件读取 / 用户输入清洗 / 协议解析

完整代码示例

我把上面几个方法整合到一个完整的类里,再写个简单的测试。你直接拿去用就行。

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <cctype>

class StringUtil {
public:
    // 去掉首尾空白
    static std::string Trim(const std::string& str) {
        if (str.empty()) return str;
        size_t start = 0;
        while (start < str.size() && std::isspace(str[start])) ++start;
        size_t end = str.size();
        while (end > start && std::isspace(str[end - 1])) --end;
        if (start >= end) return "";
        return str.substr(start, end - start);
    }

    // 按字符分割
    static std::vector<std::string> Split(const std::string& str, char delimiter) {
        std::vector<std::string> result;
        std::stringstream ss(str);
        std::string token;
        while (std::getline(ss, token, delimiter)) {
            result.push_back(token);
        }
        return result;
    }

    // 判断前缀
    static bool StartsWith(const std::string& str, const std::string& prefix) {
        if (prefix.length() > str.length()) return false;
        return str.compare(0, prefix.length(), prefix) == 0;
    }

    // 判断后缀
    static bool EndsWith(const std::string& str, const std::string& suffix) {
        if (suffix.length() > str.length()) return false;
        return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
    }

    // 替换所有
    static std::string Replace(const std::string& str, 
                               const std::string& from, 
                               const std::string& to) {
        if (from.empty()) return str;
        std::string result = str;
        size_t pos = 0;
        while ((pos = result.find(from, pos)) != std::string::npos) {
            result.replace(pos, from.length(), to);
            pos += to.length();
        }
        return result;
    }

    // 转大写
    static std::string ToUpper(const std::string& str) {
        std::string result = str;
        std::transform(result.begin(), result.end(), result.begin(), ::toupper);
        return result;
    }

    // 转小写
    static std::string ToLower(const std::string& str) {
        std::string result = str;
        std::transform(result.begin(), result.end(), result.begin(), ::tolower);
        return result;
    }

    // 判断是否为纯数字
    static bool IsNumber(const std::string& str) {
        if (str.empty()) return false;
        for (char c : str) {
            if (!std::isdigit(c)) return false;
        }
        return true;
    }
};

// 测试代码
int main() {
    std::string input = "  hello,world,test  ";
    
    std::cout << "原始: [" << input << "]" << std::endl;
    std::cout << "Trim: [" << StringUtil::Trim(input) << "]" << std::endl;
    
    auto parts = StringUtil::Split(StringUtil::Trim(input), ',');
    std::cout << "Split: ";
    for (const auto& p : parts) std::cout << p << " ";
    std::cout << std::endl;
    
    std::cout << "StartsWith 'hello': " << StringUtil::StartsWith(parts[0], "hello") << std::endl;
    std::cout << "ToUpper: " << StringUtil::ToUpper(parts[0]) << std::endl;
    std::cout << "Replace 'hello' -> 'hi': " << StringUtil::Replace(parts[0], "hello", "hi") << std::endl;
    std::cout << "IsNumber '123': " << StringUtil::IsNumber("123") << std::endl;
    std::cout << "IsNumber '12a3': " << StringUtil::IsNumber("12a3") << std::endl;
    
    return 0;
}

实际项目中的使用建议

这个工具类我用了好几年,有几个经验想分享给你:

  • 不要过度封装:如果某个方法只在某个模块用一次,就别塞进工具类里。工具类应该是通用、高频的。
  • 注意性能ReplaceSplit 会创建新字符串,如果处理超大文本,要考虑内存开销。我一般会加一个 ReplaceInPlace 版本,直接修改原字符串。
  • 考虑 Unicode:上面的代码只处理 ASCII。如果你的项目涉及中文、表情符号,建议用 std::wstring 或专门的 Unicode 库。
  • 单元测试:字符串处理最容易出边界情况——空字符串、全是空白、分隔符在开头结尾。我每次写完都会写几个测试用例,确保万无一失。

总结一下: 字符串工具类看似简单,但写好了能省很多事。核心就是那几个常用方法:Trim、Split、Replace、大小写转换、前后缀判断。把它们封装好,以后写业务代码时直接调用,清爽又高效。

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