STL 与 string 起步:六大组件、string 的真实身份,与迭代器为什么是类
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
进军 STL。这篇讲三件事:STL 的六大组件与学习层次、string 的真实身份(它比 STL 还早,而且它是一个模板)、迭代器的真相——它本质就是一个类,而且是为算法而生的。
一、STL 六大组件,与学习它的三层境界
STL 有六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器。这一阶段先碰前三个。
学习的层次分三层,从低到高:
二、string 的真实身份
第一个反常识的点:
string 比 STL 库诞生的早!所以它在 C++ 标准库里面,而不是 STL 库里。
它出现得比 STL 早,后来才被纳入标准,所以严格分类上它属于标准库,不属于 STL 六组件里的"容器"那批原生成员。
第二个反常识的点:string 本身是一个模板。
typedef basic_string<char> string;
basic_string 才是那个类模板,用 char 实例化之后得到一个类,再 typedef 成 string。所以:
- 底层:管理字符数组的顺序表;
- 字符数组里存的就是每一个字符的 ASCII 码值;
- 我们主要学的是 string<char>,它在 std 命名空间里。
顺带把模板和 typedef 再对比一次:
T 可以万能替换,而 typedef 只能写一个,且在预处理阶段进行替换。
typedef 是文本级的一对一替换,预处理完就没了;模板是编译器拿着图纸按类型生成代码,一个模板能生成无数份。
三、域视角下的 string:为什么标准库的类不用手动破域
一个模板、一个域,在一个命名空间域——编译器会直接自动破入命名空间域,直接见到类域。
所以 string q1; 这行背后:编译器见到了类域,直接自动破域(汇编语言层面),调用构造函数,传入 this 指针。
对比一下我们自己写的类:它在我们自己的命名空间里,就必须自己动手,写 bit:: 让编译器先见到它这个类域。
这里我记下了一段挺有态度的判断,原样留着:
我得出了:要么全权交给编译器,要么自己全部以底层写出来。因为"自动"交予编译器,底层也照样是由底层来实现的。
那么以后的取舍标准是什么?
- 对于 STL 标准库自带的,不用去手动从"域的视角、区的视角"考虑破域和类型——开发者设计之初就考虑了这些,让我们用的放心;
- 但对底层的封闭,那就成了 Java 了,对理解 C++ 根本没有帮助。这也是为什么应届大学生 C++ 毕业不敢说自己精通 C++ 的原因;
- 所以:但凡有自己设计的类或者模板,就用底层严密的视角来考虑(最简单的 Date 之类除外)。
理解一个类,要把成员变量与成员函数分开看——即使它们的域是相同的,区不一样(成员在对象的空间里,函数在代码段)。
四、迭代器:本质是类,是"类域中的类域"
它是什么
迭代器本质就是类!迭代器就是要破域——类域中的类域。
iterator 是定义在 string 内部的 public 类,它有自己的成员函数(operator*、operator++、operator!=……),是 string 的一大组件。
创建对象要让编译器找到这个类域;找到之后,调用它的每一个函数本该老老实实地破域——但这里编译器直接自动破域了,隐藏了底层,你直接写 it++、*it 就完事。
为什么有了 [] 还要迭代器
string 是顺序表,q1[i] 遍历很自然。那为什么还要迭代器?
因为迭代器是所有容器的主流!!
[] 只对连续内存的容器好用;链表、树、哈希这些容器没法用下标。而迭代器对每种容器各自实现,但接口长得一模一样,于是同一份算法代码就能跑在所有容器上。
迭代器与算法:需求方与供给方
算法组件 <algorithm> 的内部,会调用传入迭代器的类成员函数。
迭代器就是为了适配算法而生的——算法是「需求方」,迭代器是「供给方」。
换成工程语言说:
- 算法函数对每种容器都一样(sort、find、reverse 只写一份);
- 每个容器的迭代器底层实现不一样,但接口相同;
- 所以迭代器的成员函数实现,是以算法函数为基准的——算法要什么,迭代器就得提供什么。
传过去一个迭代器对象,算法内部直接调它的成员函数;传入的迭代器底层不同,算法内部调到实现就不同——同样的算法代码,跑出了适配不同容器的行为。这就是泛型编程最有价值的一次落地。
五、三种遍历方式
// 1. 下标
for (size_t i = 0; i < s.size(); ++i)
cout << s[i];
// 2. 迭代器
string::iterator it = s.begin();
while (it != s.end()) {
cout << *it;
++it;
}
// 3. 范围 for(C++11)
for (auto ch : s)
cout << ch;
auto a = 10; 是自动推导类型。遍历容器就用范围 for,需要灵活控制位置(比如反向、跳跃、插入删除)就用 iterator 类。
范围 for 的底层就是迭代器——它只是把 begin() / end() / ++ / * 这套动作包装成了语法糖。
六、最后:把构造和析构也看齐
先前几章容易把构造/析构当成"特殊的东西",现在可以拉平了:
- 构造函数是 this 指针的创建:在函数体上面开创 this,开始为 private 成员赋值;
- 析构是清理(清理内部资源,绝不释放自己);
- 其余成员函数都是拿 this 指针干活的高级语法糖。
就是将构造、析构函数、普通成员函数看齐——构造生成对象,析构销毁对象,其余全是 this 指针的高级语法糖。




