云计算百科
云计算领域专业知识百科平台

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

目录

0.1 概述&序言

一、为什么学习string类

1.1 C语言字符串的痛

1.2 现实中的需求

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

二、一张图理清string

2.1 头文件与二八原则

三、string 的构造接口

3.1 接口速查表格

3.2 测试代码

3.3 npos 是什么(重点补充)

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

4.1 第一种:operator[] + 下标

4.2 第二种:迭代器

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

4.2.2 迭代器四兄弟

4.2.3 测试代码(逐步分析)

4.2.4 迭代器的注意事项

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

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

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

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 两个面试题(先感受一下)

  • 字符串转整形数字 → LeetCode 8. 字符串转换整数 (atoi)
  • 字符串相加 → LeetCode 415. 字符串相加

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

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

二、一张图理清string

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

2.1 头文件与二八原则

头文件叫 #include <string>(注意区分 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 测试代码

#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 

#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 测试代码(逐步分析)

#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;
}

逐步分析:

  • begin() 返回一个指向字符串首字符的迭代器,交给 it
  • 循环条件 it != end():end() 是最后一个字符的下一个位置,it 走到那说明遍历完了
  • *it 解引用,拿到当前指向的字符
  • ++it 让迭代器向后走一个字符
  • 打印结果:h e l l o

    反向迭代器同理:

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

    打印结果:

    o l l e h

    4.2.4 迭代器的注意事项
  • 永远是左闭右开区间:[begin(), end())、[rbegin(), rend())
  • const 迭代器和 const 修饰的迭代器不是一回事:const 迭代器(const_iterator)是它指向的内容不能改;const 修饰的迭代器(const string::iterator)是这个迭代器本身不能改(不能 ++,一般没人这么用)
  • 迭代器失效问题等学到 vector 细讲,先埋个伏笔
  • 到这里,你应该已经开始觉得哪里不对劲了:string::iterator 这个类型名怎么这么长? 别急,下一个主角就是来解决这个痛点的。

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

    先说结论:

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

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

    string str("hello");

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

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

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

    #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 的使用规则(重点):

    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 不能直接声明数组

    坑点总结:

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

    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 把它浓缩成了一行:

    #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’,看看结果:

    #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 从头到尾无脑遍历(最常用)

    五、最终总结

  • C 字符串之痛:函数与数据分离、空间自己管、容易越界——string 一站式解决
  • 一张图记住: → basic_string 模板 → string/wstring/u16string/u32string
  • 构造重点:默认构造、C 字符串构造、拷贝构造;npos 是 size_t 最大值
  • 遍历一条龙:operator[] 按下标(最朴素)→ 迭代器(最通用,类型名长)→ auto(救星)→ 范围for(auto + 迭代器集大成)
  • 迭代器左闭右开;const_iterator 是内容只读;用法像指针,底层不一定是指针
  • auto:编译期推导、必须初始化;不能做参数、同一行类型要一致、不能声明数组
  • 范围 for:全自动遍历;修改元素必须用 auto&,否则改的是拷贝
  • 好了本期内容就到这里。下篇进入接口大全——容量、增删改查,每个接口配测试代码和打印结果,跟着敲一遍就能上手。如果对你有帮助,不要忘记点赞三连一波哦,我是白杨,我们下期见。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C++入门篇(十):string(上)——认识string:构造与三大遍历(一条龙讲透operator[]、迭代器、auto、范围for)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!