0
点赞
收藏
分享

微信扫一扫

全面理解STL标准库 vector容器


文章目录

  • 1.C++ 标准库五大件
  • (1)容器container
  • (2)迭代器(iterator)
  • (3)算法(algorithm)
  • (4)仿函数(functor)
  • (5)C++ 标准库五大件:输入输出流(stream)
  • (6)C++ 标准库五大件:分配器(allocator)
  • (7)小结
  • 2.vector 容器
  • (1)vector 容器:构造函数
  • (2)vector 容器:operator[]
  • (3)vector 容器:at
  • (4)vector 容器:构造函数使用initializer_list
  • (5)运算符重载用于打印 vector 类型
  • (6)vector 容器:resize
  • (7)vector 容器:clear
  • (8)vector 容器:clear 配合 resize
  • (9)vector 容器:push_back
  • (10)vector 容器:pop_back
  • (11)vector 容器:back
  • (12)vector 容器:front
  • (13)vector 容器:data() 获取首地址指针
  • (14)vector 容器:生命周期和RAII
  • (15)vector 容器:capacity 函数查询实际的最大容量
  • (16)resize 的优化策略与resize 的优化策略
  • (17)vector 容器:shrink_to_fit 释放多余容量
  • (18)追踪所有的内存分配与释放小工具:mallochook
  • (19)vector 容器:push_back 的问题
  • (20)vector 容器:push_back 的问题,reserve 解决
  • (21)vector 容器:clear 的问题
  • (22)vector 容器:clear 的问题,shrink_to_fit 解决
  • 2.迭代器入门
  • (1)迭代器模式
  • (2)首地址指针和数组长度
  • (3)首地址指针和尾地址指针
  • (4)迭代器模式:++ 的前置和后置
  • (5)vector 容器:begin和end
  • (6)vector 容器:insert 函数
  • (7)vector 容器:assign 函数
  • (8)vector 容器:erase 函数

1.C++ 标准库五大件

(1)容器container

  • eg:13/00_overview/01.cpp

#include <vector>
#include <algorithm>
#include <functional>
#include <iostream>

class GreaterObj
{
public:
    GreaterObj(int number) : number_(number) {}
    bool operator()(int n)
    {
        return n > number_;
    }

private:
    int number_;
};

class PrintObj
{
public:
    void operator()(int n)
    {
        std::cout << n << ' ';
    }
};

int main()
{
    std::vector<int> a = {1, 4, 2, 8, 5, 7};
    // std::less<int>()是一个仿函数对象
    auto n = std::count_if(a.begin(), a.end(), std::bind2nd(std::less<int>(), 4)); //类似于(x<4),x从a中取
    std::cout << n << std::endl;

    std::cout << std::count_if(a.begin(), a.begin() + 1, PrintObj{}) << std::endl;
    return 0;
}

  • 测试:

(2)迭代器(iterator)

功能:指向数据,移动

  • eg:my_course/course/13/00_overview/02.cpp

#include <vector>
#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>

int main() {
    std::vector<int, std::allocator<int>> a = {1, 4, 2, 8, 5, 7};
    auto n = std::count_if(a.begin(), a.end(), std::bind2nd(std::less<int>(), 4));
    std::cout << n << std::endl;
    return 0;
}

(3)算法(algorithm)

#include <functional>

(4)仿函数(functor)

std::bind2nd

(5)C++ 标准库五大件:输入输出流(stream)

std::cout<< n << std::endl;

(6)C++ 标准库五大件:分配器(allocator)

  • eg:my_course/course/13/00_overview/02.cpp

#include <vector>
#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>

int main() {
    std::vector<int, std::allocator<int>> a = {1, 4, 2, 8, 5, 7};
    auto n = std::count_if(a.begin(), a.end(), std::bind2nd(std::less<int>(), 4));
    std::cout << n << std::endl;
    return 0;
}

(7)小结

全面理解STL标准库 vector容器_#include

2.vector 容器

(1)vector 容器:构造函数

  • vector 的功能是长度可变的数组,他里面的数据存储在堆上。
  • vector 是一个模板类,第一个模板参数是数组里元素的类型。

例如,声明一个元素是 int 类型的动态数组 a:
vector a;

  • eg:my_course/course/13/01_vector/01.cpp

#include <vector>
using namespace std;

int main() {
    vector<int> a;
    return 0;
}

vector 容器:构造函数和 size

vector 可以在构造时指定初始长度。
explicit vector(size_t n);

  • 例如,要创建一个长度为 4 的 int 型数组:

vector<int> a(4);

之后可以通过 a.size() 获得数组的长度。
size_t size() const noexcept;

  • eg:my_course/course/13/01_vector/02.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a(4);
    cout << a.size() << endl;
    return 0;
}

  • 测试:

vector 容器:显式构造函数
vector 的这个显式构造函数,默认会把所有元素都初始化为 0(不必手动去 memset)。

如果是其他自定义类,则会调用元素的默认构造函数(例如:数字类型会初始化为 0,string 会初始化为空字符串,指针类型会初始化为 nullptr)
explicit vector(size_t n);

  • eg:

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a(4);
    cout << a << endl;
    // cout << "a.size() = " << a.size() << endl;
    return 0;
}

  • 测试:

这个显式构造函数还可以指定第二个参数,这样就可以用 0 以外的值初始化整个数组了。
比如要创建 4 个 233 组成的数组就可以写:

vector<int> a(4, 233);
等价于
vector<int> a = {233, 233, 233, 233};

explicit vector(size_t n, int const &val);

  • eg:

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a(4, 233);
    cout << a << endl;
    // cout << "a.size() = " << a.size() << endl;
    return 0;
}

  • 测试:

(2)vector 容器:operator[]

要访问 vector 里的元素,只需用 [] 运算符:

例如 a[0] 访问第 0 个元素(人类的第一个)
例如 a[1] 访问第 1 个元素(人类的第二个)
int &operator[](size_t i) noexcept;
int const &operator[](size_t i) const noexcept;

  • eg:my_course/course/13/01_vector/03.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a(4);
    cout << "a[0] = " << a[0] << endl;
    cout << "a[1] = " << a[1] << endl;
    cout << "a[2] = " << a[2] << endl;
    cout << "a[3] = " << a[3] << endl;
    return 0;
}

  • 测试:

值得注意的是,[] 运算符在索引超出数组大小时并不会直接报错,这是为了性能的考虑。

  • 如果你不小心用 [] 访问了越界的索引,可能会覆盖掉别的变量导致程序行为异常,或是访问到操作系统未映射的区域导致奔溃。
  • eg:my_course/course/13/01_vector/04.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a(4);
    cout << "a[0] = " << a[0] << endl;
    cout << "a[1] = " << a[1] << endl;
    cout << "a[2] = " << a[2] << endl;
    cout << "a[3] = " << a[3] << endl;
    cout << "a[1000] = " << a[1000] << endl;
    return 0;
}

(3)vector 容器:at

为了防止不小心越界,可以用 a.at(i) 替代 a[i],at 函数会检测索引 i 是否越界,如果他发现索引 i >= a.size() 则会抛出异常 std::out_of_range 让程序提前终止(或者被 try-catch 捕获),配合任意一款调试器,就可以很快速地定位到出错点。

不过 at 需要额外检测下标是否越界,虽然更安全方便调试,但和 [] 相比有一定性能损失。

int &at(size_t i);
int const &at(size_t i) const;

  • eg:my_course/course/13/01_vector/05.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a(4);
    cout << "a.at(0) = " << a.at(0) << endl;
    cout << "a.at(1) = " << a.at(1) << endl;
    cout << "a.at(2) = " << a.at(2) << endl;
    cout << "a.at(3) = " << a.at(3) << endl;
    cout << "a.at(1000) = " << a.at(1000) << endl;
    return 0;
}

  • 测试:

vector 容器:operator[] 和 at对比:

[] 和 at 除了可以读取元素,还可以写入。
这是因为他们返回的是元素的引用 int&。

  • 例如给第 i 个元素赋值 val:

a[i] = val;
读取第 i 个元素并打印:
cout << a[i] << endl;

  • eg:my_course/course/13/01_vector/06.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a(4);
    a[0] = 6;
    a[1] = 1;
    a[2] = 7;
    a[3] = 4;
    cout << "a[0] = " << a[0] << endl;
    cout << "a[1] = " << a[1] << endl;
    cout << "a[2] = " << a[2] << endl;
    cout << "a[3] = " << a[3] << endl;
    return 0;
}

  • 测试:

(4)vector 容器:构造函数使用initializer_list

除了先指定大小再一个个构造之外,还可以直接利用初始化列表(C++11 新特性)在构造时就初始化其中元素的值。

  • 例如创建具有 6, 1, 7, 4 四个元素的 vector:

vector<int> a = {6, 1, 7, 4};

和刚刚先创建再赋值的方法相比更直观。
vector(initializer_list list);

  • eg:my_course/course/13/01_vector/08.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a = {6, 1, 7, 4};
    cout << "a[0] = " << a[0] << endl;
    cout << "a[1] = " << a[1] << endl;
    cout << "a[2] = " << a[2] << endl;
    cout << "a[3] = " << a[3] << endl;
    return 0;
}

  • 测试:

初始化表达式的等号可以写也可以不写:

vector<int> a = {6, 1, 7, 4};
vector<int> a{6, 1, 7, 4};
都是等价的。

vector(initializer_list list);

  • eg:my_course/course/13/01_vector/09.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> a{6, 1, 7, 4};
    cout << "a[0] = " << a[0] << endl;
    cout << "a[1] = " << a[1] << endl;
    cout << "a[2] = " << a[2] << endl;
    cout << "a[3] = " << a[3] << endl;
    return 0;
}

(5)运算符重载用于打印 vector 类型

  • eg:my_course/course/13/01_vector/printer.h

#pragma once

#include <iostream>
#include <vector>

namespace std {

template <class T>
ostream &operator<<(ostream &os, vector<T> const &v) {
    os << '{';
    auto it = v.begin();
    if (it != v.end()) {
        os << *it;
        for (++it; it != v.end(); ++it) {
            os << ',' << *it;
        }
    }
    os << '}';
    return os;
}

}

(6)vector 容器:resize

除了可以在构造函数中指定数组的大小,还可以之后再通过 resize 函数设置大小。
这在无法一开始就指定大小的情况下非常方便。

vector<int> a(4);
等价于:
vector<int> a;
a.resize(4);

void resize(size_t n);

  • eg:my_course/course/13/01_vector/15.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a;
    cout << a << endl;
    a.resize(4);
    cout << a << endl;
    return 0;
}

  • 测试:

resize 也有一个接受第二参数的重载,他会用这个参数的值填充所有新建的元素。

vector<int> a(4, 233);
等价于:
vector<int> a;
a.resize(4, 233);

void resize(size_t n, int const &val);

  • eg:my_course/course/13/01_vector/16.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a;
    cout << a << endl;
    a.resize(4, 233);
    cout << a << endl;
    return 0;
}

  • 测试:

调用 resize(n) 的时候,如果数组里面不足 n 个元素,假设是 m 个,则他只会用 0 填充新增的 n - m 个元素,前 m 个元素会保持不变

vector<int> a = {1, 2};
a.resize(4);
等价于:
vector<int> a = {1, 2, 0, 0};

void resize(size_t n);

  • eg:my_course/course/13/01_vector/17.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2};
    cout << a << endl;
    a.resize(4);
    cout << a << endl;
    return 0;
}

  • 测试:

调用 resize(n) 的时候,如果数组已有超过 n 个元素,假设是 m 个,则他会删除多出来的 m - n 个元素,前 n 个元素会保持不变。

vector<int> a = {1, 2, 3, 4, 5, 6};
a.resize(4);
等价于:
vector<int> a = {1, 2, 3, 4};

void resize(size_t n);

  • eg:my_course/course/13/01_vector/18.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};
    cout << a << endl;
    a.resize(4);
    cout << a << endl;
    return 0;
}

  • 测试:

调用第二个重载 resize(n, val) 的时候,如果数组里面不足 n 个元素,假设是 m 个,则他只会用第二个参数 val 填充新增的 n - m 个元素,前 m 个元素会保持不变。

vector<int> a = {1, 2};
a.resize(4, 233);
等价于:
vector<int> a = {1, 2, 233, 233};

void resize(size_t n, int const &val);

  • eg:my_course/course/13/01_vector/19.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2};
    cout << a << endl;
    a.resize(4, 233);
    cout << a << endl;
    return 0;
}

  • 测试:

调用第二个重载 resize(n, val) 的时候,如果数组已有超过 n 个元素,假设是 m 个,则第二参数 val 会被无视,删除多出来的 m - n 个元素,前 n 个元素会保持不变。

vector<int> a = {1, 2, 3, 4, 5, 6};
a.resize(4, 233);
等价于:
vector<int> a = {1, 2, 3, 4};

void resize(size_t n, int const &val);

  • eg:my_course/course/13/01_vector/20.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};
    cout << a << endl;
    a.resize(4, 233);
    cout << a << endl;
    return 0;
}

  • 测试:

vector 容器:resize 到更大尺寸会导致 data 失效

当 resize 的目标长度大于原有的容量时,就需要重新分配一段更大的连续内存,并把原数组长度的部分移动过去,多出来的部分则用 0 来填充。这就导致元素的地址会有所改变,从而过去 data 返回的指针以及所有的迭代器对象,都会失效。

  • eg:my_course/course/13/01_vector/29.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    int *p = a.data();
    cout << p[0] << endl;
    cout << p[0] << endl;
    a.resize(1024);
    cout << p[0] << endl;
    return 0;
}

  • 测试:

vector 容器:resize 到更小尺寸不会导致 data 失效

当 resize 的目标长度小于原有的容量时,不需要重新分配一段连续的内存也不会造成元素的移动(这个设计是为了性能考虑),所以指向元素的指针不会失效。他只是会把数组的长度标记为新长度,后面空闲出来那一段内存不会释放掉,继续留在那里,直到 vector 对象被解构。

  • eg:my_course/course/13/01_vector/30.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    int *p = a.data();
    cout << p[0] << endl;
    cout << p[0] << endl;
    a.resize(2);
    cout << p[0] << endl;
    a.resize(5);
    cout << p[0] << endl;
    return 0;
}

  • 测试:

vector 容器:重新 resize 到原来尺寸也不会导致 data 失效

调用了 a.resize(2) 之后,数组的容量仍然是 5,因此重新扩容到 5 是不需要重新分配内存的,也就不会移动元素导致指针失效。

  • eg:my_course/course/13/01_vector/30.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    int *p = a.data();
    cout << p[0] << endl;
    cout << p[0] << endl;
    a.resize(2);
    cout << p[0] << endl;
    a.resize(5);
    cout << p[0] << endl;
    return 0;
}

  • 测试:

(7)vector 容器:clear

vector 的 clear 函数可以清空该数组,也就相当于把长度设为零,变成空数组。

  • eg:

a.clear();
等价于:
a.resize(0);   或   a = {};

通常用于后面需要重新 push_back,因此可以 clear 来把数组设为空。
void clear() noexcept;

  • eg:my_course/course/13/01_vector/20a.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    cout << a << endl;
    a.clear();
    cout << a << endl;
    return 0;
}

  • 测试:

(8)vector 容器:clear 配合 resize

resize 会保留原数组的前面部分不变,只在后面填充上 0。
如果需要把原数组前面的部分也填充上 0,可以先 clear 再 resize,这是一个常见的组合。

  • eg:my_course/course/13/01_vector/20b.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2};
    cout << a << endl;
    a.clear();
    a.resize(4);
    cout << a << endl;
    return 0;
}

  • 测试:

(9)vector 容器:push_back

著名的 push_back 函数,他可以在数组的末尾追加一个数。
实际上是resize()+1

例如:
vector<int> a = {1, 2};
a.push_back(3);
等价于:
vector<int> a = {1, 2, 3};

void push_back(int const &val);
void push_back(int &&val); // C++11 新增

  • eg:my_course/course/13/01_vector/21.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2};
    cout << a << endl;
    a.push_back(3);
    cout << a << endl;
    return 0;
}

  • 测试:

(10)vector 容器:pop_back

pop_back 函数则是和 push_back 唱反调,他是在数组的末尾删除一个数。

  • eg:

vector<int> a = {1, 2, 3};
a.pop_back();
等价于:
vector<int> a = {1, 2};

void pop_back() noexcept;

  • eg:my_course/course/13/01_vector/22.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3};
    cout << a << endl;
    a.pop_back();
    cout << a << endl;
    return 0;
}

  • 测试:

(11)vector 容器:back

是 pop_back 函数的返回类型是 void,也就是没有返回值,如果需要获取删除的值,可以在 pop_back() 之前先通过 back() 获取末尾元素的值,实现 pop 效果。

a.back();
等价于:
a[a.size() - 1]

int &back() noexcept;
int const &back() const noexcept;

  • eg:my_course/course/13/01_vector/23.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3};
    cout << a << endl;
    int val = a.back();
    a.pop_back();
    cout << "back = " << val << endl;
    cout << a << endl;
    return 0;
}

  • 测试:

(12)vector 容器:front

和 back() 相对的还有一个 front()。
back() 返回末尾元素的引用 a[a.size() - 1]。
而 front() 返回首个元素的引用 a[0]。

a.front();
等价于:
a[0]

int &front() noexcept;
int const &front() const noexcept;

  • eg:my_course/course/13/01_vector/24.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3};
    cout << "a[0] = " << a[0] << endl;
    cout << "a[a.size() - 1] = " << a[a.size() - 1] << endl;
    cout << "a.front() = " << a.front() << endl;
    cout << "a.back() = " << a.back() << endl;
    return 0;
}

  • 测试:

(13)vector 容器:data() 获取首地址指针

data() 返回的首地址指针,通常配合 size() 返回的数组长度一起使用

  • 连续的动态数组只需要知道首地址和数组长度即可完全确定

用他来获取一个 C 语言原始指针 int *,很方便用于调用 C 语言的函数和 API 等,同时还能享受到 vector 容器 RAII 的安全性。
~vector() noexcept;

  • eg:my_course/course/13/01_vector/25.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a = {1, 2, 3, 4, 5};
    int *p = a.data();
    int n = a.size();
    // memset只认识char*,所以得通过sizeof(int)告诉memset是多大的数组
    memset(p, -1, sizeof(int) * n);
    cout << a << endl;
    return 0;
}

  • 测试:

(14)vector 容器:生命周期和RAII

RAII 避免内存泄露

如果用 new/delete 或者 malloc/free 就很容易出现忘记释放内存的情况,造成内存泄露。
而 vector 会在离开作用域时,自动调用解构函数,释放内存,就不必手动释放了,更安全。

生命周期由主对象管理
C++ 中哪个运算符是最强的?我觉得是 }

因为 } 标志着一个语句块的结束,在这里,他会调用所有身处其中的对象的解构函数。比如这里的 vector,他的解构函数会释放动态数组的内存(即自动 delete)。

vector 会在退出作用域时释放内存,这时候所有指向其中元素的指针,包括 data() 都会失效。因此如果你是在语句块内获取的 data() 指针,语句块外就无法访问了。

可见 data() 指针是对 vector 的一种引用,实际对象生命周期仍由 vector 类本身管理。

  • eg:my_course/course/13/01_vector/26.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    int *p;
    {
        vector<int> a = {1, 2, 3, 4, 5};
        p = a.data();
        cout << p[0] << endl;
        cout << p[0] << endl;
    }
    cout << p[0] << endl;
    return 0;
}

  • 测试:

vector 容器:延续生命周期
如果需要在一个语句块外仍然保持 data() 对数组的弱引用有效,可以把语句块内的 vector 对象移动到外面的一个 vector 对象上。vector 在移动时指针不会失效

  • eg:a = move(b)
    则会把 b 变成空数组,a 指向原来 b 所包含的元素数组,且地址不变。

之后即使不直接使用外面的那个临时对象 a,也可以继续通过 data() 指针来访问数据

  • eg:my_course/course/13/01_vector/27.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    int *p;
    vector<int> holder;
    {
        vector<int> a = {1, 2, 3, 4, 5};
        p = a.data();
        cout << p[0] << endl;
        cout << p[0] << endl;
        holder = std::move(a);
    }
    cout << p[0] << endl;
    return 0;
}

  • 测试:

vector 容器:延续生命周期

也可以移动到一个全局变量的 vector 对象。
这样数组就会一直等到 main 退出了才释放。
(注:C++ 规定全局变量都会在进入 main 函数之前构造,main 函数返回之后解构)

  • 至于那个全局变量本身有没有被使用则无所谓(我们是通过首地址指针间接访问)。他的存在只是为了延续生命周期,告知 C++ 编译器什么时候能 delete 而已。
  • eg:my_course/course/13/01_vector/28.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

vector<int> holder;

int main() {
    int *p;
    {
        vector<int> a = {1, 2, 3, 4, 5};
        p = a.data();
        cout << p[0] << endl;
        cout << p[0] << endl;
        holder = std::move(a);
    }
    cout << p[0] << endl;
    return 0;
}

  • 测试:

(15)vector 容器:capacity 函数查询实际的最大容量

可以用 capacity() 函数查询已经分配内存的大小,即最大容量。
而 size() 返回的其实是已经存储了数据的数组长度。
可以发现当 resize 指定的新长度一个超过原来的最大容量时时,就会重新分配一段更大容量的内存来存储数组,只有这时才会移动元素的位置(data 指针失效)。

size_t capacity() const noexcept;

  • eg:my_course/course/13/01_vector/31.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(2);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(5);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(7);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    return 0;
}

  • 测试:

(16)resize 的优化策略与resize 的优化策略

resize 的优化策略

  • 注意这里 resize(7) 之后容量实际上扩充到了 10 而不是刚好 7,为什么?

因为标准库的设计者非常聪明,他料想到了你 resize(7) 以后可能还会来个 resize(8) 甚至 resize(9) 之类的。为了减少重复分配的次数,他有一个策略:当 resize 后的新尺寸变化较小时,则自动扩容至原尺寸的两倍。(gcc策略)

这里我们的原大小是 5,所以 resize(7) 会扩充容量到 10,但是尺寸为 7。

尺寸总是小于等于容量。
尺寸范围内都是已初始化的内存(零)。
尺寸到容量之间的范围是未初始化的。(vector是不会让你访问到这里的)

  • eg:my_course/course/13/01_vector/31.cpp

不过如果 resize 后的尺寸还超过了原先尺寸的两倍,就没有这个效果了。
也就是说 resize(n) 的逻辑是扩容至 max(n, capacity * 2)。

  • eg:my_course/course/13/01_vector/32.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(2);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(5);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(12);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    return 0;
}

  • 测试:

resize与reserve区别1:

  • resize已初始化,reserve未初始化

vector 容器:reserve 预留一定容量,避免之后重复分配

内存分配是需要一定时间的。如果我们程序员能预料到数组最终的大小,可以用 reserve
函数预留一定的容量,这样之后就不会出现容量不足而需要动态扩容影响性能了。例如这里我们一开始预留了 12 格容量,这样从 5 到 12
的时候就不必重新分配。 此外,还要注意 reserve 时也会移动元素。

  • 配合push_back使用,也有可能让data()失效

只能扩容不能减容,reserve(12),接着
reserve(0),是减不回去的
reserve()给的参数大于capacity()才会重新分配

  • eg:my_course/course/13/01_vector/33.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.reserve(12);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(2);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(5);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(12);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    return 0;
}

  • 测试:

(17)vector 容器:shrink_to_fit 释放多余容量

刚刚说过,当 resize 到一个更小的大小上时,多余的容量不会释放,而是继续保留。如担心内存告急可以用 shrink_to_fit 释放掉多余的容量,只保留刚好为 size() 大小的容量。

shrink_to_fit 会重新分配一段更小内存,他同样是会把元素移动到新内存中的,因此迭代器和指针也会失效。
size_t shrink_to_fit();

  • eg:my_course/course/13/01_vector/34.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(12);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.resize(4);
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    a.shrink_to_fit();
    cout << a.data() << ' ' << a.size() << '/' << a.capacity() << endl;
    return 0;
}

  • 测试:

(18)追踪所有的内存分配与释放小工具:mallochook

为了追踪所有的内存分配与释放,我们试着重写一下 malloc 和 free 函数。
这样当 vector 容器分配或是释放内存的时候,我们就能轻松看到。

  • eg:my_course/course/13/01_vector/mallochook.h

// https://github.com/sjp38/mallochook/blob/master/mallochook.c

#ifdef __unix__

#include <dlfcn.h>
#include <stdio.h>

void *malloc(size_t size)
{
    typedef void *(*malloc_t)(size_t size);
    static malloc_t malloc_fn = (malloc_t)dlsym(RTLD_NEXT, "malloc");
    void *p = malloc_fn(size);
    fprintf(stderr, "\033[32mmalloc(%zu) = %p\033[0m\n", size, p);
    return p;
}

void free(void *ptr)
{
    typedef void (*free_t)(void *ptr);
    static free_t free_fn = (free_t)dlsym(RTLD_NEXT, "free");
    fprintf(stderr, "\033[31mfree(%p)\033[0m\n", ptr);
    free_fn(ptr);
}

#endif

(19)vector 容器:push_back 的问题

由于不知道你究竟会推入多少个元素,vector 的初始容量是零,而 push_back 和 resize 一样,每次遇到容量不足时,都会扩容两倍,如图。

这也体现了实际容量(capacity)和数组大小(size)分离的好处,如果死板地让分配的内存容量始终等于当前数组大小(很多同学都号称自己实现过 vector,都是这种写法),那么如果要用 push_back 推入 n 个元素,就需要重新分配内存 n 次,移动元素 n(n+1)/2 次。
而像标准库这样允许数组大小和实际容量不同,这样 push_back 在容量不足的时候就可以一次性扩容两倍,只需重新分配 logn 次,移动元素 2n-1 次。

  • malloc(1024)是cout造成的,别看,malloc(4)代表一个int的字节数
  • eg:my_course/course/13/01_vector/34.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
#include "mallochook.h"
using namespace std;

int main() {
    vector<int> a;
    for (int i = 0; i < 100; i++)
        a.push_back(i);
    cout << a << endl;
    return 0;
}

  • 测试:

(20)vector 容器:push_back 的问题,reserve 解决

push_back 的问题的好搭档:reserve 解决

因此,如果你早就知道要推入元素的数量,可以调用 reserve 函数先预留那么多的容量,等待接下来的推入。

这样之后 push_back 时,就不会一次次地扩容两倍慢慢成长到 128,避免重新分配内存和移动元素,更高效。

  • eg:my_course/course/13/01_vector/35.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
#include "mallochook.h"
using namespace std;

int main() {
    vector<int> a;
    a.reserve(100);
    for (int i = 0; i < 100; i++)
        a.push_back(i);
    cout << a << endl;
    return 0;
}

  • 测试:

比如这里我们可以提前知道循环会执行 100 次,因此 reserve(100) 就可以了。
可以看到只有一次 malloc(400),之后那次 malloc(1024) 是 cout 造成的,不必在意。

(21)vector 容器:clear 的问题

clear 相当于 resize(0),所以他也不会实际释放掉内存,容量(capacity)还是摆在那里,clear 仅仅只是把数组大小(size)标记为 0 而已。

这可能导致在低端平台上内存告急,这是因为尽管你已经 clear 掉 vector 了而实际容量还在并没有释放。

  • eg:my_course/course/13/01_vector/36.cpp

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
#include "mallochook.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4};
    cout << "before clear, capacity=" << a.capacity() << endl;
    a.clear();
    cout << "after clear, capacity=" << a.capacity() << endl;
    return 0;
}

  • 测试:
  • 全面理解STL标准库 vector容器_ios_02

  • eg:
    vector()里面没有任何元素,即没有调用resize(),
    则其data()初始化为nullptr

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
// #include "mallochook.h"
using namespace std;

int main()
{
    vector<int> a;
    printf("%p %ld\n", a.data(), a.size());
    // cout << "before clear, capacity=" << a.capacity() << endl;
    // a.clear();
    // cout << "after clear, capacity=" << a.capacity() << endl;
    return 0;
}

  • 测试:

(22)vector 容器:clear 的问题,shrink_to_fit 解决

要真正释放掉内存,可以在 clear 之后再调用 shrink_to_fit,这样才会让容量也变成 0(这时 vector 的 data 会返回 nullptr)

当然,vector 对象解构时也会彻底释放内存,这个不用操心。clear 配合 shrink_to_fit 只是提前释放而已。

#include <vector>
#include <iostream>
#include <cstring>
#include "printer.h"
#include "mallochook.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4};
    cout << "before clear, capacity=" << a.capacity() << endl;
    a.clear();
    a.shrink_to_fit();
    cout << "after clear, capacity=" << a.capacity() << endl;
    return 0;
}

  • 测试:

2.迭代器入门

(1)迭代器模式

打印的操作封装起来,该怎么做?

  • eg:my_course/course/13/02_iterator/01.cpp

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<char> a = {'h', 'j', 'k', 'l'};
    for (int i = 0; i < a.size(); i++) {
        cout << a[i] << endl;
    }
    return 0;
}

可以用一个函数来封装打印操作:
print(vector const &a);

  • eg:my_course/course/13/02_iterator/02.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

void print(vector<char> const &a) {
    for (int i = 0; i < a.size(); i++) {
        cout << a[i] << endl;
    }
}

int main() {
    vector<char> a = {'h', 'j', 'k', 'l'};
    print(a);
    return 0;
}

  • 缺点:但是这样的缺点是他只能打印 vector 类型,没法打印 string 类型。要支持 string 只能再写一遍一样的 print 函数。

(2)首地址指针和数组长度

注意到 vector 和 string 的底层都是连续的稠密数组,他们都有 data() 和 size() 函数。
因此可改用首地址指针和数组长度做参数:
print(char const *a, size_t n);

  • 这样 print 在无需知道容器具体类型的情况下,只用最简单的接口(首地址指针)就完成了遍历和打印的操作。
  • eg:my_course/course/13/02_iterator/04.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

void print(char const *a, size_t n) {
    for (int i = 0; i < n; i++) {
        cout << a[i] << endl;
    }
}

int main() {
    vector<char> a = {'h', 'j', 'k', 'l'};
    print(a.data(), a.size());
    string b = {'h', 'j', 'k', 'l'};
    print(b.data(), b.size());
    return 0;
}

使用指针和长度做接口的好处是,可以通过给指针加减运算,选择其中一部分连续的元素来打印,而不一定全部打印出来。
比如这里我们选择打印前三个元素(去掉了最后一个元素,但不必用 pop_back 修改数组,只要传参数的时候修改一下长度 部分即可)。

  • eg:my_course/course/13/02_iterator/05.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

void print(char const *a, size_t n) {
    for (int i = 0; i < n; i++) {
        cout << a[i] << endl;
    }
}

int main() {
    vector<char> a = {'h', 'j', 'k', 'l'};
    print(a.data(), a.size() - 1);
    return 0;
}

这里我们选择打印后三个元素(去掉了第一个元素,但不必用 erase 修改数组,只要传参数的时候同时修改指针和长度部分即可)。

  • eg:my_course/course/13/02_iterator/06.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

void print(char const *a, size_t n)
{
    for (int i = 0; i < n; i++)
    {
        //等价于cout<<*(a+i*1)<<std::endl;
        cout << a[i] << endl;
        // cout << *(a + i * 1) << std::endl;
    }
}

int main()
{
    vector<char> a = {'h', 'j', 'k', 'l'};
    print(a.data() + 1, a.size() - 1);
    return 0;
}

(3)首地址指针和尾地址指针

首地址指针和数组长度看起来不太对称。
print(char const *begptr, size_t size);
不妨改用首地址指针和尾地址指针如何?
print(char const *begptr, size_t endptr);

注意看,我们在 print 里也不是用数组下标去迭代,而是用指针作为迭代变量了。

  • eg:my_course/course/13/02_iterator/07.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

void print(char const *begptr, char const *endptr)
{
    for (char const *ptr = begptr; ptr != endptr; ptr++)
    {
        // 相比较ptr[i]而言,更高效
        char value = *ptr;
        cout << value << endl;
    }
}

int main()
{
    vector<char> a = {'h', 'j', 'k', 'l'};
    char const *begptr = a.data();
    char const *endptr = a.data() + a.size();
    print(begptr, endptr);
    return 0;
}

特别注意一点:尾地址指针实际上是指向末尾元素再往后后一个元素的指针!
也就是说尾地址指针所指向的地方是无效的内存 a + a.size(),尾地址指针减1才是真正的末尾元素指针 a + a.size() - 1。

为什么要这样设计?

  • 因为如果用 a + a.size() - 1 也就是 &a.back() 作为尾地址指针,将无法表示数组长度为 0 的情况
  • 而让尾地址指针往后移动一格的设计,使得数组长度为 0 就是 begptr == endptr 的情况,非常容易判断。
  • 更方便的是你可以通过指针的减法运算: endptr - begptr 来算出数组的长度!
  • for 循环里也很容易写,判断是否继续循环的条件为 ptr != endptr 就行了。
  • eg:my_course/course/13/02_iterator/08.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

void print(char const *begptr, char const *endptr) {
    for (char const *ptr = begptr; ptr != endptr; ptr++) {
        char value = *ptr;
        cout << value << endl;
    }
}

int main() {
    vector<char> a = {'h', 'j', 'k', 'l'};
    char const *begptr = a.data();
    char const *endptr = a.data() + a.size();
    cout << "*begptr = " << *begptr << endl;
    cout << "*endptr = " << *endptr << endl;
    print(begptr, endptr);
    return 0;
}

  • 测试:

还可以让首指针和尾指针声明为模板参数,这样不论指针是什么类型,都可以使用 print 这个模板函数来打印。

  • eg:my_course/course/13/02_iterator/10.cpp

#include <vector>
#include <string>
#include <iostream>
using namespace std;

template <class Ptr>
void print(Ptr begptr, Ptr endptr) {
    for (Ptr ptr = begptr; ptr != endptr; ptr++) {
        auto value = *ptr;
        cout << value << endl;
    }
}

int main() {
    vector<char> a = {'h', 'j', 'k', 'l'};
    char const *abegptr = a.data();
    char const *aendptr = a.data() + a.size();
    print(abegptr, aendptr);
    vector<int> b = {1, 2, 3, 4};
    int const *bbegptr = b.data();
    int const *bendptr = b.data() + b.size();
    print(bbegptr, bendptr);
    return 0;
}

首指针和尾指针的组合的确能胜任 vector 这种连续数组,但是对于 list 这种不连续的内存的容器就没辙了。
没错,list 没有 data() 这个成员函数,因为他根本就不连续。

然而 list 却提供了 begin() 和 end() 函数,他们会返回两个 list::iterator 对象。
这个 list::iterator 是一个特殊定义过的类型,其具有 != 和 ++ 以及 * 这些运算符的重载。所以用起来就像普通的指针一样。而这些运算符重载,却会把 ++ 对应到 链表的 curr = curr->next 上。

  • 这样一个用起来就像普通的指针,但内部却通过运算符重载适配不同容器的特殊类,就是迭代器(iterator),迭代器是 STL 中容器和算法之间的桥梁。
  • eg:my_course/course/13/02_iterator/11.cpp

#include <iostream>
#include <vector>
#include <list>
using namespace std;

template <typename T>
struct B
{
    int var;
};

template <typename T>
struct D : B<T>
{
    D()
    {
        // var = 1;       // 错误: 'var' 未在此作用域中声明
        this->var = 1; // ok
    }
};

template <class Ptr>
void print(Ptr begptr, Ptr endptr)
{
    for (Ptr ptr = begptr; ptr != endptr; ptr++)
    {
        auto value = *ptr;
        cout << value << endl;
    }
}

int main()
{
    list<char> a = {'h', 'j', 'k', 'l'};
    list<char>::iterator begptr = a.begin();
    list<char>::iterator endptr = a.end();
    print(begptr, endptr);

    D<int> t_d();
    return 0;
}

如果写一个 list 容器和他的迭代器,他的内部具体实现可能是这样的。
迭代器的这些运算符,都是约定俗成的,其根本目的在于模仿指针的行为,方便来自 C 语言的程序员快速上手掌握 C++ 标准库。

虽然你也可以用直观的函数名 advance() 代替 ++,用 deref() 代替 *,equal_to() 代替 ==。但是模仿指针行为的这些运算符,已然成为了 C++ 事实上的标准,而且也非常简洁明了。
因此所有的用户和库,都会按照这套运算符标准来实现和使用迭代器,建立起了沟通的桥梁,节省了各自创立一套规范的成本。

  • eg:my_course/course/13/02_iterator/12.cpp

#include <cstddef>

template <class T>
struct List {
    struct Node {
        T value;
        Node *next;
    };

    struct Iterator {
        Node *curr;

        Iterator &operator++() {
            curr = curr->next;
            return *this;
        }

        T &operator*() const {
            return curr->value;
        }

        bool operator!=(Iterator const &that) const {
            return curr != that.curr;
        }
    };

    Node *head;

    Iterator begin() { return {head}; }
    Iterator end() { return {nullptr}; }
};

template <class T>
struct Vector {
    struct Node {
        T value;
        Node *next;
    };

    struct Iterator {
        Node *curr;

        Iterator &operator++() {
            curr = curr->next;
            return *this;
        }

        Iterator operator++(int) {
            Iterator tmp = *this;
            this->operator++();
            return tmp;
        }

        T &operator*() const {
            return curr->value;
        }

        bool operator!=(Iterator const &that) const {
            return curr != that.curr;
        }
    };

    Node *head;
    size_t size;

    Iterator begin() { return {head}; }
    Iterator end() { return {head + size}; }
};

void iterate_over_list(List<int> const &list) {
    for (auto curr = list.head; curr != nullptr; curr = curr->next) {
    }
}

(4)迭代器模式:++ 的前置和后置

迭代器的自增运算符分为 ++p 和 p++ 两种写法。
他们都会产生 p = p + 1 的效果,但是有一个细微的区别,就是他们被作为表达式时的返回值。
++p 会返回自增后的值 p + 1,这和 p += 1 完全一样,同样因为返回的是一个左值引用所以还可以继续自增比如 ++++p。
p++ 会返回自增前的值 p,但是执行完以后 p 却又是 p + 1 了,非常迷惑)
正因如此,后置自增需要先保存旧的迭代器,然后自增自己,再返回旧迭代器,可能会比较低效。
在 C++ 中我推荐尽可能地多用前置自增 ++p。

  • 在运算符重载上,沙雕的 C++ 标准委员会规定,operator++(int) 这个重载是后置自增 p++,不带任何参数的operator++() 这个重载是前置自增,之所以这样是因为同名函数只能通过参数列表类型来区分,这个 int 类型参数没有任何实际意义,只是为了区分不同的重载……编译器会在 p++ 的时候自动改成调用 p.operator++(0)。

(5)vector 容器:begin和end

begin 可以获取指向第一个元素所在位置的迭代器。
end 可以获取指向最后一个元素下一个位置的迭代器。

迭代器的作用类似于一个位置标记符。
虽然对于 vector 来说只需要下标(index)就能标记位置了,例如 Python 中也是通过 0 表示第一个元素,-1 表示最后一个元素。
a[0] a[1] a[-1]

而 C++ 的特色就是采用了迭代器(iterator)来标记位置,他实际上是一个指针,这样的好处是:不需要指定原来的容器本身,就能知道指定的位置。
一对迭代器 begin 和 end 就标记了一个区间(range)。区间可以是一个容器的全部,例如 {a.begin(), a.end()} 区间;也可以是一个容器的部分,例如 {a.begin() + 1, a.end() - 1} 相当于去头去尾后的列表,相当于 Python 中的 a[1:-1]。

begin 可以获取指向第一个元素所在位置的迭代器。可以通过 *a.begin() 来访问第一个元素。

迭代器支持加法运算,例如 *(a.begin() + 1) 就是访问数组的第二个元素了,和 a[1] 等价。

end 可以获取指向最后一个元素下一个位置的迭代器。也就是说 end 指向的位置是不可用的!

  • 如需访问最后一个元素必须用 *(a.end() - 1) 才行。
  • eg:my_course/course/13/02_iterator/13.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    vector<int>::iterator b = a.begin();
    vector<int>::iterator e = a.end();

    cout << "a = " << a << endl;
    cout << "*b = " << *b << endl;
    cout << "*(b + 1) = " << *(b + 1) << endl;
    cout << "*(b + 2) = " << *(b + 2) << endl;
    cout << "*(e - 2) = " << *(e - 2) << endl;
    cout << "*(e - 1) = " << *(e - 1) << endl;
    cout << "*e = " << *e << endl;

    return 0;
}

  • 测试:

冷知识,迭代器实际上还可以用 [] 运算符访问。
例如这里的 b[i] 就和 *(b + i) 等价。

不过只有 vector 这种连续的可随机访问容器的迭代器有 + 和 [] 运算符,对于 list 则只有 * 和 ++ 和 – 运算符可以用,这是迭代器的两个分类。

自此,迭代器对象和容器本身的主要区别就在于:

迭代器不掌握生命周期,从而迭代器的拷贝是平凡的浅拷贝,方便传参。但也带来了缺点,因为迭代器是一个对原容器的弱引用,如果原容器解构或发生内存重分配,迭代器就会失效。

(6)vector 容器:insert 函数

我们知道 push_back 可以往尾部插入数据,那么如何往头部插入数据呢?

  • 用 insert 函数,他的第一个参数是要插入的位置(用迭代器表示),第二个参数则是要插入的值。

注意这个函数的复杂度是 O(n),n 是从插入位置 pos 到数组末尾 end 的距离。没错,他会插入位置后方的元素整体向后移动一格,是比较低效的,因此为了高效,我们尽量只往尾部插入元素。
如果需要高效的头部插入,可以考虑用 deque 容器,他有高效的 push_front 函数替代。

insert 在容量不足时,同样会造成重新分配以求扩容,会移动其中所有元素,这时所有之前保存的迭代器都会失效。

  • eg:my_course/course/13/02_iterator/16.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.insert(a.begin(), 233);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:insert 函数,插到指定的元素前方

如果要插入到一个特定位置,可以用迭代器的加法来获取某一位置的迭代器。
例如 a.begin() + 3 就会指向第三个元素,那么用这个作为 insert 的参数就会把 233 这个值插到第三个元素的位置之前。
iterator insert(const_iterator pos, int const &val);
iterator insert(const_iterator pos, int &&val); // C++11

  • eg:my_course/course/13/02_iterator/16.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.insert(a.begin() + 3, 233);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:insert 函数,插入位置是倒数第 2 个

a.begin() 可以插入到开头位置。
a.begin() + 1 可以插入到第二个元素位置。
a.end() 可以插入到最末尾(append)。
a.end() - 1 则是插入到倒数第一个元素前。
end() 迭代器的减法和是 Python 中负数作为下标的情况很像的,不过 C++ 更加明确是从 end 开始往前数的。
iterator insert(const_iterator pos, int const &val);
iterator insert(const_iterator pos, int &&val); // C++11

  • eg:my_course/course/13/02_iterator/17.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.insert(a.end() - 2, 233);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:insert 函数,重复插入多个相同的值

insert 还有一个特殊的功能,就是他可以插入一个元素很多遍!只需多指定一个参数来表示插入多少遍,语法如下:
a.insert(插入位置, 重复多少次, 插入的值);

  • 你可能会担心,在头部 insert 是 O(n) 复杂度嘛?那如果再重复 n 次岂不是 O(n²) 复杂度了?不会哦,insert 的这个重载会一次性批量让 pos 之后的元素移动 n 格,不存在反复移动 1 格的情况,最坏复杂度仍然是 O(n)。如果你自己写个 for 循环反复调 insert 那的确是会 O(n²) 了,这就是为什么 insert 提供这个高效的重载专门负责重复插入的操作。
  • eg:my_course/course/13/02_iterator/18.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.insert(a.begin(), 4, 233);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:insert 函数,直接插入一个初始化列表

insert 还可以直接插入一个 {} 的列表!
这个花括号 {} 形成的列表就是传说中的初始化列表(initializer-list),是 C++11 新增的功能,例如这里这个列表的类型是 std::initializer_list。
a.insert(插入位置, {插入值1, 插入值2, …});

这个的最坏复杂度同样是 O(n) 的,并且因为其内部预先知道了要插入列表的长度,会一次性完成扩容,比重复调用 push_back 重复扩容要高效很多。
等价于push_back(1),push_back(2)
iterator insert(const_iterator pos, initializer_list lst);

  • eg:my_course/course/13/02_iterator/19.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.insert(a.begin(), {233, 666, 985, 211});
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:insert 函数,直接插入另一个 vector?

不过这种列表只能写在函数参数中才奏效,如果你试图用一个 vector 作为这个参数,就会出错!报错会说因为 vector 和 initializer_list 不是同一个类型。

  • 那要如何插入另一个 vector,或者说,把 a 和 b 这两个数组合并起来呢?
  • eg:

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a = {1, 2, 3, 4, 5, 6};
    vector<int> b = {233, 666, 985, 211};

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    a.insert(a.begin(), b);
    cout << "a = " << a << endl;

    return 0;
}

全面理解STL标准库 vector容器_ios_03

vector 容器:insert 函数,插入另一个 vector 需通过他的两个迭代器

  • 记得 C++ 的迭代器思想是,容器和算法之间的交互不是通过容器对象本身,而是他的迭代器,因此 insert 设计时就决心不支持直接接受 vector 作参数,而是接受他的两个迭代器组成的区间!好处有:
  1. 可以批量插入从来自另一个不同类型的容器,例如 list,只要元素类型相等,且符合迭代器规范。
  2. 我可以自由选择对方容器的一个子区间(通过迭代器加减法)内的元素来插入,而不是死板的只能全部插入。
    template <class It> // 这里 It 可以是其他容器的迭代器类型
    iterator insert(const_iterator pos, It beg, It end);

a.insert(a.begin(), b.begin(), b.end()) 会把 b 插入在原先 a 元素之前,相当于python的 a = b + a。

可以改用 a.insert(a.end(), b.begin(), b.end()) 把 b 插入到 a 元素之后,相当于python的 a += b,这样性能更好(只要容量足够就无需移动 a 的全部元素)。

当然也可以 a.insert(a.begin() + 3, b.begin(), b.end()) 这样只插入到指定位置中间,python似乎没有这个操作。

  • eg:my_course/course/13/02_iterator/21.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};
    vector<int> b = {233, 666, 985, 211};

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    a.insert(a.end(), b.begin(), b.end());
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:insert 函数,作为数据源的对方容器可以是不同类型

对方容器也可以是不同类型的,最底线的要求是只要他的迭代器有 ++ 和 * 运算符即可。
例如这里的 list::iterator 就符合需求。
template <class It> // 这里 It 可以是其他容器的迭代器类型
iterator insert(const_iterator pos, It beg, It end);

  • eg:my_course/course/13/02_iterator/22.cpp

#include <vector>
#include <list>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};
    list<int> b = {233, 666, 985, 211};

    cout << "a = " << a << endl;
    a.insert(a.end(), b.begin(), b.end());
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

    string转vector

string s;
vector<char> v;
a.assign(s.begin(),s.end());

vector 容器:insert 函数,作为数据源的对方容器可以是不同类型

对方容器还可以是个 C 语言风格的数组,因为 C 语言类型没有办法加成员函数 begin 和 end,可以用 std::begin 和 std::end 这两个全局函数代替,当然如果用了 using namespace std 时也可以不写 std:: 前缀。
这两个函数会对于具有 begin 和 end 成员函数的容器会直接调用,对于 C 语言数组则被特化为返回 b 和 b + sizeof(b)/sizeof(b[0])。
(这两个函数是 C++11 新增的)
template auto begin(T &&t);
template auto end(T &&t);

  • eg:

#include <vector>
#include <list>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    int b[] = {233, 666, 985, 211};
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.insert(a.end(), std::begin(b), std::end(b));
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:构造函数也能接受迭代器!

其实 vector 容器的构造函数也接受一对迭代器做参数,来初始化其中的元素。同样可以是不同容器的迭代器对象,只要具有 ++ 和 * 就行了。
template <class It> // 这里 It 可以是其他容器的迭代器类型
explicit vector(It beg, It end);

  • eg:my_course/course/13/02_iterator/24.cpp

#include <vector>
#include <list>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    int b[] = {233, 666, 985, 211};
    vector<int> a(std::begin(b), std::end(b));

    cout << "a = " << a << endl;

    return 0;
}

std:size(b)和C语言的sizeof(b)/sizeof(b[0])是等价的,但是用前者,因为前者包含了C++和C的不同迭代器情况:
std::begin(b),std::end(b)等于std::data(b)+std::size(b);

  • 测试:

(7)vector 容器:assign 函数

除了构造函数外,assign 这个成员函数也能在后期把元素覆盖进去。和 insert 不同的是,他会把旧有的数组完全覆盖掉,变成一个新的数组。

a.assign(beg, end) 基本和 a = vector(beg, end) 等价,唯一的区别是后者会重新分配内存,而前者会保留原来的容量不会释放掉。
template <class It> // 这里 It 可以是其他容器的迭代器类型
void assign(It beg, It end);

assign 还有一个重载,可以把 vector 批量填满一个特定的值,重复的次数(长度)也是参数里指定。
a.assign(n, val) 基本和 a = vector(n, val) 等价,唯一的区别是后者会重新分配内存,而前者会保留原来的容量。
void assign(size_t n, int const &val);

  • eg:

#include <vector>
#include <list>
#include <iostream>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a = {1, 2, 3, 4, 5, 6};
    int b[] = {985, 211};

    cout << "a = " << a << endl;
    a.assign(4, 233);
    // a.assign(std::begin(a), std::end(a));
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

assign 还可以直接接受一个初始化列表作为参数。
a.assign({x, y, …}) 和 a = {x, y, …} 完全等价,都会保留原来的容量。
而和 a = vector{x, y, …} 就不等价,这个会重新分配内存。
会先调用(1),再调用(2)
void assign(initializer_list val);
vector &operator=(initializer_list val);(1)
vector &operator=(vector const &val);
vector &operator=(vector &&val);(2)

  • eg:my_course/course/13/02_iterator/26.cpp

#include <vector>
#include <list>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.assign({233, 666, 985, 211});
    cout << "a = " << a << endl;
    a = {996, 007};
    cout << "a = " << a << endl;
    cout << "a.capacity() = " << a.capacity() << endl;
    a = vector<int>{996, 007};
    cout << "a.capacity() = " << a.capacity() << endl;

    return 0;
}

  • 测试:

(8)vector 容器:erase 函数

erase 函数可以删除指定位置的一个元素(通过迭代器指定)。
a.erase(a.begin()) 就是删除第一个元素(相当于 pop_front)。
a.erase(a.end() - 1) 就是删除最后一个元素(相当于 pop_back)。
a.erase(a.begin() + 2) 就是删除第三个元素。
a.erase(a.end() - 2) 就是删除倒数第二个元素。
erase 的复杂度最坏情况是删除第一个元素 O(n)。
如果删的是最后一个元素则复杂度为 O(1)。
这是因为 erase 会移动 pos 之后的那些元素。
iterator erase(const_iterator pos);

  • eg:my_course/course/13/02_iterator/27.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.erase(a.begin() + 3);
    cout << "a = " << a << endl;
    a.erase(a.end() - 1);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:erase 函数,批量删除一个区间

erase 也可以指定两个迭代器作为参数,表示把这个区间内的对象都删除。
比如这里 a.erase(a.begin() + 1, a.begin() + 3) 就删除了 a 的第二个和第三个元素,相当于python的 del a[1:3],注意 C++ 的 insert 和 erase 都是就地操作的。
例如:a.erase(a.begin() + n, a.end()) 就和 a.resize(n) 等价,前提是 n 小于 a.size()。
批量删除的最坏复杂度依然是 O(n) 的,不用担心。不过这里两个作为 erase 参数的迭代器必须是自己这个对象的迭代器,不能是其他容器的,这点和 insert 不一样哦。
他返回删除后最后一个元素之后那个位置的迭代器(基本不用)。
iterator erase(const_iterator beg, const_iterator end);

  • eg:my_course/course/13/02_iterator/28.cpp

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.erase(a.begin() + 1, a.begin() + 3);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:

vector 容器:迭代器区间可以反过来指定吗?

注意!第一个迭代器指向的元素必须在第二个迭代器之前,不能反过来指定!
否则会出错,甚至奔溃。包括 insert 的后两个参数也是如此,请勿指定反过来的区间!
iterator erase(const_iterator beg, const_iterator end); // 必须保证 beg <= end,否则 UB

#include <vector>
#include <iostream>
#include "printer.h"
using namespace std;

int main()
{
    vector<int> a = {1, 2, 3, 4, 5, 6};

    cout << "a = " << a << endl;
    a.erase(a.begin() + 3, a.begin() + 1);
    cout << "a = " << a << endl;

    return 0;
}

  • 测试:
  • 全面理解STL标准库 vector容器_#include_04

  • 参考:【C++公开课】全面理解STL标准库 vector容器 精讲(第1集 持续更新中)


举报

相关推荐

0 条评论