实战:字符串工具类——把string玩出花来
说实话,C++标准库里的std::string已经挺好用了。但我在实际项目中待久了,发现总有一些操作是重复写的——分割字符串、去掉首尾空格、大小写转换、判断是否以某串开头……每次都要重新写一遍,烦不烦?
所以,我习惯把这些常用操作封装成一个工具类。今天我们就来手写一个StringUtil,把字符串处理那些事儿一网打尽。
为什么要自己封装?
你想想看,标准库给了你find、substr、erase这些基础方法,但实际业务里我们要的是“把逗号分隔的字符串拆成列表”、“把字符串里的所有空格去掉”、“判断一个字符串是不是纯数字”。
这些需求,标准库没有直接提供。每次写业务代码时都去重复实现一遍,代码里到处都是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,又会出问题。
知识体系总览
下面这张图,把字符串工具类的核心方法、设计思路和常见应用场景串起来了。你看一遍,心里就有谱了。
完整代码示例
我把上面几个方法整合到一个完整的类里,再写个简单的测试。你直接拿去用就行。
#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;
}
实际项目中的使用建议
这个工具类我用了好几年,有几个经验想分享给你:
- 不要过度封装:如果某个方法只在某个模块用一次,就别塞进工具类里。工具类应该是通用、高频的。
- 注意性能:
Replace和Split会创建新字符串,如果处理超大文本,要考虑内存开销。我一般会加一个ReplaceInPlace版本,直接修改原字符串。 - 考虑 Unicode:上面的代码只处理 ASCII。如果你的项目涉及中文、表情符号,建议用
std::wstring或专门的 Unicode 库。 - 单元测试:字符串处理最容易出边界情况——空字符串、全是空白、分隔符在开头结尾。我每次写完都会写几个测试用例,确保万无一失。
总结一下: 字符串工具类看似简单,但写好了能省很多事。核心就是那几个常用方法:Trim、Split、Replace、大小写转换、前后缀判断。把它们封装好,以后写业务代码时直接调用,清爽又高效。