字符串处理与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> 也能用同一套代码。

操作方面,我挑几个最常用的说说:

  • 拼接++= 最直观,但大量拼接时用 appendpush_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 之前得用 sprintfstringstream,又慢又容易出错。现在有了 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位,而且不会做四舍五入的截断。如果你需要控制格式,还是得用 stringstreamsnprintf。我在做金融计算时就因为这个被坑过一次——金额显示多了几位小数,被测试小姐姐追着骂。

实战: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_viewfindsubstrfind_first_not_offind_last_not_of 等核心操作。实际项目中,你还需要处理引号包裹的字段、转义字符等复杂情况,但核心思路是一样的。

优化建议:如果CSV文件很大,可以考虑用 string_view 来避免子串拷贝。另外,std::getline 也可以指定分隔符,比如 std::getline(stream, line, ','),这样能直接按逗号分割行。
字符串处理知识体系 std::string 构造方式:默认、拷贝、子串、填充、迭代器 常用操作:拼接、插入、删除、替换、reserve find / rfind / find_first_of / substr string_view:零拷贝、只读视图、C++17 to_string / stoi / stod 实战:CSV解析器

嗯,以上就是字符串处理的全部核心内容。从构造到查找,从 string_view 到数值转换,再到一个完整的CSV解析器,这些知识足够你应对日常开发中90%的字符串场景了。

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