STL 介绍
STL
组件关系如下图所示。
先来看一个示例。
#include <vector> // 容器组件
#include <algorithm> // 算法组件
#include <functional> // 函数对象组件
#include <iostream> // IO流
using namespace std; // 标准命名空间
int main() {
// 原生数组初始化
int ia[6] = {27, 210, 12, 47, 109, 83}; // 原始数据
// 容器构造(显式指定allocator)
vector<int, allocator<int>> vi(ia, ia + 6);
// 算法调用链
cout << count_if(vi.begin(), vi.end(), not1(bind2nd(less<int>(), 40))) << endl;
system("pause");
return 0;
}
关键组件说明:
- 容器
首先是创建一个container
(vector
)
序列容器,此处使用原生数组区间构造:[ia, ia+6)
- 分配器
allocator
来帮助container
来分配内存(一般会忽略不写) - 算法
用一个Algorithm
来操作数据(count_if
是数出满足条件的个数) - 迭代器
iterator
就是一个泛化的指针,来告诉Algorithm
要处理哪里的数据 - 仿函数
用一个functor
来判断数据(less
其有两个参数传入,第一个 <
; 第二个就为真) - 适配器
先用一个function
adapter
(bind2nd
)绑定了第二个参数为 40 40 40 ;再用一个function
adapter
(not1
)来对整个判断结果进行否定
判断条件 谓词(predicate) 为:not1(bind2nd(less <int>(), 40))
—— 表示 >=40
数为真。
前闭后开:[
a
a
a ,
b
b
b ),基本所有容器都有 begin ()
, end ()
,但 begin
是指向的容器的第一个元素,而 end
是指向的容器最后一个元素的 下一个。
算法复杂度
大O表示法 | 中文名称 | 英文名称 | 典型场景示例 | 效率等级 |
---|---|---|---|---|
O(1) | 常数时间 | Constant Time | 数组随机访问、哈希表查找 | ⭐⭐⭐⭐⭐ |
O(log₂n) | 对数时间 | Logarithmic Time | 二分查找、平衡二叉搜索树操作 | ⭐⭐⭐⭐ |
O(n) | 线性时间 | Linear Time | 遍历数组/链表 | ⭐⭐⭐ |
O(n log₂n) | 线性对数时间 | Linearithmic Time | 快速排序、归并排序 | ⭐⭐ |
O(n²) | 平方时间 | Quadratic Time | 冒泡排序、简单矩阵运算 | ⭐ |
O(n³) | 立方时间 | Cubic Time | 三层嵌套循环、Floyd-Warshall算法 | 🐢 |
O(2ⁿ) | 指数时间 | Exponential Time | 穷举搜索、汉诺塔问题 | 💀 |
面向对象编程(OOP)与泛型编程(GP)对比
对比维度 | 面向对象编程 (OOP) | 泛型编程 (Generic Programming) |
---|---|---|
核心理念 | 通过类和对象组织代码,强调封装、继承、多态,将数据和方法关联到一起,放在一个类里面 | 通过类型抽象编写通用代码,强调算法与数据结构的解耦,把数据和方法分开 |
代码复用方式 | 继承(is-a关系)、组合(has-a关系) | 模板(C++)/泛型(Java/C#),基于类型参数化 |
典型语法 | class Animal { virtual void speak() = 0; } | template<typename T> void swap(T& a, T& b) |
多态实现 | 运行时多态(虚函数表) | 编译时多态(模板实例化) |
类型检查时机 | 运行时类型检查(动态绑定) | 编译时类型检查(静态实例化) |
性能特点 | 运行时开销(虚函数调用、RTTI) | 零运行时开销(编译期生成特化代码) |
典型应用场景 | GUI框架、业务系统、游戏实体管理 | 容器库(STL)、数学库、算法库 |
扩展性 | 通过派生类扩展 | 通过模板特化/概念(C++20)扩展 |
代码示例 | cpp<br>class Dog : public Animal {<br> void speak() override { cout << "Woof"; }<br>};<br> | cpp<br>template<typename T><br>T max(T a, T b) { <br> return a > b ? a : b; <br>}<br> |
语言代表 | Java/C#/Python | C++模板/Java泛型/Rust Traits |
设计模式关联 | 工厂模式、策略模式、观察者模式 | 策略模式、标签分发、CRTP(奇异递归模板模式) |
内存管理 | 常伴随对象生命周期管理(构造/析构) | 依赖值语义或显式内存操作(如allocator) |
关键差异说明:
-
多态机制
- OOP:运行时通过虚函数表动态绑定
- 泛型:编译时通过模板生成类型特化代码
-
设计哲学
-
性能对比(C++示例)
// OOP方式(运行时开销) void draw(vector<Shape*>& shapes) { for(auto* s : shapes) s->draw(); // 虚函数调用 } // 泛型方式(编译期优化) template<typename T> void draw_all(const vector<T>& shapes) { for(auto& s : shapes) s.draw(); // 静态绑定 }
混合使用场景:
现代C++常结合两种范式:
// 泛型容器+OOP多态
vector<unique_ptr<Animal>> animals;
animals.emplace_back(new Dog());
建议根据项目需求选择:
- 需要运行时灵活性 → OOP
- 需要极致性能/数学计算 → 泛型编程
GP是把数据和方法分开,这样做的好处:
- 容器和算法可以闭门造车,通过迭代器产生关联
- 容器和算法的团队就可以各自闭门造车,其间通过 Iterator 联通即可
- 算法通过 Iterator 确定操作范围,并通过 Iterator 取用容器的元素
- 所有的算法,其内的最终涉及元素的操作都是比大小
使用容器指定比较方法时用仿函数,使用算法指定比较方法时使用函数
容器
序列式容器:按照放入的次序进行排列
关联式容器:有 key 和 value,适合快速查找
C++强制类型转换
static_cast<type>
是 C++ 提供的类型安全的强制类型转换方式,用于在编译时进行类型检查,比 C 风格的强制转换 (type)
更安全。
1. static_cast
的作用
const string* sa = static_cast<const string*>(a);
- 功能:将
void*
指针a
安全地转换为const string*
类型。 - 适用场景:
- 基本数据类型之间的转换(如
int
→double
) - 父类指针/引用与子类指针/引用之间的转换(需有继承关系)
void*
与其他指针类型的互转
- 基本数据类型之间的转换(如
2. 对比 C 风格强制转换
特性 | static_cast | C 风格转换 (type) |
---|---|---|
类型安全检查 | 编译时检查(更安全) | 无检查(可能运行时出错) |
可读性 | 明确标识转换意图 | 语法简单但意图不明确 |
适用范围 | 有限制(如不能去掉 const ) | 无限制(强转一切类型) |
C++ 标准推荐 | ✅ 推荐使用 | ❌ 避免使用 |
3. 其他 C++ 类型转换操作符
操作符 | 用途 |
---|---|
dynamic_cast | 运行时多态类型转换(需虚函数,失败返回 nullptr 或抛异常) |
const_cast | 添加/移除 const 或 volatile 属性 |
reinterpret_cast | 低级别指针类型转换(如 int* → char* ,不检查安全性) |
移动迭代器的位置
在 C++ 中,如果想移动迭代器的位置(特别是像 std::list
这样的双向迭代器或随机访问迭代器),可以使用 std::advance
函数,也可以直接使用 +
运算符(如果迭代器支持随机访问,如 std::vector
或 std::deque
的迭代器)。
1. std::advance
函数
std::advance
是 <iterator>
头文件提供的通用方法,适用于所有类型的迭代器:
- 双向迭代器(如
std::list
、std::set
、std::map
):只能逐个移动(++
或--
)。 - 随机访问迭代器(如
std::vector
、std::deque
、std::array
):可以直接+n
或-n
跳转。
语法
#include <iterator>
std::advance(iterator, n); // 将迭代器移动 n 步
n > 0
:向前移动n < 0
:向后移动(仅双向或随机访问迭代器支持)
示例
#include <iostream>
#include <list>
#include <iterator>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin(); // 指向 1
std::advance(it, 2); // 移动 2 步,现在指向 3
std::cout << *it << std::endl; // 输出 3
std::advance(it, -1); // 移动 -1 步,现在指向 2
std::cout << *it << std::endl; // 输出 2
return 0;
}
2. 直接 +n
(仅随机访问迭代器)
对于 std::vector
、std::deque
、std::array
等随机访问迭代器,可以直接用 +
或 -
运算:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
auto it = vec.begin(); // 指向 10
it = it + 3; // 直接跳转到第 4 个元素(40)
std::cout << *it << std::endl; // 输出 40
it = it - 2; // 回退 2 步,指向 20
std::cout << *it << std::endl; // 输出 20
return 0;
}
3. std::list
例子*
std::list
是双向迭代器,不能直接 +n
,必须用 std::advance
:
std::list<std::string> c = {"a", "b", "c", "d"};
auto it = c.begin(); // 指向 "a"
// 移动到中间位置
std::advance(it, c.size() / 2); // 现在指向 "c"
// 在中间插入另一个 list
std::list<std::string> d = {"x", "y", "z"};
c.splice(it, d); // 插入后 c = {"a", "b", "x", "y", "z", "c", "d"}
4. 总结
方法 | 适用迭代器 | 示例 |
---|---|---|
std::advance(it, n) | 所有迭代器(推荐通用方法) | std::advance(it, 2) |
it + n / it - n | 仅随机访问迭代器(vector 、deque 、array ) | it = it + 3 |
++it / --it | 所有迭代器(但只能单步移动) | ++it |
如何选择?
std::list
、std::set
、std::map
:用std::advance
。std::vector
、std::deque
:可以直接+n
或-n
,也可以用std::advance
(但前者更直观)。
remove_if
函数
要在 std::list
中使用 remove_if
移除所有小于 100 的元素,需要定义一个谓词(Predicate),它是一个返回 bool
的函数或 lambda 表达式,用于判断哪些元素应该被移除。
c.remove_if([](const string &s)
{ return stoi(s) < 100; });
for (auto elem : c){
cout << elem << " ";
}
关键点说明
-
remove_if
的用法remove_if
接受一个谓词(Predicate),通常是 lambda 表达式或函数对象。- 它会遍历列表,移除所有使谓词返回
true
的元素。
-
Lambda 表达式
[](const string& s) { return stoi(s) < 100; }
[](const string& s)
:捕获列表为空,参数是const string&
(列表元素)。stoi(s)
:将字符串转为整数。return stoi(s) < 100;
:如果数值小于 100,返回true
,该元素会被移除。
-
stoi
函数stoi
(string to int
)是 C++11 引入的字符串转整数的函数。- 如果字符串不能转换为整数(如
"abc"
),会抛出std::invalid_argument
异常。
总结
remove_if
适用于std::list
、std::forward_list
等链表结构,比std::vector
的erase + remove
组合更高效。- Lambda 表达式 可以方便地定义移除条件。
stoi
用于字符串转整数,但要注意异常处理(如果输入可能包含非数字字符串)。
forward_list
std::forward_list
是 C++11 引入的单向链表,它没有 push_back()
函数,因为它只支持从前向后遍历,无法高效地在尾部插入元素(每次插入都需要遍历整个链表,时间复杂度 O(n))。
std::forward_list
的特性
- 单向链表:每个节点只存储下一个节点的指针(
std::list
是双向链表,支持push_back
和push_front
)。 - 没有
size()
函数:因为计算长度需要遍历整个链表(C++11 的设计选择,避免性能陷阱)。 - 插入/删除操作更高效:相比
std::list
,内存占用更小(少一个指针),但功能更受限。
std::forward_list
的替代操作
既然没有 push_back()
,你可以:
- 用
push_front()
在头部插入(O(1) 时间复杂度)。 - 用
insert_after()
在指定位置后插入(需要先找到前驱节点)。 - 手动维护尾指针(如果需要频繁尾部插入)。
auto tail = flist2.before_begin(); // 尾指针初始化为“虚拟头节点”
tail = flist2.insert_after(tail, elem); //在尾部插入元素elem
示例代码
#include <iostream>
#include <forward_list>
using namespace std;
int main() {
forward_list<int> flist = {1, 2, 3};
// 1. 使用 push_front() 在头部插入
flist.push_front(0); // flist = {0, 1, 2, 3}
// 2. 使用 insert_after() 在指定位置后插入
auto it = flist.begin(); // 指向 0
advance(it, 2); // 移动到 2
flist.insert_after(it, 99); // flist = {0, 1, 2, 99, 3}
// 3. 手动维护尾指针(如果需要尾部插入)
forward_list<int> flist2;
auto tail = flist2.before_begin(); // 尾指针初始化为“虚拟头节点”
for (int i = 0; i < 5; ++i) {
tail = flist2.insert_after(tail, i); // 在尾部插入
}
// flist2 = {0, 1, 2, 3, 4}
// 打印结果
for (int x : flist) cout << x << " "; // 输出: 0 1 2 99 3
cout << endl;
for (int x : flist2) cout << x << " "; // 输出: 0 1 2 3 4
return 0;
}
为什么 std::forward_list
没有 push_back()
?
- 性能问题:单向链表无法直接访问尾部,每次
push_back()
都需要遍历整个链表(O(n) 时间),违背了链表的优势。 - 设计哲学:
std::forward_list
是为极致性能场景设计的(如嵌入式系统、高频交易),牺牲便利性换取内存和速度优势。
应该选择 std::list
还是 std::forward_list
?
特性 | std::list (双向链表) | std::forward_list (单向链表) |
---|---|---|
内存占用 | 每个节点 2 个指针(prev/next) | 每个节点 1 个指针(next) |
支持 push_back | ✅ 有 | ❌ 无 |
支持 size() | ✅ 有 | ❌ 无 |
插入/删除速度 | 略慢(多一个指针操作) | 更快 |
适用场景 | 需要双向遍历或尾部操作 | 只需要单向遍历,追求极致性能 |
总结
std::forward_list
没有push_back()
,因为它无法高效支持尾部操作。- 替代方案:
- 用
push_front()
+reverse()
(如果需要顺序一致)。 - 用
insert_after()
在指定位置插入。 - 手动维护尾指针(适合频繁尾部插入)。
- 用
- 如果需要尾部操作,优先用
std::list
(除非内存/性能极其敏感)。
push_front 与 emplace_front
在 C++ 中,std::deque
(双端队列)的 push_front
和 emplace_front
都是用于在头部插入元素的成员函数,但它们的底层机制和使用方式有重要区别:
1. push_front
功能
- 将已构造的对象(或临时对象)拷贝/移动到
deque
的头部。 - 适用于已有对象的情况。
语法
void push_front(const T& value); // 拷贝插入
void push_front(T&& value); // 移动插入(C++11 起)
示例
#include <deque>
#include <string>
int main() {
std::deque<std::string> dq;
std::string s = "Hello";
dq.push_front(s); // 拷贝构造(s 仍有效)
dq.push_front(std::move(s)); // 移动构造(s 可能被置空)
dq.push_front("World"); // 隐式构造临时对象,再移动插入
}
底层行为
- 如果传递的是左值(如变量
s
),会调用拷贝构造函数。 - 如果传递的是右值(如
std::move(s)
或临时对象),会调用移动构造函数。 - 可能伴随一次额外的对象构造(如传递字符串字面量
"World"
时,会先构造临时std::string
)。
2. emplace_front
功能
- 直接在
deque
的内存中构造对象(避免拷贝或移动)。 - 适用于直接传递构造参数的情况,效率更高(C++11 引入)。
语法
template <class... Args>
reference emplace_front(Args&&... args); // 返回新元素的引用
示例
#include <deque>
#include <string>
int main() {
std::deque<std::string> dq;
dq.emplace_front("Hello"); // 直接在 deque 头部构造 std::string
dq.emplace_front(3, 'A'); // 构造 std::string(3, 'A') -> "AAA"
}
底层行为
- 直接在
deque
的头部内存中调用构造函数,参数args
直接传递给元素的构造函数。 - 没有临时对象的构造和拷贝/移动操作,性能更高。
3. 关键区别
特性 | push_front | emplace_front |
---|---|---|
参数类型 | 接受对象(T 或 T&& ) | 接受构造参数(Args&&... ) |
构造方式 | 先构造对象,再拷贝/移动到容器 | 直接在容器内存中构造对象 |
性能 | 可能有额外拷贝/移动 | 更高效(省去临时对象步骤) |
适用场景 | 已有对象需要插入 | 直接通过参数构造新对象 |
C++ 标准 | C++98 起支持 | C++11 起支持 |
4. 何时用哪个?
-
用
push_front
:- 当你要插入一个已经存在的对象(如变量)。
std::string s = "Hello"; dq.push_front(s); // 拷贝插入 dq.push_front(std::move(s)); // 移动插入
-
用
emplace_front
:- 当你想直接构造对象(避免临时对象开销)。
dq.emplace_front("Hello"); // 直接构造 std::string dq.emplace_front(3, 'A'); // 构造 std::string(3, 'A')
5. 性能对比示例
#include <deque>
#include <string>
#include <iostream>
class MyString {
public:
MyString(const char* s) {
std::cout << "构造: " << s << std::endl;
}
MyString(const MyString&) {
std::cout << "拷贝构造" << std::endl;
}
MyString(MyString&&) {
std::cout << "移动构造" << std::endl;
}
};
int main() {
std::deque<MyString> dq;
std::cout << "--- push_front ---" << std::endl;
dq.push_front("Hello"); // 1. 构造临时对象,2. 移动构造到容器
std::cout << "--- emplace_front ---" << std::endl;
dq.emplace_front("World"); // 直接构造
}
输出
--- push_front ---
构造: Hello
移动构造
--- emplace_front ---
构造: World
push_front
多了一次移动构造。emplace_front
只有一次直接构造。
6. 总结
操作 | 推荐场景 |
---|---|
push_front | 插入已有对象(变量或临时对象) |
emplace_front | 直接通过参数构造新对象(性能更优) |
在 C++11 及以后的代码中,优先使用 emplace_front
,除非你需要显式控制拷贝/移动语义。