STL的学习代码记录
前言
本人前段时间真正学习STL的源码,学习它的代码组织形式、设计理念等等,但看源码,特别还是需要兼容平台的C++来说,这实在是令人头疼,一堆莫名其妙的宏,莫名其妙的写法,看似多余的写法,可能设计者想的比你还多。
所以在我看了几天源码后,我购买了STL源码剖析
没错就是那本很老的时,但很经典,不过现在,我还没看几章
,在我阅读的这几章里,我感受到了C++的神奇,同时也感受到我自身对C++的掌握能力欠缺。
关于我为什么要记录这个,原因很简单:一是记性差,二是C++太神奇了。
注意,这里并不会记录的十分详细,还很有可能存在错误,请谨慎参考。
这里我提供两个地址,一个是C++STL的源码地址,另一个是我找到了的一个可读性很高的小型STL实现项目现在好像有1.1w start。写的不错
- 说明一下,本人还未看完整个STL库,以下是个人对STL库以及
侯捷
的书STL源码剖析
继续肤浅的理解。
第一章:空间适配器(allocator)
介绍
空间适配器,简单来说,就是用来为数据分配所需的空间,比如常用的vector,当你创建一个
vector<int>
时,空间适配器就在默默地在背后工作,你想问我为什么要使用allocator来为每个容器来分配空间,可能你会想C语言不是可以使用malloc
来对它们分配空间的吗,为什么在这些容器中,它们不用这个malloc或者说用C++的new()
来开辟空间呢?希望通过本章,你能猜到这个问题的答案。
我学到了什么?
::operator new() 关键字和new()操作符的区别
很多人应该对::operator new()关键词不熟吧,但肯定见过new()这个关键字,
new()关键字的用法大概像这样MyClass* mc = newMyClass();
而::operator new()操作符大概像这样void* ptr = ::operator new(sizeof(int));
它们其实都能动态地分配空间,只不过::operator new()它可以更加灵活的内存分配方式
比如:
char buffer[sizeof(int)]; // 预留足够大小的缓冲区
int* ptr = new(buffer) int(42); // 在 buffer 中构造一个 int 对象
这种方式并不进行新的内存分配,而是在指定的地址上分配对应字节的内存,对于我们平时写的哪些程序来说,很少能用到它,通常我们用new关键字来分配空间即可
注意
,我们平时重载的new运算符是::operator new(),而不是关键词new()。
在C++中,new()和delete关键字,一般分为四个过程来执行,
new() : 它分为两个过程,第一步,先分配一块合适大小的空间,也就是使用::operator new()
来分配相应的空间,第二步,调用类的构造函数进行对对象的构造。
delete(): 第一步,调用类的析构函数对对象进行析构,第二步,调用::operator delete()
操作符对已分配的空间进行回收。
STL里面的
allocator
这个模块就是对内存分配进行统一管理,也就是说所有的容器,都需要allocator来分配空间,同样地,销毁回收也是由它控制。
什么是两级配置器
那么我们分配空间是像C++那样简单地使用
malloc
分配对应大小的空间吗?当然没那么简单。
不直接使用
malloc
分配空间的理由也是很简单的,比如频繁分配空间,可能会对性能造成影响,此外,频繁地分配和释放空间,难免要产出内存碎片,导致我们实际可使用的内存空间减少,这时就需要对上述情况,做出对应的解决方案。
对于分配的大小大于
128 bytes
的就正常使用malloc()和free()
对于分配的大小小于等于128 bytes
的需要使用一个16个free-list,也就是链表来对对应大小的空间分配合适的区块。
但此时你可能又有个疑问,这里引入了链表,那么必然要用指针来维护它,这样看起来会是另一个负担,你考虑的的确不错,因此,区别于我们常用的结构体
struct
,这里STL使用联合体union
来实现,这样它们共享同一块空间,就能解决刚才的顾虑了。
free-list的节点结构如下:
union obj{
union obj* free_list_link;
char client_data[1];
};
内存池是什么
上一节,我们说到了第二级配置器的结构,接下来,简单说说,内存池是什么。
简单来说,预先给内存池分配一块大空间,它的功能就是为了给free-list当前存有的内存,free-list从内存池里去取相应内存,以免频繁分配小空间,带来的性能损失和内存碎片。
这里我看的不是太明白就只能简单写写了,噗
第二章:迭代器(iterators)
介绍
如
侯捷
所说,迭代器可以胶合数据容器和算法,不再使一个容器就对应一种算法模板的实现,而是多种容器使用同一个算法模板,这仅仅只需要传入对应的迭代器类型即可,复用同一个模板。而要想设计出一个好的胶合剂,往往比设计一个数据容器或算法还要困难。
迭代器是什么?它被看做是一个指针,由于它的行为表现的像指针一样,其实就是一个类实现了多种运算符,如自增,自减,->等等。
我学到了什么?
如果你有一个指针类型,那该如何推导出指针之前的类型的?
现在假设有一个这样的场景,你有一个函数,它接收一个指针类型的变量,而你的函数要做的事情是
使用传进来这个指针数据类型,使用指针之前的类型来定义一个变量
,比如传入了一个int*
类型的数据,但此时我想要用int来定义一个变量,这该如何办?C++里面可没有判断数据类型的函数呢。
代码如下:
template <typename T>
void stl_fn(T pointer)
{
// 我想要声明一个指针之前的类型(int)的变量该如何办?
}
int main()
{
int a = 1;
stl_fn(&a);
return 0;
}
这里我介绍两个方法:
第一种使用利用std::remove_pointer<T>::type
T是指针类型
第二种是使用函数模板进行推导
代码如下:这里我引入了一个
help_fn()
它接收一个指针类型和一个指针之前的类型,这样我通过help_fn就可以实现定义一个这样的类型U2 tmp = 2;
template <typename U1,typename U2>
void help_fn(U1 pointer,U2 value)
{
U2 tmp = 2; // 这里U2就是int类型
std::cout << tmp;
}
template <typename T>
void stl_fn(T pointer) // T是int*类型
{
help_fn(pointer,*pointer);
}
迭代器是如何作为一个胶合剂的
接回上节,如果我们的函数还想要接收
指针之前类型(int)
的返回值,这可咋办,现在用模板函数推导可推导不出哦,在书上提到,STL是这样解决的,它通过内嵌类型来实现这个功能,这个功能仅仅只是这个内嵌类型解决的一个小问题。
现在我们来聊聊什么是内嵌类型,比如在一个
类或结构体
中使用typedef或是using来对一个数据类型进行起名,这样我们就能直接使用这个新的名字了。更更重要的是,可以通过外部来用域作用符(::)
来取到这个数据类型,如MyStruct<int>::value_type a = 10;
这段代码就是定义了一个int类型的变量a
参考如下代码:
template<typename T>
struct MyStruct{
using value_type = T;
using reference = T&;
using const_reference = const T&;
};
好了我们使用内嵌类型来实现让函数拥有
指针之前的类型返回值
吧
代码如下:
#include <iostream>
#include <vector>
template<typename T>
struct MyStruce {
using value_type = T;
using reference = T&;
};
template <typename T>
typename T::value_type stl_fn()
{
using ValueType = typename T::value_type;
ValueType num = 10;
return num;
}
int main()
{
std::cout << stl_fn<MyStruce<int>>();
return 0;
}
typename T::value_type
这里我使用typename对类型说明,告诉编译器这是一个数据类型,而不是什么tm的什么错误,当然我们上面这个代码还不能很好地体现了STL中迭代器作为容器和算法的胶合剂,在这里我提供了一个详细的代码
,代码功能是将一个容器的所有元素求和,并输出。
具体代码如下:
#include <iostream>
#include <vector>
template<typename T> // 模拟迭代器作为胶合剂
struct iterator_trait {
using value_type = typename T::value_type;
using reference = typename T::reference;
};
template <typename T> // 模拟STL中算法
typename iterator_trait<T>::value_type stl_fn(T iterBegin, T iterEnd)
{
using ValueType = typename iterator_trait<T>::value_type;
ValueType sum = 0;
for (auto p = iterBegin; p != iterEnd; ++p) {
sum += *p;
std::cout << *p << std::endl;
}
std::cout << std::endl;
return sum; // 返回范围内元素的总和
}
int main()
{
std::vector<double> vec = {1.2, 2.4, 3.6}; // 这就是容器数据vector
std::vector<double>::iterator it = vec.begin();
std::cout << "总和: " << stl_fn(it, vec.end()) << std::endl;
return 0;
}
相信这一章坚持看到这里的都是有着不错的C++基础的,如果能很快看懂的,那就能不错了,我可还是参考了gpt给的代码进行修改了的,当时写完这份代码后,我无不惊叹STL的牛逼,这设计真的巧妙。
第三章:容器(containers)
介绍
容器
我学到了什么?
xxx
第四章:算法(algorithms)
介绍
算法
我学到了什么?
xxx
第五章:仿函数(functors)
介绍
仿函数
我学到了什么?
xxx
第六章:配接器(apapters)
介绍
配接器
我学到了什么?
xxx
最后
文章会不定时更新(我也不知道什么时候)
有疑问发在评论区
也可以联系我 qq2636427505