✨ 把代码写进星轨,
用逻辑丈量宇宙。

导航 链接
个人主页 🏠 星轨初途
基础语言专栏 💻 C语言📚 数据结构
C++ 进阶专栏 🏆 C++学习(竞赛类)⚙️ C++专栏(开发类)
刷题实战专栏 🚀 算法及编程题分享

在这里插入图片描述


前言

嗨!我们又见面啦٩(๑❛ᴗ❛๑)۶!欢迎回到星轨的 C++ 进阶频道!

在上一篇 【C++进阶】让编译器替你打工!一文打通泛型编程与 C++ 模板 中,我们手撕了底层的“造物模具”,彻底解锁了让编译器自动写代码的黑魔法。

有了模板的加持,今天我们终于要正式敲开 C++ 顶级宝库的大门——STL(标准模板库)

而在 STL 这支钢铁洪流中,打头阵的先锋正是我们日常开发最常用、也最离不开的 std::string。为了让大家既能“用得溜”又能“懂底层”,我将这部分内容拆分成了硬核的双子篇:本篇我们将先聚焦 string 的核心用法,带你掌握现代 C++ 的字符串操作技巧;下一篇,我们将继续深挖,徒手拆解它的底层实现原理与黑科技。

准备好迎接 STL 带来的降维打击了吗?废话不多说,本篇 string 核心使用指南,我们正式起飞!🚀


一、学习string的目的

1、C语言中字符串的劣势

在 C 语言中,字符串本质上是以 '\0' 结尾的字符集合。它就像一团没有理顺的毛线球,非常让人头疼:

  1. 数据与方法分离 (不符合 OOP):C 标准库提供了 <string.h> 系列的库函数(如 strlenstrcat),但这些函数与字符串本身是分离的,调用起来极其原始。
  2. 内存管理的无底洞:底层空间需要程序员手动 malloc/free,稍不留神就会引发数组越界(Buffer Overflow)或内存泄漏,甚至成为黑客攻击的漏洞。
  3. 动态扩容极其痛苦:如果你想拼接两个长字符串,必须先计算两个字符串的长度,然后重新开辟一块更大的空间,再把原数据拷贝过去。代码又臭又长,效率极低。

2. C++ 中 string 类的绝对优势

string 类完美解决了上述痛点。它将底层的 char* 数组完全封装,自带内存池管理与自动扩容机制,天然支持包括拼接、查找、截取在内的百余种操作。

二、string类的了解及使用

string的参考文档 string

1、了解string类

在这里插入图片描述
我们可以通过string文档来进行对string的了解,相当于就是字符串的封装,
在使用string类时,必须包含#include头文件以及using namespace std;

🚨 预备工作:使用 string 类,必须包含头文件 #include <string>,并展开命名空间 using namespace std;。注意,是 <string> 而不是 C语言的 <string.h>

2. 字符串创建 :核心 4 种姿势

构造文档链接:Constructor
在这里插入图片描述

STL 标准库为 string 提供了十几种重载的构造函数,但在日常开发中,掌握以下 4 种就足以应对 99% 的场景:

void TestConstructor() 
{
    // 姿势 1:默认构造 (生成一个空字符串 "")
    string s1; 

    // 姿势 2:用 C 格式字符串构造 (最常用)
    string s2("hello bit"); 

    // 姿势 3:拷贝构造 (用已有的 string 对象生成新对象)
    string s3(s2); 

    // 姿势 4:重复字符构造 (创建包含 n 个相同字符的字符串)
    // 例如:生成 5 个 'x' 组成的字符串 "xxxxx"
    string s4(5, 'x'); 
    
    // 进阶用法:取 C 字符串的前 n 个字符
    string s5("hello world", 5); // 结果为 "hello"
}

3.字符串遍历的三种方式

在学习 string 遍历之前,为了让我们的代码更加现代、优雅,必须先补充两个 C++11 的王炸语法糖:

3.1前置知识

3.1.1 auto 关键字

早期 C++ 中 auto 意义不大。但在 C++11 中,它化身为类型推导指示符。它可以让编译器在编译期自动推导出变量的类型。

int a = 10;
auto b = a;   // 编译器自动推导 b 为 int
auto c = 'x'; // 编译器自动推导 c 为 char

// 🚨 避坑注意:用 auto 声明引用时必须加 &
auto& d = a;  // d 是 a 的引用,修改 d 就会修改 a
3.1.2 范围 for 循环 (Range-based for)

遍历一个数组或容器,由程序员去指定边界既繁琐又容易写错(比如漏掉 <=)。范围 for 循环能够自动取数据、自动判断结束。

int arr[] = {1, 2, 3, 4, 5};
// 自动将 arr 中的每一个元素依次赋值给 e
for (auto e : arr)
 {
    cout << e << " ";
}

注:范围 for 的底层其实就是替换成了迭代器(Iterator),这个我们后面马上会讲到。

3.2.字符串遍历三种方式

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

假设我们要遍历字符串,将内部的每一个小写字母都修改为大写,我们有以下三种方式:

void TestTraversal() 
{
    string str("hello world");

    // 方式 1:operator[] 下标访问(最符合 C 程序员直觉)
    // size() 函数可以获取字符串有效长度
    for (size_t i = 0; i < str.size(); ++i) 
    {
        str[i] -= 32; // 将小写转大写,str[i] 越界时底层会触发 assert 报错
    }

    // 方式 2:正向迭代器 iterator 遍历(🔥 工业级代码推荐)
    // 迭代器是 STL 容器通用的遍历方式,不管底层是数组还是链表、树,都可以用迭代器!
    for (string::iterator it = str.begin(); it != str.end(); ++it) 
    {
        *it -= 32;
    }

    // 方式 3:范围 for 遍历(底层基于 begin 和 end 迭代器实现,最简洁)
    // 🚨 注意:如果需要修改字符串里的内容,变量前必须加引用 & !
    for (auto& ch : str) 
    {
        ch -= 32; 
    }
}

💡 黄金开发经验:反向遍历怎么写?
如果需要从尾到头倒着遍历,下标控制常常让人头晕(一不小心 i = -1 变成超级大正数)。此时反向迭代器闪亮登场:
for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit)

4、string 对象的容量操作

容量操作是面试中最常考察、也是日常开发中最容易踩坑的地方!

函数名称 功能说明
size()/ length() 返回字符串有效字符的长度。两者底层完全一致,通常推荐使用 size() 保持与 STL 其他容器接口一致。
capacity 返回底层实际开辟的总空间大小(通常大于等于 size)。
empty() 判断字符串是否为空,空返回 true
clear() 清空所有有效字符。
reserve(n) 预分配空间:将容量扩展到至少 n,不改变有效字符内容。
resize(n, c) 调整有效字符数:将 size 改成 n,多出的空间用字符 c 填充。

我们用代码来进行直观理解

#include <iostream>
#include <string>
using namespace std;
void TestStringCapacity()
{
    // 1. 初始化字符串,观察基础容量属性
    string s("Hello C++");
    cout << "========== 1. 初始状态 ==========" << endl;
    cout << "当前内容: " << s << endl;
    cout << "size()   : " << s.size() << " (有效字符数)" << endl;
    cout << "length() : " << s.length() << " (等同于 size())" << endl;
    // capacity 的具体数值受编译器优化(如 SSO)影响,通常大于等于 size
    cout << "capacity : " << s.capacity() << " (底层开辟的总空间)" << endl;
    cout << "empty()  : " << (s.empty() ? "true" : "false") << endl;

    // 2. clear() 清空数据测试
    cout << "\n========== 2. 调用 clear() ==========" << endl;
    s.clear();
    cout << "调用后 size()   : " << s.size() << " (数据已被清空)" << endl;
    // 🚨 核心避坑:clear 只清空数据,绝不释放底层容量!
    cout << "调用后 capacity : " << s.capacity() << " (容量保持不变!)" << endl;
    cout << "调用后 empty()  : " << (s.empty() ? "true" : "false") << endl;

    // 3. reserve(n) 预分配空间测试
    cout << "\n========== 3. 调用 reserve(100) ==========" << endl;
    s = "Star"; // 重新赋个短值
    s.reserve(100);
    cout << "当前内容: " << s << endl;
    cout << "调用后 size()   : " << s.size() << " (size 绝对不发生改变)" << endl;
    cout << "调用后 capacity : " << s.capacity() << " (容量已扩充至至少 100)" << endl;

    // 4. resize(n, c) 扩充大小测试
    cout << "\n========== 4. 调用 resize(10, '*') ==========" << endl;
    s.resize(10, '*');
    cout << "当前内容: " << s << endl;
    cout << "调用后 size()   : " << s.size() << " (有效字符扩充到 10,多出的用 * 填充)" << endl;
    cout << "调用后 capacity : " << s.capacity() << endl;

    // 5. resize(n) 缩小大小测试
    cout << "\n========== 5. 调用 resize(4) ==========" << endl;
    s.resize(4);
    cout << "当前内容: " << s << endl;
    cout << "调用后 size()   : " << s.size() << " (后面的字符被截断丢弃)" << endl;
    // 🚨 核心避坑:resize 缩小 size 时,同样不会缩容!
    cout << "调用后 capacity : " << s.capacity() << " (底层容量依然保持不变!)" << endl;
}

int main()
{
    TestStringCapacity();
    return 0;
}

在这里插入图片描述
我们通过代码能很清楚了解到string对象的容量操作的过程,其中我们有几点要注意

  • clear() 的陷阱
    当你调用 s.clear() 时,它确实会把字符串里的所有字符清空(size 变为 0),但是它绝对不会缩容! 底层的 capacity 依然保持原来的大小,以便你后续再插入数据时不用重新申请内存。

  • reserve() vs resize() 的终极对决

    • resize(n):不仅会改变底层容量,还会真正的插入数据!如果指定的 n 大于当前的 size,它会把新增的空间全部用 \0(或你指定的字符)填满。
    • reserve(n)只改变容量,绝不插入数据! 它是纯粹的提前向操作系统申请好内存。

💡 黄金避坑与性能压榨指南:
如果你在做一个循环,需要往 string 里面 push_back (下一条会讲)追加 10万 个字符。如果不做任何处理,string 会在底层频繁触发**“申请新空间 -> 拷贝旧数据 -> 释放旧空间”**的扩容机制,极其消耗 CPU!
正确做法:在循环开始前,直接调用 s.reserve(100000)!一次性把空间申请到位,消灭所有的扩容开销,性能瞬间实现降维打击!

5、字符串的修改操作

在实际开发中,我们对字符串的操作无外乎四种:追加、插入删除、查找截取、格式转换。STL 为此提供了极其丰富的接口,下面我们逐一拆解最常用的几个核心函数,并附上避坑指南。

函数名称 功能说明
push_back 在字符串后尾插字符 c
append 在字符串后追加一个字符串
operator+=( 重点) 在字符串后追加字符串 str
insert 在指定位置 pos 插入字符或字符串
erase 从指定位置 pos 开始删除 n 个字符
c_str (重点) 返回 C 格式字符串
find + npos 从前往后找字符/子串,返回位置
substr 截取子串
pop_back 删除字符串中尾部的一个字符

1. 尾部追加拼接:push_back / append / operator+=

这是平时写代码最常用到的操作。C++ 提供了三种方式在字符串尾部添加数据:

  • push_back(c):只能在字符串尾部追加单个字符 c
  • append(str):在字符串尾部追加一个字符串
  • operator+= (🔥 绝对主力):既能追加单个字符,也能追加字符串。

💡 黄金开发经验:无脑使用 +=
在实际工程中,我们强烈建议无脑使用 operator+=!因为它的底层经过了充分优化,且大小通吃,代码可读性极强。只有在必须配合某些泛型算法时,才会去使用 push_backappend

void TestAppend() 
{
    string s = "Hello";
    
    s.push_back(' ');           // 尾插单字符
    s.append("World");          // 追加字符串
    s += "!!!";                 // 🔥 最推荐的做法:极其直观
    s += 'A';                   // += 也能追加单字符
    
    cout << s << endl;          // 输出: Hello World!!!A
}

在这里插入图片描述

2.尾部删除数据 pop_back

pop_back() 用于删除字符串中尾部的一个字符。
在这里插入图片描述

在这里插入图片描述
实现

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

int main()
{
    string s = "hello";
    cout << "s:" << s << endl;
    //尾删
    s.pop_back();
    cout << "s:" << s << endl;
    //尾删
    s.pop_back();
    cout << "s:" << s << endl;
    return 0;
}

效果

在这里插入图片描述

注意:不可对空字符串继续进⾏pop_back()操作,否则程序出现异常

3. 任意位置的安插与抹除:inserterase

如果你不想在尾部追加,而是想在字符串中间“搞事情”,inserterase 这对“好兄弟”就能派上用场。值得注意的是,C++ 标准委员会为 insert 提供了极其丰富的重载版本,除了常规的下标索引,它还完美支持迭代器 (Iterator) 操作,以无缝衔接整个 STL 泛型算法生态。

👉 insert 的四大高频重载用法:

  1. 插入单个或整个字符串insert(pos, str) —— 在 pos 索引位置插入另一个 string 或 C 风格字符串。
  2. 连续插入多个相同字符insert(pos, n, c) —— 在 pos 索引位置连续插入 n 个字符 c
  3. 插入别人的一部分 (子串)insert(pos, str, subpos, sublen) —— 把另一个字符串 str 中从 subpos 开始的 sublen 个字符截取过来插入。
  4. 迭代器单字符插入 (✨ 泛型利器)insert(iterator, char) —— 在迭代器指向的位置插入单个字符,这是配合 STL 算法库时的核心姿势!

在这里插入图片描述

👉 erase 的核心用法:

  • erase(pos, n):从 pos 位置开始,删除 n 个字符。如果省略 n(即缺省参数为 string::npos),则默认从 pos 一直删到字符串末尾。
  • 在这里插入图片描述

💻 核心实战代码演示:

void TestInsertEraseDetailed() 
{
    string s("hello");

    // ==========================================
    // 1. insert 的花式插入
    // ==========================================

    // 用法一:在指定位置插入一个完整的字符串
    s.insert(0, "star ");       // 在头部 (索引0) 插入 -> "star hello"

    // 用法二:在指定位置连续插入 n 个相同的字符
    // 例如:在末尾插入 3 个感叹号 '!'
    s.insert(s.size(), 3, '!'); // -> "star hello!!!"

    // 用法三:高端操作!插入另一个字符串的部分子串
    string other = "welcome to earth";
    // 解析:在 s 的索引 5 位置,插入 other 中从索引 0 开始的 8 个字符 ("welcome ")
    s.insert(5, other, 0, 8);   // -> "star welcome hello!!!"

    // 用法四:迭代器单字符插入
    string s3("world");
    // 在字符串开头(begin() 返回的迭代器位置)插入一个空格字符
    s3.insert(s3.begin(), ' '); // -> " world"


    // ==========================================
    // 2. erase 的精准抹除
    // ==========================================

    // 用法一:指定起点和长度进行删除
    // 从索引 0 开始,删除 5 个字符 (把 "star " 删掉)
    s.erase(0, 5);              // -> "welcome hello!!!"

    // 用法二:只传起点,默认“一刀切”到结尾!
    // 从索引 7 的位置开始,后面全不要了
    s.erase(7);                 // -> "welcome"
}

🚨 极限避坑预警 (O(N) 灾难):
string 的底层是一个连续的字符数组。当你在头部或中间使用 inserterase 时,会导致操作点之后的所有字符进行大规模的整体挪动!如果在一个大循环里频繁头插,时间复杂度会飙升至 O ( N 2 ) O(N^2) O(N2),直接导致程序超时。非必要,不头插!

4. 提取信息的黄金搭档:findsubstr

  1. find(str, pos) (精确查找):从 pos 位置开始,从前往后查找完全匹配的子串或字符。
  2. find(str, pos) (字符集查找/✨ 高阶利器):从 pos 开始,查找目标字符串中任意一个字符首次出现的位置。
  3. find 从前往后找,rfind 从后往前找。如果找到了,返回该字符所在的索引 pos如果没找到,会返回一个特殊的常量 string::npos(本质上是 size_t 的最大值)。
    在这里插入图片描述

👉 substr 的精准切割:

  • substr(pos, n):从 pos 位置开始,截取 n 个字符并返回一个新字符串。如果省略 n,则默认直接截取到结尾。

在这里插入图片描述

void TestFindSubstr() 
{
    // 经典场景:提取文件后缀名
    string file = "project.src.main.cpp";
    
    // 从后往前找第一个 '.'
    size_t pos = file.rfind('.'); 
    //从前往后找第一个 '.'
    size_t pos = file.find('.'); 
    //从pos往后找第一个 '.'
    size_t pos = file.find(pos,'.'); 
    
    // 必须判断是否等于 string::npos,防止越界崩溃!
    if (pos != string::npos) 
    {
        // 缺省第二个参数,默认直接截取到字符串末尾
        string ext = file.substr(pos); 
        //从 pos 位置开始,截取 11 个字符并返回一个新字符串
         string ext = file.substr(pos,11); 
        cout << "文件后缀名为: " << ext << endl; // 输出: .cpp
    }
}

5. 跨时代沟通的桥梁:c_str

C++ 的 string 虽好,但我们在做系统级开发(如网络 Socket 编程、文件操作)时,常常需要调用 C 语言遗留下来的底层 API(如 fopenprintf)。这些老古董是不认识 std::string 对象的,它们只认 const char* 指针。

  • c_str():返回一个指向正规 C 风格字符串(以 \0 结尾)的常量指针。
void TestCStr() 
{
    string filename = "test.txt";
    
    // 🚨 错误示范:fopen 无法接收 string 对象
    // FILE* fout = fopen(filename, "r"); 
    
    // ✅ 正确姿势:使用 c_str() 转换为 C 风格字符串指针
    FILE* fout = fopen(filename.c_str(), "r"); 
    
    // 配合 printf 使用也必须加 c_str()
    printf("打开的文件是: %s\n", filename.c_str());
}

6.读取带空格的完整字符串:getline()

在这里插入图片描述

  1. getline是C++标准库中的一个函数,用于从输入流中读取一行文本,并将其存储为字符串。
  2. getline函数有两种不同的形式,分别对应着字符串的结束方式。
istream& getline (istream& is, string& str);
istream& getline (istream& is, string& str, char delim);

✅提示:

  • istream是输入流类型,cin是istream类型的标准输入流对象。
  • ostream是输出流类型,cout是ostream类型的标准输出流对象。
  • getline函数是从输入流中读取一行文本信息,所以如果是在标准输入流(键盘)中读取数据,就可以传cin给第一个参数。
第一种(默认以‘\n’为结束标志)

第一种 getline 函数以换行符(‘\n’)作为字符串的结束标志,它的一般格式是:

getline(cin, string str)
//cin -- 表示从输入流中读取信息
//str 是存放读取到的信息的字符串

这种形式的 getline 函数从输入流(例如cin)中读取文本,直到遇到换行符(‘\n’)为止,然后将读取到的文本(不包括换行符)存储到指定的string类型的变量str中。
实例:

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

int main ()
{
    string name;
    getline (cin, name);
    cout << name << endl;
    return 0;
}

效果:

在这里插入图片描述

第二种(自定义结束标志)

第二种 getline 函数允许用户自定义结束标志,它的一般格式是:

getline(cin, string str, char delim)
//cin -- 表示从输入流中读取信息
//str 是存放读取到的信息的字符串
//delim 是自定义的结束标志

这种形式的 getline 函数从输入流中读取文本,直到遇到用户指定的结束标志字符(delim)为止,然后将读取到的文本(不包括结束标志字符)存储到指定的string类型的变量str中。
实例:

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

int main ()
{
    string name;
    getline (cin, name, 'q');
    cout << name << endl;
    return 0;
}

效果


在这里插入图片描述

三、string中字符串与基本数值类型的转换工具函数

(1)stoi/stol

  • stoi:将字符串转换为int类型的值
  • stol:将字符串转换为long int类型的值
    两者用法类似,以stoi为例讲解:stoi可将string类型的字符串转化为整型,函数原型如下:

函数参数

int stoi (const string& str, size_t* idx = 0, int base = 10);
long stol (const string& str, size_t* idx = 0, int base = 10);
  • str:被转换的string类型字符串
  • idx:输出型参数(指针类型)。需在外创建size_t类型变量,传递其地址给idx;该参数会带回str无法匹配数字的第一个字符的位置
  • base:被解析字符串的进制值(支持2、8、10、16、0):
    • 默认值为10,表示解析10进制数字;
    • 传2:解析2进制数字,最终转为10进制;
    • 传8:解析8进制数字,最终转为10进制;
    • 传16:解析16进制数字,最终转为10进制;
    • 传0:自动推导进制(含0x则为16进制,0开头则为8进制),最终转为10进制。

代码举例

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

int main()
{
    size_t pos = 0;
    string s1 = "11x34";
    int ret1 = stoi(s1, &pos, 16);
    cout << ret1 << endl;
    cout << "pos:" << pos << endl;

    string s2 = "11x34";
    int ret2 = stoi(s2, &pos, 2);
    cout << ret2 << endl;
    cout << "pos:" << pos << endl;

    string s3 = "0x11x34";
    int ret3 = stoi(s3, &pos, 0);
    cout << ret3 << endl;
    cout << "pos:" << pos << endl;

    return 0;
}

效果如下:

在这里插入图片描述

(2)stod/stof

stod是将字符串转换成double类型的值,就是参数少了进制的描述
参数

double stod (const string& str, size_t* idx = 0);
float stof (const string& str, size_t* idx = 0);

代码示例

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

int main()
{
    string s = "3.14x456";
    double ret = stod(s, NULL);
    cout << ret << endl;
    return 0;
}

(3)to_string

就是将数字转换为字符串

string to_string(数字类型 val);

代码举例

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

int main()
{
    string pi = "pi is " + to_string(3.14159);
    cout << pi << endl;
    return 0;
}

结束语

嗨!本篇到这里就结束啦!今天我们学习了string的相关操作,下一篇我们将要了解string的底层实现,敬请期待吧!感谢大家的支持啦!

更多推荐