C++ STL浅析
STL是什么
standard-template-libary,标准模板库。
算法库,容器库,函数对象库,迭代器库,智能指针库等都属于STL
在C++标准中,STL被组织为下面的13个头文件:<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack>和<utility>。
我认为type_traits也是STL当中重要的一部分
OOP与GP
全局sort
中使用的迭代器指针是要求支持随机访问的(RandomAccessIterator),也就是说该泛型指针支持++操作。而list
是双向链表,每个节点在内存空间上的分布不是连续的,所以不支持使用全局的sort
方法。
Malloc
malloc
操作具有一定的开销,其申请的内存大小其实略大于程序员要求的大小,因为包含了一定大小的头部和尾部,成为Cookies,用来记录一些必要的信息(比如这块被申请的内存的长度)。所以说,当程序员申请的内存空间非常小时,Cookies的占比就会非常的高。若程序员多次申请非常小的内存,例如一百万次,那么就会产生难以忍受的额外开销。解决这种问题的方法之一是内存池(一次性申请一大块,然后自己写一个类来分配)
所以假设容器中有一百万个小元素,那么使用相应的allocator
去申请一百万次内存空间,而该allocator
只是对malloc
的简单包装,没有做任何相关的优化,也就是说调用了一百万次malloc
,那么效率会很低,导致总的Cookies占量非常高。
STL中共有几种类型的迭代器
STL中共有五种类型的迭代器(c++20前)
LegacyInputIterator
:输入迭代器,例如std::istream_iterator
LegacyOutputIterator
:输出迭代器,例如std::ostream_iterator
,std::back_insert_iterator
和std::front_insert_iterator
LegacyForwardIterator
:前向迭代器,同时它也是输入迭代器LegacyBidirectionalIterator
:双向迭代器,同时它也是前向迭代器LegacyRandomAccessIterator
:随机访问迭代器,这是唯一一个支持比较大小的迭代器,同时也是双向迭代器
而这五种类型,分别代表了五种空结构体标签(通过标签调用到不同的重载函数)
当遗留向前迭代器 (LegacyForwardIterator) 、遗留双向迭代器 (LegacyBidirectionalIterator) 或遗留随机访问迭代器 (LegacyRandomAccessIterator) 在自身的要求之外还满足遗留输出迭代器(LegacyOutputIterator)的要求时,它即被描述为可变的(mutable)
贴几张cppreference的定义
那么我们可以利用这个特性,这么干
void display_category(std::random_access_iterator_tag)
{
std::cout << "random_access_iterator" << std::endl;
}
int main()
{
using it_tag_type = typename std::vector<int>::iterator::iterator_category;
display_category(it_tag_type());
}
实现了一个简单的方法用于输出容器的迭代器类型
template<typename T>
void display_category()
{
using it_tag = typename iterator_traits<T>::iterator_category;
std::cout << typeid(it_tag).name() << std::endl;
}
int main()
{
// struct std::bidirectional_iterator_tag
display_category<std::list<int>::iterator>();
// struct std::bidirectional_iterator_tag
display_category<std::list<int>::const_iterator>();
// struct std::forward_iterator_tag
display_category<std::forward_list<int>::iterator>();
// struct std::bidirectional_iterator_tag
display_category<std::unordered_map<std::string, int>::iterator>();
// struct std::random_access_iterator_tag
display_category<std::deque<int>::iterator>();
}
随机访问意味着内存连续吗
不是,对于std::vector而言,由于它的本质是动态数组,所以它的iterator
是random-access-iterator,且每两个相邻元素之间是内存连续的。对于std::deque
而言,它的iterator
也是random-access-iterator,但它并不是内存连续的
random-access-iterator代表我们对迭代器进行加减操作以访问元素的时间复杂度为O(1),例如数组支持随机访问;而链表(
std::list
)不支持,访问的时间复杂度为O(n)
逆向迭代器rbegin是否等于end
不等于,逆序迭代器重载了++
和--
操作,逆序的++
操作其实相当于正序迭代器的--
利用逆序迭代器可以反向排序数组
int main()
{
std::vector<int> vec = { 3, 6, 1, -9, 4 };
// 4 -9 1 6 3
for (auto it = vec.rbegin(); it != vec.rend(); ++it)
std::cout << *it << std::endl;
// 使用greater仿函数 代表左大于右 也就是降序数组
std::sort(vec.rbegin(), vec.rend(), greater<int>());
// 逆序排列后结果 升序数组
for (int num : vec)
std::cout << num << std::endl;
}
类型萃取
iterator_traits
的简单机理实现
更多相关内容可以参考我的博客:C++ 11中的可变模板参数,C++中的模板元编程(待发布)
std::remove_if,std::find_if,std::find和std::find_if的区别是什么
侯捷曾经说过,如果容器中有自己的find
函数,那么它一定比<algorithm>
中的std::find
要更高效。一般情况下,非关联容器(比如std::set
,std::unorder_map
)都实现了find
。这里主要介绍<algorithm>
中的std::find
和std::find_if
std::vector<std::string> vec{"Jelly", "Mike", "Zed", "John", "Dragon"};
// 在容器中找John 找到了则输出John
if (auto it = std::find(vec.cbegin(), vec.cend(), "John"); it != vec.cend())
std::cout << *it << std::endl;
使用find
时传进的时容器元素的类型,而使用find_if
时参数传入的是函数对象
std::vector<std::string> vec{"Jelly", "Mike", "Zed", "John", "Dragon"};
if (auto it = std::find_if(vec.cbegin(), vec.cend(), [](const auto& str) { return str == "John"; }); it != vec.cend())
std::cout << *it << std::endl;
引用SOF上的一个问题和回答
Q: Why does the standard library have
find
andfind_if
? Couldn'tfind_if
just be an overload offind
?A: Consider
find_if
is renamedfind
, then you have:template <typename InputIterator, typename T> InputIterator find(InputIterator first, InputIterator last, const T& value); template <typename InputIterator, typename Predicate> InputIterator find(InputIterator first, InputIterator last, Predicate pred);
If
find()
andfind_if()
had the same name, surprising abmiguities would have resulted. In general, the_if
suffix is used to indicate that an algrithm takes a predicate.
再来看看remove
和remove_if
。两者的区别和find
系列类似,一个接受容器类型,一个接受函数对象。下文中的某个话题讲过,由于remove
和remove_if
操作是<algorithm>
中的方法,因此他们并不能直接删除容器里的元素,因此为了实现删除功能它们时常和成员函数erase
搭配使用
std::vector<std::string> vec{"Jelly", "Mike", "Zed", "John", "Mike"};
auto it = std::remove(vec.begin(), vec.end(), "Mike");
// 输出一个空的字符串 因为移动赋值使原字符串置空
std::cout << *it << std::endl;
// "Jelly", "Zed", "John", "", "Mike"
// it
for (const auto& str : vec)
std::cout << str << std::endl;
std::remove
操作的返回值有三种情况
- 当要“移除”的元素不在容器中时,会返回
end
- 当要移除的元素在最末尾时,无法执行覆盖操作,所以无法“移除”,也会返回末尾元素所在的迭代器
- 当能正常“移除”时,会根据移除的元素的数量,返回倒数第N个元素的迭代器
那么你能猜出这两种情况的结果是什么吗,是的答案就是这么的神奇。但不管结果怎么样,它保证了数组的前部分是我们想要的数据
// "Jelly", "Zed", "John", "Jane", "", ""
std::vector<std::string> vec{"Jelly", "Mike", "Zed", "Mike", "John", "Jane"};
// "Jelly", "Zed", "John", "Jane", "Mike", ""
std::vector<std::string> vec{"Jelly", "Mike", "Zed", "John", "Mike", "Jane"};
std::remove
和erase
搭配使用能够真正删除容器中的元素,这种搭配也称之为erase-remove_idiom
std::vector<std::string> vec{"Jelly", "Mike", "Zed", "John", "Mike", "Jane"};
vec.erase(std::remove(vec.begin(), vec.end(), "Mike"), vec.end());
// "Jelly", "Zed", "John", "Jane"
for (const auto& str : vec)
std::cout << str << std::endl;
对于没有移动赋值的内置类型来说
// std::remove前
// 1 2 3 4 5
// std::remove第二个元素
// 1 3 4 5 5
erase
和std::remove
会优先调用移动赋值函数,没有的话再调用拷贝赋值函数。以std::string
为例,当A移动赋值给B时,B会获得A的值,同时A置空
std::remove的实现
template<class ForwardIt, class T >
ForwardIt remove(ForwardIt first, ForwardIt last, const T& value)
{
first = std::find(first, last, value);
// 如果找得到符合条件的元素
if (first != last)
{
// 开始逐个遍历后续元素然后进行移动赋值
for (ForwardIt i = first; ++i != last; )
{
// 跳过满足条件的元素
if (*i != value)
*(first++) = std::move(*i);
}
}
return first;
}
std::remove_if的实现
和std::remove的实现几乎是一致的
template<class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p)
{
first = std::find_if(first, last, p);
if (first != last)
for(ForwardIt i = first; ++i != last; )
if (!p(*i))
*first++ = std::move(*i);
return first;
}
std::initializer_list<T>
initializer_list的浅拷贝
std::initializer_list
的实现其实很简单
template <class _Elem>
class initializer_list {
public:
using value_type = _Elem;
using reference = const _Elem&;
using const_reference = const _Elem&;
using size_type = size_t;
using iterator = const _Elem*;
using const_iterator = const _Elem*;
constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {}
constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept
: _First(_First_arg), _Last(_Last_arg) {}
_NODISCARD constexpr const _Elem* begin() const noexcept {
return _First;
}
_NODISCARD constexpr const _Elem* end() const noexcept {
return _Last;
}
_NODISCARD constexpr size_t size() const noexcept {
return static_cast<size_t>(_Last - _First);
}
private:
const _Elem* _First;
const _Elem* _Last;
};
可以通过源码看出,std::initializer_list
并没有实现拷贝构造函数,而C++默认提供的版本是浅拷贝的,也就是说在拷贝的过程中是指针的复制,而更重要的是,与STL中的其他容器不同,std::initializer_list<T>
的迭代器是const T*
,并且迭代器记录的数据是储存在栈上的,因此当它作为函数值返回的时候,访问其中的元素将会出现不确定的结果
std::initializer_list
中维护了一个数组指针,而其指向的数组是储存在栈上的
std::initializer_list<int> create_inlist() {
// 浅拷贝
auto initList = {1, 2, 3};
return initList;
}
int main()
{
// 出现不确定的结果
for (auto i : create_inlist())
std::cout << i << std::endl;
}
如何访问initializer_list中的元素
除了for-range-loop外,我们还可以通过begin()
来获取到迭代器,然后使用访问数组指针的方式来访问std::initializer_list
中的元素
auto initList = { 1, 2, 3 };
for (int i = 0; i < initList.size(); i++)
std::cout << initList.begin()[i] << std::endl;
对于std::vector
而言,容器本身重载了operator[]
,它的迭代器(random-access-iterator)也重载了operator[]
,因此
std::vector<int> initList = { 1, 2, 3 };
int data1 = initList[0]; // 1
int data2 = initList.begin()[1]; // 2
C++类中的初始化顺序是什么
struct MyStruct
{
MyStruct() { cout << "无参构造" << endl; }
MyStruct(int x) { cout << "有参构造" << endl;}
};
struct OrderTest
{
MyStruct m1;
MyStruct m2;
MyStruct m3;
static MyStruct sm;
OrderTest() : m2(20) {}
};
MyStruct OrderTest::sm;
int main()
{
// 无参构造 无参构造 有参构造 无参构造
// sm m1 m2 m3
OrderTest o;
}
vector相关问题
为什么vector使用的是push_back而不是push_front
因为vector
中的数据在内存上是连续的,vector每次扩容的时候都会以当前大小的两倍去申请,也就是说在已经分配好内存的“末尾”添加一个元素是十分方便的。如果是从头进的话,那么代表vector
里头的每个元素都要往后移动一位,也就意味着要执行数组当前容量次的拷贝操作,十分耗时耗空间
执行erase()会发生什么事情
移除单个元素的情况
class TestErase
{
public:
int data;
TestErase(int _data) : data(_data) { std::cout << "构造数据为" << data << "的类" << std::endl; }
TestErase(const TestErase& _copy) : data(_copy.data) { std::cout << "拷贝数据为" << data << "的类" << std::endl; }
TestErase& operator=(const TestErase& _copy)
{
data = _copy.data;
std::cout << "赋值数据为" << data << "的类" << std::endl;
return *this;
}
~TestErase() { std::cout << "析构数据为" << data << "的类" << std::endl; }
};
int main()
{
std::vector<TestErase> vec;
vec.reserve(5);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(2);
std::cout << "开始移除" << std::endl;
for (auto it = vec.begin(); it != vec.end();)
{
// erase返回的是删除项的最后一项
if (it->data == 2)
it = vec.erase(it);
else
it++;
}
for (auto& t : vec)
std::cout << t.data << std::endl;
}
执行一次erase
会发生的事情
- 获取传入的迭代器
- 将迭代器后的元素逐个往前移,调用到
operator=
- 将
end
指针往前移,然后释放end
指针所指的内存 - 返回所清除的最后一个项的下一项(如果清除的是末尾,那么会返回
end
)
移除多个元素的情况
int main()
{
std::vector<TestErase> vec;
vec.reserve(7);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(20);
vec.emplace_back(3);
vec.emplace_back(8);
vec.emplace_back(4);
vec.emplace_back(5);
std::cout << "开始移除" << std::endl;
auto first = ::find_if(vec.begin(), vec.end(), [](const auto& t) { return t.data == 2; });
auto last = ::find_if(vec.begin(), vec.end(), [](const auto& t) { return t.data == 4; });
// 前闭后开的区间
vec.erase(first, last);
for (auto& t : vec)
std::cout << t.data << std::endl;
}
为什么push_back会有两个版本的重载
首先先来看看源码,push_back
其实会调用到emplace_back
,因此push_back
也是能调用到移动构造函数的,这取决于参数类型
template <class... _Valty>
inline decltype(auto) emplace_back(_Valty&&... _Val)
{
// insert by perfectly forwarding into element at end, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
pointer& _Mylast = _My_data._Mylast;
if (_Mylast != _My_data._Myend) {
return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
}
_Ty& _Result = *_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...);
return _Result;
}
inline void push_back(const _Ty& _Val) {
// insert element at end, provide strong guarantee
emplace_back(_Val);
}
inline void push_back(_Ty&& _Val) {
// insert by moving into element at end, provide strong guarantee
emplace_back(_STD move(_Val));
}
push_back
中实现了const T&
以及T&&
两个版本的重载,而不是使用通用引用进行完美转发,因为在push_back
中,&&
并不是一个通用引用,因为不存在类型推导,它其实表示的是右值引用
emplace_back
中存在推导语境,因此使用搭配通用引用实现完美转发
但正是由于emplace_back
是通用引用,因此也会引发下面这个问题
对于vector<vector<int>>
来说,push_back
这个成员函数知道容器的类型是vector<int>
,那么理所应当的push_back({1, 2})
成立,而emplace_back
中存在推导,但是{1, 2}
本身什么也不是,那么自然推导也就不成立了
以下是异想天开环节,我试图将push_back
整合为一个函数。但这都是错误的实现,不具备参考学习意义
template<typename T>
inline std::enable_if_t<std::is_same_v<std::decay_t<T>, _Ty>, void> push_back(T&& _Val) {
emplace_back(std::forward<_Ty>(_Val));
}
template<typename T>
struct vec
{
template<std::convertible_to<T> U = T>
inline void push_back(T&& data) {
emplace_back(std::forward<T>(data));
}
};
List相关问题
什么时候需要使用std::list
- 当我们需要频繁的向头部插入或删除元素的时候
- 且我们不需要容器支持随机访问
list的结构是怎么样的
list
是一个双向循环链表
为什么list要使用循环链表
对于常规的数据结构来说,循环链表的优点是:不论从哪个节点切入,都能成功遍历整个链表,而对于STL,个人见解:为了节省空间。
以下是我在IDE中按F12扒来的源码,VS2019
// list standard header
_NODISCARD iterator begin() noexcept {
return iterator(_Mypair._Myval2._Myhead->_Next, _STD addressof(_Mypair._Myval2));
}
_NODISCARD iterator end() noexcept {
return iterator(_Mypair._Myval2._Myhead, _STD addressof(_Mypair._Myval2));
}
使用循环链表,不需要在类中存储额外的指针指向头和尾。STL将_Myhead
指向“虚无”的尾节点,而获取头节点的操作则直接对Head
取next
即可
list的初始头节点指向哪里,和end指向同一块空间吗
因为begin
和end
操作都会构造一个iterator
(其实这里的iterator
套了using
之后的一种类型,暂且不深究)出来,从测试结果来看两者确实是相等的。
但是对于刚初始化时的_Myhead
来说,它的next
指向的是哪里呢?应该不是一个空类型,如果是的话,因为end
的结果和begin
相等,也就是说_Myhead
也是个空类型,那么这个空类型是怎么获取到next
的呢?本人暂时没有更深究list
的源码,对这个问题并不清楚。
GCC2.9和GCC4.9中list的大小有什么区别,当前呢
从上面贴的list
结构图里可以看出,GCC2.9中存的是一个指针,而指针指向的对象里头存放的是两个void
类型的指针,一个是next
一个是pre
。不论是什么类型指针的大小都是4个字节。所以GCC2.9中list
的大小是4
而GCC4.9中,通过一层一层的关系,放的其实是两个指针,所以GCC4.9中list
的大小是8
而我在当前的编辑器中对list
进行sizeof
操作,得到的结果是12
寄!
list和forward_list的end有什么区别
list
的end
指向的一个“虚无的”节点,也是整个循环链表的“头”,而forward_list
指向的end
可以看作是一个指向新建的空节点的iterator
,与链表本身并无关系。forward_list
是一个线形单向链表
与list
类似,forward_list
的“头”仍然指向一个“虚无的”节点,对begin
的获取则只需要取一位next
即可。
GCC4.9版本中对forward_list
的end
指针的构造是直接传递0
,而现VS2019使用MSVC版本则是传递一个nullptr
,本质上是一样的
如何快速获取list的尾元素
应该不会有人直接暴力遍历一遍吧?
// 假设l为一个int的链表
cout << *(--l.end()) << endl;
既然list是循环链表,那么能通过尾巴的++操作访问到头部吗
据我目前所知,不能
list的迭代器支持++操作是否代表它是random-access-iterator
其实不是的,正如上文中说过的,random-access-iterator要求能对任意元素的访问达到O(1)的时间复杂度,std::list
只是实现了对++
,--
运算符的重载
int main()
{
std::list<int> l{ 1, 2, 3, 4, 5, 6 };
for (auto it = l.cbegin(); it != l.cend(); ++it)
std::cout << *it << std::endl;
}
// 使用了无意义的int标识参数 所以重载的是后置 it++ 操作
_List_const_iterator operator++(int) noexcept {
_List_const_iterator _Tmp = *this;
++*this;
return _Tmp;
}
deque相关问题
deque的优点是什么
deque结合了std::vector
和std::list
的优点
- 他能高效的从头部删除和插入元素,时间复杂度为O(1)
- 是随机访问迭代器
同时它作为其他adaptor类型的默认容器,如std::stack
,std::queue
。换句话说std::stack
和std::queue
的底层容器都是std::deque
,它们只是在底层之上做了方法的封装
adaptor:适配器,比如lighting转type-c接口的adaptor就是负责转化功能
deque的结构是什么样的(待完成)
map,set等关联式容器相关问题
std::set和std::map容器的key有什么要求
不管是单一的还是可重复的std::set
/std::map
,它们的底层实现都是红黑树,所以它们都是有序的。所以该容器要求其key类型实现operator<
std::set和std::map两者的迭代器有什么区别
共同点是它们都不是随机访问迭代器
std::set
,std::multiset
的对迭代器进行解引用,得到的是元素的引用
std::map
, std::multimap
对迭代器进行解引用,得到的是std::pair<key_type, value_type>
的引用。std::pair
比较时是字典序的
using Date = std::pair<int, std::pair<int, int>>;
// 可以实现一个有序的时期集合 升序
std::set<Date> dates;
dates.emplace(std::make_pair(1998, std::make_pair(12, 2)));
关联式容器的迭代器类型是什么
以std::map
为例,它的迭代器解引用后的类型为
std::map<std::string, int> m;
// const volatile std::pair<const std::string, int>
*m.begin();
因此修改关联式容器中的键值对或key都是非法行为
API时间复杂度
insert
/erase
/count
/find/lower_bound/upper_bound
操作的时间复杂度都是O(log n)
insert操作返回的是什么
对于唯一元素容器来说(非multi
,例如std::map
,std::unordered_set
),insert
操作会返回std::pair<iterator, bool>
std::map<int, std::string> myMap;
std::map<int, std::string>::iterator iterator;
bool myBool;
auto returnPair = myMap.insert({ 1, "10" });
std::cout << std::boolalpha << returnPair.second << std::ends;
std::tie(iterator, myBool) = myMap.insert(std::make_pair(1, "100")); // 使用tie解包pair
std::cout << std::boolalpha << myBool << std::ends;
std::tie(std::ignore, myBool) = myMap.insert(std::make_pair(2, "100")); // 使用占位符
std::cout << std::boolalpha << myBool << std::ends;
// C++ 17
auto [newIterator, newBool] = myMap.insert(std::make_pair(3, "300"));
std::cout << std::boolalpha << newBool << std::endl;
输出结果为true false true true
注意,对于唯一容器而言,插入失败并不会返回{end(), false}
,而是会返回{oldIt, false}
,oldIt
是操作的key
的迭代器
std::map<int, int> m;
auto it1 = m.insert({1, 1});
auto it2 = m.insert({1, 2});
// 成立
if (it1.first == it2.first)
std::cout << "equal" << std::endl;
对于multi
容器而言,insert
操作会返回插入的新元素的迭代器
题外话
std::tie
一般是和std::tuple
搭配使用的,但是它也兼容std::pair
。std::pair
具有first
和second
能用于访问第一和第二元素,而std::tuple
没有。
int myInt;
std::string myStr;
bool myBool;
// 通过上面三个变量访问tuple的数据 较为麻烦
std::tie(myInt, myStr, myBool) = std::make_tuple(1, "123", true);
// C++17
auto [newInt, newStr, newBool] = std::make_tuple(2, "456", false);
unorded_multimap和multimap怎么遍历他们中重复的元素
unorded_multimap
std::unordered_multimap<std::string, int> peopleMap = {{"Mike", 10}, {"Mike", 20}, {"Jelly", 18}, {"Mike", 15}, {"Zed", 23}};
using umpIterator = std::unordered_multimap<std::string, int>::iterator;
std::pair<umpIterator , umpIterator> range = peopleMap.equal_range("Mike");
for (auto it = range.first; it != range.second; ++it)
std::cout << it->first << std::ends << it->second << std::endl;
multimap
std::multimap<std::string, int> peopleMap = {{"Mike", 10}, {"Mike", 20}, {"Jelly", 18}, {"Mike", 15}, {"Zed", 23}};
std::iterator_traits<decltype(peopleMap.begin())>::value_type::first_type name = "Mike";
for (auto it = peopleMap.lower_bound(name); it != peopleMap.upper_bound(name); ++it)
std::cout << it->first << std::ends << it->second << std::endl;
关联容器都含有成员函数
lower_bound
和upper_bound
一句话系列
std::vector使用建议
- 当容器的size不大时,经过测试,即使有大量的插入操作,仍然建议使用std::vector,因为相比std::list而言它不需要内存的分配
- 如果想使用固定大小的数组,可以使用
std::array
std::vector<bool>
的实现是经过特化的,所以它的性能非常的好,可以当作bitmap来使用- STL中的容易已经实现了很好的移动构造以及移动赋值,所以可以放心的以值返回,不必担心有多余的拷贝操作