C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝

C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 ✨ 数据即知识,压缩即智能


C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝

目录

  • [C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝](#C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝)
  • [C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝](#C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝)
  • [一、string 常见构造方式](#一、string 常见构造方式)
    • [1.1 构造空字符串](#1.1 构造空字符串)
    • [1.2 用 C 字符串构造](#1.2 用 C 字符串构造)
    • [1.3 拷贝构造](#1.3 拷贝构造)
    • [1.4 构造 n 个相同字符](#1.4 构造 n 个相同字符)
  • [二、string 的容量相关接口](#二、string 的容量相关接口)
    • [2.1 size 和 length](#2.1 size 和 length)
    • [2.2 capacity](#2.2 capacity)
    • [2.3 empty](#2.3 empty)
    • [2.4 clear](#2.4 clear)
    • [2.5 reserve](#2.5 reserve)
    • [2.6 resize](#2.6 resize)
    • [2.7 reserve 和 resize 的核心区别](#2.7 reserve 和 resize 的核心区别)
  • [三、string 的访问和遍历](#三、string 的访问和遍历)
    • [3.1 operator\[\]](#3.1 operator[])
    • [3.2 迭代器遍历](#3.2 迭代器遍历)
    • [3.3 反向迭代器](#3.3 反向迭代器)
    • [3.4 范围 for 遍历](#3.4 范围 for 遍历)
  • [四、string 的修改操作](#四、string 的修改操作)
    • [4.1 push_back](#4.1 push_back)
    • [4.2 append](#4.2 append)
    • [4.3 operator+=](#4.3 operator+=)
    • [4.4 operator+ 为什么要少用?](#4.4 operator+ 为什么要少用?)
  • [五、string 的查找和截取](#五、string 的查找和截取)
    • [5.1 find](#5.1 find)
    • [5.2 rfind](#5.2 rfind)
    • [5.3 substr](#5.3 substr)
    • [5.4 c_str](#5.4 c_str)
  • [六、输入输出:cin、getline 和空格问题](#六、输入输出:cin、getline 和空格问题)
    • [6.1 operator>>](#6.1 operator>>)
    • [6.2 getline](#6.2 getline)
    • [6.3 cin 和 getline 混用时的坑](#6.3 cin 和 getline 混用时的坑)
  • [七、几个典型 OJ 字符串场景](#七、几个典型 OJ 字符串场景)
    • [7.1 仅反转字母](#7.1 仅反转字母)
    • [7.2 找第一个只出现一次的字符](#7.2 找第一个只出现一次的字符)
    • [7.3 最后一个单词的长度](#7.3 最后一个单词的长度)
    • [7.4 验证回文串](#7.4 验证回文串)
    • [7.5 字符串相加](#7.5 字符串相加)
  • [八、不同编译器下 string 的底层结构](#八、不同编译器下 string 的底层结构)
    • [8.1 string 不是简单的 char*](#8.1 string 不是简单的 char*)
    • [8.2 小字符串优化](#8.2 小字符串优化)
    • [8.3 写时拷贝](#8.3 写时拷贝)
  • [九、为什么要模拟实现 String?](#九、为什么要模拟实现 String?)
  • [十、一个有问题的 String 类](#十、一个有问题的 String 类)
  • 十一、浅拷贝的问题
    • [11.1 什么是浅拷贝?](#11.1 什么是浅拷贝?)
    • [11.2 浅拷贝为什么危险?](#11.2 浅拷贝为什么危险?)
  • 十二、深拷贝解决问题
    • [12.1 什么是深拷贝?](#12.1 什么是深拷贝?)
    • [12.2 传统写法:拷贝构造](#12.2 传统写法:拷贝构造)
    • [12.3 传统写法:赋值运算符重载](#12.3 传统写法:赋值运算符重载)
  • 十三、现代写法:拷贝交换
    • [13.1 拷贝构造中的交换思想](#13.1 拷贝构造中的交换思想)
    • [13.2 赋值运算符的现代写法](#13.2 赋值运算符的现代写法)
  • [十四、String 模拟实现的基本版本](#十四、String 模拟实现的基本版本)
  • 十五、本文总结

前言

上一篇我们从 STL 的整体结构出发,介绍了 string 在 C++ 标准库中的位置。

这一篇继续深入:

string 到底怎么用?


一、string 常见构造方式

1.1 构造空字符串

cpp 复制代码
string s1;

这会构造一个空字符串。

此时:

cpp 复制代码
s1.size() == 0
s1.empty() == true

1.2 用 C 字符串构造

cpp 复制代码
string s2("hello bit");

或者:

cpp 复制代码
string s2 = "hello bit";

这是最常用的构造方式。

相比 C 字符数组,string 会自己管理内部空间。


1.3 拷贝构造

cpp 复制代码
string s3(s2);

或者:

cpp 复制代码
string s3 = s2;

这会用已有的 string 对象构造新对象。

从使用层面看,就是复制一份字符串内容。

从实现层面看,现代 string 通常需要保证两个对象的内容独立管理,避免浅拷贝导致的重复释放问题。


1.4 构造 n 个相同字符

cpp 复制代码
string s4(5, 'x');

结果是:

cpp 复制代码
"xxxxx"

这种写法在刷题中比较常用。

比如要构造一个长度为 n 的初始字符串:

cpp 复制代码
string ans(n, '0');

二、string 的容量相关接口

容量相关接口是 string 中非常重要的一组。

常见函数有:

cpp 复制代码
size()
length()
capacity()
empty()
clear()
reserve()
resize()

2.1 size 和 length

cpp 复制代码
s.size()
s.length()

这两个函数都返回字符串中有效字符的个数。

例如:

cpp 复制代码
string s = "hello";
cout << s.size() << endl;    // 5
cout << s.length() << endl;  // 5

从使用习惯上来说,C++ 容器普遍使用 size()

所以日常更推荐使用:

cpp 复制代码
s.size()

length() 更多是为了字符串语义保留的接口。


2.2 capacity

cpp 复制代码
s.capacity()

返回的是当前 string 底层已经分配的容量。

注意:

cpp 复制代码
size != capacity

size() 表示有效字符个数。

capacity() 表示当前底层空间最多能容纳多少有效字符。

比如:

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

size() 是 5。

capacity() 可能大于等于 5。

因为底层通常会预留一些空间,避免每次追加字符都重新申请内存。


2.3 empty

cpp 复制代码
s.empty()

用于判断字符串是否为空。

cpp 复制代码
if (s.empty())
{
    cout << "空字符串" << endl;
}

它等价于判断:

cpp 复制代码
s.size() == 0

但语义更清晰。


2.4 clear

cpp 复制代码
s.clear();

clear() 会清空字符串中的有效字符。

例如:

cpp 复制代码
string s = "hello";
s.clear();

cout << s.size() << endl;  // 0

注意:

clear 通常不会改变底层容量。

也就是说,它只是让字符串逻辑上变空,不一定释放已经申请好的空间。

这样做是为了后续继续使用时减少重新分配的开销。


2.5 reserve

cpp 复制代码
s.reserve(100);

reserve 的作用是预留容量。

它不会改变有效字符个数。

例如:

cpp 复制代码
string s = "hello";
s.reserve(100);

cout << s.size() << endl;      // 5
cout << s.capacity() << endl;  // 至少能容纳 100 左右

它适合在你大致知道字符串会增长到多长时提前使用。

比如频繁拼接字符串:

cpp 复制代码
string ret;
ret.reserve(10000);

for (...)
{
    ret += part;
}

这样可以减少扩容次数,提高效率。


2.6 resize

cpp 复制代码
s.resize(n);
s.resize(n, ch);

resize 会改变有效字符个数。

如果 n 小于当前 size(),字符串会被截断。

如果 n 大于当前 size(),字符串会扩展。

扩展出来的位置:

cpp 复制代码
resize(n)

通常用 '\0' 填充。

cpp 复制代码
resize(n, ch)

用指定字符 ch 填充。

例如:

cpp 复制代码
string s = "hello";
s.resize(8, 'x');

cout << s << endl;  // helloxxx

2.7 reserve 和 resize 的核心区别

这两个函数非常容易混。

cpp 复制代码
reserve

只管容量,不改变有效字符个数。

cpp 复制代码
resize

改变有效字符个数,必要时也可能改变容量。

例子:

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

s.reserve(100);
cout << s.size() << endl;  // 5

s.resize(100, 'x');
cout << s.size() << endl;  // 100

三、string 的访问和遍历

3.1 operator\[\]

最常用的访问方式是下标:

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

cout << s[0] << endl;  // h
s[0] = 'H';

operator[] 使用起来很方便,但要注意下标不能越界。

合法下标范围是:

cpp 复制代码
0 ~ s.size() - 1

3.2 迭代器遍历

string 支持迭代器:

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

string::iterator it = s.begin();
while (it != s.end())
{
    cout << *it << " ";
    ++it;
}

也可以用 auto 简化:

cpp 复制代码
auto it = s.begin();
while (it != s.end())
{
    cout << *it << " ";
    ++it;
}

begin() 指向第一个字符。

end() 指向最后一个字符的下一个位置。

注意:

end() 不是最后一个字符,而是最后一个字符后面的位置。


3.3 反向迭代器

string 也支持反向遍历:

cpp 复制代码
auto rit = s.rbegin();
while (rit != s.rend())
{
    cout << *rit << " ";
    ++rit;
}

这会从后往前访问字符串。


3.4 范围 for 遍历

最简单的遍历方式是范围 for

cpp 复制代码
for (auto ch : s)
{
    cout << ch << " ";
}

如果要修改字符,需要引用:

cpp 复制代码
for (auto& ch : s)
{
    ch = toupper(ch);
}

这里一定要注意:

cpp 复制代码
auto ch

是副本。

cpp 复制代码
auto& ch

才是原字符引用。


四、string 的修改操作

4.1 push_back

push_back 用于在字符串尾部追加一个字符:

cpp 复制代码
string s = "hello";
s.push_back('!');

cout << s << endl;  // hello!

4.2 append

append 用于追加字符串:

cpp 复制代码
string s = "hello";
s.append(" world");

cout << s << endl;  // hello world

也可以追加若干个字符:

cpp 复制代码
s.append(3, 'x');

4.3 operator+=

日常最常用的是 +=

它既可以追加字符:

cpp 复制代码
s += '!';

也可以追加字符串:

cpp 复制代码
s += " world";

还可以追加另一个 string

cpp 复制代码
string t = " C++";
s += t;

一般情况下,尾部追加更推荐使用 +=

写法自然,表达清楚。


4.4 operator+ 为什么要少用?

比如:

cpp 复制代码
string s1 = "hello";
string s2 = "world";

string s3 = s1 + " " + s2;

这当然可以用。

但如果在循环中大量使用 + 拼接字符串,可能会产生较多临时对象。

例如:

cpp 复制代码
string ret;

for (int i = 0; i < n; ++i)
{
    ret = ret + part;
}

这种写法可能效率较低。

更推荐:

cpp 复制代码
string ret;
ret.reserve(预估长度);

for (int i = 0; i < n; ++i)
{
    ret += part;
}

核心思想是:

能预估长度时先 reserve,频繁拼接时优先 +=。


五、string 的查找和截取

5.1 find

find 用于从字符串中查找字符或子串。

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

size_t pos = s.find("world");

如果找到了,返回第一次出现的位置。

如果没找到,返回:

cpp 复制代码
string::npos

所以使用时一定要判断:

cpp 复制代码
if (pos != string::npos)
{
    cout << "找到了" << endl;
}

不要直接拿 npos 当正常下标使用。


5.2 rfind

rfind 从后往前查找。

比如求最后一个单词长度时,经常会用:

cpp 复制代码
size_t pos = line.rfind(' ');

如果字符串是:

cpp 复制代码
"hello world"

最后一个空格的位置就是 5。

最后一个单词长度可以写成:

cpp 复制代码
line.size() - pos - 1

不过实际写题时要注意没有空格的情况。

如果 rfind 返回 npos,说明整行就是一个单词。


5.3 substr

substr 用于截取子串。

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

string sub = s.substr(6, 5);

结果是:

cpp 复制代码
"world"

第一个参数表示起始位置。

第二个参数表示截取长度。

如果第二个参数省略,默认截取到字符串末尾:

cpp 复制代码
string sub = s.substr(6);

5.4 c_str

c_str() 用于返回 C 风格字符串。

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

这个接口在和 C 库函数、某些系统接口交互时很有用。

但要注意:

返回的指针指向 string 内部数据。

string 被修改或销毁后,这个指针可能失效。

所以不要长期保存这个指针后继续使用。


六、输入输出:cin、getline 和空格问题

6.1 operator>>

可以直接输入字符串:

cpp 复制代码
string s;
cin >> s;

但它有一个特点:

遇到空白字符就停止。

比如输入:

cpp 复制代码
hello world

cin >> s 只能读到:

cpp 复制代码
hello

6.2 getline

如果要读取一整行,应该使用:

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

例如:

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

它会读取一整行,包括中间的空格,直到换行结束。

所以处理整行文本、句子、带空格输入时,getline 更合适。


6.3 cin 和 getline 混用时的坑

如果前面用了:

cpp 复制代码
cin >> n;

然后立刻:

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

可能会读到一个空行。

原因是 cin >> n 读取数字后,换行符还留在输入缓冲区中。

getline 会把这个残留换行符当作一行结束。

常见处理方式是先消耗掉这个换行符:

cpp 复制代码
cin.ignore();
getline(cin, line);

更严谨时可以写:

cpp 复制代码
cin.ignore(numeric_limits<streamsize>::max(), '\n');

七、几个典型 OJ 字符串场景

7.1 仅反转字母

题意:

字符串中只有字母需要反转,其他字符位置不动。

思路:

用双指针。

左指针从前往后找字母。

右指针从后往前找字母。

找到后交换。

核心逻辑:

cpp 复制代码
bool isLetter(char ch)
{
    return (ch >= 'a' && ch <= 'z')
        || (ch >= 'A' && ch <= 'Z');
}

string reverseOnlyLetters(string s)
{
    if (s.empty())
        return s;

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

    while (left < right)
    {
        while (left < right && !isLetter(s[left]))
            ++left;

        while (left < right && !isLetter(s[right]))
            --right;

        swap(s[left], s[right]);
        ++left;
        --right;
    }

    return s;
}

这个题适合练习:

  • string 下标访问
  • 双指针
  • 字符判断
  • swap

7.2 找第一个只出现一次的字符

思路:

先统计每个字符出现次数。

再从前往后扫描字符串。

第一个出现次数为 1 的字符,就是答案。

cpp 复制代码
int firstUniqChar(string s)
{
    int count[256] = {0};

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

    for (int i = 0; i < s.size(); ++i)
    {
        if (count[(unsigned char)s[i]] == 1)
            return i;
    }

    return -1;
}

这个题适合练习:

  • 计数数组
  • 字符作为下标
  • 按原字符串顺序查找答案

7.3 最后一个单词的长度

输入一整行,求最后一个单词长度。

不能用:

cpp 复制代码
cin >> line;

因为它遇到空格就停止。

应该用:

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

一种写法:

cpp 复制代码
int LastWordLength(const string& line)
{
    size_t pos = line.rfind(' ');

    if (pos == string::npos)
        return line.size();

    return line.size() - pos - 1;
}

不过如果字符串末尾可能有空格,需要先处理尾部空格。


7.4 验证回文串

常见题意:

只考虑字母和数字,忽略大小写。

思路:

  • 双指针
  • 跳过非字母数字字符
  • 比较时统一大小写
cpp 复制代码
bool isLetterOrNumber(char ch)
{
    return (ch >= '0' && ch <= '9')
        || (ch >= 'a' && ch <= 'z')
        || (ch >= 'A' && ch <= 'Z');
}

char ToLower(char ch)
{
    if (ch >= 'A' && ch <= 'Z')
        return ch + 32;

    return ch;
}

bool isPalindrome(string s)
{
    int left = 0;
    int right = s.size() - 1;

    while (left < right)
    {
        while (left < right && !isLetterOrNumber(s[left]))
            ++left;

        while (left < right && !isLetterOrNumber(s[right]))
            --right;

        if (ToLower(s[left]) != ToLower(s[right]))
            return false;

        ++left;
        --right;
    }

    return true;
}

7.5 字符串相加

题意:

两个非负整数字符串相加,返回字符串形式的结果。

不能直接转成整数,因为数字可能非常大。

思路:

从后往前逐位相加,维护进位。

cpp 复制代码
string addStrings(string num1, string num2)
{
    int i = num1.size() - 1;
    int j = num2.size() - 1;
    int carry = 0;
    string ret;

    while (i >= 0 || j >= 0 || carry)
    {
        int x = i >= 0 ? num1[i--] - '0' : 0;
        int y = j >= 0 ? num2[j--] - '0' : 0;

        int sum = x + y + carry;
        ret += char(sum % 10 + '0');
        carry = sum / 10;
    }

    reverse(ret.begin(), ret.end());
    return ret;
}

这个题很好地体现了 string 的使用价值:

  • 下标访问
  • 尾插字符
  • reverse
  • 字符和数字之间的转换

八、不同编译器下 string 的底层结构

8.1 string 不是简单的 char*

很多人刚开始会把 string 理解成:

cpp 复制代码
char*

这不准确。

string 是一个类对象,内部通常会管理:

  • 字符串数据
  • 有效长度
  • 当前容量
  • 分配器或其他辅助信息

不同编译器和不同标准库实现,内部结构可能不同。

所以不要在代码中依赖某个实现细节。


8.2 小字符串优化

很多现代 string 实现会使用一种优化:

小字符串优化。

大致意思是:

当字符串比较短时,不一定去堆上申请空间,而是直接把字符存放在 string 对象内部的小缓冲区里。

这样可以减少小字符串频繁动态分配的成本。

比如很多名字、短单词、路径片段都比较短,直接存在对象内部效率更高。

当字符串超过内部缓冲区容量时,再转到堆上分配空间。


8.3 写时拷贝

早期某些实现中,string 可能使用写时拷贝。

写时拷贝的思想是:

拷贝时先共享资源,真正要修改时再拷贝一份。

它通常配合引用计数使用。

优点是减少不必要的拷贝。

缺点是实现复杂,在多线程和字符访问语义上会带来一些问题。

现代 C++ 标准库实现中,主流 string 已经不再把写时拷贝作为推荐实现方式。

入门阶段只需要了解:

写时拷贝是一种历史上出现过的优化思想,不是我们模拟实现 string 时的第一选择。


九、为什么要模拟实现 String?

标准库里的 string 很强大。

那为什么面试和学习中还要模拟实现?

因为模拟实现 String 能训练几个非常关键的 C++ 能力:

  • 构造函数
  • 析构函数
  • 拷贝构造
  • 赋值运算符重载
  • 深拷贝
  • 资源管理
  • 自赋值处理
  • 异常安全意识

这不是为了替代标准库 string

而是为了理解:

一个管理动态资源的类,应该怎么写默认成员函数。


十、一个有问题的 String 类

先看一个简化版本:

cpp 复制代码
class String
{
public:
    String(const char* str = "")
    {
        if (str == nullptr)
        {
            assert(false);
            return;
        }

        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }

    ~String()
    {
        delete[] _str;
        _str = nullptr;
    }

private:
    char* _str;
};

构造函数申请空间。

析构函数释放空间。

看起来没问题。

但是如果写:

cpp 复制代码
String s1("hello");
String s2(s1);

这里会调用编译器默认生成的拷贝构造函数。

默认拷贝构造会把 _str 的值直接复制给 s2._str

也就是说:

cpp 复制代码
s1._str == s2._str

两个对象指向同一块堆空间。

这就是浅拷贝。


十一、浅拷贝的问题

11.1 什么是浅拷贝?

浅拷贝也叫位拷贝。

意思是:

只把对象中的成员变量值原样复制一份。

如果成员变量是普通 int,没问题。

但如果成员变量是指针,就会把地址复制过去。

结果就是多个对象共享同一块资源。


11.2 浅拷贝为什么危险?

继续看:

cpp 复制代码
String s1("hello");
String s2(s1);

浅拷贝后:

cpp 复制代码
s1._str
s2._str

都指向同一块字符数组。

当程序结束时,两个对象都会析构:

cpp 复制代码
delete[] s1._str;
delete[] s2._str;

同一块空间被释放两次,程序很可能崩溃。

此外,一个对象修改字符串内容,另一个对象也会受到影响。

所以对管理资源的类来说,浅拷贝通常是不够的。


十二、深拷贝解决问题

12.1 什么是深拷贝?

深拷贝的核心是:

每个对象都拥有自己独立的资源。

对于 String 来说,拷贝时不能只复制 _str 指针,而是要重新申请一块空间,再把字符串内容复制过去。


12.2 传统写法:拷贝构造

cpp 复制代码
String(const String& s)
{
    _str = new char[strlen(s._str) + 1];
    strcpy(_str, s._str);
}

这样:

cpp 复制代码
String s2(s1);

会让 s2 自己拥有一块新的空间。

内容和 s1 一样,但地址不同。

cpp 复制代码
s1._str != s2._str

析构时各自释放自己的空间,不会重复释放。


12.3 传统写法:赋值运算符重载

赋值运算符用于两个已经存在的对象之间赋值。

cpp 复制代码
String& operator=(const String& s)
{
    if (this != &s)
    {
        char* tmp = new char[strlen(s._str) + 1];
        strcpy(tmp, s._str);

        delete[] _str;
        _str = tmp;
    }

    return *this;
}

这里有几个细节:

第一,检查自赋值:

cpp 复制代码
if (this != &s)

处理:

cpp 复制代码
s1 = s1;

第二,先申请新空间,再释放旧空间。

这样如果申请失败,原对象还没有被破坏。

第三,返回 *this

为了支持连续赋值:

cpp 复制代码
s1 = s2 = s3;

十三、现代写法:拷贝交换

13.1 拷贝构造中的交换思想

可以写成:

cpp 复制代码
String(const String& s)
    : _str(nullptr)
{
    String tmp(s._str);
    swap(_str, tmp._str);
}

这段代码的思路是:

先用 s._str 构造一个临时对象 tmp

然后交换当前对象和 tmp 的资源。

tmp 析构时,会释放当前对象原来的资源。

这种写法简洁,并且复用了构造函数。


13.2 赋值运算符的现代写法

一种非常经典的写法是传值:

cpp 复制代码
String& operator=(String s)
{
    swap(_str, s._str);
    return *this;
}

这里参数 s 是按值传参。

调用时会先生成一份拷贝。

然后当前对象和这份拷贝交换资源。

函数结束时,形参 s 析构,释放当前对象原来的旧资源。

这就是 copy-and-swap 思想。

它的优点是:

  • 代码短
  • 自赋值也能安全处理
  • 异常安全性更好
  • 复用拷贝构造和析构逻辑

十四、String 模拟实现的基本版本

cpp 复制代码
#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;

class String
{
public:
    String(const char* str = "")
    {
        if (str == nullptr)
        {
            assert(false);
            str = "";
        }

        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }

    String(const String& s)
    {
        _str = new char[strlen(s._str) + 1];
        strcpy(_str, s._str);
    }

    String& operator=(const String& s)
    {
        if (this != &s)
        {
            char* tmp = new char[strlen(s._str) + 1];
            strcpy(tmp, s._str);

            delete[] _str;
            _str = tmp;
        }

        return *this;
    }

    ~String()
    {
        delete[] _str;
        _str = nullptr;
    }

    const char* c_str() const
    {
        return _str;
    }

private:
    char* _str;
};

这个版本不是为了替代标准库。

它的重点是展示:

只要类里管理动态资源,就必须认真处理拷贝构造、赋值运算符和析构函数。


十五、本文总结

这一篇主要围绕 string 的使用和模拟实现展开。

string 的容量接口中:

size()length() 都表示有效字符个数。

capacity() 表示底层容量。

clear() 清空有效字符,但通常不释放容量。

reserve() 只预留空间,不改变有效长度。

resize() 会改变有效字符个数。

string 的访问方式包括:

  • 下标访问
  • 迭代器
  • 反向迭代器
  • 范围 for

string 的修改方式中:

  • push_back 追加单个字符
  • append 追加字符串
  • += 更常用、更自然
  • 大量拼接时少用 +,优先 reserve + +=

string 的查找和截取中:

  • find 找不到返回 string::npos
  • rfind 从后往前找
  • substr 用于截取子串
  • c_str 用于获得 C 风格字符串

输入时:

  • cin >> s 遇到空格停止
  • getline(cin, s) 读取整行

模拟实现 String 时:

  • 默认拷贝会导致浅拷贝
  • 浅拷贝会导致资源共享和重复释放
  • 深拷贝让每个对象拥有独立资源
  • 管理资源的类必须认真写拷贝构造、赋值运算符和析构函数

最后用一句话总结:

string 的接口是用来解决字符串问题的,而 string 的模拟实现是用来理解 C++ 资源管理本质的。


相关推荐
yyuuuzz1 小时前
2026游戏云服务器推荐的技术判断思路
运维·服务器·开发语言·网络·人工智能·游戏·php
小二·1 小时前
Rust 爬虫与数据处理实战:大规模并发抓取 + 流式处理
开发语言·爬虫·rust
searchforAI1 小时前
2026国产AI笔记工具横评:Get笔记、Ai好记、通义听悟、BiBiGPT各有什么特色?
人工智能·笔记·学习·ai·音视频·知识图谱·知识库
程序喵大人1 小时前
【C++并发系列】第二章:锁解决了什么问题?
开发语言·c++·并发编程·
天天代码码天天1 小时前
用 TensorRT 加速 PP-OCR:一套 C++ DLL + C# 调用的高性能 OCR 推理方案
c++·c#·ocr
guygg881 小时前
二维弹塑性有限元分析(von Mises 等向硬化)— MATLAB 实现
开发语言·人工智能·matlab
在放️2 小时前
Python 练习题讲解 2 · 循环计算
开发语言·python
知识分享小能手2 小时前
Hadoop学习教程,从入门到精通,Flume日志采集系统 — 完整知识点与案例代码(9)
hadoop·学习·flume
我不是懒洋洋2 小时前
从零实现一个分布式链路追踪:TraceId与Span
c++