1. STL 的基本组成部分
STL(Standard Template Library)是 C++ 标准库中核心的一部分,它提供了一套通用的类和函数模板,用于数据结构和算法的实现。STL 的组成部分不仅限于容器、算法和迭代器,还包括仿函数、适配器和空间配置器。可以按以下六个部分来归纳:
-
容器(Container)
容器用于存储和管理一组数据。STL 中有多种容器,分为序列容器和关联容器。常见的容器包括vector
,deque
,list
,set
,map
等。 -
算法(Algorithm)
STL 提供了一系列通用的算法操作,如排序、查找、修改等。这些算法大多是通过迭代器作用于容器上的。比如sort()
,find()
,copy()
。 -
迭代器(Iterator)
迭代器提供了访问容器内元素的方式,可以被视为通用指针。STL 中有五类迭代器:输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。不同容器支持不同的迭代器。 -
仿函数(Function Object, Functor)
仿函数是一种类似函数的对象。它通过重载operator()
来使对象像函数一样调用。STL 中许多算法可以接受仿函数来定义自定义行为,比如在sort
中传入自定义排序规则。 -
适配器(Adaptor)
适配器是对现有组件的封装或调整,用于改变接口或行为。容器适配器(如stack
,queue
)将底层的容器封装成特定的数据结构,而迭代器适配器(如reverse_iterator
)则改变了迭代器的行为。 -
空间配置器(Allocator)
allocator
负责内存的分配与回收。STL 容器使用分配器来管理内存。默认分配器是std::allocator
,但可以提供自定义分配器来优化内存管理。
组成部分 | 功能 | 示例 |
---|---|---|
容器 | 存储数据的结构,提供不同的数据存储方式 | vector , deque , list , map , set |
算法 | 对容器中的数据进行操作,如排序、查找、修改等 | sort , find , transform , accumulate |
迭代器 | 提供对容器元素的访问方式,类似通用指针 | begin() , end() , rbegin() , rend() , advance() |
仿函数 | 像函数一样使用的对象,常用于自定义算法行为 | plus<int> , minus<int> , 自定义排序函数对象 |
适配器 | 修改现有组件的行为或接口,用于特定需求 | 容器适配器 stack , queue ,迭代器适配器 reverse_iterator |
空间配置器 | 管理内存分配和回收,容器默认使用 std::allocator | 自定义内存分配器 allocator |
2. STL 中常见的容器及其实现原理
STL 提供了多种容器用于不同的数据存储和操作场景,常见的容器主要包括:
a. 序列容器(Sequence Containers)
序列容器用于存储元素的线性序列,插入和访问顺序与元素的存储顺序一致。
-
vector
vector
是一个动态数组,支持随机访问。它在内存中使用连续的内存块存储数据。当容量不足时,vector
会重新分配更大的内存,并将原数据复制过去。插入和删除操作通常在末尾处最有效,复杂度为 O(1)。但在中间插入元素的效率较低,通常需要移动后续的所有元素,复杂度为 O(n)。 -
deque
deque
是双端队列,允许在两端进行常量时间的插入和删除。它并不是像vector
那样连续存储,而是使用一系列小块的内存段进行存储。deque
在内存中使用多个段,段与段之间不一定是连续的。 -
list
list
是双向链表,每个元素由一个节点组成,每个节点包含指向前后节点的指针。list
支持在任意位置快速插入和删除元素(O(1)),但不支持随机访问,访问元素的时间复杂度为 O(n)。
b. 关联容器(Associative Containers)
关联容器存储键值对(或仅键),并根据键进行排序。
-
map
map
是基于红黑树实现的有序映射。红黑树是一种自平衡二叉搜索树,确保插入、删除和查找操作的时间复杂度为 O(log n)。map
自动根据键的顺序进行排序,且不允许重复键。 -
unordered_map
unordered_map
基于哈希表实现,元素的顺序无关紧要。哈希表通过哈希函数将键映射到存储桶(bucket)中,查找、插入和删除的平均复杂度为 O(1),最坏情况下为 O(n)(当哈希冲突严重时)。
容器 | 类型 | 实现原理 | 特点 |
---|---|---|---|
vector | 序列容器 | 动态数组,连续内存 | 支持随机访问,末尾插入/删除高效 |
deque | 序列容器 | 分段连续内存 | 双端插入/删除高效 |
list | 序列容器 | 双向链表,节点指针 | 任意位置插入/删除高效,但不支持随机访问 |
map | 关联容器 | 红黑树 | 键值对有序存储,插入/删除/查找复杂度 O(log n) |
unordered_map | 关联容器 | 哈希表 | 键值对无序存储,插入/删除/查找平均复杂度 O(1) |
3. STL 中 map
、hashtable
、deque
、list
的实现原理
a. map
实现原理
map
使用红黑树(Red-Black Tree)作为底层数据结构。红黑树是一种自平衡二叉搜索树,它在插入和删除节点时通过重新调整节点颜色(红或黑)和旋转来保持树的平衡。红黑树的操作(如插入、删除和查找)的时间复杂度是 O(log n)。
b. hashtable
实现原理
unordered_map
是基于哈希表的无序关联容器。哈希表通过哈希函数将键映射到存储桶中,然后在每个存储桶中存储对应的值。为了处理哈希冲突,哈希表通常采用链表法(链地址法)或开放地址法。哈希表的平均时间复杂度为 O(1),但在最坏情况下(所有键哈希到同一个桶)会退化到 O(n)。
c. deque
实现原理
deque
使用分段连续的内存块来存储元素。每个块都是一段连续的内存,这样既允许快速的随机访问(如 vector
),又允许在前后进行高效的插入和删除。deque
的设计旨在优化双端插入和删除的性能,而不像 vector
那样需要在首部插入时移动所有元素。
d. list
实现原理
list
是基于双向链表实现的容器。每个节点存储数据和两个指针,分别指向前驱节点和后继节点。双向链表允许在常量时间内进行插入和删除操作,但不支持随机访问。遍历链表的时间复杂度为 O(n)。
4. STL 的空间配置器(Allocator)
allocator
是 STL 中用于管理内存分配的组件。每个容器都使用分配器来处理内存的分配和释放,默认分配器是 std::allocator
。分配器的工作流程包括以下几个步骤:
- allocate:为指定数量的对象分配未构造的内存。
- construct:在已分配的内存上构造对象。
- destroy:调用对象的析构函数,销毁对象但不释放内存。
- deallocate:释放先前分配的内存。
分配器的核心函数包括:
template <typename T>
struct MyAllocator {
// 分配内存
T* allocate(size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
// 构造对象
void construct(T* p, const T& val) {
new(p) T(val);
}
// 销毁对象
void destroy(T* p) {
p->~T();
}
// 释放内存
void deallocate(T* p, size_t n) {
::operator delete(p);
}
};
功能 | 函数 | 作用 |
---|---|---|
分配内存 | allocate | 分配一段未构造的内存 |
构造对象 | construct | 在已分配的内存上构造对象 |
销毁对象 | destroy | 调用对象的析构函数 |
释放内存 | deallocate | 释放分配的内存 |
两种C++类对象实例化方式的异同 在c++中,创建类对象一般分为两种方式:一种是直接利用构造函数,直接构造类对象,如 Test test();另一种是通过new来实例化一个类对象,如 Test *pTest = new Test;那么,这两种方式有什么异同点呢?
我们知道,内存分配主要有三种方式:
(1)静态存储区分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行空间内都存在。如全局变量,静态变量等。
(2) 栈空间分配:程序在运行期间,函数内的局部变量通过栈空间来分配存储(函数调用栈), 当函数执行完毕返回时,相对应的栈空间被立即回收。主要是局部变量。
(3)堆空间分配:程序在运行期间,通过在堆空间上为数据分配存储空间,通过malloc和new创 建的对象都是从堆空间分配内存,这类空间需要程序员自己来管理,必须通过free()或者是delete() 函数对堆空间进行释放,否则会造成内存溢出。
那么,从内存空间分配的角度来对这两种方式的区别,就比较容易区分:
(1)对于第一种方式来说,是直接通过调用Test类的构造函数来实例化Test类对象的,如果该实例 化对象是一个局部变量,则其是在栈空间分配相应的存储空间。
(2)对于第二种方式来说,就显得比较复杂。这里主要以new类对象来说明一下。new一个类对象, 其实是执行了两步操作:首先,调用new在堆空间分配内存,然后调用类的构造函数构造对象的内 容;同样,使用delete释放时,也是经历了两个步骤:首先调用类的析构函数释放类对象,然后调 用delete释放堆空间。
2. C++ STL空间配置器实现很容易想象,为了实现空间配置器,完全可以利用new和delete函数并对其进行封装实现STL的空间配置器,的确可以这样。但是,为了最大化提升效率,SGI STL版本并没有简单的这样做,而是采取了一定的措施,实现了更加高效复杂的空间分配策略。由于以上的构造都分为两部分,所以,在SGI STL中,将对象的构造切分开来,分成空间配置和对象构造两部分。 内存配置操作: 通过alloc::allocate()实现 内存释放操作: 通过alloc::deallocate()实现 对象构造操作: 通过::construct()实现 对象释放操作: 通过::destroy()实现 关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间, 利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
5. STL 容器查找的时间复杂度是多少,为什么?
1. vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
2. deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
3. list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1)
4. map、set、multimap、multiset 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN)
5. unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N)
注意:容器的时间复杂度取决于其底层实现方式。
6 迭代器什么时候会失效?
1. 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个 元素都往前移动一位,erase返回下一个有效的迭代器。
2. 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
7.STL中迭代器的作用,有指针为何还要迭代器?
1. 迭代器的作用
(1)用于指向顺序容器和关联容器中的元素
(2)通过迭代器可以读取它指向的元素
(3)通过非const迭代器还可以修改其指向的元素
2. 迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操 作符,-->、++、--等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容 器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更 高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。 迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输 出其自身。
3. 迭代器产生的原因 Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达 到循环遍历集合的效果。