C++入门篇(十):string(上)——认识string:构造与三大遍历(一条龙讲透operator[]、迭代器、auto、范围for)

目录

[0.1 概述&序言](#0.1 概述&序言)

一、为什么学习string类

[1.1 C语言字符串的痛](#1.1 C语言字符串的痛)

[1.2 现实中的需求](#1.2 现实中的需求)

[1.3 两个面试题(先感受一下)](#1.3 两个面试题(先感受一下))

二、一张图理清string

[2.1 头文件与二八原则](#2.1 头文件与二八原则)

[三、string 的构造接口](#三、string 的构造接口)

[3.1 接口速查表格](#3.1 接口速查表格)

[3.2 测试代码](#3.2 测试代码)

[3.3 npos 是什么(重点补充)](#3.3 npos 是什么(重点补充))

四、三大遍历方式(一条龙)

[4.1 第一种:operator\[\] + 下标](#4.1 第一种:operator[] + 下标)

[4.2 第二种:迭代器](#4.2 第二种:迭代器)

[4.2.1 迭代器是什么、为什么存在](#4.2.1 迭代器是什么、为什么存在)

[4.2.2 迭代器四兄弟](#4.2.2 迭代器四兄弟)

[4.2.3 测试代码(逐步分析)](#4.2.3 测试代码(逐步分析))

[4.2.4 迭代器的注意事项](#4.2.4 迭代器的注意事项)

[4.3 第三种:auto------类型太长?交给编译器](#4.3 第三种:auto——类型太长?交给编译器)

[4.4 第四种:范围for------全自动遍历](#4.4 第四种:范围for——全自动遍历)

[4.4.1 一道经典题引入:范围for修改元素](#4.4.1 一道经典题引入:范围for修改元素)

[4.4.2 三大遍历方式小结](#4.4.2 三大遍历方式小结)

五、最终总结


0.1 概述&序言

这里是白杨,上一篇我们打开了 STL 的大门(没看过的同学请走传送门:C++入门篇(九):模板与STL初探https://blog.csdn.net/Bai_YangSQ/article/details/166643289)。从今天开始逐个拆解容器,第一站------string。

string 可能是你 STL 生涯里使用频率最高的容器,没有之一。内容比较多,我拆成三篇:

  • 上篇(本文):认识 string------为什么学、怎么构造、三大遍历方式一条龙(operator\[\] → 迭代器 → auto → 范围for,环环相扣)
  • 中篇:接口大全------容量操作、增删改查,一篇吃透所有常用接口
  • 下篇:实战与原理------OJ 刷题 + 手写模拟实现(面试官最爱问的浅拷贝/深拷贝)

内容依旧干货满满,让我们开始吧!!!

一、为什么学习string类

1.1 C语言字符串的痛

先说结论:

C 语言的字符串是 '\0' 结尾的字符集合,靠 str 系列库函数操作:函数和数据分离、空间要自己管、一不小心就越界。

在 C 里操作字符串的日常:strlen要自己算长度、strcat要自己管容量、strstr要判空------字符串和操作它的函数是分离 的,底层空间还得自己维护,稍不留神就越界访问。而 string 把数据和操作封装在一起,自动管理内存、自动扩容,接口直观好记。

1.2 现实中的需求

现实中有太多"数字大到存不下"的场景:身份证号、银行卡号、订单号 ......这些"数字"早就超过了 int 甚至 long long 的存储上限,更别说它们前面还可能有前导 0。这类数据本质上就是一串字符 ,string就是为它们而生的。

1.3 两个面试题(先感受一下)

先别急着做------现在你连 string 的接口都还没学,硬做只会打击信心。这两题的完整思路和解答我都放在了下篇的 OJ 实战里(未来即将更新),学完接口回来收它们,正好检验你的熟练度。

在 OJ 中,字符串相关的题基本都以 string 形式出现;工作中也几乎没人再用 C 库字符串函数。string 不是选修,是刚需。

二、一张图理清string

初见 string,先捋一捋知识结构,心里有张地图,后面就不迷路:

2.1 头文件与二八原则

头文件叫 #include (注意区分 C 语言的 <string.h>)。

string 因为历史问题接口非常多,但根据二八原则,我们只需要掌握常用的 20% 就够了,剩下冷门的用到了再查文档。本文每张表格里我都会标注:

  • (重点):必须会用
  • (了解):知道有这回事,用到了再查
  • (冗余):有其他接口可以替代,面试能说上两句即可

查接口的传送门:<string> - C++ Reference (C++ Reference 文档,遇到不会的接口就来这查)

三、string 的构造接口

3.1 接口速查表格

名称 函数签名 说明
默认构造(重点) string() 构造空的 string 对象
拷贝构造(重点) string(const string& str) 用另一个 string 对象拷贝
子串构造 string(const string& str, size_t pos, size_t len = npos) 取 str 从 pos 开始的 len 个字符;len 超出范围则取到末尾
C 字符串构造(重点) string(const char s)* 用 C 风格字符串初始化
字符序列构造 string(const char s, size_t n)* 取字符数组的前 n 个字符
填充构造 string(size_t n, char c) 构造包含 n 个字符 c 的字符串

3.2 测试代码

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

int main()
{
    string s1;                  // (1) 默认构造:空串
    string s2("hello bit");     // (4) C字符串构造
    string s3(s2);              // (2) 拷贝构造
    string s4(3, 'x');          // (6) 填充构造:"xxx"
    string s5 = "hello bit";    // 隐式类型转换构造
    string s6("hello world", 5);// (5) 取前5个字符:"hello"

    cout << "s1: [" << s1 << "]" << endl;
    cout << "s2: " << s2 << endl;
    cout << "s3: " << s3 << endl;
    cout << "s4: " << s4 << endl;
    cout << "s5: " << s5 << endl;
    cout << "s6: " << s6 << endl;
    return 0;
}

打印结果:如下图

s1: \[\] s2: hello bit s3: hello bit s4: xxx s5: hello bit s6: hello

3.3 npos 是什么(重点补充)

先说结论:

npos 是 string 的静态成员常量,值为 size_t 的最大值(size_t 是无符号类型,-1 转过去就是最大值)。用作参数时表示"直到字符串末尾",用作返回值时表示"没找到"。

很多接口都默认带pos参数,比如子串构造string(str, pos, len = npos)------len 默认就是 npos,意思是"不指定就从 pos 取到结尾"。这个老朋友在查找接口**(find)**里还会再见面。

四、三大遍历方式(一条龙)

遍历 string 有三种方式,它们的讲解顺序是精心设计的:operator\[\](最朴素)→ 迭代器(最通用,但类型名长)→ auto(解决类型名长的痛点)→ 范围for(auto + 迭代器的集大成)。学完你会发现它们环环相扣,不是三个孤立的知识点。

4.1 第一种:operator\[\] + 下标

先说结论:

string 本质是"字符数组",可以像数组一样用 下标 访问和修改每个字符。下标范围是 [0, size)。

在正式遍历前,先补两个小接口:

接口 作用 说明
size() 返回字符串有效字符个数 不含 '\0'
c_str() 返回 C 风格字符串指针(const char*) 和 C 库函数配合时用

截图皆来自:<string> - C++ Reference

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

int main()
{
    string str("123456789");

    // 用 size() 控制下标范围
    for (size_t i = 0; i < str.size(); ++i)
        cout << str[i] << " ";
    cout << endl;

    // operator[] 还支持修改
    str[0] = 'A';
    cout << str << endl;         // A23456789

    // c_str():和 C 库函数配合
    printf("%s\n", str.c_str());
    return 0;
}

打印结果:如图

1 2 3 4 5 6 7 8 9 A23456789 A23456789

坑点:operator\[\] 越界访问不会报编译错误,运行时直接崩(assert 断言失败)。数组可没这种"安全检查"。

4.2 第二种:迭代器

4.2.1 迭代器是什么、为什么存在

先说结论:

迭代器是一个"抽象的访问器",用统一的方式顺序访问容器元素,而不暴露容器的底层存储结构。它的用法和指针非常像,但底层不一定是指针。

为什么要费劲发明迭代器?因为每个容器的存储结构不同 (string 是数组、list 是链表、map 是红黑树),如果每个容器都用自己的一套遍历方式,程序员每学一个容器就要学一套新语法。迭代器把遍历统一成一套:begin() 拿起点、++往后走、*取元素、end() 判断结束------一次学会,处处通用。

**string 底层是数组,所以 string 的迭代器底层就是指针;**但后面学的 list、map 的迭代器底层就不是指针了。

4.2.2 迭代器四兄弟

我们先理解一下这一堆接口:

函数 说明
begin() / end() 正向迭代器:begin 指向第一个字符,end 指向最后一个字符的下一个位置
rbegin() / rend() 反向迭代器:rbegin 指向最后一个字符,rend 指向第一个字符的前一个位置
cbegin() / cend() const 正向迭代器(只读),C++11
crbegin() / crend() const 反向迭代器(只读),C++11
4.2.3 测试代码(逐步分析)
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string str("hello");
    string::iterator it = str.begin();   // 1.拿到指向首字符的迭代器
    while (it != str.end())              // 2.end() 是结束标志
    {
        cout << *it << " ";              // 3.解引用拿到字符
        ++it;                            // 4.迭代器向后移动
    }
    cout << endl;
    return 0;
}

逐步分析:

  1. **begin()**返回一个指向字符串首字符的迭代器,交给 it
  2. 循环条件**it != end():**end() 是最后一个字符的下一个位置,it 走到那说明遍历完了
  3. *it 解引用,拿到当前指向的字符
  4. ++it 让迭代器向后走一个字符

**打印结果:**h e l l o

反向迭代器同理:

cpp 复制代码
string::reverse_iterator rit = str.rbegin();   // 指向最后一个字符
while (rit != str.rend())                      // rend 是第一个字符的前一个位置
{
    cout << *rit << " ";
    ++rit;                                     // 注意:反向迭代器 ++ 是往前走
}

打印结果:

o l l e h

4.2.4 迭代器的注意事项
  1. 永远是左闭右开区间 :[begin(), end())、[rbegin(), rend())
  2. const 迭代器和 const 修饰的迭代器不是一回事 :const 迭代器(const_iterator)是它指向的内容不能改 ;const 修饰的迭代器(const string::iterator)是这个迭代器本身不能改(不能 ++,一般没人这么用)
  3. 迭代器失效问题等学到 vector 细讲,先埋个伏笔

到这里,你应该已经开始觉得哪里不对劲了:string::iterator 这个类型名怎么这么长? 别急,下一个主角就是来解决这个痛点的。

4.3 第三种:auto------类型太长?交给编译器

先说结论:

auto 是类型指示符,编译器在编译期根据初始化表达式推导变量类型。它最大的用武之地,就是上一节我们刚体验过的"类型名长到离谱"的场景。

刚写完 string::iterator it = str.begin(),再把迭代器类型替换成 auto:

cpp 复制代码
string str("hello");

// 完整写法:类型名很长
string::iterator it1 = str.begin();

// auto 写法:让编译器自己推导
auto it2 = str.begin();

两者完全等价。如果觉得 string::iterator 还不够长,看看 map 的迭代器------没有 auto 的时候,一行都写不下:

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

int main()
{
    map<string, string> dict = { { "apple", "苹果" }, { "orange", "橙子" }, { "pear", "梨" } };

    // 没有 auto:一行写不下,手都写抽筋
    // map<string, string>::iterator it = dict.begin();

    auto it = dict.begin();   // 有 auto:爽!
    while (it != dict.end())
    {
        cout << it->first << ":" << it->second << endl;
        ++it;
    }
    return 0;
}

打印结果:

apple:苹果 orange:橙子 pear:梨

auto 的使用规则(重点):

cpp 复制代码
int x = 10;
auto y = &x;      // y 是 int*,auto 和 auto* 没区别
auto* z = &x;     // z 是 int*,同上
auto& m = x;      // m 是 int&,声明引用必须加 &

auto aa = 1, bb = 2;         // OK:同一行类型必须一致
// auto cc = 3, dd = 4.0;    // error:编译器只对第一个类型推导,3 和 4.0 类型不同
// auto e;                   // error:auto 必须初始化
// auto array[] = {4,5,6};   // error:auto 不能直接声明数组

坑点总结:

  1. auto 声明的变量必须初始化------编译器要"看见"初始值才能推导
  2. 同一行多个 auto 变量类型必须相同
  3. auto 声明引用要加 &;声明指针 auto和 **auto***等价
  4. auto 不能做函数参数(void f(auto a) 报错);可以做返回值,但建议谨慎使用(降低了可读性)
  5. auto 不能直接声明数组

如何查看推导出来的类型?用typeid(x).name():

cpp 复制代码
int a = 10;
auto b = a;          // int
auto c = 'a';        // char
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;

打印结果

int char

顺带说一句:auto 不只是给迭代器用的,它适用于一切类型推导场景(比如 auto d = 3.14;),只不过在 STL 里它的主场就是迭代器。

4.4 第四种:范围for------全自动遍历

先说结论:

范围 for 自动迭代、自动取数据、自动判断结束。括号里冒号左边是迭代变量,右边是被迭代的范围。学完 4.2 再看它:范围 for 的底层就是迭代器------如果说迭代器是半自动机,范围 for 就是全自动机。

还记得 4.2 的迭代器遍历吗?范围 for 把它浓缩成了一行:

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

int main()
{
    string str("hello world");

    for (auto ch : str)      // 自动取出每个字符给 ch
        cout << ch << " ";
    cout << endl;

    int arr[] = { 1, 2, 3, 4, 5 };
    for (auto e : arr)       // 数组也能范围for
        cout << e << " ";
    cout << endl;
    return 0;
}

打印结果:

h e l l o w o r l d

1 2 3 4 5

4.4.1 一道经典题引入:范围for修改元素

下面代码想通过范围 for 把字符串每个字符都改成 'x',看看结果:

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

int main()
{
    string str("hello");

    for (auto ch : str)   // 想修改每个字符
        ch = 'x';

    cout << str << endl;  // 打印出来是什么?
    return 0;
}

打印结果:

hello

傻眼了吧,根本没改!为什么?

原因与原理: 范围 for 底层会被编译器替换成迭代器遍历**(4.2 讲的),ch 拿到的是 *it 的一份拷贝,改拷贝当然影响不到原字符串。**

解决办法: 把 auto 改成 auto&------ch 变成字符串里字符的引用(别名),改 ch 就是改原字符:

看前后运行结果:

原则:要修改集合里的元素,迭代变量必须用引用 auto&;对象比较大的话,也建议加引用(避免拷贝的开销)。

4.4.2 三大遍历方式小结
方式 适合场景
operator\[\] 需要按下标访问(比如第 i 个字符)
迭代器 通用写法,STL 全家桶通用
范围 for 从头到尾无脑遍历(最常用)

五、最终总结

  1. C 字符串之痛:函数与数据分离、空间自己管、容易越界------string 一站式解决
  2. 一张图记住: → basic_string 模板 → string/wstring/u16string/u32string
  3. 构造重点:默认构造、C 字符串构造、拷贝构造;npos 是 size_t 最大值
  4. 遍历一条龙:operator\[\] 按下标(最朴素)→ 迭代器(最通用,类型名长)→ auto(救星)→ 范围for(auto + 迭代器集大成)
  5. 迭代器左闭右开;const_iterator 是内容只读;用法像指针,底层不一定是指针
  6. auto:编译期推导、必须初始化;不能做参数、同一行类型要一致、不能声明数组
  7. 范围 for:全自动遍历;修改元素必须用 auto&,否则改的是拷贝

好了本期内容就到这里。下篇进入接口大全 ------容量、增删改查,每个接口配测试代码和打印结果,跟着敲一遍就能上手。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。

相关推荐
FlightYe1 小时前
视界原理之2D视频(二):视频文件里有什么
android·linux·网络·c++·ffmpeg·音视频·aac
可乐鸡翅yeah_1 小时前
hls.js 切换多个视频源,新手开发常见踩坑
开发语言·前端·javascript·ios·ffmpeg·音视频·safari
Tanshu_API君1 小时前
API 聚合平台选型指南:从参数拆解到生产落地的十个评估维度
开发语言·api
Chen—LSN1 小时前
C语言——⽂件操作(1)
c语言·开发语言·c++·经验分享·笔记·算法·链表
狗凯之家源码网1 小时前
PHP 开源 IM 即时通讯系统效果实测与功能展示
开发语言·开源·php
anew___1 小时前
《从零手写操作系统 (11):文件系统初探——initrd与VFS抽象层》
java·javascript·算法
天天向上2911 小时前
Spring AI 实战:14 个踩坑记录,其中一个让我的测试全绿但产品是坏的
java·开源
正在走向自律1 小时前
从只写接口到独立交付前后端,飞算JavaAI能给Java后端工程师多少溢价?
java·springboot·ai编程·全栈开发·java求职·飞算javaai·金九银十
辛苦才能1 小时前
C++ map/set 深度解析:从关联式容器的本质到 operator[] 的三重身份
开发语言·c++