【C++ string 上篇】从字符串基础到容量管理、迭代器与经典算法题

🔥 本文定位 :本文围绕 std::string 的"使用层"展开,从 C 风格字符串的痛点出发,系统梳理构造、访问、遍历、容量管理、追加、查找、截取、输入输出,以及字符串算法题中最常见的双指针、计数和模拟加法。

💡 学习目标 :不仅会调用接口,还要真正理解 sizecapacity 的区别、reserveresize 为什么不能混用、范围 for 何时必须加引用、npos 为什么要用 size_type 接收、getline 为什么会读到空行,以及修改字符串后迭代器为什么可能失效

📌 阅读说明:本文以 C++11 及之后的主流写法为背景,并标注 C++17、C++20 中容易混淆的变化。不同标准库可以采用不同的容量增长策略和小字符串优化,文章不会把某个编译器版本的对象布局当成语言规则。


文章目录


一、为什么有了字符数组还要学习 string

1.1 C 风格字符串是什么

C 风格字符串通常由一段连续字符和结尾的空字符 '\0' 组成:

cpp 复制代码
char text[] = "hello";

数组中实际包含 6 个字符:

text 复制代码
'h' 'e' 'l' 'l' 'o' '\0'

<cstring> 提供了 strlenstrcpystrcatstrcmp 等函数,但"字符数据"和"管理数据的操作"彼此分离。调用者还要自己处理:

  1. 目标缓冲区是否足够;
  2. 是否为结尾的 '\0' 留出空间;
  3. 源地址和目标地址是否重叠;
  4. 动态空间何时扩容、由谁释放;
  5. 空指针、越界和异常路径如何处理。

这些问题并非不能解决,但很容易让业务逻辑被内存管理细节淹没。

1.2 std::string 带来了什么

std::string 把字符序列和资源管理封装在一个对象中:

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    std::string name = "Alice";
    name += " Chen";

    std::cout << name << '\n';
    std::cout << name.size() << '\n';
}

它负责维护有效字符数、可用空间和对象生命周期,并提供迭代器、比较、查找、截取等统一接口。

需要注意,std::string 并没有让所有错误自动消失。下标越界、错误保存迭代器、误解编码、反复触发扩容等问题仍需要程序员负责。

1.3 std::string 不是"只能装普通文本"

std::string 本质上是字符序列,可以包含位于中间的 '\0'

cpp 复制代码
std::string data{"A\0B", 3};

std::cout << data.size() << '\n'; // 3

如果把 data.c_str() 交给只认 '\0' 结尾的旧式 C 接口,对方可能只看到 "A"。因此:

  • 在 C++ 对象内部,长度由 size() 记录;
  • 在传统 C 字符串接口中,'\0' 常被当作终点;
  • 文本、字节序列和编码是三个相关但不同的问题。

二、先认识 std::string 的基本模型

2.1 类型与头文件

使用字符串类需要包含:

cpp 复制代码
#include <string>

std::stringstd::basic_string<char> 的别名。日常代码中直接使用 std::string 即可:

cpp 复制代码
std::string message = "hello";

示例代码为了简洁有时会写 using namespace std;,但大型项目和头文件中更建议保留 std::,避免名字冲突。

2.2 一个稳定的心智模型

可以先把一个字符串对象抽象成三项信息:

text 复制代码
string 对象
├─ 字符序列:当前保存的有效字符
├─ size:有效字符数量
└─ capacity:无需重新分配即可容纳的字符数量

并始终满足:

text 复制代码
0 <= size() <= capacity()

标准库会保证用于 C 字符串访问的末尾终止字符,因此:

cpp 复制代码
std::string s = "hello";
const char* p = s.c_str();
// p[0] ... p[s.size() - 1] 是有效字符
// p[s.size()] 是 '\0'

但不要把这个抽象误读成固定的对象内存布局。标准没有规定 std::string 对象必须包含"一个指针、一个长度、一个容量",也没有规定对象必须占多少字节。

2.3 小字符串优化不是语言保证

主流实现常采用 Small String Optimization,简称 SSO:短字符串直接放在对象内部,较长字符串才申请动态空间。它能减少短文本的堆分配,但属于实现策略:

  • 阈值可能因标准库、位数、编译选项而不同;
  • sizeof(std::string) 不能推导出统一的 SSO 长度;
  • 代码不能依赖某个版本"长度小于 16 一定不分配";
  • 性能结论应通过目标平台上的基准和分析工具验证。

三、auto 与范围 for:遍历 string 的基础工具

3.1 auto 会推导类型,不会让 C++ 变成动态类型

auto 要求编译器在编译期从初始化表达式推导类型:

cpp 复制代码
int number = 10;
auto copy = number;      // int
auto pointer = &number;  // int*
auto& alias = number;    // int&

几个关键规则:

  • 普通 auto 通常会丢掉顶层 const 和引用;
  • 需要引用语义时要显式写 auto&const auto&
  • 变量通常必须有初始化器;
  • 一个声明中的多个声明符必须得到相容的推导结果;
  • auto 不是运行时"万能类型"。
cpp 复制代码
const int value = 42;

auto a = value;        // int
const auto b = value;  // const int
const auto& c = value; // const int&

早期教程常说"auto 不能作为函数参数"。这对 C++11 到 C++17 的普通函数参数成立;从 C++20 开始,void print(auto value) 是合法的简写函数模板。

3.2 范围 for 的值、引用与只读引用

遍历字符串时,循环变量的写法决定了是复制、修改还是只读访问:

cpp 复制代码
std::string text = "hello";

for (char ch : text) {
    // ch 是副本,修改 ch 不影响 text
}

for (char& ch : text) {
    // ch 引用原字符,可以修改 text
    ch = static_cast<char>(std::toupper(
        static_cast<unsigned char>(ch)
    ));
}

for (const char& ch : text) {
    // 只读引用,不复制元素
}

char 而言复制成本很低,for (char ch : text) 完全合理;当元素类型较大,或需要保留引用语义时,再使用 auto& / const auto&

3.3 为什么 toupper 前常转换成 unsigned char

<cctype> 中的 std::toupperstd::tolowerstd::isalnum 等函数只接受 EOF 或能表示为 unsigned char 的值。若普通 char 是有符号类型,直接传入负值可能产生未定义行为。

更稳妥的写法是:

cpp 复制代码
char upper_ascii(char ch) {
    const auto byte = static_cast<unsigned char>(ch);
    return static_cast<char>(std::toupper(byte));
}

这仍然只是按当前 C locale 处理单字节字符,不能把 UTF-8 中文字符当成一个 char 来做大小写或字符分类。


四、string 的构造、赋值与字符访问

4.1 常见构造方式

cpp 复制代码
std::string s1;                   // 空字符串
std::string s2 = "hello";         // 从 C 字符串构造
std::string s3("hello", 3);       // "hel"
std::string s4(5, '*');           // "*****"
std::string s5 = s2;              // 拷贝构造
std::string s6 = std::move(s5);   // 移动构造

const char* 构造时,传入的指针必须满足该重载的前置条件。不要把空指针当成空字符串:

cpp 复制代码
const char* p = nullptr;
// std::string bad(p); // 错误:不是合法的 C 字符串

std::string safe = p ? p : "";

4.2 operator\[\] 与 at

operator[] 适合已经证明下标合法的热路径,at 会检查边界:

cpp 复制代码
std::string word = "cat";

word[0] = 'C';
std::cout << word.at(1) << '\n';

访问有效字符时,应把合法下标理解为:

text 复制代码
0 到 size() - 1

不要用"末尾存在 '\0'"作为越界访问普通字符的理由。需要边界检查时使用 at,越界会抛出 std::out_of_range

4.3 front、back、data 与 c_str

cpp 复制代码
std::string path = "demo.txt";

char first = path.front();
char last = path.back();
const char* c_text = path.c_str();

对空字符串调用 front()back() 不满足前置条件,调用前先判断 empty()

从 C++17 开始,非 const std::stringdata() 可以返回可修改的字符指针,但不能借此破坏字符串的不变量,也不能写到 size() 之外:

cpp 复制代码
std::string text = "abc";
char* raw = text.data();
raw[0] = 'A'; // C++17 起可行

任何可能导致重新分配的操作之后,都要重新取得 data() / c_str() 指针。


五、size、capacity、reserve 与 resize

5.1 size 与 length

size()length() 都返回有效字符数量:

cpp 复制代码
std::string s = "hello";

std::cout << s.size() << '\n';   // 5
std::cout << s.length() << '\n'; // 5

容器代码更常使用 size(),因为它与 vectorlist 等接口一致。

5.2 clear 只清空内容,不负责"归还容量"

cpp 复制代码
std::string s = "a long text";
const auto old_capacity = s.capacity();

s.clear();

std::cout << s.size() << '\n';     // 0
std::cout << s.empty() << '\n';    // true

clear() 将有效字符数变为 0,但不要依赖它缩小 capacity()。如果确实希望请求收缩,可以调用 shrink_to_fit(),但它本身也是非强制请求。

5.3 reserve 只调整容量预期

当大致知道最终长度时,可以提前预留:

cpp 复制代码
std::string result;
result.reserve(1024);

for (int i = 0; i < 1000; ++i) {
    result += 'x';
}

reserve 不会凭空增加有效字符:

cpp 复制代码
std::string s = "abc";
s.reserve(100);

// s 仍然是 "abc"
// s.size() 仍然是 3
// s.capacity() 至少能够满足请求

5.4 resize 会改变有效字符数量

resize 直接改变 size()

cpp 复制代码
std::string s = "abc";

s.resize(5, 'x'); // "abcxx"
s.resize(2);      // "ab"

扩展时:

  • resize(n, ch)ch 填充新增字符;
  • resize(n) 对新增字符进行值初始化,char 得到 '\0'
  • 若容量不足,可能重新分配。

缩小时,后面的字符被移除,但容量通常不会因此自动缩小。

5.5 reserve 与 resize 的一句话区别

text 复制代码
reserve:我以后可能要放这么多,先准备空间
resize :我现在就有这么多个有效字符

如果只是为了减少扩容次数,不要用 resize 代替 reserve,否则会真的向字符串中加入字符。


六、四种常见遍历方式

6.1 下标遍历

cpp 复制代码
for (std::string::size_type i = 0; i < s.size(); ++i) {
    std::cout << s[i] << ' ';
}

适合需要位置、相邻元素或双指针的场景。

6.2 正向迭代器

cpp 复制代码
for (auto it = s.begin(); it != s.end(); ++it) {
    std::cout << *it << ' ';
}

迭代器是容器与算法之间的通用接口。很多标准算法只需要一对迭代器:

cpp 复制代码
#include <algorithm>

std::reverse(s.begin(), s.end());

6.3 反向迭代器

cpp 复制代码
for (auto it = s.rbegin(); it != s.rend(); ++it) {
    std::cout << *it << ' ';
}

6.4 范围 for

cpp 复制代码
for (char ch : s) {
    std::cout << ch << ' ';
}

它最简洁,但默认不暴露下标。需要位置时可以使用下标循环,或在 C++20 中结合 std::views 设计更高层的遍历逻辑。

6.5 size_type 与"倒序循环"陷阱

size() 返回无符号的 size_type。下面的倒序循环会出错:

cpp 复制代码
// 错误示范:i 永远不会小于 0
for (std::size_t i = s.size() - 1; i >= 0; --i) {
}

可以改成:

cpp 复制代码
for (std::size_t i = s.size(); i-- > 0;) {
    std::cout << s[i];
}

或者直接使用反向迭代器。


七、追加、插入、删除与替换

7.1 尾部追加

cpp 复制代码
std::string s = "hello";

s.push_back('!');
s.append(" C++");
s += " string";

常见选择:

  • 追加一个字符:push_back(ch)+= ch
  • 追加整个字符串:+= other
  • 追加区间、重复字符或部分字符串:append 的重载更丰富。

7.2 insert、erase 与 replace

cpp 复制代码
std::string s = "I C++";

s.insert(2, "love "); // "I love C++"
s.erase(2, 5);        // "I C++"
s.replace(2, 3, "like C++"); // "I like C++"

这些函数有很多重载。阅读代码时要特别确认参数到底表示:

  • 位置和数量;
  • 迭代器区间;
  • C 字符串;
  • 另一个 string 的子区间。

7.3 为什么不必机械地排斥 operator+

早期教程常说"operator+ 一定产生深拷贝,所以尽量不用"。现代 C++ 有移动语义、返回值优化和实现优化,不能用一句固定结论概括所有表达式。

真正值得警惕的是循环中的链式累加:

cpp 复制代码
std::string result;

for (const auto& part : parts) {
    result = result + part; // 可能反复创建临时对象
}

更直接的写法是:

cpp 复制代码
std::string result;
result.reserve(total_size);

for (const auto& part : parts) {
    result += part;
}

先写清楚,再通过性能分析确认是否值得优化。


八、find、rfind、substr 与 npos

8.1 find 从前向后查找

cpp 复制代码
std::string path = "/usr/local/bin";
const auto pos = path.find("local");

if (pos != std::string::npos) {
    std::cout << pos << '\n';
}

若没有找到,返回 std::string::npos

8.2 为什么不要用 int 接收 find

find 的返回类型是 std::string::size_type。推荐使用 auto 或明确的 size_type

cpp 复制代码
std::string::size_type pos = text.find(':');

npos 通常是 size_type 能表示的最大值。把它粗暴地放入 int 可能截断,也会混淆"合法位置"和"未找到"。

8.3 rfind 从后向前查找

cpp 复制代码
std::string filename = "archive.tar.gz";
const auto dot = filename.rfind('.');

if (dot != std::string::npos) {
    std::cout << filename.substr(dot + 1) << '\n'; // gz
}

8.4 substr 截取子串

cpp 复制代码
std::string s = "hello world";

std::string first = s.substr(0, 5); // hello
std::string rest = s.substr(6);     // world

参数含义是 substr(pos, count),第二个参数是"数量",不是结束下标。

8.5 最后一个单词长度的边界

若输入保证末尾没有空格,可以写:

cpp 复制代码
const auto pos = line.rfind(' ');
const auto length = line.size() - pos - 1;

这里 npos 恰好会通过无符号回绕得到正确结果,但这种技巧可读性不高,而且遇到尾随空格就失败。更稳健的写法是主动跳过尾部空格:

cpp 复制代码
std::size_t end = line.size();
while (end > 0 && line[end - 1] == ' ') {
    --end;
}

std::size_t begin = end;
while (begin > 0 && line[begin - 1] != ' ') {
    --begin;
}

std::cout << end - begin << '\n';

九、输入输出:cin、getline 与缓冲区陷阱

9.1 operator>> 读取一个"单词"

cpp 复制代码
std::string word;
std::cin >> word;

默认情况下,它会跳过前导空白,并在下一个空白字符处停止。

9.2 getline 读取整行

cpp 复制代码
std::string line;
std::getline(std::cin, line);

getline 会读取到分隔符,默认分隔符为换行符,并把分隔符从输入流中取走,但不放进字符串。

9.3 cin >> 后紧接 getline 为什么常读到空行

cpp 复制代码
int age = 0;
std::string name;

std::cin >> age;
std::getline(std::cin, name); // 可能立即读到上一行留下的 '\n'

一种常见解决方式:

cpp 复制代码
#include <limits>

std::cin >> age;
std::cin.ignore(
    std::numeric_limits<std::streamsize>::max(),
    '\n'
);
std::getline(std::cin, name);

如果输入格式允许,也可以统一使用 getline,再对整行做解析。


十、五类经典字符串算法

10.1 只反转字母:相向双指针

目标是只交换字母,其他字符保持原位置:

cpp 复制代码
#include <cctype>
#include <string>
#include <utility>

bool is_letter(char ch) {
    return std::isalpha(
        static_cast<unsigned char>(ch)
    ) != 0;
}

std::string reverse_only_letters(std::string s) {
    if (s.empty()) {
        return s;
    }

    std::size_t left = 0;
    std::size_t right = s.size() - 1;

    while (left < right) {
        while (left < right && !is_letter(s[left])) {
            ++left;
        }
        while (left < right && !is_letter(s[right])) {
            --right;
        }

        if (left < right) {
            std::swap(s[left], s[right]);
            ++left;
            --right;
        }
    }

    return s;
}

核心不是记代码,而是识别模式:

text 复制代码
左指针找可交换元素
右指针找可交换元素
交换
继续向中间收缩

10.2 第一个只出现一次的字符:计数两遍

如果题目明确字符范围是 ASCII,可以使用固定数组:

cpp 复制代码
#include <array>
#include <string>

int first_unique_char(const std::string& s) {
    std::array<int, 256> count{};

    for (unsigned char ch : s) {
        ++count[ch];
    }

    for (std::size_t i = 0; i < s.size(); ++i) {
        const auto ch = static_cast<unsigned char>(s[i]);
        if (count[ch] == 1) {
            return static_cast<int>(i);
        }
    }

    return -1;
}

第一遍统计,第二遍按原顺序找答案。若字符集不受限,应改用适合题意的映射结构;若输入是 UTF-8,"一个用户可见字符"也不等于一个字节。

10.3 验证回文:过滤规则 + 双指针

cpp 复制代码
#include <cctype>
#include <string>

bool is_palindrome(const std::string& s) {
    std::size_t left = 0;
    std::size_t right = s.size();

    while (left < right) {
        while (left < right &&
               !std::isalnum(static_cast<unsigned char>(s[left]))) {
            ++left;
        }

        while (left < right &&
               !std::isalnum(static_cast<unsigned char>(s[right - 1]))) {
            --right;
        }

        if (left < right) {
            const auto a = std::tolower(
                static_cast<unsigned char>(s[left])
            );
            const auto b = std::tolower(
                static_cast<unsigned char>(s[right - 1])
            );

            if (a != b) {
                return false;
            }

            ++left;
            --right;
        }
    }

    return true;
}

这里把 right 设计成"开区间终点",空字符串也不需要先计算 size() - 1,可以避免无符号下溢。

10.4 大整数相加:从低位向高位模拟

cpp 复制代码
#include <algorithm>
#include <string>

std::string add_strings(
    const std::string& lhs,
    const std::string& rhs
) {
    std::size_t i = lhs.size();
    std::size_t j = rhs.size();
    int carry = 0;
    std::string result;
    result.reserve(std::max(lhs.size(), rhs.size()) + 1);

    while (i > 0 || j > 0 || carry != 0) {
        int sum = carry;

        if (i > 0) {
            sum += lhs[--i] - '0';
        }
        if (j > 0) {
            sum += rhs[--j] - '0';
        }

        result.push_back(static_cast<char>('0' + sum % 10));
        carry = sum / 10;
    }

    std::reverse(result.begin(), result.end());
    return result;
}

为什么先尾插再反转?

  • push_back 在尾部追加,通常更符合连续容器的使用方式;
  • 每次在开头 insert 都要搬移已有字符;
  • 预留最大长度加 1,可以减少扩容。

10.5 翻转单词:先明确"空格语义"

"翻转句子中的单词"至少有三种不同题意:

  1. 单词顺序不变,只翻转每个单词内部字符;
  2. 单词内部不变,只反转单词顺序;
  3. 同时规范多余空格。

写代码前应先确认:

  • 分隔符是否只有普通空格;
  • 是否存在连续空格、前导空格和尾随空格;
  • 是否要原地修改;
  • 是否要支持 Unicode 空白字符。

算法错误经常不是循环写错,而是没有先定义输入模型。


十一、性能、失效规则与实现边界

11.1 容量增长策略是实现细节

连续追加时,标准库通常按某种增长策略扩容,以获得摊销效率。但增长倍率、SSO 阈值和对象大小都不由 C++ 标准固定。

可移植代码应依赖接口契约:

  • size() 是有效字符数;
  • capacity() 是当前无需重新分配可容纳的数量;
  • reserve 可以提前提出容量需求;
  • 不要依赖某次追加后的精确 capacity()

11.2 重新分配会让旧地址失效

cpp 复制代码
std::string s = "hello";
const char* old = s.c_str();

s += std::string(1000, 'x');

// old 可能已经失效,必须重新获取
const char* current = s.c_str();

同理,保存的指针、引用和迭代器也可能失效。稳妥原则是:

在执行可能修改字符串结构的操作后,不要继续使用先前取得的元素地址或迭代器,除非对应接口明确保证仍然有效。

11.3 时间复杂度只是第一层

常见操作的直觉成本:

操作 常见复杂度直觉 备注
size()empty() 常数时间 长度由对象维护
operator[] 常数时间 不做常规边界检查
尾部 push_back 摊销常数时间 扩容时需要搬移字符
中间 insert/erase 线性时间 后续字符通常需要移动
find 与文本和模式长度相关 不承诺某个固定高级算法
substr 与结果长度相关 会构造新字符串

优化时还要考虑分配次数、缓存局部性、临时对象和真实数据规模。

11.4 编码边界

std::string 管理的是 char 序列,不自动理解"字符"的语言学含义。UTF-8 中一个汉字通常占多个字节,因此:

cpp 复制代码
std::string text = "中国";
std::cout << text.size() << '\n';

输出往往不是 2。按字节 reverse 还可能破坏编码。处理 Unicode 码点、字素簇、规范化和本地化比较时,应使用明确的 Unicode 方案,而不是把 char 当作用户可见字符。


十二、高频面试题与排查清单

12.1 常见误区

  1. 认为 reserve(100) 会让 size() 变成 100;
  2. 认为 resize(100) 只是准备空间,不会加入字符;
  3. int 接收 find 返回值;
  4. 直接判断 find(...) >= 0
  5. 对空字符串调用 front() / back()
  6. 在修改字符串后继续使用旧的 c_str() 指针;
  7. 倒序循环使用无符号变量并判断 i >= 0
  8. clear() 当作释放全部容量;
  9. 把某个编译器的 SSO 阈值当作标准保证;
  10. 使用 cin >> s 读取带空格的整行;
  11. 直接把可能为负的 char 传给 std::tolower
  12. 把字节数量 size() 当成 Unicode 字符数量。

12.2 高频面试题

问题 1:std::string 与 C 字符串的核心区别是什么?

std::string 是负责资源生命周期和字符序列操作的对象;C 字符串通常依赖 '\0' 作为结束标记,容量和生命周期由调用者额外管理。二者可以互操作,但长度模型和接口约定不同。

问题 2:size 与 capacity 有什么区别?

size 是当前有效字符数,capacity 是当前无需重新分配可以容纳的字符数。始终有 size() <= capacity()

问题 3:reserve 与 resize 有什么区别?

reserve 面向容量,不改变有效字符数;resize 直接改变有效字符数,扩展时会加入字符,容量不足时还可能触发重新分配。

问题 4:clear 会释放字符串内存吗?

clear 保证清空有效字符,不保证缩小容量。shrink_to_fit 也只是收缩请求,不能作为强制释放容量的可移植保证。

问题 5:为什么 find 的结果要与 npos 比较?

查找失败时返回 std::string::npos,它属于 size_type。使用 auto 接收并与 npos 比较,可以避免有符号/无符号和截断问题。

问题 6:operator\[\] 与 at 有何区别?

operator[] 面向已证明下标合法的访问;at 会做边界检查,并在越界时抛出 std::out_of_range

问题 7:c_str 返回的指针能保存多久?

它与字符串对象当前内部存储关联。对象销毁后失效;许多非 const 修改操作也可能使它失效。修改后应重新调用 c_str()

问题 8:std::string 一定在堆上保存字符吗?

不一定。主流实现常对短字符串使用 SSO,直接把字符放进对象内部。具体布局和阈值属于实现细节。

问题 9:现代 std::string 采用写时拷贝吗?

不能这样概括。历史实现曾采用 Copy-on-Write,但现代 C++ 的迭代器、并发和复杂度要求使主流标准库采用独立值语义配合 SSO、移动语义等策略。代码不应依赖写时拷贝。

问题 10:范围 for 中 auto、auto&、const auto& 如何选择?

按值用于只需要副本且元素便宜的情况;auto& 用于修改原元素;const auto& 用于只读且避免复制。对 char 而言按值遍历很常见。

问题 11:为什么大整数相加适合先 push_back 再 reverse?

计算从最低位开始,自然产生逆序结果。尾插比反复头插更适合连续存储,最后一次线性反转即可。

问题 12:std::string::size() 是字符数吗?

它是 char 元素数量,也就是代码单元/字节序列长度。对 UTF-8 文本,它通常不等于 Unicode 码点数,更不等于用户看到的字素数量。

12.3 排查清单

遇到字符串问题时,可以按下面的顺序检查:

text 复制代码
输入是单词、整行还是任意字节序列?
        ↓
下标是否始终落在 [0, size())?
        ↓
是否发生 size() - 1 的无符号下溢?
        ↓
find 失败是否与 npos 正确比较?
        ↓
reserve 与 resize 是否用反?
        ↓
修改后是否还在使用旧指针、引用或迭代器?
        ↓
循环中是否反复创建临时字符串或触发扩容?
        ↓
字符分类函数是否安全接收 unsigned char?
        ↓
题目说的"字符"究竟是字节、码点还是字素?

12.4 一组可运行的最小练习

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <string>

int main() {
    std::string text;
    std::getline(std::cin, text);

    std::cout << "size = " << text.size() << '\n';
    std::cout << "capacity = " << text.capacity() << '\n';

    text.reserve(text.size() + 32);
    text += " [C++]";

    const auto pos = text.find("C++");
    if (pos != std::string::npos) {
        std::cout << text.substr(pos, 3) << '\n';
    }

    std::reverse(text.begin(), text.end());
    std::cout << text << '\n';
}

建议依次尝试:

  1. reserve 改成 resize,观察内容和 size 的变化;
  2. 保存 c_str() 后追加一个很长的字符串,比较修改前后的地址;
  3. cin >> text 替换 getline,输入带空格的句子;
  4. 对空字符串故意计算 size() - 1,观察无符号结果;
  5. 在 ASCII 和 UTF-8 中文文本上分别测试 size()reverse

12.5 参考资料


总结

std::string 的使用主线可以压缩为:

text 复制代码
C 字符串需要手工维护缓冲区与终止字符
        ↓
std::string 封装字符序列和资源生命周期
        ↓
size 描述有效字符,capacity 描述可用空间
        ↓
reserve 预留容量,resize 改变有效长度
        ↓
下标、迭代器和范围 for 完成遍历
        ↓
find/rfind 定位,substr 提取,npos 表示失败
        ↓
双指针、计数、扫描和进位模拟解决经典题型
        ↓
修改后重新审视地址、迭代器、复杂度与编码边界

请牢记:

  1. std::string 是值类型容器,不是一个自动安全的裸字符指针
  2. sizecapacity 分工不同,reserveresize 不能互换
  3. 范围 for 是否加引用,取决于要复制、修改还是只读访问
  4. find 失败返回 npos,应使用匹配的 size_typeauto
  5. getline 读取整行,operator>> 默认按空白切分
  6. 重新分配后,旧的 data/c_str 指针、引用和迭代器可能失效
  7. 容量增长、对象大小和 SSO 阈值都是实现细节
  8. 字符串算法先定义输入模型,再选择双指针、计数或模拟流程
  9. char 序列不等于 Unicode 用户字符序列
  10. 能正确使用标准 string,是下一篇模拟实现资源管理语义的前提。

如果本文对你有帮助,欢迎点赞、收藏。真正掌握 std::string 的标志,不是背下几十个成员函数,而是能在看到一段字符串代码时,立即判断它修改了内容还是容量、会不会重新分配、边界是否安全,以及题目中的"字符"究竟指什么。

相关推荐
啦啦啦啦啦zzzz32 分钟前
ET和LT详解
linux·运维·服务器·网络·c++·网络编程
曹牧36 分钟前
Java:Java 数组转 List
java·开发语言·windows
skr爱码士38 分钟前
10_Qt Designer 与 UI 文件(.ui 原理、uic 编译、动态加载)
c++·qt·ui·客户端
Keven_111 小时前
算法札记:ACM中将高精度算法融于题目的模板
算法·acm·精度
小七在进步1 小时前
数据结构:快速排序
数据结构·算法·排序算法
下辈子不要当码农1 小时前
Day7-Linux软件编程(进程与线程(3))
java·开发语言
闻缺陷则喜何志丹1 小时前
【计算几何 第十一章】凸包:混合物
数学·算法·计算几何·凸包·混合物·凸组合
土司大王1 小时前
LeetCode hot100——二叉树的最大深度
算法·leetcode·职场和发展
高亦真1 小时前
今天是学习嵌入式的第31天
linux·学习·算法