字符串处理与string类:从基础到实战
字符串处理,说白了就是C++里最日常的工作之一。我做了这么多年项目,几乎每个系统都离不开字符串的拼接、查找、切割。今天我们就来彻底搞定 std::string 以及它的小伙伴们。
string的构造与操作
先聊聊构造。很多人觉得string构造不就是 std::string s("hello") 吗?其实远不止这些。
#include <string>
#include <iostream>
int main() {
// 常用构造方式
std::string s1; // 空字符串
std::string s2("Hello"); // 从C字符串构造
std::string s3(s2); // 拷贝构造
std::string s4(s2, 1, 3); // 从s2的索引1开始取3个字符 -> "ell"
std::string s5(5, 'A'); // 5个'A' -> "AAAAA"
std::string s6(s2.begin(), s2.end()); // 迭代器范围构造
std::cout << s4 << std::endl; // 输出: ell
std::cout << s5 << std::endl; // 输出: AAAAA
return 0;
}
我个人习惯用 s2.begin() 和 s2.end() 这种迭代器方式,因为它在泛型编程里特别灵活。你想想看,换成 vector<char> 也能用同一套代码。
操作方面,我挑几个最常用的说说:
- 拼接:
+和+=最直观,但大量拼接时用append或push_back性能更好 - 插入:
insert(pos, str),注意位置是迭代器或索引 - 删除:
erase(pos, len)或erase(iter) - 替换:
replace(pos, len, str) - 大小:
size()和length()完全等价,我个人偏爱size()
reserve(n) 可以预分配内存。如果你知道最终字符串大概多长,提前 reserve 能避免多次重新分配。我在写日志系统时就靠这个把性能提升了30%。
find/rfind/substr:查找与子串
查找是字符串操作的重头戏。我遇到过不少新手在查找时踩坑,咱们一个一个说清楚。
std::string text = "Hello world, welcome to C++ world";
size_t pos = text.find("world");
if (pos != std::string::npos) {
std::cout << "第一次出现位置: " << pos << std::endl; // 输出: 6
}
// rfind 从后往前找
pos = text.rfind("world");
std::cout << "最后一次出现位置: " << pos << std::endl; // 输出: 33
// find_first_of 找任意字符首次出现
pos = text.find_first_of("wo");
std::cout << "'w'或'o'首次出现: " << pos << std::endl; // 输出: 6
// substr 截取子串
std::string sub = text.substr(6, 5); // 从索引6开始取5个字符
std::cout << "子串: " << sub << std::endl; // 输出: world
find 返回 size_t 类型,失败时返回 std::string::npos。千万不要写成 if (pos >= 0),因为 size_t 是无符号的,-1 会变成超大正数!我曾经在代码审查时看到过这种bug,排查了半天才发现。
另外,substr 的第二个参数是长度,不是结束位置。如果你只传一个参数,它会一直取到字符串末尾。这个设计其实挺合理的——你想想看,大多数时候我们就是想「从某处开始取到结尾」。
string_view(C++17):零拷贝的字符串视图
这个特性我特别喜欢。说白了,string_view 就是一个指向已有字符串的「窗口」,它不拥有数据,只负责「看」。
#include <string_view>
void process(std::string_view sv) {
// 不会拷贝字符串
std::cout << sv.substr(0, 3) << std::endl;
}
int main() {
std::string s = "Hello, string_view!";
process(s); // 从std::string隐式转换
process("C-style string"); // 从const char*隐式转换
return 0;
}
为什么需要它?我举个实际例子。以前写一个解析函数,参数是 const std::string&,调用方传了个 const char*,每次都要临时构造一个 std::string,堆内存分配一次,拷贝一次。换成 string_view 后,零拷贝,性能立竿见影。
string_view 不保证以 '\0' 结尾,所以不要对它调用 c_str()。另外,它指向的数据必须比它活得久,否则就是悬空引用。
字符串与数值转换:to_string / stoi 家族
数值转字符串,C++11 之前得用 sprintf 或 stringstream,又慢又容易出错。现在有了 to_string,一行搞定。
int num = 42;
double pi = 3.14159;
std::string s1 = std::to_string(num); // "42"
std::string s2 = std::to_string(pi); // "3.141590"(注意精度)
// 字符串转数值
std::string str = "123";
int val = std::stoi(str); // 123
long lval = std::stol(str); // 123L
double dval = std::stod("3.14"); // 3.14
// 带错误处理的转换
try {
int x = std::stoi("abc");
} catch (const std::invalid_argument& e) {
std::cerr << "转换失败: 无效参数" << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "转换失败: 数值越界" << std::endl;
}
这里有个坑:to_string 对浮点数的默认精度是6位,而且不会做四舍五入的截断。如果你需要控制格式,还是得用 stringstream 或 snprintf。我在做金融计算时就因为这个被坑过一次——金额显示多了几位小数,被测试小姐姐追着骂。
实战:CSV解析器
理论说完了,咱们来点真家伙。写一个简单的CSV解析器,把上面这些知识点串起来。
#include <string>
#include <vector>
#include <sstream>
#include <iostream>
class CSVParser {
public:
explicit CSVParser(std::string_view data) : data_(data) {}
std::vector<std::vector<std::string>> parse() {
std::vector<std::vector<std::string>> result;
std::string line;
std::istringstream stream(data_.data());
while (std::getline(stream, line)) {
result.push_back(parseLine(line));
}
return result;
}
private:
std::vector<std::string> parseLine(const std::string& line) {
std::vector<std::string> fields;
size_t start = 0;
size_t end = 0;
while (end != std::string::npos) {
end = line.find(',', start);
std::string field = line.substr(start, end - start);
// 去除首尾空格
trim(field);
fields.push_back(field);
start = end + 1;
}
return fields;
}
void trim(std::string& s) {
// 去除前导空格
size_t start = s.find_first_not_of(" \t");
if (start == std::string::npos) {
s.clear();
return;
}
// 去除尾部空格
size_t end = s.find_last_not_of(" \t");
s = s.substr(start, end - start + 1);
}
std::string data_;
};
int main() {
std::string csv = "name, age, city\nAlice, 30, Beijing\nBob, 25, Shanghai";
CSVParser parser(csv);
auto rows = parser.parse();
for (const auto& row : rows) {
for (const auto& field : row) {
std::cout << "[" << field << "] ";
}
std::cout << std::endl;
}
return 0;
}
这个解析器虽然简单,但涵盖了 string_view、find、substr、find_first_not_of、find_last_not_of 等核心操作。实际项目中,你还需要处理引号包裹的字段、转义字符等复杂情况,但核心思路是一样的。
string_view 来避免子串拷贝。另外,std::getline 也可以指定分隔符,比如 std::getline(stream, line, ','),这样能直接按逗号分割行。
嗯,以上就是字符串处理的全部核心内容。从构造到查找,从 string_view 到数值转换,再到一个完整的CSV解析器,这些知识足够你应对日常开发中90%的字符串场景了。