string学习总结

string 学习总结

std::string 是 C++ 标准库中最常用的字符串类,本文从零到熟练梳理它的所有核心用法。

1. string 是什么

  • 存储连续的字符序列,支持随机访问(按下标 O(1) 访问)。
  • 底层是动态字符数组:在堆上分配一块连续内存,字符紧密排列,末尾自动加 \0(但 \0 不是 string 的一部分)。
  • 自动扩容:字符增多时自动重新分配更大内存(O(n) 均摊插入)。
  • 属于"字符串类",头文件 <string>。
#include <string>

string s; // 空 string

对比记忆:string 是"字符序列容器",内存连续、访问快、支持丰富字符串操作;vector<char> 是"字符动态数组",功能更底层但缺少字符串专属接口(如 find、substr);C 风格字符串 char[] 是"字符数组",无自动管理,容易越界。

2. 定义与初始化

#include <string>
using namespace std;

// 1. 空 string
string s1;

// 2. 字面量初始化
string s2 = "hello";
string s3("world");

// 3. 填充初始化
string s4(5, 'a');       // "aaaaa",5 个 'a'
string s5(3, 'x');       // "xxx"

// 4. 从数组/指针构造
char arr[] = "abc";
string s6(arr);          // "abc"
string s7(arr, 2);       // "ab",取前 2 个字符

// 5. 从迭代器构造
string s8(s2.begin(), s2.begin() + 3); // "hel"

// 6. 拷贝构造
string s9(s2);           // "hello"

// 7. 移动构造(C++11)
string s10(std::move(s2)); // s2 被掏空,s10 = "hello"

// 8. 初始化列表(C++11)
string s11{'h', 'e', 'l', 'l', 'o'}; // "hello"

关键概念:

  • size / length:当前字符个数(两者等价,size() 更常用)
  • capacity:已分配内存能容纳的最大字符个数(含 \0)
  • c_str():返回以 \0 结尾的 C 风格字符串指针
  • reserve() 只改 capacity,不改 size;resize() 两者都改

3. 核心操作一览

操作 写法 说明 复杂度
尾部追加 s += str 在末尾添加字符串 O(n) 均摊
尾部追加 s.append(str) 在末尾添加字符串 O(n) 均摊
尾部追加 s.push_back(c) 在末尾添加单个字符 O(1) 均摊
指定位置插入 s.insert(pos, str) 在下标 pos 处插入字符串,插入会使插入位置后的元素向后搬移 O(n)
迭代器插入 s.insert(it, str) 在迭代器 it 前插入 O(n)
删除 s.erase(pos, len) 从下标 pos 开始删除 len 个字符 O(n)
删除 s.erase(it) 删除迭代器 it 处字符 O(n)
按下标访问 s[i] 不检查越界 O(1)
按下标访问 s.at(i) 越界抛 out_of_range 异常 O(1)
首字符 s.front() 返回第一个字符的引用 O(1)
末字符 s.back() 返回最后一个字符的引用 O(1)
C 风格字符串 s.c_str() 返回以 \0 结尾的 const char* O(1)
底层指针 s.data() 返回底层数组指针(C++11 后保证有 \0) O(1)
大小 s.size() 当前字符个数 O(1)
容量 s.capacity() 当前分配的内存容量 O(1)
预留空间 s.reserve(n) 至少分配 n 个字符的空间 O(n) 最坏
调整大小 s.resize(n) 改变 size,多删少补(补 \0) O(n) 最坏
收缩空间 s.shrink_to_fit() 使 capacity == size(非强制) O(n)
清空 s.clear() 删除所有字符,size=0,capacity 不变 O(n)
判空 s.empty() size == 0 时返回 true O(1)
交换 s.swap(other) 与另一个 string 交换内容 O(1) O(1)
子串 s.substr(pos, len) 返回从 pos 开始长度为 len 的子串 O(n)
查找 s.find(str) 从头查找 str,返回下标或 npos O(n*m)
查找 s.rfind(str) 从尾查找 str,返回下标或 npos O(n*m)
比较 s1.compare(s2) 返回 0(相等)、正数(s1>s2)、负数(s1<s2) O(n)
比较 s1 == s2 直接比较内容 O(n)
追加 s += c 追加单个字符 O(1) 均摊
追加 s += {a, b, c} 追加初始化列表(C++11) O(n)
赋值 s1 = s2 拷贝赋值 O(n)
赋值 s1 = std::move(s2) 移动赋值(C++11) O(1)
流输出 cout << s 输出整个字符串 O(n)
流输入 cin >> s 读入一个单词(遇空格停止) O(n)
整行读入 getline(cin, s) 读入一整行(遇换行停止) O(n)
替换 s.replace(pos, len, content) 替换字符串内容(删除旧范围并写入),如果字符不足,则向后扩展,如果此时用copy迭代器算法实现,则不会进行扩容,这是因为copy不会删除,不会插入,不会自动扩容 O(n)

4. 增删查改实战

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

int main() {
    string s;

    // ===== 插入/追加 =====
    s = "hello";
    s += " world";           // "hello world"
    s.append("!");           // "hello world!"
    s.push_back('?');        // "hello world!?"
    s.insert(5, ",");        // "hello, world!?"

    // ===== 访问 =====
    cout << s[0] << endl;     // h
    cout << s.at(5) << endl;  // ,
    cout << s.front() << endl; // h
    cout << s.back() << endl;  // ?
    cout << s.c_str() << endl; // hello, world!?

    // ===== 查找 =====
    size_t pos = s.find("world");
    if (pos != string::npos) {
        cout << "Found at index: " << pos << endl; // 7
    }

    // rfind 从后往前找
    size_t rpos = s.rfind('o');
    cout << "Last 'o' at: " << rpos << endl; // 8

    // ===== 子串 =====
    string sub = s.substr(7, 5); // "world"
    cout << sub << endl;

    // ===== 遍历 =====
    // 1. 下标遍历
    for (int i = 0; i < s.size(); i++) {
        cout << s[i] << " ";
    }
    cout << endl;

    // 2. 迭代器遍历
    for (auto it = s.begin(); it != s.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    // 3. 范围 for(C++11)
    for (auto& c : s) {
        cout << c << " ";
    }
    cout << endl;

    // ===== 删除 =====
    s.erase(5, 2);           // 删除下标 5 开始的 2 个字符
    s.pop_back();            // 删除末尾字符
    s.erase(s.begin());      // 删除首字符
    s.clear();               // 清空所有字符
    cout << s.size() << endl; // 0

    return 0;
}

5. reserve() vs resize()(重点)

这是面试高频考点,两者本质不同:

string s;
s.reserve(10);
// size = 0, capacity = 10, 没有任何字符
// s[0] 是未定义行为!

s.resize(10);
// size = 10, capacity >= 10, 10 个 '\0' 字符
cout << s.size(); // 合法,输出 10
reserve(n) resize(n)
改变 size 不改 改
改变 capacity 改 可能改
字符个数 不变 多删少补(补 \0)
用途 预分配内存,避免频繁扩容 改变字符个数

坑:reserve 后直接用下标访问未初始化的内存 → 未定义行为!

string s;
s.reserve(100);
for (int i = 0; i < 100; i++) {
    s[i] = 'a' + i; // 未定义行为!size 还是 0
}
// 正确做法:
for (int i = 0; i < 100; i++) {
    s += 'a' + i; // 每次 += 才会真正构造字符
}
// 或者先 resize
s.resize(100);
for (int i = 0; i < 100; i++) {
    s[i] = 'a' + i; // 合法,字符已构造
}

6. 内存布局:size 与 capacity 的关系

string 扩容时会分配一块更大的内存,把旧字符拷贝过去,再释放旧内存:

size:     0  1  2  3  4  5  6  7  8 ...
capacity: 0  1  2  4  4  8  8  8  8 16 ...
  • 每次追加导致 size > capacity 时触发扩容。
  • 扩容倍数由实现决定,通常是 2 倍(GCC)或 1.5 倍(MSVC)。
  • 扩容代价大:O(n) 拷贝 + 内存分配,所以批量拼接前用 reserve() 预分配。
  • SBO(Small Buffer Optimization):许多实现对短字符串(通常 ≤ 15~22 字符)直接存储在栈上,避免堆分配,这是 string 比 vector<char> 快的一个原因。
string s;
s.reserve(1000); // 预分配,后续拼接不触发扩容
for (int i = 0; i < 1000; i++) {
    s += 'a'; // 全部 O(1) 均摊,无重新分配
}

7. 迭代器的特点

  • string 的迭代器是随机访问迭代器:支持 ++、--、it + n、it - n、it1 - it2、下标比较。
  • 这是最强大的迭代器类型,可以配合任何 STL 算法。

迭代器失效规则(重点):

操作 失效情况
+= / append / push_back 可能导致全部迭代器失效(扩容时)
pop_back 尾迭代器失效
insert / erase 插入点及之后的所有迭代器失效
reserve / resize / shrink_to_fit 全部迭代器失效
clear 全部迭代器失效
// 危险:边遍历边 insert 可能死循环
for (auto it = s.begin(); it != s.end(); ++it) {
    if (*it == 'a') {
        s.insert(it, 'X'); // it 失效!后续 ++it 未定义行为
    }
}

// 正确做法:insert 返回新的有效迭代器
for (auto it = s.begin(); it != s.end(); ) {
    if (*it == 'a') {
        it = s.insert(it, 'X'); // 用返回值更新
        ++it;                    // 跳过新插入的字符
        ++it;                    // 跳过原字符
    } else {
        ++it;
    }
}

// erase 的正确用法:erase 返回下一个有效迭代器
for (auto it = s.begin(); it != s.end(); ) {
    if (*it == 'a') {
        it = s.erase(it); // 返回下一个
    } else {
        ++it;
    }
}

8. string 与 vector、char[] 的对比

string vector<char> char[]
底层 动态字符数组 动态字符数组 固定字符数组
内存连续 是 是 是
自动管理内存 是 是 否
字符串操作 丰富(find, substr, +) 无(需手动实现) 无(需手动实现)
\0 处理 自动管理,不算入 size 需手动管理 以 \0 结尾
随机访问 O(1) O(1) O(1)
尾部增删 O(1) 均摊 O(1) 均摊 不支持
中间增删 O(n) O(n) 不支持
SBO 优化 是(短字符串栈上存储) 否 栈上分配
与 C API 交互 s.c_str() 需手动加 \0 直接使用

选择建议:

  • 默认选择 string:任何涉及字符串的场景都用 string。
  • 需要底层字符操作(如自定义协议解析)→ vector<char>。
  • 已有 C API 需要兼容 → string 的 c_str() 即可,无需降级到 char[]。
  • 永远不要用裸 char[]:除非在极端性能敏感的嵌入式场景。

9. 字符串拼接的多种方式

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

int main() {
    // 方式 1:+ 运算符(产生临时对象,多次拼接效率低)
    string s1 = "hello" + string(" ") + "world";

    // 方式 2:+= 追加(原地修改,效率高)
    string s2;
    s2 += "hello";
    s2 += " ";
    s2 += "world";

    // 方式 3:append(与 += 等价,可指定子串)
    string s3;
    s3.append("hello");
    s3.append("world", 0, 5); // 取 "world" 的前 5 个

    // 方式 4:stringstream(多类型混合拼接最方便)
    stringstream ss;
    ss << "Score: " << 95 << ", Name: " << "Alice";
    string s4 = ss.str();

    // 方式 5:format(C++20,推荐)
    // string s5 = format("Score: {}, Name: {}", 95, "Alice");

    cout << s1 << endl; // hello world
    cout << s2 << endl; // hello world
    cout << s3 << endl; // helloworld
    cout << s4 << endl; // Score: 95, Name: Alice

    return 0;
}

经验法则:少量拼接用 +/+=,大量或混合类型拼接用 stringstream,C++20 优先用 format。

10. 常见坑汇总

  1. reserve 后用下标访问:size 仍是 0,字符未构造,未定义行为。
  2. += 导致迭代器失效:扩容时所有迭代器失效,不能在循环中依赖旧迭代器。
  3. insert / erase 后使用旧迭代器:插入/删除点及之后的迭代器全部失效。
  4. 在循环中 += 可能无限扩容:循环前用 reserve 预分配。
  5. string 的 size() 返回 size_t(无符号):与 int 比较可能产生意外结果:
string s = "hello";
for (int i = 0; i < s.size(); i++) { // 警告:有符号/无符号比较
    cout << s[i];
}
// 正确:用 size_t 或 auto
for (size_t i = 0; i < s.size(); i++) {
    cout << s[i];
}
  1. clear 不释放内存:size 变 0 但 capacity 不变。想释放内存用 shrink_to_fit() 或 swap 技巧:
string s(1000000, 'x');
s.clear(); // size=0, capacity 还是 1000000,内存未释放

// 释放内存的技巧
string().swap(s); // 临时空 string 和 s 交换,s 的内存被释放
// 或 C++11
s.shrink_to_fit();
  1. cin >> s 只读一个单词:遇空格/换行停止。读整行用 getline(cin, s)。
  2. getline 与 cin >> 混用时的换行符残留:
int n;
string s;
cin >> n;     // 读入数字,换行符留在缓冲区
getline(cin, s); // 读到空行!
// 解决:cin >> n; cin.ignore(); getline(cin, s);
  1. c_str() 返回的指针在 string 修改后失效:不要缓存 c_str() 的返回值。
  2. string 不是以 \0 为结尾的:s.size() 不包含 \0,但 c_str() 会自动追加 \0。
  3. 误以为 string 是线程安全的:多线程读写同一个 string 需要加锁。

11. C++11 新特性

11.1 移动语义

string s1 = "hello";
string s2 = std::move(s1); // s1 被掏空,零拷贝
// s1 现处于有效但未指定状态,可以继续赋值使用
s1 = "world"; // 合法

11.2 右值引用与移动赋值

string s;
s = "hello" + string(" ") + string("world"); // 临时对象被移动,避免拷贝

11.3 初始化列表

string s{'h', 'e', 'l', 'l', 'o'}; // "hello"

11.4 原始字符串字面量

// 普通字符串:需要转义
string path = "C:\\Users\\test\\file.txt";

// 原始字符串:所见即所得(C++11)
string raw = R"(C:\Users\test\file.txt)"; // 无需转义

// 含 ) 的内容
string raw2 = R"(hello )world )"; // 有问题!

// 自定义分隔符
string raw3 = R"++(hello )world )++"; // 完美

11.5 constexpr(C++11/14/17 逐步增强)

constexpr string_view sv = "hello"; // C++17 string_view
// string 本身 C++20 才支持 constexpr 构造

11.6 string_view(C++17)

#include <string_view>
using namespace std;

// string_view 是 string 的只读视图,零拷贝
string_view sv = "hello world";
cout << sv.substr(0, 5) << endl; // "hello"
// sv 不拥有内存,底层字符串必须存活

// 与函数参数配合,避免拷贝
void print(string_view sv) {
    cout << sv << endl;
}
print("hello");      // 隐式转换
print(string("abc")); // 零拷贝

12. 实战:字符串处理(string 最经典的场景)

#include <iostream>
#include <string>
#include <sstream>
#include <algorithm>
#include <vector>
using namespace std;

// 分割字符串
vector<string> split(const string& s, char delim) {
    vector<string> result;
    stringstream ss(s);
    string token;
    while (getline(ss, token, delim)) {
        result.push_back(token);
    }
    return result;
}

// 去除首尾空格
string trim(const string& s) {
    size_t start = s.find_first_not_of(" \t\n\r");
    size_t end = s.find_last_not_of(" \t\n\r");
    return (start == string::npos) ? "" : s.substr(start, end - start + 1);
}

// 转小写
string toLower(string s) {
    transform(s.begin(), s.end(), s.begin(), ::tolower);
    return s;
}

int main() {
    string input = "  Hello, World!  ";

    // 去空格
    string trimmed = trim(input);
    cout << "[" << trimmed << "]" << endl; // [Hello, World!]

    // 转小写
    string lower = toLower(trimmed);
    cout << lower << endl; // hello, world!

    // 分割
    string csv = "apple,banana,cherry";
    vector<string> fruits = split(csv, ',');
    for (const auto& f : fruits) {
        cout << f << " "; // apple banana cherry
    }
    cout << endl;

    // 替换
    string text = "hello world hello";
    size_t pos = text.find("world");
    if (pos != string::npos) {
        text.replace(pos, 5, "C++");
    }
    cout << text << endl; // hello C++ hello

    // 反转
    string rev = "abcdef";
    reverse(rev.begin(), rev.end());
    cout << rev << endl; // fedcba

    // 字符统计
    string s = "hello";
    int freq[26] = {};
    for (char c : s) {
        freq[c - 'a']++;
    }
    for (int i = 0; i < 26; i++) {
        if (freq[i] > 0) {
            cout << char('a' + i) << ": " << freq[i] << " ";
        }
    }
    cout << endl; // e: 1 h: 1 l: 2 o: 1

    return 0;
}

13. 实战场景

给定一个需求:需要处理用户输入的文本,支持查找、替换、分割、拼接等操作。

方法一:使用std::string建模,内存连续、缓存友好、提供丰富的字符串操作接口(find、substr、replace 等),默认选择。
方法二:使用std::vector<char>建模,更底层的字符序列操作,适合需要自定义协议解析或与底层 C API 交互的场景。
方法三:使用char[]/char*建模,C 风格字符串,无自动内存管理,容易越界,仅在极端性能敏感的嵌入式场景使用。

总结:
1、默认选择 string,内存连续缓存命中率最高,提供丰富的字符串操作接口
2、需要底层字符操作或与 C API 交互时选择 vector
3、永远不要用裸 char[],除非在极端性能敏感的嵌入式场景
4、string 的迭代器是随机访问迭代器,支持 it+n 这种跳跃操作
5、批量拼接前用 reserve 预分配内存,避免频繁扩容的拷贝开销
6、大量或混合类型拼接用 stringstream,C++20 优先用 format
7、string_view(C++17)是只读视图,零拷贝,适合作为函数参数传递字符串,避免不必要的 string 拷贝
8、string 的 size() 返回 size_t(无符号类型),与 int 比较时注意符号问题
9、getline 与 cin >> 混用时注意换行符残留问题,用 cin.ignore() 解决

posted @ 2026-09-08 23:53  LemHou  阅读(6)  评论(0)    收藏  举报