文章目录
STL: standard template library
C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈等。
STL的一个重要特点就是数据结构 和 算法的分离。例如,STL中sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包括链表,容器和数组。
STL另一个重要特性是它不是面向对象的,主要依赖于模版,而不是封装和继承
STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的。
1.容器
简单的理解容器,它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。
序列式容器:
所谓序列容器,即以线性排列(类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会自动对存储的元素按照值的大小进行排序。
需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:
- array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
- vector(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
- deque(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
- list(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
- forward_list(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。
注意,其实除此之外,stack 和 queue 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器。
序列式容器
array
和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。
array 容器以类模板的形式定义在 头文件,并位于命名空间 std 中, array<T,N> 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。
创建具有 10 个 double 类型元素的 array 容器:
std::array<double, 10> values;
创建 array 容器的方式,可以将所有的元素初始化为 0 或者和默认元素类型等效的值:
std::array<double, 10> values {};
在创建 array 容器的实例时,也可以像创建常规数组那样对元素进行初始化, 这里只初始化了前 4 个元素,剩余的元素都会被初始化为 0.0。:
std::array<double, 10> values {0.5,1.0,1.5,2.0};
array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全,且效率并没有因此变差。当需要访问容器中某个指定元素时,建议使用 at(),除非确定索引没有越界, 当传给 at() 的索引是一个越界值时,程序会抛出 std::out_of_range 异常。
除此之外,array 容器还提供了 get 模板函数,它是一个辅助函数,能够获取到容器的第 n 个元素。需要注意的是,该模板函数中,参数的实参必须是一个在编译时可以确定的常量表达式,所以它不能是一个循环变量。也就是说,它只能访问模板参数指定的元素,编译器在编译时会对它进行检查。
用 array 容器替换普通数组的好处是,array 模板类中已经封装好了大量实用的方法,在提高开发效率的同时,代码的运行效率也会大幅提高。array 容器提供的 at() 成员函数,可以有效防止越界操纵数组的情况;fill() 函数可以实现数组的快速初始化;swap() 函数可以轻松实现两个相同数组(类型相同,大小相同)中元素的互换。
vector
STL 众多容器中,vector 是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。
vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。
vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。
vector 容器以类模板 vector( T 表示存储元素的类型)的形式定义在 头文件中,并位于 std 命名空间中。
创建存储 double 类型元素的一个 vector 容器:
std::vector values;
除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数
std::vector primes {2, 3, 5, 7, 11, 13, 17, 19};
在创建 vector 容器时,也可以指定元素个数:
std::vector values(20);
注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。
如果不想用 0 作为默认值,也可以指定一个其它值,例如:
std::vector values(20,1.0)
圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:
int num=20;
double value =1.0;
std::vector values(num, value);
通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:
std::vectorvalue1(5, ‘c’);
std::vectorvalue2(value1);
在此基础上,如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:
int array[]={1,2,3};
std::vectorvalues(array, array+2);//values 将保存{1,2}
std::vectorvalue1{1,2,3,4,5};
std::vectorvalue2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}
和 array 容器一样,vector 容器也提供了 at() 成员函数,当传给 at() 的索引会造成越界时,会抛出std::out_of_range异常。
vector 容器还提供了 data() 成员函数,该函数的功能是返回指向容器中首个元素的指针。通过该指针也可以访问甚至修改容器中的元素。
vector 容器的容量(用 capacity 表示),指的是在不分配更多内存的情况下,容器可以保存的最多元素个数;而 vector 容器的大小(用 size 表示),指的是它实际所包含的元素个数。
当 vector 容器的大小和容量相等时,如果再向其添加(或者插入)一个元素,vector 往往会申请多个存储空间,而不仅仅只申请 1 个。
vector扩大容量的本质
另外需要指明的是,当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
完全弃用现有的内存空间,重新申请更大的内存空间;
将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
最后将旧的内存空间释放。
这也就解释了,为什么 vector 容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。
由此可见,vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
vector 容器扩容时,不同的编译器申请更多内存空间的量是不同的。以 VS 为例,它会扩容现有容器容量的 50%。
1.vector添加元素(push_back()和emplace_back())详解
向 vector 容器中添加元素的唯一方式就是使用它的成员函数,如果不调用成员函数,非成员函数既不能添加也不能删除元素。这意味着,vector 容器对象必须通过它所允许的函数去访问,迭代器显然不行。
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。
显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。因此,在实际使用时,建议大家优先选用 emplace_back()。
由于 emplace_back() 是 C++ 11 标准新增加的,如果程序要兼顾之前的版本,还是应该使用 push_back()。
2.vector插入元素(insert()和emplace())详解
insert() 函数的功能是在 vector 容器的指定位置插入一个或多个元素。该函数的语法格式有多种,如下图
emplace()
emplace() 是 C++ 11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。
再次强调,emplace() 每次只能插入一个元素,而不是多个。
该函数的语法格式如下:
iterator emplace (const_iterator pos, args…);
其中,pos 为指定插入位置的迭代器;args… 表示与新插入元素的构造函数相对应的多个参数;该函数会返回表示新插入元素位置的迭代器。
简单的理解 args…,即被插入元素的构造函数需要多少个参数,那么在 emplace() 的第一个参数的后面,就需要传入相应数量的参数。
通过 insert() 函数向 vector 容器中插入 testDemo 类对象,需要调用类的构造函数和移动构造函数(或拷贝构造函数);而通过 emplace() 函数实现同样的功能,只需要调用构造函数即可。
参考文档
3.STL vector删除元素的几种方式
前面提到,无论是向现有 vector 容器中访问元素、添加元素还是插入元素,都只能借助 vector 模板类提供的成员函数,但删除 vector 容器的元素例外,实现删除除了可以借助本身提供的成员函数,还可以借助一些全局函数。
参考文档