4. string容器(上):string的构造与赋值、string的拼接与查找、string的替换与比较

大家好,欢迎来到string容器的上半部分。说实话,string在C++里是个很有意思的存在——它既不是基本类型,也不是纯粹的STL容器,但几乎每个项目都离不开它。我个人习惯把string看作是「带智能管理的字符数组」,你想想看,它帮我们处理了内存分配、边界检查这些烦心事,让我们能专注于业务逻辑。

这一章我们重点聊四个核心操作:构造与赋值、拼接与查找、替换与比较。嗯,这些都是日常编码中高频使用的功能,我会结合自己踩过的坑来讲解。

核心提示:string的本质是basic_string<char>的typedef,它管理一块动态内存,支持自动扩容。理解这一点,很多行为就说得通了。

4.1 string的构造与赋值

string的构造方式比你想的要多。我刚开始用C++时,总觉得string就是"hello world"这么简单,直到在项目中遇到各种初始化场景,才发现这里门道不少。

4.1.1 常用构造函数

#include <iostream>
#include <string>
using namespace std;

int main() {
    // 1. 默认构造
    string s1;
    cout << "s1: [" << s1 << "]" << endl;  // 空字符串

    // 2. 用C风格字符串构造
    string s2("hello");
    cout << "s2: " << s2 << endl;

    // 3. 用n个字符c构造
    string s3(5, 'A');
    cout << "s3: " << s3 << endl;  // "AAAAA"

    // 4. 拷贝构造
    string s4(s2);
    cout << "s4: " << s4 << endl;

    // 5. 子串构造(从位置pos开始,取len个字符)
    string s5("hello world", 6, 5);
    cout << "s5: " << s5 << endl;  // "world"

    // 6. 用迭代器范围构造
    string s6(s2.begin(), s2.end());
    cout << "s6: " << s6 << endl;

    return 0;
}

这里有个细节我想强调一下:子串构造的第二个参数是起始位置,第三个参数是长度。我曾经在代码里把顺序搞反了,结果调试了半天才发现是参数传错了。嗯,这种低级错误,犯过一次就记住了。

4.1.2 赋值操作

赋值操作同样有多种形式,我个人最常用的是operator=assign方法。

string s1, s2, s3;

// 1. 用operator=
s1 = "hello";
s2 = s1;

// 2. 用assign方法
s3.assign("world");
s3.assign("hello world", 5);  // 取前5个字符 -> "hello"
s3.assign(3, 'X');            // "XXX"

// 3. 用C风格字符串赋值(注意:不推荐,容易越界)
// char buf[] = "test";
// s3 = buf;  // 可以,但buf必须是'\0'结尾

我的建议:日常开发中优先使用operator=,代码更简洁。只有在需要指定长度或重复字符时,才用assign。另外,尽量避免用C风格字符串和string混用,容易出bug。

4.2 string的拼接与查找

拼接和查找是string最常用的两个操作。我在做日志系统时,经常需要拼接大量字符串,那时候对性能特别敏感。

4.2.1 拼接操作

拼接有三种主要方式:operator+=appendpush_back。你猜哪个效率最高?

string s = "hello";

// 1. operator+=
s += " world";      // "hello world"
s += '!';           // "hello world!"

// 2. append方法
s.append(" C++");   // "hello world! C++"
s.append(" is fun", 2);  // 追加"is"的前2个字符 -> "is"
s.append(3, '.');   // 追加3个'.' -> "..."

// 3. push_back(只能追加单个字符)
s.push_back('!');   // 追加一个感叹号

cout << s << endl;  // "hello world! C++ is..."

说实话,operator+=append在底层实现上差别不大,但append更灵活——你可以指定追加的长度或重复次数。我在项目中遇到过一个问题:用+=拼接大量短字符串时,频繁的内存重分配导致性能下降。后来改用append配合reserve预分配空间,性能提升很明显。

避坑指南:千万不要在循环里用s = s + "xxx"这种方式拼接!每次都会创建临时对象,效率极低。我曾经在一个网络消息处理模块里看到这种写法,每秒处理的消息数直接掉了30%。正确的做法是用+=append,或者先reserve足够的容量。

4.2.2 查找操作

查找是string里最容易出bug的地方之一。为什么?因为查找失败时的返回值是npos,而不是-1。很多新手会写成if (s.find("xxx") == -1),这在某些平台上能工作,但严格来说是未定义行为。

string s = "hello world, hello C++";

// 1. find:从前往后找
size_t pos = s.find("hello");
if (pos != string::npos) {
    cout << "找到hello,位置: " << pos << endl;  // 0
}

// 2. rfind:从后往前找
pos = s.rfind("hello");
if (pos != string::npos) {
    cout << "从后找hello,位置: " << pos << endl;  // 13
}

// 3. find_first_of:查找第一个匹配的字符
pos = s.find_first_of("ow");
cout << "第一个'o'或'w'的位置: " << pos << endl;  // 4('o')

// 4. find_last_of:查找最后一个匹配的字符
pos = s.find_last_of("ow");
cout << "最后一个'o'或'w'的位置: " << pos << endl;  // 16('o')

// 5. find_first_not_of:查找第一个不匹配的字符
pos = s.find_first_not_of("helo ");
cout << "第一个不是'helo '的字符位置: " << pos << endl;  // 5('w')

这里有个小技巧:查找时一定要用string::npos做比较。我见过有人用if (pos < 0)来判断,这在64位系统上会出问题,因为size_t是无符号类型,永远不小于0。

重要:string::npos的值是size_t(-1),也就是最大的无符号整数。所以判断查找失败的正确写法是:if (pos == string::npos)

4.3 string的替换与比较

替换和比较操作,说白了就是「改内容」和「比大小」。这两个功能在文本处理、配置解析等场景中非常常见。

4.3.1 替换操作

replace方法有很多重载版本,我挑几个最常用的来说。

string s = "I like C++";

// 1. 替换指定位置的子串
// replace(pos, len, str):从pos开始,替换len个字符为str
s.replace(2, 4, "love");  // "I love C++"
cout << s << endl;

// 2. 用迭代器范围替换
string s2 = "hello world";
s2.replace(s2.begin(), s2.begin() + 5, "hi");
cout << s2 << endl;  // "hi world"

// 3. 替换为重复字符
string s3 = "*****";
s3.replace(0, 3, 3, 'A');  // 将前3个*替换为3个A
cout << s3 << endl;  // "AAA**"

// 4. 用另一个字符串的子串替换
string s4 = "I like C++";
string src = "I love Java";
s4.replace(2, 4, src, 2, 4);  // 从src的索引2开始取4个字符 -> "love"
cout << s4 << endl;  // "I love C++"

替换操作有个容易忽略的点:替换的长度和原长度不一致时,string会自动调整大小。这意味着替换后字符串的长度可能会变化。我在做文本模板替换时,就利用这个特性实现了动态内容插入。

4.3.2 比较操作

string的比较支持operator==operator!=operator<等,也提供了compare方法。我个人更推荐用compare,因为它能指定比较的范围,更灵活。

string s1 = "apple";
string s2 = "banana";

// 1. 用operator比较
if (s1 < s2) {
    cout << "apple < banana" << endl;  // 字典序比较
}

// 2. 用compare方法
int result = s1.compare(s2);
if (result < 0) {
    cout << "s1 < s2" << endl;
} else if (result > 0) {
    cout << "s1 > s2" << endl;
} else {
    cout << "s1 == s2" << endl;
}

// 3. 比较子串
string s3 = "I have an apple";
string s4 = "apple pie";
// 比较s3从索引7开始的5个字符与s4的前5个字符
result = s3.compare(7, 5, s4, 0, 5);
if (result == 0) {
    cout << "子串相等" << endl;  // 输出这个
}

// 4. 与C风格字符串比较
result = s1.compare("apple");
if (result == 0) {
    cout << "s1 == apple" << endl;
}

我的经验:在需要忽略大小写比较时,不要直接用compare,因为它区分大小写。我一般会先把两个字符串都转成小写(用std::transform配合tolower),然后再比较。或者直接用C++20的starts_withends_with,这些新特性用起来很舒服。

4.4 知识体系总览

为了让你对本章内容有个整体认识,我画了一张结构图。你看,string的操作其实围绕「创建-修改-查询-比较」这条主线展开。

string容器(上) 构造与赋值 默认构造 C风格字符串构造 拷贝构造 / 子串构造 operator= / assign 拼接与查找 operator+= / append push_back find / rfind find_first_of / find_last_of 替换操作 replace(pos, len, str) 迭代器范围替换 替换为重复字符 子串替换 比较操作 operator== / != / < compare() 完整比较 子串比较 与C风格字符串比较 核心原则:优先用string成员函数,避免C风格字符串混用 查找失败用 npos 判断,替换注意长度变化

这张图把本章的知识点串起来了。你看,构造与赋值是基础,拼接与查找是日常高频操作,替换和比较则是进阶功能。掌握了这些,string的基本用法你就拿下了。

本章小结:

  • 构造方式多样,推荐用string s("hello")string s(n, 'c')
  • 赋值用operator=最简洁,assign更灵活
  • 拼接避免s = s + x,用+=append
  • 查找必须用string::npos判断失败
  • 替换注意长度变化,比较推荐用compare

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