文章目录
0、背景
本篇博客用来记录我在学习 C++ 的 STL 过程的笔记以及问题
0.1、准备工作
需要提前安装 MinGW,VSCode
一些好用的 VSCode 插件
- vscode-icons:不同的文件显示不同的风格
- CodeGeeX:AI 辅助编码工具
- Better Comments:代码注释扩展插件
- GitLens:Git 代码管理插件
1、学习笔记
1.1、基础和概述
1.1.1、基础知识
GP:泛型编程,就是使用 template(模板)为主要工具来进行编写程序
C++ STL 正是泛型编程最成功的作品
定义
- C++ Standard Library:C++标准库(编译好的头文件,header files)
- Standard Template Library:STL,标准模板库(由6大部件构成)
标准库 > STL
标准库大部分内容就是 STL
- C++ 标准库的头文件(headers)不带副档名(.h),例如
#include <vector>
- 新的 C 的头文件不带副档名 .h,例如
#include <cstdio>
- 旧的 C 的头文件带副档名.h,例如
#include <stdio.h>
新式 headers 内的组件封装于 namespace “std”
using namespace std;
命名空间(namespace)
作用:是一种用于组织代码、防止名称冲突的机制。通过将相关的类、函数、变量等封装在命名空间中,可以有效避免不同库或模块之间的命名冲突。std
作用:std 是 C++ 标准库所有组件所在的命名空间,包括string,vector,cout
等
重要的学习网站和网页
CPlusPlus.com
CPPReference
gcc.gnu
1.1.2、STL 体系结构
STL 包含六大部件(Components):
- 容器(Containers)
- 分配器(Allocators)
- 算法(Algorithms)
- 迭代器(Iterators)
- 适配器(Adapters)
- 仿函式(Functors)
- 数据存在容器里面
- 分配器用来支持容器,分配内存
- 算法:模板函数
- 迭代器像是泛化的指针
- 仿函数,作用像是一个函数
- 适配器:做转换作用
STL 六大部件的例子
复杂度(Big-oh)
目前常见的 Big-oh有下列几种情况:
- O(1):常数时间
- O(n):线性时间
- O(log2n):次线性时间
- O(n2):平方时间
- O(2n):指数时间
复杂度数量必须足够大才能呈现其复杂度
前闭后开区间
STL 规定容器都是前闭后开区间
定义:区间包含起始点,不包含结束点,表示为 [begin, end)。
- begin() 指向第一个元素
- end() 指向最后一个元素的下一个元素
容器(不一定是连续空间)
Container<T> array;
...
Container<T>::iterator ite = array.begin();
for (; ite != array.end(); ++ite)
...
range-based for statement
是 C++11 引入的一项重要特性,极大地简化了代码编写,特别是遍历容器或序列的操作。
for ( decl : coll) {
statement
}
auto 关键字
是 C++11 引入的一项重要特性,它允许编译器根据变量的初始化表达式自动推导出变量的类型。
推导是在编译时完成的,不会对运行时性能造成影响
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto i : vec) {
std::cout << i << " ";
}
1.1.3、容器
1.1.3.1、结构与分类
容器大致分为两类:序列式容器,关联式容器
序列式容器
- Array:数组,长度固定,不可扩充
- Vector:动态数组,后向扩充
特性:可以扩展,两倍空间扩展 - Deque:双向队列:双向扩充
- List:双向链表
- Forward-List:单向链表
关联式容器
- Set/Multiset:key 等于 value(Multiset就是 key 可以重复)
- Map/Multimap:key 和 value
- Unordered Set/Multiset:HashTable(Separate Chaining)
- Unordered Map/Multimap:HashTable(Separate Chaining)
一般用红黑树(高度平衡二叉树)来实现 Set 和 Map
学习笔记
- 标准库会提供 sort 算法,每个容器自己也会提供 sort 算法,但容器自己的 sort 算法效率一定更高
标准库的全局 sort
::sort()
容器自己的 sortc.sort()
-
Vector 在扩充容量的时候,先找到一个两倍大的容量,再把原来的元素全部拷贝过去
-
Deque 是分段连续
-
C++ 的 std::deque 容器本身没有提供专门的排序算法,但可以通过标准库中的 std::sort 函数对其进行排序。
-
stack 和 queue 是容器适配器,一个是先进后出,一个是先进先出,因此这两个没有 iterator
-
关联式容器,查找特别快
-
unordered_set :哈希表(散列表)做底层实现;篮子个数是一定比元素多
1.1.4、分配器-allocator
作用:支持容器进行内存的分配和使用
容器有默认的分配器
list<string, allocator<string>> c1
不应该使用分配器,因为 deallocate 的时候,不仅仅要传入指针,还要传入大小
int* p;
allocator<int> alloc1;
p = alloc1.allocate(1);
alloc1.deallocate(p, 1);
allocate(1) 代表分配 1 个元素(也就是上面模板参数中的 1 个 int)
1.2、STL
1.2.1、OOP 和 GP
OOP:Object-Oriented programming
OOP数据和方法放在一起
GP:Generic Programming
GP是将数据和方法分开,通过迭代器连接
GP 的好处
- Containers 和 Algorithms 可以各自闭门造车,其通过 Iterator 沟通就可以
学习笔记
- 只有随机访问迭代器才可以使用 + ,- 和 /
- 而 ::sort() 全局排序算法就是通过 +,- 和 / 实现
- 因此 list 无法使用 ::sort() 全局排序算法 进行排序
- 所有 algorithms,其内最终设计元素本身的操作,无非就是比大小
1.2.2、模板与操作符重载
操作符重载
定义:C++ 操作符重载是 C++ 的一个重要特性,它允许程序员为已有的运算符赋予新的含义,使得这些运算符可以用于用户自定义的数据类型。
学习笔记
- 迭代器必须重载
*
,->
,++
等操作符,因为这些指针都有的功能
模板
模板分为两大类:类模板和函数模板
类模板的例子:
template <typename T>
class complex
{
public:
complex (T r = 0, T = 0) : re (r), im (i) {}
private :
T re, im;
}
# 模板使用
complex<double> c1(2.5, 1.5);
函数模板的例子:
template <tclass T>
inline
const T& min(const T& a, const T& b)
{
return b < a ? b : a;
}
函数模板可以进行参数推导
stone r1(2, 3)
r3 = min(r1, r2); # 参数推导可以得知 T 为 stone
模板中的泛化与特化
C++ 中的泛化与特化是模板编程的核心概念,它们允许开发者编写能够处理多种数据类型的通用代码,同时还能针对特定类型提供优化或定制化的实现。
template <>
float max<float>(float a, float b) {
return fabs(a - b) < 0.0001 ? a : ((a > b) ? a : b);
}
全特化与偏特化
- 全特化意味着模板的所有参数都被具体化了。
- 偏特化是指模板的部分参数被具体化,而其余部分仍然保持泛化状态。(数据的偏特化与范围的偏特化)
1.2.3、分配器 allocators
C++ 分配器(allocator)是C++标准模板库(STL)中的一个重要组件,它提供了一种机制来解耦内存管理和容器的具体实现。分配器的主要目的是允许开发者自定义内存管理策略,从而优化程序性能或适应特定的应用场景。
先介绍 operator new()
和 malloc()
operator new()
底层都会调用malloc()
无论分配多大的内存,都会存在固定的开销,分配的内存越小,固定开销所占的比例就越大
其中红色的部分是 cookie,记录元素的大小,且上下都有
VC6中的分配器介绍
STL 中的分配器 std::allocator
,在分配内存时 allocate()
, 底层会调用 operator new()
,然后再调用 malloc()
在回收内存时deallocate()
,底层会调用operator delete()
,然后再调用 free()
样例
#include <iostream>
#include <memory> // std::allocator
int main() {
// 创建一个可以分配 int 类型对象的 allocator 实例
std::allocator<int> alloc;
// 分配 10 个 int 的原始内存(未构造的对象)
int* p = alloc.allocate(10);
// 使用 placement new 构造对象
for (int i = 0; i < 10; ++i) {
alloc.construct(p + i, i * 2); // 在每个位置构造一个值为 i*2 的 int 对象
}
// 使用已构造的对象
std::cout << "Allocated and constructed integers: ";
for (int i = 0; i < 10; ++i) {
std::cout << p[i] << ' ';
}
std::cout << std::endl;
// 销毁对象
for (int i = 0; i < 10; ++i) {
alloc.destroy(p + i); // 调用每个对象的析构函数
}
// 释放分配的内存
alloc.deallocate(p, 10); // 参数分别是起始地址和分配的数量
return 0;
}
传递给
deallocate()
的指针必须是由allocate()
返回的有效指针,并且大小参数应与当初分配时一致
BC5 中的分配器介绍
跟 VC
相同
G2.9 中的分配器介绍
跟 VC
相同
问题
如果申请的内存过小,则额外开销所占比例会过大
G2.9 中的新的分配器 alloc
主要诉求:减少额外开销
malloc
给各式各类的元素去使用
而容器的特点是,元素大小是一样的
而 alloc
则目标则是减少记录元素大小的 cookie
其中第 0 号链表表示大小为 8 个字节的元素
第 1 号链表表示大小为 16 个字节的元素
在分配内存时,会先进行字节对齐,得到 8 的倍数,然后再去查找对应的链表
如果链表下没有挂载内存块,则调用 malloc 去跟操作系统请求内存,然后做切分,这样每一块都不包含 cookie,因此节省内存开销
G4.9 中分配器
默认使用的是 ::allocator
跟之前的一样
其中 __pool_alloc
就是 G2.9
中的 alloc
并不知道为什么预设不使用好的分配器
alloc
1.2.4、容器
结构与分类
- 序列式容器
array
vector
–heap
:heap
里面有一个vector
—priority_queue
list
:双向链表slist
:单向链表deque
–stack
:stack
里面有一个deque
–queue
:queue
里面有一个deque
- 关联式容器
rb_tree
–set
–map
–multiset
–multimap
hashtable
–hash_set
–hash_map
–hash_multiset
–hash_multimap
1.2.4.1、List
list 是一个双向链表
list
的数据是 node
,类型是一个节点指针 list_node*
,大小是四个字节
G2.9节点的结构如下所示
struct __list_node {
typedef void* void_pointer;
void_pointer prev;
void _pointer next;
T data;
}
所有容器的 iterator
都是一个 class
iterator
里面有大量的操作符重载,会模拟指针的操作
iterator
里面有 5 个 typedef
# 5 个 typedef
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef ptrdiff_t difference_type;
# 做操作符重载
operator*()
operator->()
operator++() # 前++没有参数
operator++(int) # 后++有参数
...
链表的 ++
# 前++
self& operator++() {
node = (link_type)((*node).next);
return *this;
}
# 后++
self operator++(int) {
self tmp = *this; # 记录原值
++*this; # 进行操作(调用了前++)
return tmp; # 返回原值
}
前++和后++的返回值类型不同,是为了模拟整数的操作
整数允许: ++++i;
但是不允许:i++++;
取值
T& operator*() const {
return (*node).data;
}
取地址
T* operator->() const {
return &(operator*());
}
G4.9 相比 G2.9 的改进
- iterator 的模板参数只有一个(更容易理解)
- node结构有其 parent
- node 的成员的 type 更精确(而不是 void*)
1.2.4.2、iterator 的 traits
人为制造的萃取机,为迭代器和算法之间提供了交互的桥梁
iterator 需要遵循的原则
算法提问 -> 迭代器回答
算法需要知道什么呢? 包括如下五种(被称为 associated types):
iterator_category()
:iterator
的分类,即迭代器移动性质(有的只能++,有的只能–,有的可以 + 2)difference_type
:两个iterator
之间的距离的类型value_type
:iterator
所指指的元素的类型reference_type
:表示迭代器指向的对象的引用类型。通常为T&
,允许直接修改迭代器指向的对象。pointer_type
:表示迭代器指向的对象的指针类型。这通常是T*
,其中T
是value_type
最后两种没有被用到过
C++ 迭代器的 associated types 是一个重要的概念,它允许我们在模板编程中关联类型和模板参数。这些类型提供了关于迭代器的重要信息,使得泛型算法能够更好地操作不同的容器和数据结构。
例子:
#include <cstddef> // for ptrdiff_t
template <typename T>
class MyForwardIterator {
public:
// 定义 associated types
typedef std::forward_iterator_tag iterator_category; // 迭代器类别
typedef T value_type; // 迭代器指向的对象类型
typedef std::ptrdiff_t difference_type; // 两个迭代器之间的距离类型
typedef T* pointer; // 指针类型
typedef T& reference; // 引用类型
private:
T* ptr;
public:
// 构造函数
explicit MyForwardIterator(T* p) : ptr(p) {}
// 解引用操作符
reference operator*() const { return *ptr; }
// 前置递增操作符
MyForwardIterator& operator++() {
++ptr;
return *this;
}
// 后置递增操作符
MyForwardIterator operator++(int) {
MyForwardIterator tmp = *this;
++(*this);
return tmp;
}
// 比较操作符
bool operator==(const MyForwardIterator& other) const { return ptr == other.ptr; }
bool operator!=(const MyForwardIterator& other) const { return ptr != other.ptr; }
};
为什么会用到 traits ?
如果是退化的迭代器,即指针,如何回答算法的五个问题,因为指针不是类,没有那五个 typedef
即需要用到 traits
,即一个中间层
traits
可以区分传入的是指针还是迭代器
根据不同的类别,来输出对应的 type
std::iterator_traits
在 C++ 标准模板库(STL)中,std::iterator_traits
是一个模板结构,用于提取迭代器的特性或关联类型(associated types)。这些特性包括 value_type、difference_type、pointer、reference 和 iterator_category 等。通过使用 std::iterator_traits,我们可以以一种统一的方式访问这些特性,而无需直接操作迭代器类型本身。这不仅提高了代码的可重用性和通用性,还简化了复杂算法的设计与实现。
使用偏特化实现
正常定义(迭代器):
template <typename Iterator>
struct iterator_traits {
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
typedef typename Iterator::iterator_category iterator_category;
};
偏特化定义(指针):
template <typename T>
struct iterator_traits<T*> {
typedef ptrdiff_t difference_type;
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef std::random_access_iterator_tag iterator_category;
};
template <typename T>
struct iterator_traits<const T*> {
typedef ptrdiff_t difference_type;
typedef T value_type;
typedef const T* pointer;
typedef const T& reference;
typedef std::random_access_iterator_tag iterator_category;
};
iterator_traits
使用方法举例,编写一个算法print_iterator_traits()
,利用 iterator_traits
来打印对应迭代器的 associated_type
template <typename Iterator>
void print_iterator_traits() {
typedef typename std::iterator_traits<Iterator>::difference_type difference_type;
typedef typename std::iterator_traits<Iterator>::value_type value_type;
typedef typename std::iterator_traits<Iterator>::pointer pointer;
typedef typename std::iterator_traits<Iterator>::reference reference;
typedef typename std::iterator_traits<Iterator>::iterator_category iterator_category;
std::cout << "difference_type: " << typeid(difference_type).name() << std::endl;
std::cout << "value_type: " << typeid(value_type).name() << std::endl;
std::cout << "pointer: " << typeid(pointer).name() << std::endl;
std::cout << "reference: " << typeid(reference).name() << std::endl;
std::cout << "iterator_category: " << typeid(iterator_category).name() << std::endl;
}
1.2.4.3、vector
C++ 中的 vector
是标准模板库(STL)的一部分,它是一种动态数组容器,能够根据需要自动调整大小。vector 提供了数组的效率和灵活性,同时避免了固定大小数组的一些限制。
如果容器增大,会进行两倍增长,无法原地扩充
vector 的底层实现原理
通过三个迭代器实现
- start:指向 vector 容器对象的起始字节位置。
- finish:指向当前最后一个元素的末尾字节。
- end_of_storage:指向整个 vector 容器所占用内存空间的末尾字节。
问题:每次扩容,会触发大量的拷贝构造函数,还有析构函数,非常大的成本!!!
1.2.4.4、array
C++ 中的 std::array
是 C++11 引入的一个容器,用于表示固定大小的数组。它结合了传统数组的优点(如内存连续性和高效性)和标准库容器的安全性和便利性。
特点
必须在创建时指定大小
为什么要包装成容器?
要遵循容器的规律和规则,要提供iterator
,associated type
,以便于让算法/仿函数等实现
TR1 中的 array 实现
特点
- 没有构造和析构函数
- 直接将指针作为迭代器
int[100] array; // failed
int array2[100]; // successful
1.2.4.5、deque
deque
(双端队列)是C++标准模板库(STL)中的一个容器,支持在两端高效地插入和删除元素。它结合了数组和链表的优点,提供了随机访问的能力,同时允许在头尾快速操作。
可以实现双向扩充
分段连续(分段是事实,连续是假象)
迭代器的内容
- 迭代器包含四个指针
- node:控制中心地址,当++和–时,若需要跳转到另一个 buffer,就可以实现
- cur:当前迭代器的节点位置
- first:当前 buffer 的第一个节点(用来标识当前 buffer 的边界)
- last:当前 buffer 的最后一个节点(用来标识当前 buffer 的边界)
- 迭代器大小为 16 字节(4 * 4)
- 迭代器类型,random_access_iterator_tag(随机访问迭代器)
Deque
对象为 40 个字节
start 指针(16字节)+finish指针(16字节)+map(4字节)+map size(4字节)
Deque
的模板参数
template <class T, class Alloc=alloc, size_t Buffer=0> // Buffer 标识一个缓冲区可以容纳的元素个数
class deque{
Deque 如何模拟连续空间?
通过 iterator
来实现
difference_type
operator-(const self& x) const
{
return difference_type(buffer_size()) * (node - x.node - 1) +
(cur - first) + (x.last - x.cur);
}
控制中心,底层实现是 vector
,在扩充的时候,同样是双倍容量扩充,但是放在中间,方便前向以及后向进行扩充
容器queue
和容器stack
都是容器适配器,底层实现都是 Deque
有一个成员是 Deque
,然后转调用 Deque
的方法去实现
stack
和queue
都可以选择 list 或者 deque 作为底层结构
stack
可以选择vector
作为底层结构
stack
和queue
不运行便利,也不提供iterator
,只能从头或者尾端获取元素
1.2.4.6、rb_tree
红黑树(Red-Black Tree)是一种自平衡二叉搜索树,广泛应用于 C++ 标准模板库(STL)中,例如 std::set
、std::map
和 std::multiset
等关联容器。这些容器底层通常使用红黑树实现,以保证插入、删除和查找操作的时间复杂度为 O(log n)。
Red-Black tree(红黑树)是平衡二元搜索树
特点
高度平衡
按照正常规则(++ite)遍历,便能获得排序状态
我们不应使用 rb_tree 的 iterators 改变元素值,因为元素值有其对应的排列规则
rb_tree 可以实现 set 和 map,而 map 允许元素的 data 被改变,而元素的 key 才是不可以被改变
re_tree 提供两种 insert 操作:insert_unique()
和insert_equal()
,前者标识 key 是独一无二的,否则插入失败,后者表示 key 可重复。
rb_tree 提供遍历操作以及 iterators
红黑树的实现
template <class Key, // key
class Value, // key + data
class KeyOfValue, // 如何从 Value 中获得 key
class Compare,
class Alloc = alloc>
class rb_tree {
protected:
size_type node_count; // rb_tree 的大小(即节点个数)
link_type header; // 红黑树的指针
Compare key_compare; // key 的大小比较规则
}
容器 rb_tree
的大小为 (4 + 4 + 1 -> 12) 个字节
1.2.4.7、set / multiset
set
和 multiset
是 C++ 标准库中的关联容器,用于存储有序元素集合。set 中的元素是唯一的,而 multiset 允许重复元素。
定义:set 是一个有序的、不包含重复元素的容器。
底层实现:通常基于红黑树(一种自平衡二叉搜索树)实现。
特点:
- 自动对插入的元素进行排序。
- 不允许重复值。
- 插入、删除和查找的时间复杂度为 O(log n)。
- 不可以使用迭代器修改元素
底层实现:
template <class Key,
class Compare = less<Key>,
class Alloc = alloc>
class set {
public:
typedef typename rep_type::const_iterator iterator; // 将 iterator 修改为 const 来防止元素被修改
1.2.4.7、map / multimap
map
和 multimap
是 C++ 标准库中的关联容器,用于存储键值对。map
中的键是唯一的,而 multimap
允许重复键。
定义:multimap 是一个有序的、键值对(key-value pair)的集合,允许重复键。
底层实现:与 map 类似,基于红黑树实现。
特点:
- 键自动排序。
- 允许重复键。
- 插入、删除和查找的时间复杂度为 O(log n)。
底层实现
template <class Key, // key_type
class T, // data_type
class Compare = less<Key>,
class Alloc=alloc>
class map {
private:
typedef pair<const Key, T> value_type; // 通过把 key 变成 const key 来阻止 key 被修改
容器 map
,独特的 operator[]
- 用于访问或插入键值对。
- 如果键不存在,则会自动插入该键,并将对应的值初始化为默认值。
代码示例
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> m;
// 键 "apple" 不存在,因此插入 "apple" -> 0,并返回值的引用
m["apple"] += 5; // 等价于 m["apple"] = m["apple"] + 5;
std::cout << "apple: " << m["apple"] << std::endl; // 输出:apple: 5
// 键 "banana" 不存在,因此插入 "banana" -> 0
std::cout << "banana: " << m["banana"] << std::endl; // 输出:banana: 0
// 键 "apple" 已存在,直接返回对应的值
m["apple"] += 3;
std::cout << "apple: " << m["apple"] << std::endl; // 输出:apple: 8
return 0;
}
特点与注意事项
- 自动插入:如果键不存在,operator[] 会自动插入该键,并将其值初始化为默认值。这在某些场景下非常有用,但也可能导致意外的行为。
- 性能影响:由于可能涉及插入操作,operator[] 的时间复杂度为 O(log n)。
- 只读访问的风险:如果仅需要检查某个键是否存在而不希望插入新键,建议使用 find() 或 count() 方法,而不是 operator[]。
1.2.4.8、hashtable
哈希表是一种数据结构,用于存储键值对(key-value pairs)。它通过哈希函数将键映射到表中的一个位置,从而实现快速的数据存取。
- bucket:篮子(采用质数作为篮子数量)
- 负载因子: 表示当前存储的元素数与篮子数量的比例。
如果负载因子过高,即元素个数超过篮子个数,则需要把篮子个数扩大两倍
hashtable 的底层实现
template <class Value, // Value
class Key, // Key
class HashFcn, // 哈希函数,计算处理哈希值
class ExtractKey, // 如何从 date 里面拿出来 Key
class EqualKey, // Key 如何比较大小
class Alloc=alloc // 分配器
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*, Alloc> buckets; // 篮子
size_type num_elements // 元素个数
hashtable 的大小
前三个函数大小为 3
buckets,是一个 vector,大小 12 字节(三个指针)
num_elements:4 个字节
hashtable 的大小 19 -> 20(对齐调整为 20 字节)
hash 函数的设计
泛化:
template <class Key> struct hash{};
特化:
__STL_TEMPLATE_NULL struct hash<char> {
size_t operator() (char x) const {return x;}
}
1.2.4.9、hash_set,hash_map,hash_multiset,hash_multimap
在C++中,unordered_set
、unordered_multiset
、unordered_map
和 unordered_multimap
是标准模板库(STL)提供的四种无序关联容器。它们基于哈希表实现,提供平均常数时间复杂度的插入、删除和查找操作。
C++ 11 之前,命名是
- hash_set
- hash_multiset
- hash_map
- hash_multimap
C++ 11 之后,命名是
- unordered_set
- unordered_multiset
- unordered_map
- unordered_multimap
1.2.5、算法
1.2.5.1、算法概述
Algorithms
看不见 Containers
,因此它所需要的一切信息都必须通过 iterators
取得
而 iterators
必须能够回答 Algorithm
的所有提问,才能搭配该 Algorithm
的所有操作。
算法的两种形式:
函数模板
template<typename Iterator>
Algorithm(Iterator itr1, Iterator itr2)
{
...
}
template<typename Iterator, typename Cmp>
Algorithm(Iterator itr1, Iterator itr2, Cmp comp)
{
...
}
1.2.5.2、迭代器的分类
五种迭代器分类
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag {}
struct bidirectional_iterator_tag {};
struct random_access_iterator_tag {};
- 输入迭代器(Input Iterator):只读,单向移动。支持++运算符前进到下一个元素,以及解引用操作符*读取当前元素值。主要用于从输入流中读取数据。
- 输出迭代器(Output Iterator):只写,单向移动。与输入迭代器类似,但仅支持写入数据到流或容器中,不支持直接比较两个迭代器。
- 前向迭代器(Forward Iterator):继承了输入迭代器的所有功能,并且可以多次安全地对同一位置进行读写操作。同样支持单向移动。
- 双向迭代器(Bidirectional Iterator):增加了向前(++)和向后(–)双向移动的能力。这使得它可以在序列中前后移动,适用于如std::list这样的容器。
- 随机访问迭代器(Random Access Iterator):提供了最全面的功能集,包括所有上述类型的特性,并额外支持通过偏移量直接访问元素(it + n)。适用于数组和std::vector等支持快速随机访问的容器。
1.2.5.3、迭代器对算法的影响
迭代器类别之间是一种继承关系
通过继承可以不用对每一种算法都设计出 5 种特化版本
根据不同的迭代器类型,去执行不同的特化或重载的算法
算法源码中会对迭代器有暗示,即模板参数的参数名称,会取得跟想要的迭代器类型的名称相同
又或者是:
这里的 InputIt 是一个模板参数,代表输入迭代器类型。标准库并不显式地要求特定的迭代器类别,而是依赖于编译时类型检查和可用的操作符。
template <class InputIt, class T>
InputIt find(InputIt first, InputIt last, const T& value);
1.2.5.4、算法源码例子
- 计算
template<class _InIt, class _Ty, class _Fn = std::plus<>>
_NODISCARD inline _Ty accumulate(const _InIt _First, const _InIt _Last, _Ty _Val, _Fn _Reduce_op = {})
{ // return noncommutative and nonassociative reduction of _Val and all in [_First, _Last), using _Reduce_op
auto _UFirst = _Get_unwrapped(_First); // 获取迭代器的真实位置
const auto _ULast = _Get_unwrapped(_Last); // 获取结束迭代器的真实位置
for (; _UFirst != _ULast; ++_UFirst) { // 遍历从_First到_Last的所有元素
_Val = _Reduce_op(_Val, *_UFirst); // 使用_reduce_op处理_val和当前元素
}
return (_Val); // 返回最终结果
}
- 算法 for_each
template<class _InIt, class _Fn>
_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func)
{ // perform function for each element [_First, _Last)
_DEBUG_RANGE_PTR(_First, _Last, _Func); // 确保迭代器的有效性
auto _UFirst = _Get_unwrapped(_First); // 获取迭代器的真实位置
const auto _ULast = _Get_unwrapped(_Last); // 获取结束迭代器的真实位置
for (; _UFirst != _ULast; ++_UFirst) { // 遍历从_First到_Last的所有元素
_Func(*_UFirst); // 将当前元素传递给函数对象进行处理
}
return (_Func); // 返回经过处理的函数对象
}
- 算法 count
#include <iterator> // for std::iterator_traits
template <class InputIt, class T>
typename std::iterator_traits<InputIt>::difference_type count(InputIt first, InputIt last, const T& value) {
typename std::iterator_traits<InputIt>::difference_type n = 0; // 初始化计数器为0
// 遍历从 first 到 last 的所有元素
for (; first != last; ++first) {
if (*first == value) { // 如果当前元素等于目标值
++n; // 增加计数器
}
}
return n; // 返回最终的计数值
}
在 C++ 中,rbegin()
和 rend()
是用于支持反向遍历容器的成员函数。它们分别返回指向容器最后一个元素和“第一个元素之前的位置”的反向迭代器。这些方法对于需要从后向前处理序列中的元素非常有用。
在 C++ 标准库中,std::lower_bound
和 std::upper_bound
是用于在有序范围内进行高效查找的算法。它们都定义在 头文件中,并且利用了二分查找的思想来实现高效的查找操作。这两个函数特别适用于需要查找特定值或确定其位置的场景。
1.2.6、仿函数 functors
在 C++ 中,仿函数或称为函数对象,是指可以像函数一样被调用的类实例。仿函数本质上是一个重载了 operator()
的类的对象。它们不仅提供了函数的功能,还允许携带状态,这使得它们比普通函数更加灵活和强大。
仿函数为算法服务
分类
算术类,逻辑运算类,相对关系类
仿函数的例子
#include <iostream>
class Adder {
public:
// 构造函数,初始化成员变量
Adder(int value) : val(value) {}
// 重载 operator(),使对象可以像函数一样调用
int operator()(int a) const {
return a + val;
}
private:
int val; // 成员变量,用于保存状态
};
int main() {
Adder addFive(5); // 创建一个 Adder 对象,初始值为 5
std::cout << "Adding 5 to 10 gives: " << addFive(10) << '\n'; // 输出 15
}
仿函数 functors
可适配(adapter
,即被适配器使用)的条件
必须要继承 typedef 的父类(可以回答三个 typedef 的问题)
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
继承自 unary_function 或 binary_function
因为adapter
会询问 typedef
例如:
#include <functional> // for std::unary_function
class GreaterThan : public std::unary_function<int, bool> {
public:
explicit GreaterThan(int val) : value(val) {}
bool operator()(int arg) const { return arg > value; }
private:
int value;
};
然而,在 C++17 中,std::unary_function
和 std::binary_function
已经被弃用,并且在 C++20 中被移除。现代 C++ 鼓励直接使用模板和概念(Concepts),而不是依赖这些基类。
提供适当的类型成员
即使不继承自 unary_function
或 binary_function
,仿函数仍然需要提供适当的类型成员以便与适配器配合工作。具体来说:
对于一元仿函数,应该定义 argument_type
和 result_type
。
对于二元仿函数,除了 result_type
外,还需要定义 first_argument_type
和 second_argument_type
。
例如:
class GreaterThan {
public:
using argument_type = int;
using result_type = bool;
explicit GreaterThan(int val) : value(val) {}
result_type operator()(const argument_type& arg) const { return arg > value; }
private:
int value;
};
1.2.7、适配器 adapters
在C++标准模板库(STL)中,适配器(Adapter)是一种设计模式,它用于将一个类的接口转换成另一个接口。这种转换使得原本由于接口不兼容而不能一起工作的类能够协同工作。
分类
- 容器适配器
- 迭代器适配器
- 仿函数适配器
通过内含(组合)的方式去实现
有很多可以和 copy
结合
将范围
[first,last)
内的元素复制到从结果开始的范围内。
template<class InputIterator, class OutputIterator>
OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result)
{
while (first!=last) {
*result = *first;
++result; ++first;
}
return result;
}
1.2.7.1、容器适配器
容器适配器
容器适配器是对基础容器进行封装,并提供不同的接口以满足特定的需求。STL提供了三种容器适配器:
Stack
:实现了栈数据结构,即后进先出(LIFO)的数据存储方式。默认使用deque
作为底层容器,但也可以用vector
或list
。Queue
:实现了队列数据结构,即先进先出(FIFO)的数据存储方式。默认使用deque
作为底层容器,但也可以用list
。Priority Queue
:实现了优先级队列,其中元素根据它们的优先级来排序,默认使用vector
作为底层容器,但也可以用deque
。
1.2.7.2、仿函数适配器
仿函数适配器
仿函数适配器是对现有的函数对象(仿函数)进行修改,以便适应不同的需求。仿函数适配器通常用于算法中,以调整函数对象的行为。一些常用的仿函数适配器包括:
Binders
:绑定器,如bind1st
和bind2nd
,它们可以固定函数对象的一个参数。Negators
:否定器,如not1
和not2
,它们可以反转函数对象的结果。Composers
:组合器,如compose1
和compose2
,它们可以组合两个函数对象的行为。
bind2nd 是一个辅助函数,模板参数是 operation,在被调用的时候,才会绑定第二参数
bind2nd 会进行类型推导,推导第一个参数的类型(操作类型),然后调用 binder2nd 函数适配器
bind2nd 函数
template <class _Fn2, class _Ty>
inline binder2nd<_Fn2> bind2nd(const _Fn2& _Func, const _Ty& _Right) {
// return a binder2nd functor adapter
typename _Fn2::second_argument_type _Val(_Right);
return (binder2nd<_Fn2>(_Func, _Val));
}
bind2nd
是一个模板函数,它接受一个二元函数对象和一个值作为参数,并返回一个类型为binder2nd
的对象。这个对象是一个一元函数对象,它会在调用时将原始的二元函数对象与固定的那个值一起应用到传入的参数上1。
在这个例子中,_Func
是你要绑定的二元函数对象,而_Right
是要绑定给_Func
作为第二个参数的值。_Val
是_Right
的副本,它被传递给binder2nd
构造函数来创建一个新的适配器对象。
binder2nd 类
template <class _Fn2>
class binder2nd : public unary_function<typename _Fn2::first_argument_type,
typename _Fn2::result_type> {
public:
typedef typename _Fn2::argument_type argument_type;
typedef typename _Fn2::result_type result_type;
binder2nd(const _Fn2& _Func, const typename _Fn2::second_argument_type& _Right)
: op(_Func), value(_Right) {}
result_type operator()(const argument_type& _Left) const {
return (op(_Left, value));
}
protected:
_Fn2 op; // the functor to apply
typename _Fn2::second_argument_type value; // the right operand
};
binder2nd
是一个模板类,它继承自unary_function
,这是一个基类模板,定义了一元函数对象的基本类型信息。binder2nd
存储了原始的二元函数对象和那个固定的值,并提供了一个重载的operator()
来模拟一元函数的行为
在上面的代码中,binder2nd
的构造函数接收并保存了原始的二元函数对象和固定的值。当binder2nd
对象被调用时,它会将保存的值作为第二个参数传递给原始的二元函数对象,并将传入的一元参数作为第一个参数。
新型适配器,bind
标准模板库(Standard Template Library, STL)中的std::bind
函数是一个通用的绑定器,可以用来创建具有部分参数固定的函数适配器。
std::bind 可以绑定
- function
- function objects
- member functions
- data members
例子
// bind example
#include <iostream> // std::cout
#include <functional> // std::bind
// a function: (also works with function object: std::divides<double> my_divide;)
double my_divide (double x, double y) {return x/y;}
struct MyPair {
double a,b;
double multiply() {return a*b;}
};
int main () {
using namespace std::placeholders; // adds visibility of _1, _2, _3,...
// binding functions:
auto fn_five = std::bind (my_divide,10,2); // returns 10/2
std::cout << fn_five() << '\n'; // 5
auto fn_half = std::bind (my_divide,_1,2); // returns x/2
std::cout << fn_half(10) << '\n'; // 5
auto fn_invert = std::bind (my_divide,_2,_1); // returns y/x
std::cout << fn_invert(10,2) << '\n'; // 0.2
auto fn_rounding = std::bind<int> (my_divide,_1,_2); // returns int(x/y)
std::cout << fn_rounding(10,3) << '\n'; // 3
MyPair ten_two {10,2};
// binding members:
auto bound_member_fn = std::bind (&MyPair::multiply,_1); // returns x.multiply()
std::cout << bound_member_fn(ten_two) << '\n'; // 20
auto bound_member_data = std::bind (&MyPair::a,ten_two); // returns ten_two.a
std::cout << bound_member_data() << '\n'; // 10
return 0;
}
其中 _1 和 _2 是占位符
bind 有一个模板参数,该模板参数就是返回值,例如 bind(my_divide, _1, _2)
成员函数有个默认参数 this
1.2.7.3、迭代器适配器
reverse_iterator
对逆向迭代器取值,就是将对应的正向迭代器退一位取值
reference operator*() const {
Interator tmp = current;
return *--tmp;
}
inserter
在C++中,inserter
是一个非常有用的工具,它属于标准模板库(STL)的一部分,主要用于容器之间元素的插入操作。inserter
函数通过指定的插入点将元素添加到容器中,并且通常与算法如 copy
, move
或者其他需要输出迭代器的算法一起使用。
#include <iostream>
#include <list>
#include <algorithm>
#include <iterator>
int main() {
std::list<int> source = {1, 2, 3};
std::list<int> destination = {10, 20, 30};
// 在destination的第一个元素前插入source的所有元素
std::copy(source.begin(), source.end(), std::inserter(destination, destination.begin()));
for (auto &elem : destination) {
std::cout << elem << " ";
}
// 输出可能是: 1 2 3 10 20 30
}
X 迭代器 ostream_inserter
在C++中,ostream_iterator
是标准模板库(STL)提供的一个输出迭代器,用于将数据从容器或算法直接输出到输出流(如 std::cout
或文件流)。
之所以是 X 迭代器,因为是不属于上面所说的三个迭代器适配器的种类,即容器、函数、迭代器适配器,这其实是一种 stream 适配器
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 创建一个指向 std::cout 的 ostream_iterator
std::ostream_iterator<int> out_it(std::cout, " ");
// 使用 copy 算法和 ostream_iterator 输出 vector 中的元素
std::copy(vec.begin(), vec.end(), out_it);
return 0;
}
X 迭代器 istream_inserter
在C++中,istream_iterator
是标准库提供的一个输入迭代器,用于从输入流(如 std::cin
、文件流等)中顺序读取数据。
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> numbers;
// 使用 istream_iterator 读取所有输入到 vector
std::copy(
std::istream_iterator<int>(std::cin), // 输入流的起始迭代器
std::istream_iterator<int>(), // 结束迭代器(EOF)
std::back_inserter(numbers) // 将元素添加到 vector 末尾
);
// 输出结果
for (int num : numbers) {
std::cout << num << " ";
}
return 0;
}
// istream_iterator example
#include <iostream> // std::cin, std::cout
#include <iterator> // std::istream_iterator
int main () {
double value1, value2;
std::cout << "Please, insert two values: ";
std::istream_iterator<double> eos; // end-of-stream iterator
std::istream_iterator<double> iit (std::cin); // stdin iterator
if (iit!=eos) value1=*iit;
++iit;
if (iit!=eos) value2=*iit;
std::cout << value1 << "*" << value2 << "=" << (value1*value2) << '\n';
return 0;
}
1.3、一些好用的东西
1.3.1、一个万用的 Hash Function
template <typename... Types> // ... 表示可以接受任意多的模板参数
1.3.2、tuple
C++ 中的 tuple
是一种用于存储多个不同类型元素的容器,自 C++11 引入。它类似于 pair
,但可以容纳任意数量和类型的元素。
基本用法:
#include <tuple>
std::tuple<int, double, std::string> t1(42, 3.14, "hello");
int a = std::get<0>(t1); // 获取第 0 个元素(42)
double b = std::get<1>(t1); // 获取第 1 个元素(3.14)
// 自动类型推导
auto t2 = std::make_tuple(10, 2.718, "world");
实现原理:
tuple
通过递归继承或嵌套类模板实现,每个元素存储在不同的基类中。例如:
template <typename... Ts>
struct Tuple;
template <typename T, typename... Ts>
struct Tuple<T, Ts...> : Tuple<Ts...> {
T value;
// ...
};
1.3.3、traits
Traits
是一种模板技术,通过定义包含类型属性或行为的模板类,允许在编译期提取、判断或转换类型的特性。它的核心思想是:
- 将类型的属性或行为外化,避免直接修改类型定义。
- 通过模板特化为不同类型提供差异化逻辑。
为什么需要 traits
?
- 处理未知类型:在泛型编程中,模板参数类型未知,但可能需要根据类型特性选择不同实现(如优化算法)。
- 统一接口:为内置类型(如指针)和用户自定义类型(如迭代器)提供一致的元数据接口。
- 编译期决策:通过类型特性在编译期优化代码,避免运行时开销。
traits
的实现机制
Traits 通常通过 模板特化(Template Specialization) 实现,分为以下步骤:
- 定义主模板:声明默认行为(可能为空或默认值)。
- 特化模板:为特定类型提供定制化的元数据或行为。
例子
// 主模板:默认非指针类型
template <typename T>
struct is_pointer {
static constexpr bool value = false;
};
// 特化版本:匹配指针类型
template <typename T>
struct is_pointer<T*> {
static constexpr bool value = true;
};
// 使用
static_assert(is_pointer<int*>::value, "T must be a pointer type");
常见的 traits 的分类
- 类型特性(Type Traits)
用于查询或修改类型属性,定义在 <type_traits> 头文件:
- 类型判断:
is_pointer
,is_integral
,is_class
- 类型转换:
remove_pointer
,add_const
,decay
- 关系判断:
is_same
,is_base_of
// 判断类型是否为整数
static_assert(std::is_integral<int>::value, "Not integral");
// 移除 const 修饰符
using NakedType = std::remove_const<const int>::type; // int
- 迭代器特性(Iterator Traits)
定义在 <iterator>
,为迭代器提供统一接口:
iterator_category
(迭代器类型,如随机访问迭代器)value_type
(元素类型)difference_type
(距离类型)
template <typename Iterator>
void advance(Iterator& it, int n) {
using Category = typename std::iterator_traits<Iterator>::iterator_category;
if constexpr (std::is_same_v<Category, std::random_access_iterator_tag>) {
it += n; // 随机访问迭代器直接跳转
} else {
while (n-- > 0) ++it; // 其他迭代器逐步移动
}
}
- 类型转换
Traits
用于类型推导和转换,例如 std::decay
模拟参数按值传递时的类型退化:
// 移除引用和 const,数组退化为指针,函数退化为函数指针
using DecayedType = std::decay<const int&>::type; // int
1.3.4、cout
std::cout
是 C++ 标准库中用于标准输出的核心工具,属于 <iostream>
头文件中的 ostream
类对象。它提供类型安全、灵活且可扩展的输出方式,是替代 C 语言 printf
的现代解决方案。
示例
#include <iostream>
#include <iomanip>
#include <thread>
#include <mutex>
using namespace std;
int main() {
// 基本输出
cout << "Integer: " << 10 << ", Double: " << 3.14 << endl;
// 格式化
cout << hex << showbase << 255 << endl; // 输出: 0xff
// 宽度与填充
cout << setw(10) << setfill('*') << left << "Hi" << endl; // 输出: Hi********
// 多线程同步
mutex mtx;
thread t1([&mtx]{
lock_guard<mutex> lock(mtx);
cout << "Thread 1\n";
});
thread t2([&mtx]{
lock_guard<mutex> lock(mtx);
cout << "Thread 2\n";
});
t1.join();
t2.join();
return 0;
}
1.3.4、moveable 元素对于 vector 速度性能的影响
在 C++ 中,移动语义(Move Semantics) 是 C++11 引入的核心特性之一,旨在通过避免不必要的深拷贝来优化资源管理。
它的核心思想是转移资源所有权而非复制资源。
为什么要使用移动语义?
- 传统拷贝的痛点:当对象持有大量资源(如动态内存、文件句柄)时,深拷贝会带来性能损耗。
- 临时对象的浪费:临时对象(如函数返回值)在拷贝后会被销毁,其资源被浪费。
- 移动语义的解决方案:直接“窃取”临时对象的资源,避免深拷贝。
移动语义的基础是 右值引用(T&&
),它绑定到即将销毁的临时对象(右值):
int a = 10;
int&& rref = 42; // 合法:绑定到字面量(右值)
int&& rref2 = a; // 错误:a 是左值
int&& rref3 = std::move(a); // 合法:强制转换为右值
移动构造函数
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 置空原对象的资源指针
other.size_ = 0;
}
private:
char* data_;
size_t size_;
};
移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放当前资源
data_ = other.data_; // 窃取资源
size_ = other.size_;
other.data_ = nullptr; // 置空原对象
other.size_ = 0;
}
return *this;
}
移动语义通过转移资源所有权避免了不必要的拷贝,是 C++ 高性能编程的核心机制。
正确实现移动构造函数和移动赋值运算符,并合理使用 std::move,可以显著提升程序效率。
!!!!需要注意,使用 move 后,原来的资源不能使用,被释放了
2、常见问题
2.1、VSCode 的终端中无法查看 gcc 和 g++ 的版本
【问题描述】
在 VSCode 的终端中,执行 gcc --version
和 g++ --version
无法获取到正常的结果
【问题原因】
没有安装 gcc 和 g++
gcc 和 g++ 是GNU Compiler Collection(GNU编译器集合)中的两个关键组件,专门用于编译C语言和C++语言的程序。
【解决方法】
1、安装 Mingw
Mingw链接
2、然后配置环境变量
3、检查结果,关闭 VSCode 后打开
2.2、VSCode 安装 C/C++ 插件失败
【问题描述】
在 VSCode 中,C/C++ 插件安装失败,报错
Unable to read file 'c:\Users\Administrator.vscode\extensions\ms-vscode.cpptools-1.23.6-win32-x64\package.json' (Error: Unable to resolve nonexistent file 'c:\Users\Administrator.vscode\extensions\ms-vscode.cpptools-1.23.6-win32-x64\package.json')
【问题原因】
插件安装因为未知原因失败
【解决方法】
删除插件后再安装,问题解决
2.3、前置++和后置++的区别
【问题描述】
C++ 中前置 ++ 和后置 ++ 的区别
【问题原因】
【解决方法】
在 C++ 中,前缀 ++(++i)和后缀 ++(i++)都是用于对变量进行自增操作的运算符,但它们的行为有所不同:前缀 ++ 会直接返回自增后的值,而后缀 ++ 会先返回原始值,再进行自增。
++ 操作符的重载
class MyClass {
public:
int value;
// 后缀 ++ 重载
MyClass operator++(int) { // 注意额外的 int 参数
MyClass temp = *this; // 保存原始值
++(*this); // 调用前缀 ++ 实现自增
return temp; // 返回原始值的副本
}
// 前缀 ++ 重载
MyClass& operator++() {
++value;
return *this;
}
};
2.4、C++ 风格的字符串和 C 风格的字符串
C++ 风格的字符串和 C 风格的字符串的区别
C风格的字符串
C风格的字符串实际上是一个以空字符\0
结尾的字符数组。
char str[] = "Hello, World!";
C++风格的字符串
C++引入了std::string
类(位于<string>
头文件中),为字符串处理提供了更高级、更方便的方式。与C风格字符串相比,std::string支持自动内存管理、动态扩展大小,并提供了一系列用于操作字符串的方法。
#include <string>
std::string str = "Hello, World!";
3、C++ 好用的新特性
3.1、range-based for statement
C++11 引入了基于范围的 for 循环(range-based for statement),它提供了一种更加简洁和直观的方式遍历容器或数组中的元素。这种循环结构特别适合于那些需要对序列中每个元素执行相同操作的情况,而不需要显式地使用迭代器或索引。
例子:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (int n : v) { // 使用值进行复制
std::cout << n << ' ';
}
std::cout << '\n';
for (int& n : v) { // 使用引用,允许修改
n *= 2;
}
for (const int& n : v) { // 使用常量引用,避免复制
std::cout << n << ' ';
}
std::cout << '\n';
}
3.2、using 和 typedef
using
是 C++11 引入的功能,旨在提供一种更加现代化的方式来定义类型别名。
相同点
两者都可以用来给已有类型创建一个新的名字,即创建类型别名。
都可以在全局作用域、命名空间作用域或类内部使用。
不同点
- 语法差异
typedef
的语法稍微复杂一些,特别是当涉及到指针和数组时。using
提供了一种更加直观和一致的方式来定义类型别名,特别是在处理复杂的模板时。
Cpp
// 使用 typedef 定义函数指针类型
typedef void (*FuncPtr)(int);
// 使用 using 定义同样的函数指针类型
using FuncPtr = void (*)(int);
- 模板支持
typedef
不支持模板别名。这意味着你不能用typedef
来创建模板化的类型别名。using
支持模板别名,这使得它在泛型编程中非常有用。
// 使用 using 创建模板别名
template <typename T>
using Vec = std::vector<T>;
Vec<int> myVec; // 等价于 std::vector<int> myVec;
- using 支持嵌套