学习笔记源自博览网侯捷大师的C++课程,在原视频及讲义的基础上填充注释。
如有侵权,请联系删除,抱歉。
C++ Standard Library —— architecure & sources
目标:根据源代码分析C++ STL的体系结构(STL是泛型编程GP的代表),以STL为目标深层次地探讨泛型编程。
C标准库是功能相互独立的函数库;
C++标准库分为6个主要部件(容器、算法、迭代器、分配器、适配器、仿函数),彼此关系紧密。
C++ STL标准库与泛型编程
C++ Standard Template Library and Generic Programming
C++标准库(C++ Standard Library) vs. C++标准模板库(C++ Standard Template Library)
注:C++标准模板库主要为6大部件,C++标准库除6大部件外还包括其它内容。
C++标准库以头文件(header files
)的形式呈现。
C++标准库的头文件不带后缀名.h
,例如#include
新式C的头文件不带后缀名.h
,例如#include
旧式C的头文件带后缀名.h
仍然可以使用,例如#include
新式头文件内的组件封装在命名空间std
中:
using namespace std;
using std::cout;
或std::vector vec;
旧式头文件内的组件不封装在命名空间std
中。
常用网站:
http://www.cplusplus.com/
https://en.cppreference.com/w/
http://gcc.gnu.org/
常用书籍:
《The C++ Standard Library》
《STL源码剖析》
容器与分配器:分配器管理容器元素的内存。
容器与算法、迭代器:算法是操作或处理容器元素的模板函数;迭代器是容器和算法之间的桥梁,是泛化的指针。
[ )
:迭代器位置包左不包右
begin()
:迭代器指向容器第1个元素的位置
end()
:迭代器指向容器最后1个元素的下一个位置
注:迭代器相当于泛化的指针,可使用指针的操作:
++
、--
、*
、->
。
语法:
for( decl : coll ) {
statement
}
for( 声明 : 容器 ) {
语句
}
注:该写法比迭代器、for_each()简单。【语法糖】
示例1:
for(int i : {1, 3, 5, 7, 9}) {
std::cout << i << std::endl;
}
示例2:
std::vector<int> vec;
...
/* 传值:pass by value */
// 拷贝容器中的元素并赋值给局部变量elem——不影响容器中的原有元素
for(auto elem : vec) {
std::cout << elem << std::endl;
}
/* 传引用:pass by reference */
// 对容器中的元素进行操作——影响容器中的原有元素
for(auto& elem : vec) {
elem *= 3;
}
使用auto
关键字声明变量或对象时,编译器自动对其类型进行推导。【语法糖】
/* 正常写法 */
list<string> c;
...
list<string>::iterator ite;
ite = find(c.begin(), c.end(), targetStr);
/* 使用auto关键字 */
list<string> c;
...
auto ite = find(c.begin(), c.end(), targetStr);
注:使用
auto
关键字声明变量或对象的同时,必须进行定义(显式赋值操作)。
/* 错误写法 */
list<string> c;
...
// auto ite;
// ite = find(c.begin(), c.end(), targetStr);
序列式容器(Sequence Containers):按照元素的存储顺序依次排列。
注:栈stack和队列queue底层依赖于deque容器,本质上属于容器适配器。
关联式容器(Associative Containers):包括键和值,适合快速查找。
集合(Set/Multiset):由红黑树(高度平衡二叉树)实现
映射表(Map/Multimap):由红黑树(高度平衡二叉树)实现
注1:红黑树可自动调整达到平衡,避免在最坏情况下退化为线性链表。
注2:STL未规定set或map的具体实现,但编译器多采用红黑树实现。
注3:set的键和值不作区分:键就是值,值就是键。
注4:set的值不可重复,map的键不可重复;Multiset的值可重复,Multimap的键可重复。
不定序容器(Unordered Containers):底层使用哈希表实现。
注1:哈希表包括若干个bucket,每个bucket对应一个链表。
哈希冲突时,应避免每个bucket对应的链表长度过长而导致元素搜索时间较长。
注2:g++编译器提供非标准的hash_set/hash_multiset/hash_map/hash_multimap
,与标准库提供的unordered_set/unordered_multiset/unordered_map/unordered_multimap
基本一致,使用时需包含头文件#include
。
注1:vector容器的容量支持动态扩容,且扩展后的容量是当前容量的2倍,并将扩容前旧容器的原有元素拷贝至扩容后的新容器中。
注2:STL算法中的::find()
函数是全局函数,且是顺序查找。
注3:vector容器扩容时容量成倍增长,可能会造成内存的浪费。
注1:当STL和容器类均提供用于排序的
sort()
函数时,优先使用容器类的sort()
函数,效率更高。
注2:单向链表forward_list
不存在成员函数back()
和size()
。
注3:g++编译器提供非标准的单向链表slist
,与标准库提供的forward_list
基本一致,使用时需包含头文件#include
。
注4:list容器扩容时每次新增1个节点,内存利用率较高,但查找效率较低。
deque容器包括中央控制器和若干分段连续的缓冲区(buffer)。
当deque容器向前或向后填充元素时,先查看对应缓冲区是否还有剩余空间,当不存在剩余空间时,会申请额外的缓冲区且中央控制器的指针指向新的缓冲区。
注:栈stack和队列queue底层依赖于deque容器,本质上属于容器适配器。
注1:当STL和容器类均提供用于查找的
find()
函数时,优先使用容器类的find()
函数,效率更高。
注2:multimap插入元素时,不可使用[]
,例如mmp[0] = 1; mmp[0] = 2;
,编译器不知道是插入2个键相同的键值对或修改键对应的值。
哈希表包括若干个bucket,每个bucket对应一个链表。
哈希冲突时,应避免每个bucket对应的链表长度过长而导致元素搜索时间较长。
经验法则:当元素个数大于等于bucket个数时,bucket个数会扩充至约原先的2倍。
故bucket数量一定多于元素数量。
容器包含默认的分配器(模板参数使用默认值)。
示例:各类STL容器的默认分配器
template<typename _Tp, typename _Alloc = std::allocator<_Tp>> // 使用默认参数
class vector : protected _Vector_base<_Tp, _Alloc>
template<typename _Tp, typename _Alloc = std::allocator<_Tp>> // 使用默认参数
class list : protected _List_base<_Tp, _Alloc>
template<typename _Tp, typename _Alloc = std::allocator<_Tp>> // 使用默认参数
class deque : protected _Deque_base<_Tp, _Alloc>
template<typename _Key, typename _Compare = std::less<_Key>,
typename _Alloc = std::allocator<_Key>>
class set ...
template<typename _Key, typename _Tp, typename _Compare = std::less<_Key>,
typename _Alloc = std::allocator<std::pair<_Key, _Tp>>
class map ...
template<class _Value, class _Hash = hash<_Value>,
class _Pred = std::equal_to<_Value>, class _Alloc = std::allocator<_Value>>
class unordered_set ...
template<class _Key, class _Tp, class _Hash = hash<_Key>,
class _Pred = std::equal_to<_Key>,
class _Alloc = std::allocator<std::pair<_Key, _Tp>>
class unordered_map ...
g++不同类型的分配器
示例:直接使用分配器管理内存(不建议)
注:不建议手动使用分配器直接管理内存,释放内存时需释放对应大小的内存。
/* 测试不同类型分配器的allocate()和deallocate()函数 */
#include // 包含各类分配器的头文件
int *p;
/* 默认分配器:allocator */
allocator<int> a1;
p = a1.allocate(1); // 分配1个int大小的空间
a1.deallocate(p, 1); // 需要同时释放指针和对应的内存大小
/* malloc分配器:malloc_allocator */
__gnu_cxx::malloc_allocator<int> a2;
p = a2.allocate(2); // 分配2个int大小的空间
a2.deallocate(p, 2); // 需要同时释放指针和对应的内存大小
/* new分配器:new_allocator */
__gnu_cxx::new_allocator<int> a3;
p = a3.allocate(3); // 分配3个int大小的空间
a3.deallocate(p, 3); // 需要同时释放指针和对应的内存大小
/* 池分配器:__pool_alloc */
__gnu_cxx::__pool_alloc<int> a4;
p = a4.allocate(4); // 分配4个int大小的空间
a4.deallocate(p, 4); // 需要同时释放指针和对应的内存大小
/* 多线程分配器:__mt_alloc */
__gnu_cxx::__mt_alloc<int> a5;
p = a5.allocate(5); // 分配5个int大小的空间
a5.deallocate(p, 5); // 需要同时释放指针和对应的内存大小
/* bitmap分配器:bitmap_allocator */
__gnu_cxx::bitmap_allocator<int> a6;
p = a6.allocate(6); // 分配6个int大小的空间
a6.deallocate(p, 6); // 需要同时释放指针和对应的内存大小
Visual C++编译器的STL源码路径:...\include
g++编译器的STL源码路径:
STL:...\include\c++\bits
扩展:...\include\c++\ext
面向对象编程(OOP,Object-Oriented Programming):将数据data
和操作method
相关联。
// list容器的迭代器不支持随机访问
template <class T, class Alloc = alloc>
class list {
...
void sort(); // list容器类的成员函数sort()
};
标准库提供的全局
::sort()
算法所使用的迭代器必须是随机访问迭代器(Random Access Iterator)。链表list的迭代器不支持随机访问,无法使用全局
::sort()
算法进行排序。
/* STL标准库提供的全局::sort() */
// ::sort(c.begin(), c.end())
template <class RandomAccessIterator> // 随机访问迭代器
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
__introsort_loop(first, value_type(first), __lg(last - first) * 2);
__final_insertion_sort(first, last);
}
template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first, RandomAccessIterator last,
T*, Size depth_limit) {
...
// 只有随机访问迭代器,才能跳跃多个元素位置first + (last- first)/2
// 非随机访问迭代器,只能向前向后移动1个位置
RandomAccessIterator cut = __unguarded_partition(
first, last, T(__median(*first, *(first + (last- first)/2), *(last -1))));
...
}
泛型编程(GP,Generic Programming):将数据data
和操作method
相分离。
容器类(Containers)
vector和deque的迭代器支持随机访问,可使用全局
::sort()
算法进行排序。
// vector容器的迭代器支持随机访问
template <class T, class Alloc = alloc>
class vector {
...
};
// deque容器的迭代器支持随机访问
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
...
};
算法类(Algorithms)
/* STL标准库提供的全局::sort() */
// ::sort(c.begin(), c.end())
template <class RandomAccessIterator> // 随机访问迭代器
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
...
}
template <class RandomAccessIterator, class Compare> // 比较规则
inline void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp) {
...
}
采用泛型编程的优点:
所有的算法(Algorithms),涉及元素本身的操作时,本质是比较元素大小。
如:查找是找到与指定值相等的元素;排序是比较元素间的大小顺序。
/* max函数的重载 */
template <class T>
inline const T& max(const T& a, const T& b) {
return a < b ? b : a;
}
// Compare 自定义的比较规则
template <class T, class Compare>
inline const T& max(const T& a, const T& b, Compare comp) {
return comp(a, b) ? b : a;
}
bool strLonger(const string& s1, const string& s2) {
return s1.size() < s2.size(); // 比较字符串长度
}
int main(){
// 按默认字典序比较
cout << max(string("zoo"), string("hello")) << endl; // zoo
// 按自定义的比较规则比较(字符串长度)
cout << max(string("zoo"), string("hello"), strLonger) << endl; // hello
}
操作符重载规则
操作符重载在一定条件下,可作为成员函数或全局函数。
模板:编码时将某些类型暂定为通用类型,在使用时再指定具体类型。
调用者在使用时再指定泛型的具体类型。
注:类模板使用时,需显式指定具体的泛型类型。编译器不会对类模板进行类型推导。
template<typename T> // 类模板
class complex{
private:
T re, im; // 模板
friend complex& __doap1 (complex *ths, const complex& r); // 友元函数
public:
complex(T r = 0, T i = 0) : re(r), im(i){} // 默认参数、初始化列表
complex& operate+=(const complex&); // 重载运算符
T real() const { return re; } // 成员函数-常函数
T imag() const { return im; }
};
int main(){
complex<double> c1(1.1, 2.2);
complex<int> c2(3, 4);
}
注1:关键字
class
可替换为typename
。
注2:函数模板使用时,不必显式指定具体的泛型类型。编译器会对函数模板进行实参推导/类型推导(argument deduction)。类模板使用时,需显式指定具体的泛型类型。
// 函数模板
template<class T>
inline const T& min(const T& a, const T& b){
return b < a ? b : a; // 运算符< 可被重载
}
class stone{
public:
stone(int w, int h, int we) : _w(w), _h(h), _weight(we){}
// 重载运算符<
bool operator< (const stone& rhs){
return this->_weight < rhs._weight;
}
private:
int _w, _h, _weight;
}
int main(){
stone r1(2, 3), r2(3, 3), r3;
// 调用模板函数min,类型推导
// 类型推导T为stone类型,进一步调用stone::operator<()
r3 = min(r1, r2);
}
成员模板:类模板中的成员(函数/属性)也为模板。
template <class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
// 成员变量
T1 first;
T2 second;
// 构造函数
pair() : first(T1()) , second(T2()) {} // 匿名对象T1()、T2()
pair(const T1& a, const T2& b) : first(a), second(b) {} // 有参构造
/* 成员模板 */
template <class U1, class U2>
pair(const pair<U1, U2>& p) : first(p.first), second(p.second) {} // 拷贝构造
};
应用场景:STL中的拷贝构造函数和有参构造函数常设计为成员模板。
成员模板的泛型类型是类模板的泛型类型的子类。
注:成员模板的泛型类型U1/U2,继承自类模板的泛型类型T1/T2。反之不成立。
class Base1 {};
class Derived1 : public Base1 {};
class Base2 {};
class Derived2 : public Base2 {};
pair</* U1 */Devired1, /* U2 */Devired2> p;
pair<Base1, Base2> p2(p);
/* 等价写法 */
// 使用成员模板创建匿名对象,用于类的拷贝构造函数
pair</*T1*/Base1, /*T2*/Base2> p2(pair</*U1*/Devired1, /*U2*/Devired2>());
示例:智能指针类成员模板的应用
智能指针子类shared_ptr
有参构造函数的参数(被包装的指针),是智能指针父类__shared_ptr
有参函数构造函数的参数(被包装的指针)的子类。
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
...
/* 成员模板 */
template<typename _Tp1>
// 构造函数
// 子类构造函数shared_ptr的参数__p,对父类构造函数__shared_ptr<_Tp>赋值
explicit shared_ptr(/*Devired*/_Tp1* __p) : __shared_ptr</*Base*/_Tp>(__p) {}
...
};
/* 指针类的多态 */
Base1 *ptr = new Devired1; // 向上转型up-cast
/* 智能指针类的多态 */
// 使用子类对象
shared_ptr<Base1*> sptr(new Devired1); // 模拟向上转型up-cast
注1:特化的优先级高于泛化。当存在合适的特化版本时,编译器优先调用相应的特化版本。
注2:全特化的所有模板参数均被指定,故模板参数列表为空,即
template <>
。
示例:类模板的泛化和特化
/* 一般的泛化 */
template <class Key>
struct hash {};
/* 不同版本的特化 */
// 本案例只包含1个模板参数,特化后即全特化
template<> // 全特化的所有参数均被指定,故模板参数列表为空。
struct hash<char> {
size_t operator()(char x) const {
return x;
}
};
template<>
struct hash<int> {
size_t operator()(int x) const {
return x;
}
};
template<>
struct hash<long> {
size_t operator()(long x) const {
return x;
}
};
/* 测试案例 */
// 1.当未指定类型时,则使用泛化的类模板
// 2.当指定特定类型时,则使用相应特化版本的类模板
cout << hash<long>()(1000); // 第1个()表示匿名对象;第2个()表示调用函数调用运算符
偏特化/局部特化(partial specialization):指定部分模板参数的具体类型。
对若干数量的模板参数进行特化(显式指定类型)
注1:偏特化的模板参数,必须从左到右连续指定(如偏特化第1/2/3个),而不能穿插指定(如偏特化第1/3/5个),类似于默认参数必须从参数列表的最右侧往左连续。
注2:偏特化仅指定部分模板参数,剩余未被指定的模板参数需存在于模板参数列表,否则报错。
/* 泛化的类模板 */
template<typename T, typename Alloc = .../*默认参数*/> // 模板参数
class vector
{
...
};
/* 偏特化的类模板-个数的偏特化 */
// 通过偏特化/局部特化,模板参数的2个泛型类型变为1个
// 偏特化仅指定部分模板参数,剩余未被指定的模板参数需存在于模板参数列表
template<typename Alloc = .../*默认参数*/>
class vector<bool, Alloc> // 偏特化/局部特化:使用指定的bool类型,绑定泛型类型T
{
...
};
将模板参数类型的表示范围缩小:
T
缩小为对应的指针类型T*
;T
缩小为const限定符修饰T const
。示例:类模板的泛化和偏特化
/* 泛化的类模板 */
template<typename T> // 模板参数
class C
{
...
};
/* 偏特化的类模板-范围的偏特化:指针类型偏特化 */
template<typename T> // 模板参数
class C<T*>
{
...
};
// 等价写法
template<typename U> // 模板参数
class C<U*>
{
...
};
/* 偏特化的类模板-范围的偏特化:const偏特化 */
template<typename T> // 模板参数
class C<T const>
{
...
};
/* 测试案例 */
/* 使用泛化版本的类模板 */
C<string> obj1;
/* 使用偏特化版本的类模板-范围的偏:指针类型偏特化 */
C<string*> obj2; // 任意类型T → 任意指针类型T*
/* 使用偏特化版本的类模板-范围的偏:const偏特化 */
C<string const> obj3; // 任意类型T → const类型T const
示例:类模板的泛化、全特化及偏特化的调用顺序
#include
using namespace std;
/* 泛化的类模板 */
template<typename T1, typename T2>
class Test {
public:
Test(T1 a, T2 b) : _a(a), _b(b) {
cout << "泛化的类模板" << endl;
}
private:
T1 _a;
T2 _b;
};
/* 全特化的类模板 */
template<> // 全特化的所有参数均被指定,故模板参数列表为空
class Test<int, double> {
public:
Test(int a, double b) : _a(a), _b(b) {
cout << "全特化的类模板" << endl;
}
private:
int _a;
double _b;
};
/* 偏特化的类模板-个数的偏特化 */
template<typename T2> // 偏特化仅指定部分模板参数,剩余未被指定的模板参数需存在于模板参数列表
class Test<int, T2> {
public:
Test(int a, T2 b) : _a(a), _b(b) {
cout << "偏特化的类模板-个数的偏特化" << endl;
}
private:
int _a;
T2 _b;
};
/* 偏特化的类模板-范围的偏特化:指针类型偏特化 */
template<typename T1, typename T2>
class Test<T1*, T2*> { // 将泛型类型 T 缩小为对应的指针类型 T*
public:
Test(T1* a, T2* b) : _a(a), _b(b) {
cout << "偏特化的类模板-范围的偏特化:指针类型偏特化" << endl;
}
private:
T1* _a;
T2* _b;
};
/* 偏特化的类模板-范围的偏特化:const偏特化 */
template<typename T1, typename T2>
class Test<T1 const, T2 const> { // 将泛型类型 T 缩小为const限定符修饰 T const
public:
Test(T1 a, T2 b) : _a(a), _b(b) {
cout << "偏特化的类模板-范围的偏特化:const偏特化" << endl;
}
private:
T1 _a;
T2 _b;
};
int main()
{
Test<string, double> test1("hello", 1.1); // 泛化的类模板
Test<int, double> test2(1, 2.2); // 全特化的类模板
Test<int, string> test3(2, "world"); // 偏特化的类模板-个数的偏特化
Test<int*, int*> test4(nullptr, nullptr); // 偏特化的类模板-范围的偏特化:指针类型偏特化
Test<const int, const int> test5(1, 2); // 偏特化的类模板-范围的偏特化:const偏特化
return 0;
}