12. 标准模板库STL

125 阅读37分钟

智能指针

智能指针能够自动管理内存,用于避免内存泄漏、野指针和悬空指针。

#include <memory>// 智能指针是使用智能指针类模板创建,
// 其使用方法就是实例化一个参数类型的类模板
// 可以使用隐式调用构造函数将其实例化// 三种指针用法相同
// 智能指针名<类型(基本类型或自建类)名> 指针名(new 类型名(初始值));  // 类模板实例化 
// 类型名 指针名 = new 类型名(初始值);  // new指针auto_ptr<double> pd(new double);
auto_ptr<string> ps(new string("a string"));  // 声明了一个auto_ptr<string>并初始化unique_ptr<int> pi(new int);
unique_ptr<string> pi(new string("a string"));
​
shared_ptr<int> pi(new int);
shared_ptr<string> pi(new string("a string"));
​
​

new double是使用new分配一个堆区的动态内存,其返回值是一个指针,指向新分配的内存块,将其作为构造函数auto_ptr<double>的参数,即对应原型中形参p的实参。

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数。因此,不会自动(隐式的)将指针转换为智能指针对象。

shared_ptr<double> pd;
double * p_reg = new double;
pd = p_reg;  // 不被允许,构造函数不能被隐式调用
pd = shared_ptr<double>(p_reg);  // 允许,显式调用
shared_ptr<double> pshared = p_reg;  // 不被允许,隐式
shared_ptr<double> pshared(p_reg);  // 允许,显式
  1. 为什么智能指针可以像普通指针那样使用 因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象。

  2. 智能指针三个常用方法(模板类的方法函数)

    1. .get()

      获取指针托管的指针地址

      auto_ptr<Test> test(new Test);
      ​
      Test *tmp = test.get();     // 获取指针返回
      
    2. .release()

      取消智能指针对内存的托管,其会返回原指针托管的地址。

      // 定义智能指针
      auto_ptr<Test> test(new Test);
      ​
      Test *tmp2 = test.release();    // 取消智能指针对动态内存的托管
      delete tmp2;    // 之前分配的内存需要自己手动释放
      
    3. .reset()

      重置或更新指针托管的地址,原托管地址会被释放。

      // 定义智能指针
      auto_ptr<Test> test(new Test);
      ​
      test.reset();           // 释放掉智能指针托管的指针内存,并将其置NULL
      ​
      test.reset(new Test()); // 释放掉智能指针托管的指针内存,并将参数指针取代之
      
  3. 如何选择智能指针?

    1. 如果需要多个指针共同指向同一个对象,则使用shared_ptr
    2. 如果不需要多个指向一个,或者需要创建一个管理动态数组的指针(即数组指针)时,使用unique_ptr

auto_ptr

auto_ptr<double> pd(new double);
auto_ptr<string> ps(new string("a string")); 

其构造函数:

template<class X>
class auto_ptr
{
public:
    explicit auto_ptr(X *p = 0) throw();  // 参数为一个指针 
    ...;
}
  1. 为什么弃用auto_ptr<T>?

    auto_ptr<double> pd1(new double(12.21));
    auto_ptr<double> pd2;
    pd2 = pd1;
    
    1. 所有权转移语义不明确

      auto_ptr的复制和赋值都会转移指针的所有权。 这可能会导致意外的行为或者资源泄漏,比如有两个auto指针pd1和pd2,将pd2赋给pd1,pd1自身管理的内存将会被释放。

    2. 不适用于容器和STL算法

      容器内元素必须支持可赋值和可复制。

    3. 无法正确管理动态数组

      auto_ptr<int[]> array(new int[5]);  // 不能这样定义
      

      其构造函数没有传递new[]作为参数的重载类型,且析构函数使用的delete而不是delete[]

unique_ptr

unique:唯一的,独有的

所有权智能指针,对于特定的对象,只能有一个智能指针可拥有它。

相对于auto_ptr的特性:

  • 禁止左值直接赋值,体现所有权不能通过赋值直接转移。
  • 允许临时右值赋值,允许临时右值对象转移。
  • 允许通过**move()函数**改变所有权,这是移动构造函数。
  • 能够用于动态数组
unique_ptr<string> demo(const char *s)
{
    unique_ptr<string> temp(new string(s));
    return temp;
}
​
unique_ptr<string> ps1, ps2;
ps1 = demo("Uniquely special");  // 允许临时右值赋值
ps2 = ps1;  // 不被允许,禁止左值使用赋值运算符直接赋值
ps2 = move(ps1);  // 允许通过move函数转移所有权, ps1 将被释放
ps1 = demo("and more");  // 再赋值// 尝试创建一个名为 pda 的 unique_ptr 对象,用于管理动态分配的 double 类型数组,数组中有五个double元素。
unique_ptr<double[]> pda(new double[5]);   // 允许管理动态数组,当过期式调用delete[]

shared_ptr

shared:分享

引用计数智能指针,将自动跟踪引用的特定对象的智能指针数。当赋值或复制时,计数+1,当指针过期时,计数-1。如果计数为0,表示没有智能指针指向这块内存,才调用delete

适用于多个指针共同指向同一块内存的情况。

.use_count()方法将返回shared_ptr指针的当前引用计数。

shared_ptr<double> spd1(new double(5.2));  // 创建一个智能指针。
shared_ptr<double> spd2(new double);  
​
spd2 = spd1;  // 将spd1共享给spd2,将引用计数加一
shared_ptr<double> sp3(spd1);  // 复制构造函数,计数加一
​
cout << sp1.use_count() << endl;  // 输出值应为3。

weak_ptr

weak:弱指针

用以协助共享智能指针shared_ptr工作,将weak_ptr或shared_ptr复制或赋值给weak_ptr弱指针,不会增加shared_ptr的引用计数。 即其只提供了对shared_ptr管理对象的非拥有式访问。

  1. 为什么要有弱指针

    弱指针通常用于解决由于交叉引用而导致的内存泄漏问题。例如,在一个对象 A 中包含一个指向对象 B 的 shared_ptr,而对象 B 又包含一个指向对象 A 的 shared_ptr,这样就形成了交叉引用。在这种情况下,对象 A 和对象 B 会因为彼此之间仍然持有对方的引用而无法被释放,导致内存泄漏。

    class B;
    struct A{
        shared_ptr<B> pb_;
    };
    struct B{
        shared_ptr<A> pa_;
    };
    

    而弱指针提供一种非拥有式的访问方式,而不会增加所指向对象的引用计数,从而解决循环引用导致的内存泄漏问题。

    class B;
    struct A{
        weak_ptr<B> pb_weak;
    };
    struct B{
        shared_ptr<A> pa_;
    };
    
  2. 弱指针的创建

    // 三种定义方式
    // 空,从共享或弱指针赋值,从共享或弱指针复制shared_ptr<string> sp1(new string("a string1"));
        
    weak_ptr<string> wp1;  // 构造空弱指针
    weak_ptr<string> wp2(sp1);  // 使用共享指针构造
    wp1 = sp2;  // 使用共享指针赋值
  3. weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其.lock()方法来获得弱指针共享的shared_ptr实例,进而访问原始对象。

    shared_ptr<string> sp1;
    sp1 = wp1.lock();  // 返回共享指针
    
  4. 可以使用.use_count()方法获取共享指针的强指针数量。.expired()方法获取弱指针协助的共享指针是否已被释放。

    wp1.use_count();  // 返回现存共享指针数量
    wp1.expired();  // 返回true则共享指针已被释放,否则false
    

迭代器

  1. 啥叫迭代器

    可以将其看作是一个广义的指针,用于指向容器中的元素。对于容器类,迭代器可能是指针,也可能是对象,不管实现如何。迭代器都提供了所需的操作,如 ***** 和++。其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。每个容器类都有一个begin()和end方法。他们分别返回指向容器第一个元素和超尾位置的迭代器。每个容器类都能使用++操作,使得迭代器从第一个元素逐步指向超尾位置。

  2. 为什么要有迭代器

    模板使得算法独立于存储的数据类型,而迭代器使得算法独立于使用的容器类型,迭代器是为了算法操作容器而设立的。

  3. 怎样使用迭代器

    容器名<存储的数据类型>::iterator pr;  // 声明某个容器的迭代器// 声明ex:
    vector<double>::iterator pr;  // vector<double>的迭代器
    list<double>::iterator pr;  // list<double>的迭代器// 使用ex:
    vector<double>sources = {13.15, 15.85, 157.55};
    vector<double>::iterator pr = sources.begin();  // 声明并将sources的第一个元素赋给迭代器
    for(pr = sources.begin(); pr < sources.end(); pr++) {...;}  // 遍历容器
    for(auto ps : sources) {}  // 基于范围的for,auto自动推断ps的类型为迭代器// 可以将迭代器看作广义指针来使用
    // 即可以使用解除引用运算符*
    *pr;  // 解除当前迭代器指向元素地址的引用,即取值
    *(pr+10);
    pr++;  // 指针自增加
    *(pr++);  // 自增加后解除引用
  4. 迭代器类型是从什么角度定义的?(为什么迭代器的名称这么反直觉?)

    迭代器的类型是从算法的角度而言的。 迭代器不是为了容器而定义,而是为了算法操作容器而定义。以算法为主体。

    输入指算法的输入,算法的输入来自容器,所以输入迭代器就指对容器元素的读取。

    输出指算法的输出,算法的输出要写入容器,所以输出迭代器就指对容器元素的写入。

  5. 迭代器的单通行和多通行什么意思?

    通行也是指的算法,基于xx迭代器的算法是单/多通行的。

    1. 单通行迭代器:

      单通行迭代器(在本次遍历中)只允许对容器进行一次顺序遍历,遍历过程中不能多次使用迭代器。一旦迭代器进行了前进(递增)操作,就无法再次回到之前的位置。

    2. 多通行迭代器:

      多通行迭代器(在本次遍历中)允许对容器进行多次遍历,遍历过程中可以多次使用迭代器。即如果在前面保存了迭代器的值,那么仍然可以对前面的迭代器值接触引用,并得到相同的值。

    3. 总结:

      对于单通行迭代器,一旦进行了递增操作,之前保存的迭代器可能已经失效,不再指向原来的位置。因此,你不能保证保存下来的迭代器仍然指向原来的元素,也不能保证对其解引用得到的是原来的值。

      而对于多通行迭代器,保存下来的迭代器仍然有效,仍然指向原来的位置。即使进行了递增操作,保存下来的迭代器仍然指向原来的元素。因此,你可以保证保存下来的迭代器仍然指向原来的元素,对其解引用得到的是原来的值。

      什么意思?举个例子:当我使用迭代器遍历到容器中第三个元素时,我将这个迭代器保存下来,然后我执行了it++,迭代器走到了下一个元素的位置。

      如果是单通行迭代器,那么这个保存下来的迭代器值不一定还是刚刚保存下来的第三个元素,对其解引用也不一定是刚刚的值。

      而如果是多通行迭代器,这个值还是刚刚保存的第三个元素值,对其解除引用也是刚刚第三个元素的数据。

注意:下面的例子仅仅是为了展示特性,使用的迭代器不一定就是那一小节讲的迭代器,只是为了体现功能。

例如,std::vector 的迭代器类型是随机访问迭代器,而 std::list 的迭代器类型是双向迭代器。

迭代器功能输入输出正向双向随机访问
解除引用读取
解除引用写入
固定和可重复排序
++i, i++
--i, i--
i[n]
i + n
i - n
i += n
i -= n
  1. 固定排序:固定排序指的是排序算法在排序的过程中,能够保持相等元素之间的相对顺序不变。换句话说,如果两个元素在排序前是相等的,并且在输入序列中先出现的元素在排序后仍然排在后出现的元素的前面,那么这个排序算法就是稳定的。稳定性对于一些应用是非常重要的,因为它可以保证在排序后保持原有的顺序关系。
  2. 可重复排序:可重复排序指的是排序算法在对已经排好序的序列进行再次排序时,能够保持原来相等元素之间的相对顺序。换句话说,如果对已经排好序的序列再次进行排序,那么排序算法不应该改变相等元素之间的相对顺序。具有这种性质的排序算法可以保证在多次排序后,结果是相同的,不会因为排序的次序不同而得到不同的结果。

Q: 保持固定排序或可重复排序有什么用?

A: 举个例子,假设你有一个学生名单,每个学生有姓名和年龄两个属性。你想按照学生的年龄对名单进行排序,但是如果年龄相同的学生之间的顺序被打乱了,那么这个排序就不再是正确的了。在这种情况下,你希望排序算法能够保持年龄相同的学生之间的原始顺序,这样才能确保排序的正确性。

输入迭代器

用来读取容器中的元素的迭代器,不一定能让程序修改(写入)值。

    std::vector<int> numbers = {1, 2, 3, 4, 5};
​
    // 使用输入迭代器遍历容器  const_iterator
    std::cout << "Using input iterator:" << std::endl;
    for (std::vector<int>::const_iterator it = numbers.cbegin(); it != numbers.cend(); ++it) {
        std::cout << *it << " "; // 输出当前元素的值
        if (*it == 3) {
            // 修改容器中的元素(这里并不会成功)
            *it = 10;
        }
    }
  1. 用法:

    • 单向递增不可随机跳跃。it++, ++it
    • 解除引用*读取
  2. 注意:

    • 输入迭代器必须能够访问容器中所有的值。这是通过支持++(it++, ++it)运算符来实现的。
    • 任何基于输入迭代器的算法应是单通行的。不依赖前一次遍历的值,也不依赖本次遍历中前面的值。
    • 输入迭代器是单向迭代器。 只能递增(能且仅能++,不支持随机访问),不能递减。

输出迭代器

用来将信息从算法写入容器的迭代器,不能读取容器中元素。例如cout,只能向输出流写入数据,不能读取。用于不用管容器中有什么元素,只需要写入数据就好的算法。

std::vector<int> numbers = {1, 2, 3, 4, 5};
​
// 使用输出迭代器将数据写入到标准输出流 std::cout 中
std::cout << "Using output iterator:" << std::endl;
std::copy(numbers.begin(), numbers.end(), std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
​
// 输出:
// Using output iterator:
// 1 2 3 4 5
  1. 用法:

    • 单向递增不可随机跳跃。it++, ++it
    • 解除引用*写入
  2. 注意:

    • 单通行
    • 只写
    • 单向迭代

正向迭代器

和输入和输出迭代器类似,只能使用++遍历容器,但正向迭代器可读可写数据,且能够按照相同的顺序遍历一系列的值,即可以对前面迭代器的值接触引用,并且可以得到相同的值。

#include <iostream>
#include <vector>int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
​
    // 使用正向迭代器遍历容器
    std::cout << "Using forward iterator:" << std::endl;
    std::vector<int>::iterator save_it;  // 用于保存迭代器
    for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " "; // 输出当前元素的值
        if (*it == 3) {
            // 保存 it
            save_it = it;
        }
    }
    std::cout << std::endl;
    
    // 依旧可以输出正确的值
    std::cout << "saved it can use: " << *(save_it+1) << std::endl;
​
    return 0;
}
​
/*
Using forward iterator:
1 2 3 4 5
saved it can use: 4
*/
  1. 用法:

    • 单向递增不可随机跳跃。it++, ++it
    • 解除引用*读取
    • 解除引用*写入
  2. 注意:

    • 可读
    • 可写
    • 多通行,可对前面保存下来的迭代器值进行解引用,并且可以得到相同的值。

双向迭代器

具有正向迭代器的全部特性,且支持++和--。

    // 使用双向迭代器从前向后遍历容器
    std::cout << "Forward traversal:" << std::endl;
    for (std::list<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
​
    // 使用双向迭代器从后向前遍历容器
    std::cout << "Reverse traversal:" << std::endl;
    for (std::list<int>::reverse_iterator it = numbers.rbegin(); it != numbers.rend(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
​
/*
Forward traversal:
1 2 3 4 5
Reverse traversal:
5 4 3 2 1
*/
  1. 用法:

    • 可递增++、可递减--,不可跳跃
    • 解除引用*读取
    • 解除引用*写入
  2. 注意:

    • 可读
    • 可写
    • 多通行
    • 可增可减

随机访问迭代器

能够直接跳跃到容器中的任何一个元素,这叫随机访问。 具有此功能的迭代器叫随机访问迭代器。并且随机访问迭代器还可以实现两个指向同一个容器的迭代器相互操作(即指针算数:指向同一个数组的两指针,指针的数加,数减,指针相减(得到的是相差几个位置或索引),指针比较(比较的也是位置))。

std::vector<int> numbers = {1, 2, 3, 4, 5};
​
    // 使用随机访问迭代器访问容器中的元素
    std::cout << "Using random access iterator:" << std::endl;
​
    // 通过索引直接访问元素
    std::cout << "Element at index 2: " << numbers[2] << std::endl;
​
    // 使用迭代器进行遍历
    std::vector<int>::iterator it = numbers.begin();
    std::cout << "Element at index 2 (via iterator): " << *(it + 2) << std::endl;
​
    // 使用算术运算进行遍历
    std::cout << "Elements after index 2: ";
    for (std::vector<int>::iterator it = numbers.begin() + 2; it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
​
/*
Using random access iterator:
Element at index 2: 3
Element at index 2 (via iterator): 3
Elements after index 2: 3 4 5
*/
  1. 用法:

    • 可递增++、可递减--,可跳跃 it+,-,+=,-=n
    • 可算术运算,类似指针
    • 解除引用*读取
    • 解除引用*写入
  2. 注意:

    • 可读
    • 可写
    • 多通行
    • 可增可减
    • 可跳跃

迭代器操作:

a , b为迭代器值,n为整数,r为随即迭代器变量或其引用

表达式描述
a + n指向a所指向的元素后的第n个元素
n + a与 a + n 相同
a - n指向a所指向的元素前的第n个元素
r += nr = r + n
r -= nr = r - n
a[n]*(a + n)
b - a类似于指针相减,返回两个迭代器之间的间隔,是整数,n = b - a
a < b如果b - a > 0, 则为真,就是迭代器指向的位置在前在后
a > b如果 b < a则为真
a >= b如果!(a < b),则为真
a <= b如果!(b < a),则为真

Q: a + n与r += n有什么异同?

A: 都是将迭代器指向当前元素的后面第n个元素,但不同的是a+n是个临时值,其返回一个新的迭代器,r += n是在原迭代器上操作。

容器

本章内容参考知乎专栏(在ref可以找到)较多,尤其描述容器结构的图片,能够帮助容易理解。

  1. 容器是存储其他对象的对象。被存储的对象必须是同一种类型的,他们可以是OOP意义上的对象,也可以是内置类型值。

  2. 什么类型的值可以被存在容器中:需要这个类满足:

    • 可复制插入(复制、赋值)

      在可复制插入中,将一个已经存在的对象的副本(拷贝)插入到容器中。这意味着在插入操作时,会发生对象的拷贝构造或者赋值操作,复制原对象的值到容器中新创建的对象中。这种方式适用于对象的拷贝代价较低,或者对象是不可移动的情况。

    • 可移动插入(移动)

      在可移动插入中,通过移动语义将一个对象移动到容器中。这意味着在插入操作时,对象的资源所有权从源对象转移到容器中的新对象,而不涉及对象值的复制操作。这种方式适用于对象的拷贝代价较高,或者对象可以被移动但不可复制的情况。

      移动语义:移动语义通过将资源的所有权从一个对象转移到另一个对象来实现,而不是通过复制资源的内容。可以通过移动构造函数(移动构造与复制、赋值操作的区别在于,后者是复制,保留源对象,前者可能修改源对象,还可能转让所有权,而不是复制。)实现。

  3. STL算法是操作容器的的方法:

    • 都使用模板来提供泛型
    • 都使用迭代器来访问容器中数据的通用表示。
  4. 容器的基本特征,(容器必备的操作,但不一定适合适配器):

    X表示容器类型,如vector<T>;T代表存储在容器中的对象类型;a和b都是类型为X的值;r表示类型为X&的值;u表示类型为X的标识符(如果X表示vector<int>,则u是一个vector<int>对象。就是变量名 int a = 5的a);rv表示X类型的非常量右值,如函数返回值。

    表达式返回类型说明复杂度
    X::iterator指向T的迭代器类型满足正向迭代器要求的任何迭代器编译时间
    X::value_typeT返回存储对象T的类型编译时间
    X u;创建了一个名为u的空容器固定
    X();创造了一个匿名的空容器固定
    X u(a);调用X容器的复制构造函数后 将a复制到u线性
    X u = a;拷贝初始化,调用复制构造函数,作用等同于X u(a);线性
    r = a;X&将a赋给r,调用赋值运算符。线性
    (&a)->~X()void对容器中每个元素都应用析构函数。线性
    a.begin()迭代器返回指向容器中第一个元素的迭代器固定
    a.end()迭代器返回超尾值迭代器固定
    a.size()无符号整型返回元素个数,等价于a.end() - a.begin()固定
    a.swap(b)void交换容器a和b中的类型固定
    a == b可转换为bool如果a和b的长度相同,且a中每个元素都等于(==)中相应的元素,则为真线性
    a != b可转换为bool返回!(a == b),即如果两个相等则为false,否则为真线性
    C++11新增:
    X u(rv);调用移动构造函数后,将rv的所有权交给u线性
    X u = rv;同X u(rv);线性
    u = rv;X&调用移动赋值运算符后,将rv的所有权交给u线性
    a.cbegin()const_iterator返回一个指向容器第一个元素的const迭代器,const指只能读取固定
    a.cend()const_iterator返回一个指向容器超尾的const迭代器固定

    复杂度:

    • 编译时间:操作在编译时进行,执行时间为0
    • 固定时间:操作发生在运行阶段,单独立于对象中的数目个数。
    • 线性时间:操作发生在运行阶段,且时间与元素数目成正比。

顺序性容器(序列容器)

  1. 什么叫顺序性容器?

    1. 顺序是指元素插入的顺序。如果元素的顺序与插入的顺序一致,那么当遍历容器时,会按照插入的顺序依次访问每个元素,这样容器称为顺序性容器。
    2. 顺序性容器在内存中不一定是顺序内存存储的。如std::vector<>其内存存储是连续的,这样支持高效随机访问,deque内部通常是由多个连续的块组成,每个块都是一个数组,元素在块内是连续存储的,但不同块之间并不一定是连续的。list其中的元素并不存储在连续的内存区域中,而是通过指针连接在一起的。
  2. 顺序性容器基本方法

    t表示类型为T的值,n表示整数,pqij均为迭代器。

    为什么都是前闭后开?因为超尾。

    表达式返回类型说明
    X a(n, t);声明一个由n个t值组成的序列
    X(n, t);匿名的
    X a(i, j)声明一个名为a的序列,并将其初始化为区间[i, j)的内容
    X(i, j)匿名的
    a.insert(p, t)迭代器将t插入到p的前面
    a.insert(p, n, t)void将n个t插到p的前面
    a.insert(p, i, j)void将区间[i, j)中的元素插入到p的前面
    a.erase(p)迭代器删除p指向的元素
    a.erase(p, q)迭代器删除区间[p, q)之间的元素
    a.clear()void等价a.erase(a.begin(), a.end()),清空容器。

    总结:基本方法其实就四种:

    • 创建容器X a(n, t)X a(i, j)

      创建容器又分为创建命名的和匿名的容器。可以按一个值初始化或者按区间初始化

    • 容器插值 .insert()

      插单值、重复单值、区间

    • 容器删值.erase()

      删单值、区间

    • 容器清空.clear

      清空容器

vector

#include <vector>  // 包含vector头文件

img

vector 是数组的一种类表示,它提供了

  • 自动内存管理,可以动态改变vector对象的长度。
  • []对元素的随机访问。
  • 在尾部增删元素的时间复杂度是固定时间,在头部或者中间增删的时间复杂度为线性时间。
  • push_back(const T& value)pop_back(),在尾部增加或弹出一个元素。
  • .front().back(),返回对第一个和最后一个元素的引用
  • .capacity(),容量。返回为当前vector分配的存储空间大小。
  • at(size_type pos): 返回向量中指定位置的元素,带有边界检查
  • operator[](size_type pos): 返回向量中指定位置的元素,不进行边界检查。
  • .rbegin().rend()返回反向迭代器(reverse_iterator)分别指向反转序列的第一个元素和超尾,也有对应的常量迭代器方法.crbegin().crend()。对reverse_iterator++将导致反向遍历。
heap
#include <queue>

堆,一种数据结构,底层使用vector容器的适配器

priority_queue
#include <queue>

优先队列,基于heap堆数据结构实现。最大的元素会被移动到队首。

方法:

  • 不支持随机访问。
  • .empty(),查看优先队列是否为空。
  • .size(),返回队列中元素数目。
  • .front().back(),返回对第一个和最后一个元素的引用
  • .push(const T& value).pop(),在队尾部插入和弹出。不能操作队首。
  • emplace(Args&&... args): 使用参数 args 在队列的尾部构造一个新的元素。

deque

#include <deque>

img

双端队列(double-ended queue) ,支持随机访问,与vector容器的主要区别在于从deque对象的开始位置插入和删除元素的时间是固定的。但在中部增删还是线性时间。

方法:

  • 元素的随机访问
  • 在头、尾增删元素时间固定,在中间为线性时间
  • .front().back(),返回对第一个和最后一个元素的引用
  • .push_back(const T& value).pop_back(),在尾部增加或弹出一个元素。
  • .push_front(t)``.pop_front(),在头部增加或弹出一个元素。
  • .emplace_back(Args&&... args): 使用参数 args 在双端队列的尾部构造一个新的元素。
  • .emplace_front(Args&&... args): 使用参数 args 在双端队列的头部构造一个新的元素。
  • .at(size_type pos): 返回向量中指定位置的元素,带有边界检查。
  • .operator[](size_type pos): 返回向量中指定位置的元素,不进行边界检查。

.emplace_back(t).push_back(t)的区别:

std::deque<int> vec;
​
// 使用 push_back 添加新元素
vec.push_back(42);
​
// 使用 emplace_back 在尾部构造新元素
vec.emplace_back(42);
​
/*
这两个调用的结果是相同的,都会将值为 42 的新元素添加到容器的尾部。
但是,emplace_back 可能更加高效,因为它可以直接在尾部构造新元素,
而不需要创建一个临时的 int 对象并将其拷贝到容器中。
*/
stack
#include <stack>

img

栈,一种后进先出(LIFO)数据结构,其中元素仅从容器的一端插入和提取。默认基于双端队列deque实现。

方法:

  • empty(),判断栈是否为空
  • size(),返回栈中元素数目
  • push(t),元素压栈,将元素压入顶部。
  • .pop(),弹出栈顶结构
  • .top(),返回栈顶元素的引用
  • .emplace(Args&&... args): 使用参数 args 在栈的顶部构造一个新的元素。
queue
#include <queue>

img

队列,一种先入先出(FIFO)的数据结构,其中元素插入到容器的一端并从另一端提取。默认使用双端队列实现。

方法和优先队列相同:

  • 不支持随机访问。
  • .empty(),查看优先队列是否为空。
  • .size(),返回队列中元素数目。
  • .front().back(),返回对第一个和最后一个元素的引用
  • .push(const T& value).pop(),在队尾部插入和弹出。不能操作队首。
  • emplace(Args&&... args): 使用参数 args 在队列的尾部构造一个新的元素。

list

img

双向链表。除了第一个元素和最后一个元素外,每个元素都与前后的元素相链接,这意味着可以双向遍历。与vector的主要区别在于list在链表中任一位置进行插入和删除的时间都是固定的。

从list容器中插入或删除元素后,链表迭代器指向的元素将不变。 解释:比如vector,现在迭代器指向第五个元素,现在在头部插入了一个元素,那么现在迭代器还是指向第五个元素,但是第五个元素包含的指是之前第四个元素的值。而链表不会移动已有的元素,而只是修改链接信息,然后找地方新添加的元素,此时第五个元素依旧是原来的值。

方法:

  • 不能随机访问
  • 在任一位置增删元素复杂度都是固定时间
  • .push_back(t)``.pop_back(),尾部插入或删除元素
  • .push_front(t)``.pop_front(),头部插入或删除元素
  • emplace_back(Args&&... args): 使用参数 args 在链表的尾部构造一个新的元素。
  • emplace_front(Args&&... args): 使用参数 args 在链表的头部构造一个新的元素。
  • .merge(list<T> &x),将链表x与调用链表合并。两个链表必须已经排序,合并后的经过排序的链表保存在调用列表中,x为空,线性时间。
  • .remove(val),从链表中删除val的所有实例,线性时间。
  • sort(),对链表进行排序,N个元素的复杂度为NlogN。
  • .splice(iterator pos, list<T> x),将链表x的内容插入到pos前,x将为空
  • .unique(),将连续的相同元素压缩为单个元素,线性时间。
  • .reverse(),反转链表
  • .rbegin().rend()返回反向迭代器(reverse_iterator)分别指向反转序列的第一个元素和超尾,也有对应的常量迭代器方法.crbegin().crend()。对reverse_iterator++将导致反向遍历。

forward_list

#include <forward_list>

单链表,每个节点都只链接到下一个节点,不可反转。

方法:

  • .push_front(t)``.pop_front(),头部插入或删除元素
  • emplace_front(Args&&... args): 使用参数 args 在链表的头部构造一个新的元素。
  • .remove(val),从链表中删除val的所有实例
  • .sort(),对链表进行排序
  • insert_after(iterator pos, const T& value): 在指定位置之后插入一个元素。
  • emplace_after(iterator pos, Args&&... args): 使用参数 args 在指定位置之后构造一个新的元素。
  • erase_after(iterator pos): 删除指定位置之后的元素。

array

#include <array>

array是C++11中新增的容器,它与其他容器不同的是,它的大小是固定的,无法动态扩展或收缩,只允许访问或者替换存储的元素


关联性容器

  1. 什么叫关联性容器?

    关联容器将值与键关联到一起,并使用键来查找值。例如对于员工的管理,值可以表示雇员信息,比如姓名、地址等,而键可以是唯一的员工编号,可以通过键来查找雇员结构。X::value_type指出值类型,在关联容器中X::key_type将会指出键类型。

    关联容器支持插入元素,但是不能指定元素插入的位置。原因是关联容器的元素的顺序是按照某种排列规则来存储和访问的,以便实现对元素的快速访问。

set

#include <set>

集合,且是有序集合。有几个特征:

  • 值类型与键类型相同,且值就是键。
  • 键是唯一的。也就是说值也是唯一的,不能存储多个相同值。
  • 其中元素对按照某种排序方式存储。默认为从小到大。
  • multiset,与set类似,但允许存储重复元素。

方法或算法:

  • 方法:插入,遍历,查找,删除
  • .lower_bound()、.upper_bound()
  • 并交差集。函数set_union,set_intersection, set_difference
// 构造函数
set<T> my_set;  // 创建空容器,存储类型为T
set<T> my_set(other_container.begin(), other_container.begin() + n);  // 从其他迭代器区间构造函数
set<T> my_set(other_set);  // 复制构造函数// 插入,遍历,查找,删除
my_set.insert(10);  // 插入,且会自动排序
my_set.insert(A.begin(), A.end())
for(auto it = my_set.begin(); it != my_set.end(); ++it){...;}  
if(my_set.find(10) != my_set.end()){...;}
my_set.erase(10);
​
// 方法 lower_bound、upper_bound。二分查找,都返回一个迭代器,
// 指向集合中第一个大于等于键的成员,和指向第一个大于键的成员
​
std::set<int> mySet = {10, 20, 30, 40, 50};
​
// 使用 lower_bound 查找大于等于 25 的第一个元素
    auto lower = mySet.lower_bound(25);
​
// 使用 upper_bound 查找大于 25 的第一个元素
    auto upper = mySet.upper_bound(25);
​
/*
lower_bound(25) found: 30
upper_bound(25) found: 30
*/
​
​
// 算法函数,非方法。并集,交集, 差集
std::set<int> set1 = {1, 2, 3, 4, 5};
std::set<int> set2 = {3, 4, 5, 6, 7};
​
// 并集,第一个集合区间,第二个集合区间,插入迭代器模板
std::set<int> unionSet;
   std::set_union(set1.begin(), set1.end(), set2.begin(), set2.end(),
                  std::inserter(unionSet, unionSet.begin()));
​
// 交集,接口同并集
std::set<int> intersectionSet;
    std::set_intersection(set1.begin(), set1.end(), set2.begin(), set2.end(),
                          std::inserter(intersectionSet, intersectionSet.begin()));
​
// 集合差,第一个集合区间减去两集合区间都有的元素
std::set<int> differenceSet;
    std::set_difference(set1.begin(), set1.end(), set2.begin(), set2.end(), 
                        std::inserter(differenceSet, differenceSet.begin()));
/*
并集: 1 2 3 4 5 6 7
交集: 3 4 5
差集: 1 2
*/
​
​

pair

#include <map>

对,用于将两个值组成一个键值对。pair使用first和second两个公共成员变量存储两个值。

用法:

// 构造函数
std::pair<int, std::string> myPair;
pair<int, string> my_pair(10, "hello"); 
auto myPair = std::make_pair(10, "Hello");  // make_pair函数// 使用第一个值和第二个值
my_pair.first;  // 10
my_pair.second;  // hello

map

#include <map>

映射。将两种不同类型的变量联系联系起来,形成键值对,其元素类型实际是pair。可以通过键来查找值。与set容器一样,键也会按照一种可排序的方式存储,保持有序性。其主要特征:

  • map唯一键值对,multimap允许重复键值对。
  • 自动排序,按照键。
  • 键与值的类型可以不同。

方法或算法:

  • 键值对插入与访问
  • 遍历、查找、删除
  • multimap,计数,lower、upper, equal_range范围寻找
// map// 键值对的插入
std::map<std::string, int> myMap;
myMap["apple"] = 10;
myMap["banana"] = 20;
​
// 访问元素,通过键来取值
std::cout << "apple: " << myMap["apple"] << std::endl; // 输出:apple: 10// 遍历迭代
for (const auto& pair : myMap) 
{
    std::cout << pair.first << ": " << pair.second << std::endl;
}
​
// 查找
auto it = myMap.find("banana");
if (it != myMap.end()) 
{
    std::cout << "Found banana: " << it->second << std::endl;
}
// 删除
myMap.erase("banana");
​
​
// multimap// 插入
std::multimap<std::string, int> myMultimap;
myMultimap.insert(std::make_pair("apple", 10));
myMultimap.insert(std::make_pair("banana", 20));
myMultimap.insert(std::make_pair("apple", 30)); // 允许重复键值对// 计数
myMultimap.count("apple");  // 返回拥有该键的元素个数// lower_bound(key), upper_bound(key) 大于等于 和大于 返回指向指定键的第一个元素的迭代器
auto lower = myMultimap.lower_bound("apple");
if (lower != myMultimap.end()) 
{
   std::cout << "lower_bound("apple"): " << lower->second << std::endl;
}
​
auto upper = myMultimap.upper_bound("apple");
if (upper != myMultimap.end()) 
{
   std::cout << "upper_bound("apple"): " << upper->second << std::endl;
}
​
// equal_range:获取指定键的迭代器范围
// 返回的是一个pair,且pair内对象的类型为<iterator, iterator>
// 为什么可以用范围来表示呢?因为multimap已经自动排序,相同键的元素肯定在一起。
auto range = myMultimap.equal_range("apple");
std::cout << "Elements with key apple:" << std::endl;
for (auto it = range.first; it != range.second; ++it) 
{
   std::cout << it->second << std::endl;
}
​
// 遍历
for (const auto& pair : myMultimap) 
{
    std::cout << pair.first << ": " << pair.second << std::endl;
}
​
// 查找
// 此pair的类型为迭代器
auto range = myMultimap.equal_range("apple");
for (auto it = range.first; it != range.second; ++it) {
    std::cout << "Found apple: " << it->second << std::endl;
}
​
// 删除
auto it = myMultimap.find("banana");
if (it != myMultimap.end()) 
{
    myMultimap.erase(it);
}
​
​

函数对象

函数符

函数对象也叫函数符,指可以以函数方式与()结合使用的任意对象,是STL的概念。这包括:

  • 函数名
  • 函数指针
  • 重载了()的类对象。

根据传入参数的个数可以将函数符分为:

  • 生成器generator,不用参数就能调用的函数符
  • 一元函数,传入一个参数
  • 二元函数,传入两个参数

又有改进:

  • 返回bool值的一元函数是谓词
  • 返回bool值的二元函数是二元谓词。

lambda函数

[&count](int x){count += x %13 ==0;}
  1. 什么是lambda函数?

    lambda函数是一种匿名函数--无需给函数命名的。常用于STL算法中作函数谓词。

  2. 为什么要有lambda函数?

    1. 让函数靠近使用的地方一点
    2. 更简洁
    3. lambda函数可以访问作用域内的任何动态变量,要捕获要使用的变量,可将其名称放在中括号内。
  3. 如何使用lambda函数?

    1. 不用声明返回值,lambda函数会自动推断

      1. 如果语句只有一条,那么函数将使用decltype自动推断。没有return则推为void

        [](int x){return x % 3 == 0;}
        
      2. 如果语句不止一条,那么可以将函数的返回类型后置

        [](double x)->double {int y =x; return x - y;}
        
    2. 函数名使用[]替代--匿名函数

    3. 将作用域内的动态变量名放入中括号,可以进行捕获,以在函数体内使用。并且下面类型可以任意组合

      • [z]--按值访问z
      • [&z]--按引用访问z
      • [&]--按引用访问所有动态变量
      • [=]--按值访问所有动态变量
    4. 可按照常规函数一样传入参数。

STL算法

#include <algorithm> #include <numeric>  // 专用于数值数据

STL算法都是对区间内每个元素的操作,分为四组:

  • 非修改式序列操作--不修改容器的内容
  • 修改式序列操作--修改容器中的内容(值、排列顺序)
  • 排序和相关操作--各种排序和集合操作
  • 通用数字运算--将区间的内容累积、计算两容器的内部乘积、计算小计、计算相邻对象差。一般用于vector

有些函数有两个版本:就地版本和复制版本(以_copy结尾)。复制版本接受一个额外的输出迭代器参数,该参数指定结果的存放位置。

遍历算法

  1. for_each()

    遍历区间中每一个元素,并对元素执行函数操作

    // for_each() 适用于任何容器类型
    // 对容器中每个元素依次调用该函数。
    // 接收三个参数:[开始区间,结束区间),函数对象
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::for_each(vec.begin(), vec.end(), [](int& n){ std::cout << n << " "; });
    
  1. 通过for遍历

    1. 迭代器区间遍历

      std::vector<int> vec = {1, 2, 3, 4, 5};
      for (auto it = vec.begin(); it != vec.end(); ++it) 
      {
          std::cout << *it << " ";
      }
      
    2. 基于范围遍历

      std::vector<int> vec = {1, 2, 3, 4, 5};
      for (auto n : vec) 
      {
          std::cout << n << " ";
      }
      ​
      

查找算法

  1. find()

    用于在容器中查找特定值的元素,并返回指向第一个匹配元素的迭代器。如果未找到匹配元素,则返回指向end超尾的迭代器。

    接收三个参数:要查找的范围的起始迭代器、要查找的范围的结束迭代器以及要查找的值。线性查找。

    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = find(vec.begin(), vec.end, 3);
    if(it != vec.end())
    {
        cout << "元素在位置" << distance(vec.begin(), it) << endl;
    }
    else
    {
        cout << "元素在容器中未找到。" << endl;
    }
    
  2. find_if()

    用于在容器中查找满足特定条件的元素。并返回第一个使谓词函数返回 true 的元素的迭代器。如果未找到匹配条件的元素,则返回指向end超尾的迭代器。

    接受三个参数:要查找的范围的起始迭代器、要查找的范围的结束迭代器以及一个用于判断条件的谓词函数(可以是函数对象或者函数指针)。线性遍历。

    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = std::find_if(vec.begin(), vec.end(), [](int n) { return n > 3; });
    if (it != vec.end()) 
    {
        std::cout << "First element greater than 3 found at position: " << std::distance(vec.begin(), it) << std::endl;
    }
    else 
    {
        std::cout << "No element greater than 3 found" << std::endl;
    }
    
  3. binary_search()

    二分查找。如果找到则返回true。找不到则返回false

    接受三个参数:要查找的范围的起始迭代器、要查找的范围的结束迭代器以及要查找的值。

    std::vector<int> vec = {1, 2, 3, 4, 5};
    bool result = binary_search(vec.begin(), vec.end(), 3);
    if (result) 
    {
        std::cout << "Element found" << std::endl;
    } 
    else 
    {
        std::cout << "Element not found" << std::endl;
    }
    

排序算法

  1. sort()

    快速排序,对容器中元素进行排序

    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
    std::sort(vec.begin(), vec.end()); // 对 vec 进行排序
    
  2. stable_sort()

    稳定排序,能够保持相等元素的相对顺序。

    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
    std::stable_sort(vec.begin(), vec.end()); // 对 vec 进行稳定排序
    
  3. partial_sort()

    部分排序。将容器中的元素部分排序,即部分有序,使得前n个元素是容器中最小的n个元素,并按照升序排列。

    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
    std::partial_sort(vec.begin(), vec.begin() + 3, vec.end()); // 对前3个元素进行排序
    
  4. nth_element() :

    部分排序。保证容器中的第n个元素是排好序的位置。换句话说,它将容器中的元素分为两部分:前面的元素都不大于第 n 个元素,后面的元素都不小于第 n 个元素。不保证其他元素的相对顺序。

    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
    std::nth_element(vec.begin(), vec.begin() + 3, vec.end());  // 对第3个元素进行排序
    

复制、移动、变换

  1. copy()

    将一个容器中的元素复制到另一个容器中。

    它接受三个参数:源容器的起始迭代器、源容器的结束迭代器以及目标容器的起始位置迭代器。

    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> destination(source.size());
    std::copy(source.begin(), source.end(), destination.begin()); // 将 source 中的元素复制到 destination 中
    
  2. move()

    用于将一个容器中的元素移动到另一个容器中,而不进行复制。

    它接受三个参数:源容器的起始迭代器、源容器的结束迭代器以及目标容器的起始位置迭代器。

    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> destination(source.size());
    std::move(source.begin(), source.end(), destination.begin()); // 将 source 中的元素移动到 destination 中
    
  3. transform()

    用于对容器中的每个元素应用指定的操作,并将结果存储到另一个容器中。

    它接受四个参数:源容器的起始迭代器、源容器的结束迭代器、目标容器的起始位置迭代器以及一个用于对元素进行操作的函数对象或函数指针。

    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> destination(source.size());
    std::transform(source.begin(), source.end(), destination.begin(), [](int n) { return n * 2; }); 
    // 将 source 中的每个元素乘以2,并存储到 destination 中
    

最大值和最小值

  1. std::min_element()

    std::min_element 函数用于查找容器中的最小值,并返回指向该最小值的迭代器。

    接受两个参数:区间起始和超尾。

    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
    auto min_it = std::min_element(vec.begin(), vec.end());
    
  2. std::max_element

    std::max_element 函数用于查找容器中的最大值,并返回指向该最大值的迭代器。

    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
    auto max_it = std::max_element(vec.begin(), vec.end());
    

ref