【C++】STL标准库和泛型编程 | 深度解析容器、迭代器

STL 介绍

STL 组件关系如下图所示。

容器 Containers
迭代器 Iterators
容器适配器 Container Adapters
分配器 Allocator
算法 Algorithms
仿函数 Functors
仿函数适配器 Functor Adapters
迭代器适配器 Iterator Adapters

先来看一个示例。

#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;
}

关键组件说明:

  1. 容器
    首先是创建一个 containervector
    序列容器,此处使用原生数组区间构造:[ia, ia+6)
  2. 分配器
    allocator 来帮助 container 来分配内存(一般会忽略不写)
  3. 算法
    用一个 Algorithm 来操作数据( count_if 是数出满足条件的个数)
  4. 迭代器
    iterator 就是一个泛化的指针,来告诉 Algorithm 要处理哪里的数据
  5. 仿函数
    用一个 functor 来判断数据( less 其有两个参数传入,第一个 & lt ; 第二个就为真)
  6. 适配器
    先用一个 function adapterbind2nd )绑定了第二个参数为 40 40 40 ;再用一个 function adapternot1 )来对整个判断结果进行否定

判断条件 谓词(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#/PythonC++模板/Java泛型/Rust Traits
设计模式关联工厂模式、策略模式、观察者模式策略模式、标签分发、CRTP(奇异递归模板模式)
内存管理常伴随对象生命周期管理(构造/析构)依赖值语义或显式内存操作(如allocator)

关键差异说明:

  1. 多态机制

    • OOP:运行时通过虚函数表动态绑定
    • 泛型:编译时通过模板生成类型特化代码
  2. 设计哲学

    强调
    强调
    OOP
    数据抽象
    GP
    算法抽象
  3. 性能对比(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* 类型。
  • 适用场景
    • 基本数据类型之间的转换(如 intdouble
    • 父类指针/引用与子类指针/引用之间的转换(需有继承关系)
    • void* 与其他指针类型的互转

2. 对比 C 风格强制转换

特性static_castC 风格转换 (type)
类型安全检查编译时检查(更安全)无检查(可能运行时出错)
可读性明确标识转换意图语法简单但意图不明确
适用范围有限制(如不能去掉 const无限制(强转一切类型)
C++ 标准推荐✅ 推荐使用❌ 避免使用

3. 其他 C++ 类型转换操作符

操作符用途
dynamic_cast运行时多态类型转换(需虚函数,失败返回 nullptr 或抛异常)
const_cast添加/移除 constvolatile 属性
reinterpret_cast低级别指针类型转换(如 int*char*,不检查安全性)

移动迭代器的位置

在 C++ 中,如果想移动迭代器的位置(特别是像 std::list 这样的双向迭代器随机访问迭代器),可以使用 std::advance 函数,也可以直接使用 + 运算符(如果迭代器支持随机访问,如 std::vectorstd::deque 的迭代器)。


1. std::advance 函数

std::advance<iterator> 头文件提供的通用方法,适用于所有类型的迭代器:

  • 双向迭代器(如 std::liststd::setstd::map):只能逐个移动(++--)。
  • 随机访问迭代器(如 std::vectorstd::dequestd::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::vectorstd::dequestd::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仅随机访问迭代器(vectordequearrayit = it + 3
++it / --it所有迭代器(但只能单步移动)++it

如何选择?

  • std::liststd::setstd::map:用 std::advance
  • std::vectorstd::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 << " ";
}

关键点说明

  1. remove_if 的用法

    • remove_if 接受一个谓词(Predicate),通常是 lambda 表达式或函数对象。
    • 它会遍历列表,移除所有使谓词返回 true 的元素。
  2. Lambda 表达式

    [](const string& s) { return stoi(s) < 100; }
    
    • [](const string& s):捕获列表为空,参数是 const string&(列表元素)。
    • stoi(s):将字符串转为整数。
    • return stoi(s) < 100;:如果数值小于 100,返回 true,该元素会被移除。
  3. stoi 函数

    • stoistring to int)是 C++11 引入的字符串转整数的函数。
    • 如果字符串不能转换为整数(如 "abc"),会抛出 std::invalid_argument 异常。

总结

  • remove_if 适用于 std::liststd::forward_list 等链表结构,比 std::vectorerase + remove 组合更高效。
  • Lambda 表达式 可以方便地定义移除条件。
  • stoi 用于字符串转整数,但要注意异常处理(如果输入可能包含非数字字符串)。

forward_list

std::forward_list 是 C++11 引入的单向链表,它没有 push_back() 函数,因为它只支持从前向后遍历,无法高效地在尾部插入元素(每次插入都需要遍历整个链表,时间复杂度 O(n))。


std::forward_list 的特性

  1. 单向链表:每个节点只存储下一个节点的指针(std::list 是双向链表,支持 push_backpush_front)。
  2. 没有 size() 函数:因为计算长度需要遍历整个链表(C++11 的设计选择,避免性能陷阱)。
  3. 插入/删除操作更高效:相比 std::list,内存占用更小(少一个指针),但功能更受限。

std::forward_list 的替代操作

既然没有 push_back(),你可以:

  1. push_front() 在头部插入(O(1) 时间复杂度)。
  2. insert_after() 在指定位置后插入(需要先找到前驱节点)。
  3. 手动维护尾指针(如果需要频繁尾部插入)。
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()

  1. 性能问题:单向链表无法直接访问尾部,每次 push_back() 都需要遍历整个链表(O(n) 时间),违背了链表的优势。
  2. 设计哲学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_frontemplace_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");     // 隐式构造临时对象,再移动插入
}

底层行为

  1. 如果传递的是左值(如变量 s),会调用拷贝构造函数
  2. 如果传递的是右值(如 std::move(s) 或临时对象),会调用移动构造函数
  3. 可能伴随一次额外的对象构造(如传递字符串字面量 "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"
}

底层行为

  1. 直接在 deque 的头部内存中调用构造函数,参数 args 直接传递给元素的构造函数。
  2. 没有临时对象的构造和拷贝/移动操作,性能更高。

3. 关键区别

特性push_frontemplace_front
参数类型接受对象(TT&&接受构造参数(Args&&...
构造方式先构造对象,再拷贝/移动到容器直接在容器内存中构造对象
性能可能有额外拷贝/移动更高效(省去临时对象步骤)
适用场景已有对象需要插入直接通过参数构造新对象
C++ 标准C++98 起支持C++11 起支持

4. 何时用哪个?

  1. push_front

    • 当你要插入一个已经存在的对象(如变量)。
    std::string s = "Hello";
    dq.push_front(s);      // 拷贝插入
    dq.push_front(std::move(s)); // 移动插入
    
  2. 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,除非你需要显式控制拷贝/移动语义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清流君

感恩有您,共创未来,愿美好常伴

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值