C++STL学习笔记(第一篇:stl是什么?为什么要学习stl?迭代器在stl中扮演着什么角色?)

简介: C++STL学习笔记(第一篇:stl是什么?为什么要学习stl?迭代器在stl中扮演着什么角色?)

前言

287e8dcaf3bf4e9485632ff1358405ee.gif


从今天起,我就要正式开始学习c++的stl库了。学习呢自然不能光看别人写的,自己也要尝试动手写写,于是我打算将我学的知识以笔记的形式记录下来,方便大家共同学习😊


STL是什么?

先上个很官方的定义:STL,英文全称 standard template library,中文可译为标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。


怎么样,是不是没听懂,其实官方的我也没看太懂😅。咱先看一张图!

d95b3408b2d74ed7a2f6eaee0b639940.png

通常认为,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如表 1 所示。


image.png



对于我们初学者来说,STL的具体结构我们只用了解就行,比较对于我们大多数人来说,我们会用stl便捷的刷题就行,我们其实不太需要搞懂stl背后的复杂原理,嘻嘻🤣😂😍。


不管stl中还是有两个概念需要我们稍微理解一下的——容器和迭代器😅


STL容器就是将运用最广泛的一些数据结构实现出来。容器用来管理某类对象。常用的数据结构:数组(array) , 链表(list), tree(树),栈(stack), 队列(queue), 集合(set),映射表(map)。(其实,容器就是类似数组的单元,可存储 若干个值,且STL容器是同质的,即存储的值类型相同)

迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;

可能你对这些概念还不是太理解,没关系以后在STL中我们经常会用到它们的,在用的过程中你对这些容器和迭代器慢慢就会有深刻的理解的。


为什么要学习STL?

a5a42772d6f340fb9e134573b1701575.gif

在实际的开发过程中,合理组织数据的存取与选择处理数据的算法同等重要,存取数据的方式往往会直接影响到对它们进行增删改查操作的复杂程度和时间消耗。事实上,当程序中存在对时耗要求很高的部分时,数据结构的选择就显得尤为重要,有时甚至直接影响程序执行的成败。


大家在平时学数据结构的时候是不是经常要重复的写诸如链表、集合等等这些常见的数据结构😅,这些代码使用起来往往都十分类似,只是为了适应不同数据的变化,可能需要在一些细节上做不同的处理。


那么大家有没有想过,是不是可以重复利用那些已有的实现来完成当前的任务呢😁?当然是可行的,我相信有很多同学已经亲自编写并实现了动态数组类、链表类、集合类等程序,并精心维护着。但其实,STL 中提供了专家级的几乎我们所需要的各种容器,功能更好,复用性更高。有这样性价比更高的数据结构实现方法,我们为啥不学着去用呢😂?

1edf0b82c85140e88602d85eee81c2e1.gif


为了让大家更清楚地了解 STL 是什么,使用 STL 编程有哪些优势,这里举一个使用 STL 的例子。


以 C++ 定义数组的操作为例,在 C++ 中如果定义一个数组,可以采用如下方式:


int a[n];


这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费😅。


所以除此之外,还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:


int *p = new int[n];


这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行如下操作:


新申请一个较大的内存空间,即执行 int * temp = new int[m];


将原内存空间的数据全部复制到新申请的内存空间中,即执行 memecpy(temp, p, sizeof(int)*n);

将原来的堆空间释放,即执行 delete [] p; p = temp;


————————————


而完成相同的操作,如果采用 STL 标准库,则会简单很多❤️,因为大多数操作细节将不需要程序员关心😍。下面是使用向量模板类 vector 实现以上功能的示例:


vector <int> a; //定义 a 数组,当前数组长度为 0,但和普通数组不同的是,此数组 a 可以根据存储数据的数量自动变长。
//向数组 a 中添加 10 个元素
for (int i = 0; i < 10 ; i++)
    a.push_back(i)
//还可以手动调整数组 a 的大小
a.resize(100);
a[90] = 100;
//还可以直接删除数组 a 中所有的元素,此时 a 的长度变为 0
a.clear();
//重新调整 a 的大小为 20,并存储 20 个 -1 元素。
a.resize(20, -1)


当然了,上述代码中有一些是我们还未介绍的,大家只用大概了解代码功能即可,有关代码中涉及到具体知识,后续的学习笔记会做详细介绍😉。


容器详解

我们常见的大多都是序列容器😎


所谓序列容器,即以线性排列(类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会自动对存储的元素按照值的大小进行排序。


需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:


array(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;


vector(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);


deque(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;


list(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。


forward_list(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

3c4de3ff030b4e47a314539e189c04b5.gif

怎么样,是不是被指向概念整蒙了。没关系,在这里,大家只要对这些概念有个大致印象就行,在接下来的博客中,我会给大家详细的说这些容器的用法的


注意,其实除此之外,stack 和 queue 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器,有关它们的介绍,会放到以后的学习笔记中😉😊。


上面所说容器的区别如图所示(●'◡'●)

30c7d523df583f8b871f9b6c18ae7b9e.jpg

每一个容器都有大量的成员函数,下表只列出了一些最常用的函数 😅

image.png



比如定义一个vector型的容器,就可以这样写😍:


vector v; //定义了一个整形的vector容器v


你会写其他容器的定义吗 ?😎


迭代器详解

迭代器和 C++ 的指针非常类似,它可以是需要的任意类型,通过迭代器可以指向容器中的某个元素,如果需要,还可以对该元素进行读/写操作。


我们常用的迭代器主要有以下几种😂:


1) 前向迭代器(forward iterator)

假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p 操作,还可以被复制或赋值,可以用 == 和 != 运算符进行比较。此外,两个正向迭代器可以互相赋值。


2) 双向迭代器(bidirectional iterator)

双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)。


3) 随机访问迭代器(random access iterator)

随机访问迭代器具有双向迭代器的全部功能😎。除此之外,假设 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:


p+=i:使得 p 往后移动 i 个元素。

p-=i:使得 p 往前移动 i 个元素。

p+i:返回 p 后面第 i 个元素的迭代器。

p-i:返回 p 前面第 i 个元素的迭代器。

p[i]:返回 p 后面第 i 个元素的引用。


此外,两个随机访问迭代器 p1、p2 还可以用 <、>、<=、>= 运算符进行比较。另外,表达式 p2-p1 也是有定义的,其返回值表示 p2 所指向元素和 p1 所指向元素的序号之差(也可以说是 p2 和 p1 之间的元素个数减一)。


不同的容器要用到不同的迭代器😊

image.png



啊!好多,记不住怎么办,嘻嘻又没说让你现在马上记住,用的多了常用的自然就有印象了

a5ed584360ed46eb8103852f8eb031af.gif


再告诉你个更恐怖的事,它们的定义方法也不太一样😅🐟


image.png


注意,以上 4 种定义迭代器的方式,并不是每个容器都适用。有一部分容器同时支持以上 4 种方式,比如 array、deque、vector;而有些容器只支持其中部分的定义方式,例如 forward_list 容器只支持定义正向迭代器,不支持定义反向迭代器。


通过定义以上几种迭代器,就可以读取它指向的元素,*迭代器名(可以把迭代器类别为c语言中的指针)就表示迭代器指向的元素。其中,常量迭代器和非常量迭代器的分别在于,通过非常量迭代器还能修改其指向的元素。另外,反向迭代器和正向迭代器的区别在于:


对正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;

而对反向迭代器进行 ++ 操作时,迭代器会指向容器中的前一个元素;

我们那stl中很常用的vector容器来举例吧!

65a7e991c87b46b78bac664c3ea2b358.gif


vector容器所对应的是随机访问迭代器。假设p是一个随机访问迭代器,p的定义格式大致是


容器类名::iterator  迭代器名;


#include<iostream>
#include<vector> //vector容器包含在该头文件中
using namespace std;
int main() {
  // 定义一个包含5个整型元素的容器
  vector<int> v = { 1, 2, 3, 4, 5, 6};
  //定义该容器对应的迭代器
  vector<int>::iterator i;
  //第一种遍历方式,v类似普通数组
  for (int i = 0; i < v.size(); ++i) {
    cout << v[i] << " ";
  }
  cout << endl;
  // 第二种遍历方式
  i = v.begin(); //将迭代器指向容器的第一个元素
  for (; i != v.end(); ++i) { //用!=比较两个迭代器
    cout << *i << " ";
  }
  cout << endl;
  //第三种遍历方式
  i = v.begin();
  for (; i < v.end(); i += 2)// 随机访问器支持“+=”整数的操作
    cout << *i << " ";
  return 0;
}

7477416f92ae47bf99ad963cb155e5ee.png


关于STL呢,咱们今天只是大概讲一下,接下来的博客我会详细的讲解每个容器的用法和注意事项😁


自己最近才开始写博客,内容有很多不足之处,希望大家多多包涵😉


下期预告:为什么说C++array是普通数组的升级版?

相关文章
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
9月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
277 2
|
9月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
520 73
|
10月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
9月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
416 3
|
10月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
631 1
|
10月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
8月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
350 12
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
192 0