C++ string 详解:常用接口、字符串算法与深拷贝实现

C++ string 详解:常用接口、字符串算法与深拷贝实现

C 语言使用字符数组和 char* 表示字符串,长度、容量和空间释放需要程序员自己处理。std::string 把字符序列及其资源管理封装进一个类中,可以直接复制、拼接、比较和查找,也能和 STL 算法配合使用。

string 接口很多,逐个背函数名很快就会忘。更有效的方式是按使用场景理解:怎样构造字符串,长度与容量有什么区别,如何遍历和修改,查找失败为什么要比较 npos,以及底层管理动态空间时为什么必须处理深拷贝。

一、从 C 字符串到 std::string

C 风格字符串是一段以空字符 \0 结束的字符序列:

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

数组中实际保存 6 个字符:hello 和结尾的 \0。使用 strlenstrcpystrcat 等函数时,调用者需要保证目标空间足够,也要自己维护动态内存。

std::string 把这些操作放进类型内部:

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

int main() {
    std::string text = "hello";
    text += " world";
    std::cout << text << '\n';
}

编译运行:

bash 复制代码
g++ string_demo.cpp -std=c++11 -o string_demo
./string_demo

预期输出:

text 复制代码
hello world

使用 string 需要包含 <string>。可以写 using namespace std;,但这不是必要条件;直接使用 std::string 更明确,也能减少名称冲突。

二、先认识 auto 和范围 for

string 支持迭代器,经常与 C++11 的 auto 和范围 for 一起使用。

1. auto 是编译期类型推导

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

auto 变量必须有初始化表达式,编译器根据表达式推导类型:

cpp 复制代码
// auto value;  // 错误:没有初始值,无法推导类型

引用不会被普通 auto 自动保留下来。需要引用语义时必须写 auto&

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

for (auto ch : text) {
    ch = 'x';  // 修改的是副本,不影响 text
}

for (auto& ch : text) {
    ch = 'x';  // 修改原字符串中的字符
}

在 C++11 中,普通函数参数不能直接写成 void Func(auto value)。C++20 才加入这种简写形式,它本质上会形成函数模板。下文代码统一按 C++11 编译。

2. 范围 for 的本质是遍历范围

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

for (char ch : text) {
    std::cout << ch << ' ';
}

范围 for 会借助 begin()end() 完成遍历。只读时可以使用 const auto&,避免不必要的复制:

cpp 复制代码
for (const auto& ch : text) {
    std::cout << ch;
}

对于 char 来说复制成本很低,但这个习惯也适用于元素较大的容器。

三、string 的常见构造方式

写法 含义
std::string s; 构造空字符串
std::string s("hello"); 根据 C 字符串构造
std::string s(5, 'x'); 构造包含 5 个 x 的字符串
std::string s2(s1); 拷贝构造
std::string s(first, last); 根据迭代器范围构造

完整示例:

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

int main() {
    std::string empty;
    std::string greeting("hello");
    std::string line(5, '-');
    std::string copy(greeting);

    std::cout << '[' << empty << "]\n";
    std::cout << greeting << '\n';
    std::cout << line << '\n';
    std::cout << copy << '\n';
}

预期输出:

text 复制代码
[]
hello
-----
hello

不能用空指针构造 std::string

cpp 复制代码
const char* pointer = nullptr;
// std::string text(pointer);  // 未定义行为

如果指针可能为空,应先检查并决定业务语义,而不是直接传入构造函数。

四、size、capacity、reserve 与 resize

字符串内部通常维护有效字符数量和可用存储空间。对应接口如下:

接口 作用
size() / length() 返回有效字符数量
capacity() 返回当前无需重新分配即可容纳的字符数量
empty() 判断字符串是否为空
clear() 清空有效字符
reserve(n) 请求预留至少可容纳 n 个字符的空间
resize(n) 把有效字符数量调整为 n
resize(n, ch) 扩大时使用 ch 填充新字符

1. size 和 capacity 不是一回事

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

size() 一定是 5,capacity() 的具体结果由标准库实现决定,不能依赖某个固定值。

length()size()string 表示相同含义。日常代码更常使用 size(),因为它与其他标准容器的命名一致。

2. clear 通常不负责缩小容量

cpp 复制代码
std::string text = "hello";
text.clear();

std::cout << text.size() << '\n';  // 0
std::cout << text.empty() << '\n'; // 1

clear() 删除所有有效字符,但不应把"清空内容"理解成"必须归还全部已分配空间"。容量是否变化不能写进程序逻辑。

3. reserve 只影响存储准备,不增加字符

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

std::cout << result.size() << '\n'; // 仍然是 0

当预计会连续追加大量内容时,提前 reserve 可以减少重复扩容:

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

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

reserve(n) 请求容量至少达到 n。具体增长策略由实现决定,实际容量可以大于 n

4. resize 会改变有效字符数量

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

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

扩大时,新位置会被初始化;resize(n) 使用值初始化得到的 char() 填充,也就是空字符。空字符可以出现在 std::string 内部,因此打印效果不一定直观。

五、字符访问与遍历

1. operator\[\] 与 at

cpp 复制代码
std::string text = "hello";
text[0] = 'H';
std::cout << text << '\n';

operator[] 访问方便,但不会执行常规的边界检查。at() 越界时会抛出 std::out_of_range

cpp 复制代码
try {
    char ch = text.at(100);
    std::cout << ch << '\n';
} catch (const std::out_of_range& error) {
    std::cout << error.what() << '\n';
}

已确认下标合法时可以使用 [];需要显式防御越界时使用 at()

2. 正向与反向迭代

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

for (std::string::iterator it = text.begin();
     it != text.end();
     ++it) {
    std::cout << *it << ' ';
}

for (std::string::reverse_iterator it = text.rbegin();
     it != text.rend();
     ++it) {
    std::cout << *it << ' ';
}

end()rend() 都是范围终点,不能解引用。

3. const string 的访问

const std::string 只能通过只读接口访问。它的 operator[] 返回只读引用,begin() 得到的也是常量迭代器:

cpp 复制代码
const std::string text = "hello";
char first = text[0];
// text[0] = 'H';  // 编译失败

六、追加、插入与删除

在尾部追加内容有多种写法:

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

text.push_back(' ');
text.append("world");
text += '!';
text += " C++";

结果是:

text 复制代码
hello world! C++

常见修改接口还包括:

接口 作用
insert(pos, value) 在指定位置插入内容
erase(pos, count) 删除指定范围
replace(pos, count, value) 替换指定范围
push_back(ch) 在末尾添加一个字符
pop_back() 删除末尾字符
cpp 复制代码
std::string path = "home/user";
path.insert(0, "/");
path.replace(6, 4, "guest");
std::cout << path << '\n';

预期输出:

text 复制代码
/home/guest

operator+ 真的不能用吗

cpp 复制代码
std::string fullName = firstName + " " + lastName;

这种表达式清楚时可以正常使用。现代 C++ 有移动语义、返回值优化以及标准库内部优化,不能简单断言每次 operator+ 都会发生昂贵深拷贝。

真正需要留意的是循环中反复创建临时结果:

cpp 复制代码
// 不推荐在大量循环中反复这样写
// result = result + piece;

result += piece;

已知大致长度时再配合 reserve,通常更容易控制分配次数。

七、find、rfind、substr 与 npos

1. 正向查找

cpp 复制代码
std::string text = "one two three";
std::size_t position = text.find("two");

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

预期输出:

text 复制代码
4

查找失败时,find 返回 std::string::npos。它是 size_type 可表示的最大值,不能用 -1 的普通整数思维随意参与下标运算。

错误写法:

cpp 复制代码
std::size_t position = text.find("missing");
// std::cout << text[position];  // 未检查 npos,访问越界

2. 反向查找

rfind 从右向左寻找最后一次出现的位置:

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

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

预期输出:

text 复制代码
gz

3. 截取子串

cpp 复制代码
std::string text = "hello world";
std::string word = text.substr(6, 5);

substr(pos, count)pos 开始复制最多 count 个字符。省略 count 时会截取到末尾:

cpp 复制代码
std::string suffix = text.substr(6); // "world"

pos > size() 会抛出 std::out_of_range,因此由查找结果计算位置时要先检查 npos

八、输入输出与 c_str

1. operator>> 遇到空白就停止

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

输入 hello world 时,word 只得到 hello。需要读取整行时使用 std::getline

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

2. cin 和 getline 连用时的换行问题

cpp 复制代码
int age = 0;
std::cin >> age;

std::string name;
std::getline(std::cin, name); // 可能直接读到前一次输入留下的换行

可以使用 std::ws 跳过开头的空白:

cpp 复制代码
std::getline(std::cin >> std::ws, name);

如果行首空格属于有效数据,就不应使用 std::ws,而应有针对性地处理上一条输入留下的换行。

3. c_str 用于连接 C 接口

c_str() 返回以 \0 结尾的字符数组指针:

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

std::string fileName = "data.txt";
std::printf("%s\n", fileName.c_str());

对字符串执行可能重新分配或修改内容的操作后,之前保存的 c_str() 指针可能失效:

cpp 复制代码
const char* pointer = fileName.c_str();
fileName += ".bak";
// pointer 现在不能继续假定有效

需要传给 C 接口时,尽量在调用位置临时取得 c_str(),不要长期保存返回指针。

九、实战一:只反转字符串中的字母

要求只交换字母,其他字符保持原位置。例如:

text 复制代码
a-bC-dEf-ghIj -> j-Ih-gfE-dCba

可以使用左右指针分别寻找字母:

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

std::string ReverseOnlyLetters(std::string text) {
    if (text.empty()) {
        return text;
    }

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

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

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

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

    return text;
}

先判断空字符串,是为了避免 text.size() - 1 在无符号类型下发生下溢。调用 std::isalpha 时转成 unsigned char,可以避免负 char 值带来的未定义行为。

十、实战二:大整数相加

当整数长度超过内置类型范围时,可以用字符串保存每一位,从末尾开始逐位相加。

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

std::string AddStrings(const std::string& left,
                       const std::string& right) {
    int i = static_cast<int>(left.size()) - 1;
    int j = static_cast<int>(right.size()) - 1;
    int carry = 0;
    std::string result;
    result.reserve((left.size() > right.size() ? left.size() : right.size()) + 1);

    while (i >= 0 || j >= 0 || carry != 0) {
        int leftDigit = i >= 0 ? left[static_cast<std::size_t>(i)] - '0' : 0;
        int rightDigit = j >= 0 ? right[static_cast<std::size_t>(j)] - '0' : 0;
        int sum = leftDigit + rightDigit + carry;

        result += static_cast<char>('0' + sum % 10);
        carry = sum / 10;
        --i;
        --j;
    }

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

示例:

cpp 复制代码
std::cout << AddStrings("999999999999999999", "1") << '\n';

预期输出:

text 复制代码
1000000000000000000

这里采用尾部追加后整体反转,避免每生成一位就向字符串头部插入。头部插入需要移动后面的字符,累计成本更高。

十一、实战三:最后一个单词的长度

输入可能带有尾部空格,因此只调用一次 rfind(' ') 并不稳妥。可以从末尾跳过空格,再统计单词长度:

cpp 复制代码
#include <string>

std::size_t LastWordLength(const std::string& line) {
    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;
    }

    return end - begin;
}

这种写法同时处理空字符串、全空格字符串、无空格字符串和尾部存在多个空格的情况。

十二、string 的内部结构不能按固定大小背

std::string 的接口由标准规定,具体内存布局由标准库实现决定。下面这些内容都可能因平台、编译器版本、调试模式和 ABI 而变化:

  • sizeof(std::string) 的结果。
  • 对象内部有哪些字段。
  • 容量增长策略。
  • 短字符串能够内联保存多少字符。

短字符串优化

现代标准库常使用短字符串优化,也叫 SSO。较短的字符串直接保存在 string 对象内部,不需要单独申请堆空间;长度超过内部缓冲区后,再切换到动态存储。

SSO 是常见优化思路,不是要求所有实现使用相同缓冲区大小。不能根据某个环境中"短于 16 个字符"就写出依赖该阈值的程序。

写时拷贝属于历史实现思路

写时拷贝让多个字符串对象暂时共享字符空间,修改时再复制,并使用引用计数管理共享资源。早期的一些 string 实现采用过这种方案。

现代 C++ 标准库通常不再用传统写时拷贝实现 std::string。并发访问、迭代器语义以及 C++11 之后的要求让这种设计不再合适。理解写时拷贝有助于认识资源共享策略,但不能据此推断当前 std::string 的对象大小或拷贝行为。

十三、为什么浅拷贝会让自定义 String 崩溃

考虑一个只保存 char* 的类:

cpp 复制代码
class String {
public:
    explicit String(const char* text = "") {
        _data = new char[std::strlen(text) + 1];
        std::strcpy(_data, text);
    }

    ~String() {
        delete[] _data;
    }

private:
    char* _data;
};

如果没有定义拷贝构造函数,编译器生成的版本会逐成员复制 _data 指针:
#mermaid-svg-MzlMfY4RDEtkKDac{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MzlMfY4RDEtkKDac .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MzlMfY4RDEtkKDac .error-icon{fill:#552222;}#mermaid-svg-MzlMfY4RDEtkKDac .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MzlMfY4RDEtkKDac .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MzlMfY4RDEtkKDac .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MzlMfY4RDEtkKDac .marker.cross{stroke:#333333;}#mermaid-svg-MzlMfY4RDEtkKDac svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MzlMfY4RDEtkKDac p{margin:0;}#mermaid-svg-MzlMfY4RDEtkKDac .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MzlMfY4RDEtkKDac .cluster-label text{fill:#333;}#mermaid-svg-MzlMfY4RDEtkKDac .cluster-label span{color:#333;}#mermaid-svg-MzlMfY4RDEtkKDac .cluster-label span p{background-color:transparent;}#mermaid-svg-MzlMfY4RDEtkKDac .label text,#mermaid-svg-MzlMfY4RDEtkKDac span{fill:#333;color:#333;}#mermaid-svg-MzlMfY4RDEtkKDac .node rect,#mermaid-svg-MzlMfY4RDEtkKDac .node circle,#mermaid-svg-MzlMfY4RDEtkKDac .node ellipse,#mermaid-svg-MzlMfY4RDEtkKDac .node polygon,#mermaid-svg-MzlMfY4RDEtkKDac .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MzlMfY4RDEtkKDac .rough-node .label text,#mermaid-svg-MzlMfY4RDEtkKDac .node .label text,#mermaid-svg-MzlMfY4RDEtkKDac .image-shape .label,#mermaid-svg-MzlMfY4RDEtkKDac .icon-shape .label{text-anchor:middle;}#mermaid-svg-MzlMfY4RDEtkKDac .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MzlMfY4RDEtkKDac .rough-node .label,#mermaid-svg-MzlMfY4RDEtkKDac .node .label,#mermaid-svg-MzlMfY4RDEtkKDac .image-shape .label,#mermaid-svg-MzlMfY4RDEtkKDac .icon-shape .label{text-align:center;}#mermaid-svg-MzlMfY4RDEtkKDac .node.clickable{cursor:pointer;}#mermaid-svg-MzlMfY4RDEtkKDac .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MzlMfY4RDEtkKDac .arrowheadPath{fill:#333333;}#mermaid-svg-MzlMfY4RDEtkKDac .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MzlMfY4RDEtkKDac .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MzlMfY4RDEtkKDac .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MzlMfY4RDEtkKDac .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MzlMfY4RDEtkKDac .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MzlMfY4RDEtkKDac .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MzlMfY4RDEtkKDac .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MzlMfY4RDEtkKDac .cluster text{fill:#333;}#mermaid-svg-MzlMfY4RDEtkKDac .cluster span{color:#333;}#mermaid-svg-MzlMfY4RDEtkKDac div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MzlMfY4RDEtkKDac .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MzlMfY4RDEtkKDac rect.text{fill:none;stroke-width:0;}#mermaid-svg-MzlMfY4RDEtkKDac .icon-shape,#mermaid-svg-MzlMfY4RDEtkKDac .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MzlMfY4RDEtkKDac .icon-shape p,#mermaid-svg-MzlMfY4RDEtkKDac .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MzlMfY4RDEtkKDac .icon-shape .label rect,#mermaid-svg-MzlMfY4RDEtkKDac .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MzlMfY4RDEtkKDac .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MzlMfY4RDEtkKDac .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MzlMfY4RDEtkKDac :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} s1._data
同一块字符数组
s2._data

两个对象指向同一块动态空间。任意一个对象析构后,另一个对象就持有悬空指针;两个对象都析构时,还会发生重复释放。

这就是资源类中默认浅拷贝的典型问题。解决方式是让每个对象拥有独立副本,或者设计一套正确的共享所有权机制。

十四、用深拷贝和 copy-and-swap 实现简化 String

下面实现一个聚焦资源管理的简化版本。它支持构造、拷贝、赋值、析构和输出,不试图复刻标准库的全部接口。

cpp 复制代码
#include <cstring>
#include <iostream>
#include <utility>

class String {
public:
    explicit String(const char* text = "")
        : _data(Clone(text == nullptr ? "" : text)) {
    }

    String(const String& other)
        : _data(Clone(other._data)) {
    }

    String& operator=(String other) {
        Swap(other);
        return *this;
    }

    ~String() {
        delete[] _data;
    }

    void Swap(String& other) noexcept {
        std::swap(_data, other._data);
    }

    const char* CStr() const {
        return _data;
    }

private:
    static char* Clone(const char* text) {
        std::size_t length = std::strlen(text);
        char* copy = new char[length + 1];
        std::memcpy(copy, text, length + 1);
        return copy;
    }

private:
    char* _data;
};

int main() {
    String first("hello");
    String second(first);
    String third("temporary");

    third = first;

    std::cout << first.CStr() << '\n';
    std::cout << second.CStr() << '\n';
    std::cout << third.CStr() << '\n';
}

预期输出:

text 复制代码
hello
hello
hello

赋值运算符按值接收参数:

cpp 复制代码
String& operator=(String other) {
    Swap(other);
    return *this;
}

进入函数前,other 已经通过拷贝构造拥有独立资源。交换后,当前对象获得新资源,旧资源转移给 other,函数结束时由 other 的析构函数释放。这种 copy-and-swap 写法自然处理自赋值,并具有较好的异常安全性:如果参数副本申请内存失败,当前对象尚未被修改。

十五、常见问题与易错点

1. 空字符串上计算 size() - 1

size() 返回无符号类型。空字符串的 size() - 1 会下溢成一个很大的值。双指针算法应先检查 empty(),或者使用不会减到零以下的边界写法。

2. find 后不检查 npos

查找失败时不能把 npos 当成合法下标,也不要直接用它计算子串起点。

3. cin >> text 读取不到空格

提取运算符按单词读取,遇到空白停止。读取整行使用 getline,与前一次格式化输入混用时处理残留换行。

4. 把 capacity 当成可访问元素数量

只能访问 [0, size()) 内的有效字符。capacity() 多出来的空间尚未形成可通过下标使用的字符。

5. 长期保存 c_str 返回值

修改字符串后,旧指针可能失效。在真正调用 C 接口的位置再获取 c_str()

6. 修改字符串后继续使用旧迭代器

追加、插入、删除、替换和重新分配都可能使迭代器、引用或指针失效。修改结构后重新获取位置最稳妥。

7. 用 char 直接调用 cctype 函数

std::isalphastd::isdigitstd::tolower 等函数要求参数可表示为 unsigned char 或等于 EOF。处理普通 char 时先转换成 unsigned char

8. 把 size 当成 Unicode 字符数量

std::string::size() 返回 char 元素数量。UTF-8 中一个汉字通常由多个字节组成,因此字节数不等于用户看到的字符数。

9. 自定义资源类只写析构函数

类中直接管理动态资源时,还要检查拷贝构造和赋值运算符。只写析构函数通常会留下浅拷贝和重复释放问题。

十六、总结

std::string 把字符序列和资源管理封装在一起。构造与遍历是基础,size/capacity 用来区分有效内容和预留空间,reserve/resize 分别处理容量准备与字符数量,find/rfind/substr 组合起来可以完成大量文本拆分任务。

使用接口时要守住几个边界:end() 不能解引用,查找结果先判断 nposc_str() 指针不能跨修改长期保存,空字符串上不要直接计算 size() - 1。对象大小、SSO 阈值和增长策略都属于实现细节。

模拟 String 类的意义不在于替代标准库,而是理解资源所有权。浅拷贝只复制指针,深拷贝为对象创建独立资源,copy-and-swap 则把复制、交换和自动析构组合成更稳妥的赋值流程。

相关推荐
Keven_111 小时前
算法札记:差分约束系统为啥叫差分约束系统Σ(っ °Д °;)っ
算法·差分约束系统
XWalnut2 小时前
LeetCode刷题 day29
java·算法·leetcode
65岁退休Coder2 小时前
LangChain v1.3.4 笔记 - 01 模型的连接/输出、消息、提示词模板
后端
m0_738120722 小时前
PHP代码审计基础——面向对象(四)
android·开发语言·网络·安全·github·php
2601_963771372 小时前
10 Best PHP Media CMS and Scripts for Web Creators in 2026
开发语言·前端·php
embrace_the_sunhaha2 小时前
卡尔曼滤波理解
算法
笃行3502 小时前
数据库优化深度科普向|外连接消除底层原理,国产数据库优化器设计思路拆解
后端
阿里嘎多学长2 小时前
2026-07-09 GitHub 热点项目精选
开发语言·程序员·github·代码托管
带娃的IT创业者2 小时前
监控并非安全:当隐私成为技术的祭品
java·开发语言·安全·数据安全·监控·隐私保护·加密技术