C++ string类

1. 为什么学习string类?

1.1 C语言中的字符串

• C语言中,字符串是以'\0'结尾的字符数组。

• 标准库提供了strlen、strcpy、strcmp等一系列str函数,但这些函数与字符串本身是分离的,不符合OOP(面向对象编程)的思想。

• 底层的内存空间需要用户自己管理,稍不留意就可能导致越界访问。

1.2 两个面试题(暂不做讲解)

• 字符串转整形数字

• 字符串相加

在OJ(在线判题)中,有关字符串的题目基本都以string类的形式出现。在常规工作中,为了简单、方便、快捷,也基本都使用string类,很少有人去使用C库中的字符串操作函数。

2. 标准库中的string类

2.1 string类(了解)

• string类的文档介绍:在使用string类时,必须包含<string>头文件,并且使用using namespace std;(或std::string)。

2.2 auto和范围for

auto关键字

在C++11中,auto被赋予了全新的含义:它不再是一个存储类型指示符,而是作为一个类型指示符,由编译器在编译时期推导得出。

• 指针与引用:用auto声明指针类型时,auto和auto*没有任何区别;但用auto声明引用类型时,则必须加&。

• 同一行声明多个变量:这些变量必须推导为相同的类型,否则编译器会报错,因为编译器只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

• 函数参数与返回值:auto不能作为函数的参数,但可以做返回值,不过建议谨慎使用。

• 数组:auto不能直接用来声明数组。

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

int func1()
{
    return 10;
}

// 不能做参数
// void func2(auto a)
// {}

// 可以做返回值,但建议谨慎使用
auto func3()
{
    return 3;
}

int main()
{
    int a = 10;
    auto b = a;      // 推导为 int
    auto c = 'a';    // 推导为 char
    auto d = func1();// 推导为 int

    // 编译报错: error C3531: "e": 类型包含"auto"的符号必须具有初始值设定项
    // auto e;

    cout << typeid(b).name() << endl; // 输出 int
    cout << typeid(c).name() << endl; // 输出 char
    cout << typeid(d).name() << endl; // 输出 int

    int x = 10;
    auto y = &x;     // 推导为 int*
    auto* z = &x;    // 推导为 int*
    auto& m = x;     // 推导为 int&

    cout << typeid(y).name() << endl; // 输出 int *
    cout << typeid(z).name() << endl; // 输出 int *
    cout << typeid(m).name() << endl; // 输出 int &

    auto aa = 1, bb = 2; // 正确,均为 int
    // 编译报错: error C3538: 在声明符列表中, "auto"必须始终推导为同一类型
    // auto cc = 3, dd = 4.0;

    // 编译报错: error C3318: "auto []": 数组不能具有其中包含"auto"的元素类型
    // auto array[] = { 4, 5, 6 };

    // auto的用武之地:简化迭代器声明
    std::map<std::string, std::string> dict = { {"apple", "苹果"},{"orange", "橙子"}, {"pear","梨"} };
    // std::map<std::string, std::string>::iterator it = dict.begin();
    auto it = dict.begin();
    while (it != dict.end())
    {
        cout << it->first << ":" << it->second << endl;
        ++it;
    }

    return 0;
}

范围for

• 对于一个有范围的集合,由程序员来说明循环的范围是多余的,有时还会容易犯错误。因此,C++11中引入了基于范围的for循环。

• for循环后的括号由冒号:分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

• 范围for会自动迭代、自动取数据、自动判断结束。

• 其底层实现就是替换为迭代器。

cpp 复制代码
#include<iostream>
#include <string>
#include <map>
using namespace std;

int main()
{
    int array[] = { 1, 2, 3, 4, 5 };
    
    // C++98的遍历方式
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
    {
        array[i] *= 2; // 将每个元素乘以2
    }
    // 遍历并打印数组
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
    {
        cout << array[i] << " ";
    }
    cout << endl; // 输出:2 4 6 8 10 

    // C++11的范围for遍历
    for (auto& e : array) // 使用引用,直接修改原数组元素
        e *= 2; // 每个元素再乘以2

    // 遍历并打印数组
    for (auto e : array)  // 直接遍历打印
        cout << e << " ";
    cout << endl; // 输出:4 8 12 16 20 

    // 遍历字符串
    string str("hello world");
    for (auto ch : str)
    {
        cout << ch << " ";
    }
    cout << endl; // 输出:h e l l o   w o r l d 

    return 0;
}

◦ for (auto& e : array):使用auto&可以直接修改数组中的元素,因为e是数组元素的引用。

◦ for (auto e : array):这里的e是数组元素的拷贝,修改e不会影响原数组,适合只读遍历。

◦ 范围for循环自动处理迭代的开始和结束,代码更简洁、安全。

◦ string是一个容器,范围for循环可以直接遍历其每个字符。

◦ auto ch会被推导为char类型,逐个取出字符串中的字符。

string的三种遍历方式

  1. 下标 [] ------ 像数组一样用

  2. 迭代器 iterator ------ 通用遍历器

  3. 范围for for(auto...) ------ 最简单写法

创建字符串 + 重载 << 输出 & [] 修改

cpp 复制代码
string s1;         // 空字符串
string s2("hello world"); // 构造字符串

cout << s1 << s2 << endl;

s2[0] = 'x';       // 把第 0 个字符改成 'x'
cout << s1 << s2 << endl;  // 改成 x 后,s2 变成:xello world

重点来了:三种遍历

① 下标遍历(最简单)

cpp 复制代码
for (size_t i = 0; i < s2.size(); i++)
{
    cout << s2[i] << " ";
}
// 像数组一样,i 从 0 开始,用 [] 访问每个字符

② 迭代器 iterator(最通用)

cpp 复制代码
// string::iterator it = s2.begin();
auto it = s2.begin();

while (it != s2.end())
{
    *it += 2;      // 把字符 ASCII +2
    cout << *it << " ";
    ++it;
}

• begin() → 指向第一个字符;end() → 指向最后一个字符的下一个位置;*it → 取出当前字符

• 迭代器就是像指针一样的东西;所有容器都能用迭代器:vector / list / map...

这里 *it += 2 会让字符变大,比如:x → z,e→g 等。

③ 范围for(最简洁)

cpp 复制代码
for (auto& ch : s2)
{
    ch -= 2;     // 改回原来的字符
    cout << ch << " ";
}

• auto& ch:引用,能修改原字符串 ; auto ch:值拷贝,不能修改原字符串

• 底层原理:就是迭代器

总结:[]:像数组,方便;iterator:通用,所有容器都能用;范围for:写法最简单,底层是迭代器。

迭代器

  1. 普通迭代器:iterator

  2. 反向迭代器:reverse_iterator

  3. 只读迭代器:const_iterator

  4. 只读反向迭代器:const_reverse_iterator

  5. 普通迭代器 iterator

cpp 复制代码
string s2("hello world");
string::iterator it = s2.begin();
while (it != s2.end())
{
    *it += 2;    // ✅ 可以修改
    cout << *it << " ";
    ++it;
}

// 输出:j g n n q " y q t n f

• 可读、可修改; begin() → 开头; end() → 末尾下一个位置; ++it 往后走

  1. 反向迭代器 reverse_iterator
cpp 复制代码
string::reverse_iterator rit = s2.rbegin();
while (rit != s2.rend())
{
    cout << *rit << " ";
    ++rit;
}

// 输出:f n t q y # q n n g j

• 倒着遍历; rbegin() → 最后一个字符; rend() → 第一个字符前面; 依然用 ++rit,它自动往前走

  1. const 迭代器 const_iterator
cpp 复制代码
const string s3("hello world");
auto cit = s3.begin();
while (cit != s3.end())
{
    // *cit += 2; ❌ 不能改
    cout << *cit << " ";
    ++cit;
}

// 输出:h e l l o   w o r l d

• 只能读,不能改; 给 const string 用的; 想修改会直接报错

  1. const 反向迭代器 const_reverse_iterator
cpp 复制代码
auto rcit = s3.rbegin();
while (rcit != s3.rend())
{
    // *rcit += 2; ❌ 不能改
    cout << *rcit << " ";
    ++rcit;
}

// 输出:d l r o w   o l l e h

• 反向 + 只读; 倒着遍历,但不能修改内容

总结:

|------------------------|------|------|--------------|
| 迭代器类型 | 能否修改 | 遍历方向 | 适用对象 |
| iterator | 可改 | 正向 | 普通 string |
| reverse_iterator | 可改 | 反向 | 普通 string |
| const_iterator | 不可改 | 正向 | const string |
| const_reverse_iterator | 不可改 | 反向 | const string |

• 带 reverse → 倒着走 ; 带 const → 只能看,不能改

rbegin() 和 rend()

rbegin = reverse begin → 反向开头;rend = reverse end → 反向结尾;它们是给反向迭代器用的。

  1. 正常迭代器(begin / end)
cpp 复制代码
字符串: h e l l o
下标:   0 1 2 3 4

begin() → 指向 h
end()   → 指向 o 后面
++it    → 从左往右走
  1. 反向迭代器(rbegin / rend)
cpp 复制代码
字符串:h e l l o
rbegin() → 指向 **o**(最后一个字符)
rend()   → 指向 **h 前面**(第一个字符前面)
++rit    → **从右往左走**

重点:++ 反向迭代器,是往左边走!

  1. 画图秒懂
cpp 复制代码
字符:     h   e   l   l   o
          ↑               ↑
        rend()         rbegin()

• rbegin() = 最后一个元素; rend() = 第一个元素前面的位置; ++rit → 向左移动

  1. 代码里的效果
cpp 复制代码
string s = "hello";
auto rit = s.rbegin();
while (rit != s.rend())
{
    cout << *rit << " ";
    ++rit;
}

输出:o l l e h

超级总结(背这三句)

rbegin() → 指向最后一个字符; rend() → 指向第一个字符前面; ++rit → 从右往左遍历

一句话:rbegin / rend 就是专门用来倒着遍历的。

2.3 string类的常用接口说明

1. string类对象的常见构造

|-------------------------------|-----------------------|
| (constructor)函数名称 | 功能说明 |
| string() (重点) | 构造空的string类对象,即空字符串 |
| string(const char* s) (重点) | 用C-string来构造string类对象 |
| string(size_t n, char c) | string类对象中包含n个字符c |
| string(const string& s) (重点) | 拷贝构造函数 |

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

void Teststring()
{
    string s1;              // 1. 默认构造(空字符串)
    string s2("hello bit"); // 2. 从C风格字符串构造
    string s3(s2);          // 3. 拷贝构造
}

int main() {
    Teststring();
    return 0;
}
  1. string s1; ------ 默认构造

作用:创建一个空的 string 对象 s1。

内部状态: 有效字符长度 size() 为 0;容量 capacity() 可能为 0 或一个实现定义的初始值(例如在VS下为15);不包含任何字符,等价于 ""。

  1. string s2("hello bit"); ------ 从C风格字符串构造

作用:接收一个以 '\0' 结尾的C风格字符串(const char*),并创建一个内容相同的 string 对象 s2。

内部状态:s2 的内容为 "hello bit";size() 返回 9(h e l l o b i t 共9个有效字符)。

注意:构造时会自动忽略C字符串末尾的 '\0',string 内部不依赖 '\0' 来判断长度。

  1. string s3(s2); ------ 拷贝构造

作用:创建一个新的 string 对象 s3,其内容是 s2 的一个深拷贝。

内部状态:s3 的内容与 s2 完全相同,为 "hello bit";s3 拥有独立的内存空间,对 s3 的修改不会影响 s2,反之亦然。

2. string类对象的容量操作

|-------------|------------------------------|
| 函数名称 | 功能说明 |
| size(重点) | 返回字符串有效字符长度 |
| length | 返回字符串有效字符长度 |
| capacity | 返回空间总大小 |
| empty(重点) | 检测字符串是否为空串,是返回true,否则返回false |
| clear(重点) | 清空有效字符 |
| reserve(重点) | 为字符串预留空间** |
| resize(重点) | 将有效字符的个数改成n个,多出的空间用字符c填充 |

注意:

  1. size()与length()方法底层实现完全相同,引入size()是为了与其他容器的接口保持一致,一般情况下基本都使用size()。

  2. clear()只是将string中有效字符清空,不改变底层空间大小。

  3. resize(size_t n)与resize(size_t n, char c)都是将字符串中有效字符个数改变到n个。不同的是当字符个数增多时,resize(n)用'\0'来填充多出的元素空间,resize(n, char c)用字符c来填充。resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小;如果是将元素个数减少,底层空间总大小不变。

  4. reserve(size_t res_arg=0)为string预留空间,不改变有效元素个数。当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小。

示例:

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

int main() {
    // 先造一个字符串
    string s("hello");

    // ==============================
    // 1. size() / length()
    // ==============================
    cout << "1. size: " << s.size() << endl;
    cout << "   length: " << s.length() << endl;
    // 功能完全一样,都是返回有效字符个数
    // 输出:5

    // ==============================
    // 2. capacity()
    // ==============================
    cout << "2. capacity: " << s.capacity() << endl;
    // 输出:平台不同不一样,比如 15 / 22 等
    // 意思:当前 string 能存多少字符不用扩容

    // ==============================
    // 3. empty()
    // ==============================
    cout << "3. empty? " << boolalpha << s.empty() << endl;
    // 输出:false(不是空串)

    // ==============================
    // 4. clear()
    // ==============================
    s.clear();
    cout << "4. clear 后 empty? " << s.empty() << endl;
    // 输出:true
    // 注意:clear 只清内容,不释放空间,capacity 不变

    // 恢复字符串
    s = "hello";

    // ==============================
    // 5. reserve(n) ------ 预留空间,不改变内容
    // ==============================
    cout << "5. reserve 前 capacity: " << s.capacity() << endl;
    s.reserve(20);  // 至少能存20个字符
    cout << "   reserve(20) 后 capacity: " << s.capacity() << endl;
    cout << "   内容不变: " << s << endl;
    // 内容还是 hello,size 还是 5,只是容量变大

    // ==============================
    // 6. resize(n) / resize(n, ch)
    //    改变有效字符个数
    // ==============================
    s.resize(8);    // 长度改成8,多出来的填 '\0'
    cout << "6. resize(8) 后 size: " << s.size() << endl;

    s.resize(10, 'x'); // 长度改成10,多出来的填 'x'
    cout << "   resize(10,'x') 后: " << s << endl;

    s.resize(3);    // 长度改成3,截断
    cout << "   resize(3) 后: " << s << endl;

    return 0;
}

• size / length:字符串实际有多少字符

• capacity:现在能装多少字符不用扩容

• empty:是不是空串

• clear:清空内容,容量不变

• reserve(n):只扩容空间,不改内容

• resize(n):改字符个数,变长补字符,变短截断

默认:true=1,false=0;用 boolalpha:true=true,false=false

size / length / capacity / reserve / resize / clear

• size() / length():字符串有效字符个数,两个完全一样。

• capacity():底层已经开辟的空间大小(能存多少字符)。

• reserve(n):只开空间,不改变有效字符;作用:提前扩容,避免频繁扩容。注意:只扩不缩(不会缩小容量)。

• clear():清空数据,size 变成 0,capacity 不变。

① void TestPushBack(),功能:测试 reserve 提前开空间 + 观察自动扩容

作用:reserve(100) 直接开 100 空间

cpp 复制代码
void TestPushBack()
{
    string s;
    // 提前开 100 字符空间,避免自动扩容
    s.reserve(100);

    // 记录当前容量
    size_t sz = s.capacity();
    cout << "capacity changed: " << sz << '\n';

    cout << "making s grow:\n";

    // 尾插 100 个 'c'
    for (int i = 0; i < 100; ++i)
    {
        s.push_back('c');

        // 容量变了就打印
        if (sz != s.capacity())
        {
            sz = s.capacity();
            cout << "capacity changed: " << sz << '\n';
        }
    }
}

// 输出:capacity changed: 100
//      making s grow:

• 后面插 100 个字符 不会扩容;只会打印一次容量

② void test_string(),功能:测试容量相关函数

cpp 复制代码
void test_string()
{
    string s2("hello world");
    cout << s2.length() << endl;    // 有效字符:11
    cout << s2.size() << endl;      // 和 length 一样

    cout << s2.max_size() << endl;  // 字符串最大理论长度(很大的数)

    cout << s2.capacity() << endl;  // 当前底层空间大小

    TestPushBack(); // 测试push_back、reserve、扩容

}

输出:

cpp 复制代码
11
11
1073741820
15
capacity changed: 100
making s grow:

重点: size = length; capacity ≥ size

③ void test_string4(),功能:重点测试 reserve 各种情况 + clear

cpp 复制代码
void test_string4()
{
    string s2("hello worldxxxxxxxxxxxxx");
    cout << s2.size() << endl;     // 有效字符
    cout << s2.capacity() << endl; // 容量
    cout << endl;
    // size() = 24;编译器默认给的 capacity() 一般是 31(VS 下常见)

    // 1. reserve(20) 比当前容量小
    // reserve 只扩不缩!无效,容量不变
    s2.reserve(20);
    cout << s2.size() << endl;
    cout << s2.capacity() << endl << endl;

    // 2. reserve(28) 比当前容量小
    // 依然无效,容量不变
    s2.reserve(28);
    cout << s2.size() << endl;
    cout << s2.capacity() << endl << endl;

    // 3. reserve(40) 比当前容量大
    // 会扩容,capacity 变成≥40
    s2.reserve(40);
    cout << s2.size() << endl;
    cout << s2.capacity() << endl << endl;

    // 4. clear 清空
    // size = 0,capacity 不变!
    s2.clear();
    cout << s2.size() << endl;
    cout << s2.capacity() << endl << endl;

    // 查看迭代器类型名称
    cout << typeid(string::iterator).name() << endl;
    cout << typeid(string::reverse_iterator).name() << endl;
}
  1. 初始: size:24 ; capacity:31

  2. reserve(20)栈: 比 31 小 → 不缩容, size:24, capacity:31

  3. reserve(28), 还是比 31 小 → 不变, size:24, capacity:31

  4. reserve(40): 比 31 大 → 扩容到 40, size:24, capacity:40

  5. clear(): size 清 0, capacity 保持 40 不变

  6. 最后两行:是 typeid 打印出的迭代器真实类型。

超级重点结论:

  1. reserve(n): n > capacity → 扩容; n < capacity → 不缩容,什么都不做

  2. clear(): size 变 0; capacity 不变(空间不释放)

大小相关:

• size() = length():有效字符个数; capacity():底层实际空间大小; max_size():理论最大值(不用记)

空间操作:

• reserve(n): 只改 capacity,只扩不缩, 用来提前开空间,提高效率。

• clear():清空内容,size=0, capacity 不动。

erase 删除、replace 替换、swap、find / rfind、substr、文件读取、路径分割

1)test_string6

① erase 删除(超级常用)

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

s.erase(6, 1);    // 从下标6开始,删1个字符 → 删掉'w'
cout << s << endl;// hello orld

s.erase(0, 1);    // 从0开始删1个 → 删掉'h'
cout << s << endl;// ello orld

s.erase(s.begin());// 删迭代器位置(第一个字符)
cout << s << endl; // llo orld

s.erase(--s.end());// 删最后一个字符
cout << s << endl;  // llo orld → llo orl

s.erase(s.size()-1, 1);// 再删最后一个
cout << s << endl;      // llo or

② erase 只给起始位置,删到末尾

cpp 复制代码
string ss("hello world");
ss.erase(6);       // 从下标6开始,全删完
cout << ss << endl;// hello

③ 把空格替换成 %%(两种方法)

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

// 方法1:replace 循环替换(注释掉的)
/*size_t pos = sss.find(' ');
	while (pos != string::npos)
	{
		sss.replace(pos, 1, "%%");
		pos = sss.find(' ', pos+2);
	}
	cout << sss << endl;*/

	//sss.replace(5, 1, "%%");
	//cout << sss << endl;

// 方法2:遍历一遍,遇到空格就加%%,效率更高
string tmp;
tmp.reserve(sss.size()); // 提前开空间,避免扩容
for (auto ch : sss)
{
    if (ch == ' ')
        tmp += "%%";    // 空格 → %%
    else
        tmp += ch;
}
cout << tmp << endl;

④ swap 交换两个 string

cpp 复制代码
sss.swap(tmp);
cout << sss << endl;

swap 只交换内部指针,极快。

⑤ c_str() 配合 C 文件函数

cpp 复制代码
string file;
cin >> file; // 定义一个字符串 file,从键盘输入一个文件名,比如:test.txt
FILE* fout = fopen(file.c_str(), "r");// string → const char*
// fopen 不认识 C++ 的 string,只认识 C 语言的 const char*
// 所以必须用 .c_str() 把 string 转成 C 字符串

char ch = fgetc(fout);
while (ch != EOF)
{
    cout << ch;
    ch = fgetc(fout);
}
fclose(fout);

• fopen 必须用 c_str(),因为 C 语言文件函数不认识 C++ string, .c_str() = 把 C++ string 转成 C 语言const char*

2)test_string7

① rfind + substr 取文件后缀

cpp 复制代码
string s("test.cpp.zip");
size_t pos = s.rfind('.');// 从后往前找最后一个.
string suffix = s.substr(pos);
cout << suffix << endl;   // .zip

② find_first_not_of:找第一个不在指定集合里的字符

find_first_not_of( "要排除的字符", 从哪里开始找 );

cpp 复制代码
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_not_of("abcdef");
// 从字符串开头开始找,找到第一个不属于 {a,b,c,d,e,f} 的字符,返回它的下标。

while (found != string::npos)  // 只要找到了(没找到就是 npos)
{
    str[found] = '*';           // 把这个字符改成 *
    // 从下一个位置继续找,不再回头
    found = str.find_first_not_of("abcdef", found + 1);
}
cout << str << endl;

// 原字符串:Please, replace the vowels in this sentence by asterisks.
// 运行后:**e*e****************e****************e******************

把不是 a/b/c/d/e/f 的字符全变成 *。

③ find_last_of / substr 拆分路径

cpp 复制代码
size_t found = str.find_last_of("/\\");// 找最后一个 / 或 \
// 作用:从后往前找,找到最后一个 / 或者 \ 的位置。
// / 是 Linux/mac 路径分隔符:/usr/bin/xxx, \是 Windows 路径分隔符:D:\folder\a.txt
// 写 \\ 是因为 \ 在字符串里要转义

str.substr(0, found);      // 路径部分
// substr(起始位置, 长度),这里是:从 0 开始,到 found 位置之前,得到:路径部分

str.substr(found + 1);     // 文件名部分
// 从 found + 1 开始,一直取到字符串末尾,得到:文件名
cpp 复制代码
"/usr/bin/man"
路径:/usr/bin
文件:man

总结:

  1. erase 删除

s.erase(pos, n):从pos删n个 s.erase(pos):从pos删到尾 s.erase(it):删迭代器位置

  1. substr 截取

s.substr(pos, n):从pos取n个 s.substr(pos):从pos取到尾

  1. find 查找

find:从前找 rfind:从后找 find_first_not_of:找不在集合里的第一个字符

  1. 替换

循环遍历替换最稳、最好懂

  1. 小知识点

swap:快速交换两个 string

c_str():转C语言字符串,给文件/系统函数用

reserve:提前开空间,提高效率

3. string类对象的访问及遍历操作

|------------------|-------------------------------------------|
| 函数名称 | 功能说明 |
| operator[](重点) | 返回pos位置的字符,const string类对象调用 |
| begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
| rbegin + rend | rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器 |
| 范围for | C++11支持更简洁的范围for的新遍历方式 |

示例:

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

int main() {
    string s = "hello";

    // ==============================
    // 1. operator[]  下标访问
    // ==============================
    cout << "1. operator[]:" << endl;
    cout << s[0] << endl;   // h
    cout << s[1] << endl;   // e
    s[0] = 'H';             // 可以修改
    cout << s << endl;      // Hello


    // ==============================
    // 2. begin + end  迭代器
    // ==============================
    cout << "\n2. begin + end:" << endl;
    string::iterator it = s.begin();
    while (it != s.end()) {
        cout << *it << " "; // 逐个输出
        ++it;
    }
    cout << endl;


    // ==============================
    // 3. rbegin + rend  反向迭代器
    // ==============================
    cout << "\n3. rbegin + rend:" << endl;
    string::reverse_iterator rit;
    for (rit = s.rbegin(); rit != s.rend(); ++rit) {
        cout << *rit << " "; // 倒着输出:o l l e H
    }
    cout << endl;


    // ==============================
    // 4. 范围for C++11 最简单
    // ==============================
    cout << "\n4. 范围for:" << endl;
    for (auto ch : s) {
        cout << ch << " ";
    }
    cout << endl;

    return 0;
}

• []:像数组一样用下标访问

• begin/end:从前往后遍历

• rbegin/rend:从后往前遍历

• 范围for:最简洁,C++11 首选

4. string类对象的修改操作

|-----------------|---------------------------------|
| 函数名称 | 功能说明 |
| push_back | 在字符串后尾插字符c |
| append | 在字符串后追加一个字符串 |
| operator+=(重点) | 在字符串后追加字符串str |
| c_str(重点) | 返回C格式字符串 |
| find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
| rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
| substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |

注意:

  1. 在string尾部追加字符时,s.push_back(c)、s.append(1, c)、s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。

  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

示例:

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

int main() {
    // 1. push_back:尾部插一个字符
    string s1 = "hello";
    s1.push_back('!');        // 尾插 '!'
    cout << "push_back: " << s1 << endl;  // hello!

    // 2. append:尾部追加字符串
    string s2 = "hello ";
    s2.append("world");         // 追加 "world"
    cout << "append: " << s2 << endl;     // hello world

    // 3. operator+=:最常用的追加
    string s3 = "abc";
    s3 += "def";            // 追加字符串
    s3 += 'g';              // 追加字符
    cout << "operator+=: " << s3 << endl; // abcdefg

    // 4. c_str():转C风格字符串 const char*
    string s4 = "test";
    const char* cstr = s4.c_str();
    cout << "c_str: " << cstr << endl;    // test

    // 5. find + npos:从前找字符/字符串
    string s5 = "apple banana";
    int pos = s5.find('b');       // 找 'b'
    if (pos != string::npos)
        cout << "find 'b' at: " << pos << endl; // 6

    // 6. rfind:从后往前找
    string s6 = "abacaba";
    int rpos = s6.rfind('a');
    cout << "rfind 'a' at: " << rpos << endl; // 6

    // 7. substr:截取子串
    string s7 = "hello world";
    // 从位置 6 开始,取 5 个字符
    string sub = s7.substr(6, 5);
    cout << "substr: " << sub << endl; // world

    return 0;
}

输出结果:

cpp 复制代码
push_back: hello!
append: hello world
operator+=: abcdefg
c_str: test
find 'b' at: 6
rfind 'a' at: 6
substr: world

c_str() 详解:

c = C 语言 str = string 字符串

合起来:把 C++ 的 string → 转成 C 语言能用的字符串

为什么要转? C++ 里:string s = "hello"; C 语言里:只能用 const char*(字符指针)

很多老函数、系统函数、C 库函数只认识 C 语言的字符串,不认识 C++ string。所以必须用 .c_str() 转一下。

最简单例子

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

// 转成 C 语言字符串
const char* p = s.c_str();

内存结构图:

① string 对象 s(在栈上)

cpp 复制代码
s:  [ 指针 ] → 指向堆里的字符数据
     [ 长度 ]
     [ 容量 ]

② 堆里的真实字符数据

cpp 复制代码
堆:  'h' 'e' 'l' 'l' 'o' '\0'
      ↑
      这个地址就是 c_str() 返回的

③ p 指针(栈上)

cpp 复制代码
p:  存储的就是上面那个 'h' 的地址

连起来看(终极版)

cpp 复制代码
【栈内存】
+----------------+        +----------------------------+
|   string s     |        |    const char* p           |
|                |        |                            |
|  ┌──────────┐  |        |  ┌──────────────────────┐  |
|  │  指针    ├──┼--------┼─→│ 地址:指向 'h'        │  |
|  ├──────────┤  |        |  └──────────────────────┘  |
|  │  长度=5   |  |        |                            |
|  ├──────────┤  |        +----------------------------+
|  │  容量≥5   |  |
|  └──────────┘  |
+----------------+
         │
         ▼
【堆内存】
+-----------------------------+
| 'h' 'e' 'l' 'l' 'o'  '\0'   |
+-----------------------------+
         ↑
         └── 这个地址 = s.c_str()

• s 是 C++ string ;p 是 C 风格字符串(const char*)

• string s:是个对象,里面包着指针、长度、容量。

• s.c_str():返回的是对象里面那个指针,指向真实的字符数组。

• const char* p:只是接住了这个指针。

• c_str() 返回的是 const char*,不能修改内容。

• 一旦 string 发生扩容、修改,c_str() 的地址可能失效。

最常见用途:比如输出文件、打开文件、传给 C 函数:

cpp 复制代码
string filename = "test.txt";
FILE* fp = fopen(filename.c_str(), "r");

这里必须用 .c_str(),否则编译报错。

一句话总结:c_str() 就是 C++ string 转 C 字符串的接口。

5. string类非成员函数

|--------------------------|----------------------|
| 函数 | 功能说明 |
| operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
| operator>>(重点) | 输入运算符重载 |
| operator<<(重点) | 输出运算符重载 |
| getline(重点) | 获取一行字符串 |
| relational operators(重点) | 大小比较 |

示例:

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

int main() {
    // ==============================
    // 1. operator+  字符串拼接
    // ==============================
    string s1 = "hello";
    string s2 = " world";
    string s3 = s1 + s2;  // 产生临时对象,效率低,少用
    cout << "operator+: " << s3 << endl;  // hello world

    // ==============================
    // 2. operator>>  输入字符串(空格/回车截止)
    // ==============================
    string s4;
    cout << "请输入字符串(不含空格):";
    cin >> s4;
    cout << "operator>>: " << s4 << endl;

    // ==============================
    // 3. operator<<  输出字符串
    // ==============================
    string s5 = "test output";
    cout << "operator<<: " << s5 << endl;

    // ==============================
    // 4. getline  读取一整行(包括空格)
    // ==============================
    string s6;
    cin.ignore();  // 忽略上一次 cin 留下的换行
    cout << "请输入一行字符串(可含空格):";
    getline(cin, s6);
    cout << "getline: " << s6 << endl;

    // ==============================
    // 5. relational operators 大小比较(按字典序)
    // ==============================
    string a = "apple";
    string b = "banana";
    string c = "apple";

    cout << "a < b ?  " << (a < b) << endl;   // 1
    cout << "a > b ?  " << (a > b) << endl;   // 0
    cout << "a == c ? " << (a == c) << endl;  // 1
    cout << "a != b ? " << (a != b) << endl;  // 1

    return 0;
}

重点一句话总结

  1. operator+:拼接字符串,但效率低,推荐用 += 代替

  2. >>:读字符串,遇到空格/回车就停

  3. <<:正常输出 string

  4. getline:读一整行,包括空格,最常用在读取带空格输入

  5. == != < > <= >=:按字典序比较,直接用就行

string 字符串拼接问题

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

void test_string8()
{
    string s1("hello");

    // 合法:string + const char*
    string s2 = s1 + "world";
    cout << s2 << endl;        // 输出:helloworld

    // 非法!const char* + string 不支持
    // "world" 是 const char*,没有重载 operator+(string)
    // string s3 = "world" + s1; 
    // cout << s3 << endl;
}

关键点:

• s1 + "world" ✔:string 重载了 operator+(const char*)

• "world" + s1 ❌:字符串常量是const char*,没有重载+,不能和string相加

getline + rfind 求最后一个单词长度

cpp 复制代码
int main()
{
    string str;
    // 以 * 作为结束符读取一行
    getline(cin, str, '*');

    // 从后往前找最后一个空格
    size_t pos = str.rfind(' ');

    // 最后一个单词长度 = 总长度 - (空格位置+1)
    cout << str.size() - (pos + 1) << endl;

    return 0;
}

作用:输入一串带空格的字符串,以*结束,输出最后一个单词的长度。

auto 关键字(C++11)

cpp 复制代码
int func1()
{
    return 10;
}

// 错误:C++ 中函数形参不能用 auto
// void func0(auto a = 0)
// {}

// auto 做返回值:由返回值推导类型
auto func2()
{
    return func1(); // 推导为 int
}

auto func3()
{
    return func2(); // 推导为 int
}
int main()
{
    int a = 10;
    auto b = a;    // int
    auto c = 'a';  // char
    auto d = func1(); // int

    // 错误:auto 必须初始化才能推导
    // auto e;

    // 查看类型
    cout << typeid(b).name() << endl; // int
    cout << typeid(c).name() << endl; // char
    cout << typeid(d).name() << endl; // int

    // 错误:auto 不能用于数组定义
    // auto array[] = { 4,5,6 };

    auto ret = func3(); // int

auto 规则:

  1. 必须初始化,编译器从初始化表达式推导类型

  2. 不能做函数形参类型(C++20 概念/模板除外)

  3. 不能定义auto 数组

  4. 可做函数返回值,支持链式推导

范围 for(C++11)

cpp 复制代码
int main()
{
    int array[] = { 1, 2, 3, 4, 5 };

    // C++98 遍历
    for (int i = 0; i < sizeof(array)/sizeof(array[0]); ++i)
    {
        array[i] *= 2;
    }
    for (int i = 0; i < sizeof(array)/sizeof(array[0]); ++i)
    {
        cout << array[i] << endl;
    }

    // C++11 范围 for
    // & 引用:才能修改原数组
    for (auto& e : array)
        e *= 2;

    // 值传递:只读打印
    for (auto e : array)
        cout << e << " ";

    return 0;
}

范围 for 要点:

• for(元素 : 数组/容器)

• 要修改元素:用 auto&

• 只读取:用 auto 或 const auto&

• 适用于:数组、string、vector、list 等所有容器

cin.ignore()

  1. 为什么要用 ignore?

先看这个坑:你先用了 cin >>,再用 getline,getline 会直接跳过,不等待你输入。

原因:cin >> 读到空格 / 回车就停,但它只拿走了前面的字符,把回车留在了输入缓冲区里,后面 getline 一进来,看到剩下的回车,以为你输入完了,就直接空着结束了。

  1. cin.ignore() 是干嘛的? 把缓冲区里多余的换行/字符"吃掉",不让它干扰后面的 getline。

作用:扔掉缓冲区里的 1 个字符(通常就是那个碍事的回车)

  1. 最经典场景(必背)
cpp 复制代码
string s;
cin >> s;        // 输入后会留下一个回车

cin.ignore();    // 把回车扔掉!

getline(cin, s); // 现在就能正常输入一整行了
  1. 更安全、考试常用写法(清空一整行)
    cin.ignore(numeric_limits<streamsize>::max(), '\n');

意思:一直扔字符,直到遇到换行,把整行垃圾清空,用这个基本不会出错。

  1. 一句话总结

• cin >> 会留回车 getline 会被回车坑 cin.ignore() = 把回车吃掉,清空路障

getline

getline 就是:读取一整行输入,包括空格。

  1. 它和 cin >> 的区别:cin >> 字符串,读到 空格 / 回车 就停,只能读一个单词。

getline(cin, 字符串):读到 回车 才停,能读一整句话,带空格。

  1. 最简单例子
cpp 复制代码
string str;
getline(cin, str);

cout << str << endl;

你输入:我 爱 编 程 ,它会全部读进去,输出:我 爱 编 程

如果用 cin >>,只会读到:我

  1. 最常用场景:读带空格的名字,读一句话,读地址。只要内容里有空格,就用 getline

  2. ignore 为什么存在?

因为: cin >> 读完会留下一个回车,下一条 getline 看到回车,以为你直接回车了,就读到空字符串

所以:先用 cin → 再用 getline,中间必须加 cin.ignore()

超级总结

• cin >>:读单词,遇到空格停

• getline:读一整行,包括空格

• cin.ignore():清掉前面残留的回车,让 getline 正常工作

relational operators

就是用来比较两个字符串大小的符号:==、!=、<、>、<=、>=

  1. 怎么比?(按什么规则?):按字典序(字母顺序)比较
    从第一个字符开始,逐个比 ASCII 码:a < b,A < a(大写 < 小写),前面都一样,短的更小。
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s1 = "apple";
    string s2 = "banana";
    string s3 = "apple";
    string s4 = "app";

    // == 等于
    cout << (s1 == s2) << endl;  // 0
    cout << (s1 == s3) << endl;  // 1

    // != 不等于
    cout << (s1 != s2) << endl;  // 1

    // <  字典序更小
    cout << (s1 < s2) << endl;   // 1(apple < banana)
    cout << (s4 < s1) << endl;   // 1(app  < apple)

    // >  字典序更大
    cout << (s2 > s1) << endl;   // 1

    return 0;
}

string 的关系运算符,是按内容比较,不是比地址!

cpp 复制代码
string a = "abc";
string b = "abc";

if (a == b)  // 成立!因为内容一样

这和 C 语言的 char* 完全不一样,C 语言比的是指针地址,C++ string 比的是字符串内容。

总结:relational operators = 字符串比较符号,按字典序比内容,直接用就行。

2.4 vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

vs下string的结构

• string总共占28个字节。

• 内部有一个联合体,用来定义字符串的存储空间:

当字符串长度小于16时,使用内部固定的字符数组来存放。

当字符串长度大于等于16时,从堆上开辟空间。

• 这种设计的合理性:大多数情况下字符串的长度都小于16,string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。

• 还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间的总容量,以及一个指针做其他事情。故总共占16+4+4+4=28个字节。

cpp 复制代码
union _Bxty
{
    // storage for small buffer or pointer to larger one
    value_type _Buf[_BUF_SIZE];
    pointer _Ptr;
    char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

g++下string的结构

• G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间。

• 堆空间内部包含了如下字段:空间总大小,字符串有效长度,引用计数

cpp 复制代码
struct _Rep_base
{
    size_type _M_length;
    size_type _M_capacity;
    _Atomic_word _M_refcount;
};

• 指向堆空间的指针,用来存储字符串。

3. string类的模拟实现

3.1 经典的string类问题

在面试中,面试官喜欢让学生自己模拟实现string类,主要是实现其构造、拷贝构造、赋值运算符重载以及析构函数。

cpp 复制代码
// 为了和标准库区分,此处使用String
class String
{
public:
    /*String()
        :_str(new char[1])
    {*_str = '\0';}
    */
    //String(const char* str = "\0") 错误示范
    //String(const char* str = nullptr) 错误示范
    String(const char* str = "")
    {
        // 构造String类对象时,如果传递nullptr指针,可以认为程序非法
        if (nullptr == str)
        {
            assert(false);
            return;
        }

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

    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

// 测试
void TestString()
{
    String s1("hello world!!!");
    String s2(s1); // 这里会出现问题!
}

问题分析:

上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的。当用s1构造s2时,编译器会调用默认的拷贝构造,最终导致s1、s2共用同一块内存空间。在释放时,同一块空间被释放多次,引起程序崩溃,这种拷贝方式称为浅拷贝。

3.2 浅拷贝

• 浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源。当一个对象销毁时,就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进行操作时,就会发生访问违规。

• 可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。

3.3 深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须显式给出,一般情况都是按照深拷贝方式提供。

3.3.1 传统版写法的String类

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

        _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* pStr = new char[strlen(s._str) + 1];
            strcpy(pStr, s._str);
            delete[] _str;
            _str = pStr;
        }

        return *this;
    }

    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

3.3.2 现代版写法的String类

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

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

    String(const String& s)
        : _str(nullptr)
    {
        String strTmp(s._str);
        swap(_str, strTmp._str);
    }

    // 对比下和上面的赋值哪个实现比较好?
    String& operator=(String s)
    {
        swap(_str, s._str);
        return *this;
    }

    /*
    String& operator=(const String& s)
    {
        if(this != &s)
        {
            String strTmp(s);
            swap(_str, strTmp._str);
        }

        return *this;
    }
    */

    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

3.3 写时拷贝(了解)

• 写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

• 引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1;每增加一个对象使用该资源,就给计数增加1;当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源。如果计数为1,说明该对象是资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

4. 经典例题与练习

4.1 仅仅反转字母

cpp 复制代码
class Solution {
public:
    bool isLetter(char ch)
    {
        if(ch >= 'a' && ch <= 'z')
            return true;
        if(ch >= 'A' && ch <= 'Z')
            return true;

        return false;
    }

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

        size_t begin = 0, end = S.size()-1;
        while(begin < end)
        {
            while(begin < end && !isLetter(S[begin]))
                ++begin;

            while(begin < end && !isLetter(S[end]))
                --end;

            swap(S[begin], S[end]);
            ++begin;
            --end;
        }

        return S;
    }
};

4.2 找字符串中第一个只出现一次的字符

给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1

复制代码
输入: s = "loveleetcode"
输出: 2
cpp 复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        int count[26] = {0};
        // 统计次数
        for (auto ch : s) {
            count[ch - 'a']++;
        }

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

4.3 字符串里最后一个单词的长度

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

int main()
{
    string line;
    // 不要使用cin>>line,因为它遇到空格就结束了
    // while(cin>>line)
    while(getline(cin, line))
    {
        size_t pos = line.rfind(' ');
        cout<<line.size()-pos-1<<endl;
    }
    return 0;
}
cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    string s;
    while (cin >> s) {
        ;
    }
    cout << s.size();
    return 0;
}

4.4 验证一个字符串是否是回文

cpp 复制代码
class Solution {
public:
    // 判断是否字母或数字
    bool isLetterOrNumber(char ch)
    {
        return (ch >= '0' && ch <= '9')
            || (ch >= 'a' && ch <= 'z')
            || (ch >= 'A' && ch <= 'Z');
    }

    bool isPalindrome(string s) {
        // 先小写字母转换成大写,再进行判断
        for(auto& ch : s)
        {
            if(ch >= 'a' && ch <= 'z')
                ch -= 32;
        }

        int begin = 0, end = s.size()-1;
        while(begin < end)
        {
            while(begin < end && !isLetterOrNumber(s[begin])) // 左指针跳过非字母数字
                ++begin;

            while(begin < end && !isLetterOrNumber(s[end])) // 右指针跳过非字母数字
                --end;

            if(s[begin] != s[end])
            {
                return false;
            }
            else
            {
                ++begin;
                --end;
            }
        }
        return true;
    }
};
cpp 复制代码
class Solution {
public:
    bool isPalindrome(string s) {
        string str;
        for (char ch : s) {
            if (isalnum(ch))
                str += tolower(ch);
        }
       // 遍历原字符串 s,只保留字母和数字(isalnum 判断)
       // 并统一转为小写(tolower),存入新字符串 str。

        int left = 0, right = str.size() - 1;
        while (left < right) {
            if (str[left] != str[right])
                return false;
            ++left;
            --right;
        }
        return true;
    }
};

4.5 字符串相加

给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

方法一:

cpp 复制代码
class Solution {
public:
    string addStrings(string num1, string num2) {
        string str;
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        int next = 0; // 进位
        while (end1 >= 0 || end2 >= 0) {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = val1 + val2 + next;
            next = ret / 10;   // 新进位(0或1)
            ret = ret % 10;    // 当前位结果
            str.insert(str.begin(), '0' + ret);
        }
        // 最后如果还有进位,要多加一个 1
        if (next == 1)
            str.insert(str.begin(), '1');
        return str;
    }
};

从后往前加,模拟手算, 用 next 存进位, insert 插在开头,保证数字顺序正确, 一个数字读完了就补 0,继续加, 能处理无限大数字,不会溢出

cpp 复制代码
string addStrings(string num1, string num2) {
    string str;

    int end1 = num1.size() - 1, end2 = num2.size() - 1;
    int next = 0;

    // 两个数都没走完就继续加
    while (end1 >= 0 || end2 >= 0) {
        int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
        int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;

        int ret = val1 + val2 + next;  // 加 + 进位
        next = ret / 10;               // 新进位
        ret %= 10;                     // 当前位

        str += ('0' + ret);  // 🔥尾插(直接往后加,超快)
    }

    if (next == 1)
        str += '1';          // 最后还有进位,再补1

    reverse(str.begin(), str.end()); // 🔥反转一次就正了
    return str;
}

str += ... 是尾插,O(1) 快;reverse 只做一次,O(n) 可以接受

上一段写法:str.insert(str.begin(), ...) 是头插,每插一次都要把整个字符串往后挪,总复杂度 O(n²),慢

• 从后往前加 → 得到的是倒序数字;用 += 尾插 + reverse → 最标准、最高效、面试最爱写

4.6 反转字符串

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

  • 如果剩余字符少于 k 个,则将剩余字符全部反转。
  • 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
复制代码
输入:s = "abcdefg", k = 2
输出:"bacdfeg"
复制代码
输入:s = "abcd", k = 2
输出:"bacd"
cpp 复制代码
class Solution {
public:
    string reverseStr(string s, int k) {
        int n=s.size();
        
        // 每次跳 2k 步,对应题目:每2k个字符处理一组
        for(int i=0;i<n;i+=k*2)
        {
            reverse(s.begin()+i,s.begin()+min(i+k,n));
            // 核心:反转从 i 开始的 k 个字符
            // min(i+k, n)是为了处理最后一段不足k个的情况,剩下 <k 个就反转到末尾
        }
        return s;
    }
};

// 步长 2k,每次只反前 k 个,最后不够 k 就全反

4.7 反转字符串中的单词

给定一个字符串 s ,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

复制代码
输入:s = "Let's take LeetCode contest"
输出:"s'teL ekat edoCteeL tsetnoc"
cpp 复制代码
class Solution {
public:
    string reverseWords(string s) {
        int n = s.size();
        int i = 0;

        while (i < n) {
            // 找单词开头
            while (i < n && s[i] == ' ') i++;

            int left = i;

            // 找单词结尾
            while (i < n && s[i] != ' ') i++;

            int right = i - 1; // 单词最后一个字符下标

            // 反转这个单词
            reverse(s.begin() + left, s.begin() + right + 1);
            // reverse 是左闭右开,反转从 left 到 right 的所有字符,单词就被反转了
        }

        return s;
    }
};

举例:

字符串:"hello world"

  1. i 从 0 开始,跳过空格(这里没有)

  2. left = 0

  3. 走到空格下标 5 停下

  4. right = 4

  5. reverse(0,5) → "olleh"

  6. i 继续走,跳过空格

  7. left = 6

  8. 走到结尾停下

  9. right = 10

  10. reverse(6,11) → "world" → "dlrow"

最终:"olleh dlrow"

4.8 字符串相乘

给定两个以字符串形式表示的非负整数 num1num2,返回 num1num2 的乘积,它们的乘积也表示为字符串形式。

整体思路(最关键)

  1. 两个数相乘,结果最多有 m + n 位

  2. 先开一个 res 字符串,长度 m+n,全部填 '0'

  3. 从后往前,让 num1 的每一位 × num2 的每一位

  4. 乘出来的结果,放在 res[i+j+1] 这个位置

  5. 每次都算上原来的值 + 进位

  6. 最后去掉前面多余的 0

cpp 复制代码
class Solution {
public:
    string multiply(string num1, string num2) {
        // 两个数相乘,结果长度最多 = 长度之和
        int m = num1.size(), n = num2.size();
        string res(m + n, '0');  // 先全初始化为 '0'

        // 从后往前,逐位相乘,遍历 num1 从最后一位开始
        for (int i = m - 1; i >= 0; i--) {
            int carry = 0; // 每处理完 num1 的一位,进位要清零
            // 遍历 num2 从最后一位开始
            for (int j = n - 1; j >= 0; j--) {
                // 当前乘积 + 原来res里的值 + 进位
                int mul = (num1[i] - '0') * (num2[j] - '0');
                int sum = mul + (res[i + j + 1] - '0') + carry;

                res[i + j + 1] = sum % 10 + '0';  // 当前位,保留个位,放到 i+j+1 位置
                carry = sum / 10;                 // 进位,十位及以上作为新的进位
            }
            res[i] += carry;  // 剩下的进位,内层循环结束后,还有进位,直接加到 res[i] 上
        }

        // 去掉前面的 0
        int start = 0;
        while (start < res.size() && res[start] == '0') {
            start++;
        }

        // 全是 0 的情况
        if (start == res.size()) return "0";

        return res.substr(start); // 从 start 截取到末尾,就是最终结果。
    }
};

这段代码就是模拟竖式乘法:

• 每一位 × 每一位

• 结果放在正确位置

• 加上原来的值和进位

• 最后去 0

相关推荐
百锦再1 小时前
Java多线程编程全面解析:从原理到实战
java·开发语言·python·spring·kafka·tomcat·maven
Cosmoshhhyyy1 小时前
《Effective Java》解读第38条:用接口模拟可扩展的枚举
java·开发语言
Purple Coder1 小时前
基于神经网络的家教系统
学习
小冷coding2 小时前
【Java】最新Java高并发高可用平台技术选型指南(思路+全栈路线)
java·开发语言
爱华晨宇2 小时前
Python列表入门:常用操作与避坑指南
开发语言·windows·python
寻星探路2 小时前
【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战
java·前端·javascript·css·c++·ai·html
一切顺势而行2 小时前
python 面向对象
开发语言·python
忘梓.3 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(10)
c++·算法·动态规划·代理模式
foolish..3 小时前
动态规划笔记
笔记·算法·动态规划