标签 stl 下的文章

在写代码的过程中,CLion提醒我把push_back方法替换成emplace_back方法:

代码中我的想法是使用vector创建一个二维数组,并提前分配好空间,避免后序频繁扩容增加时间复杂度。

emplace_back函数的作用是减少对象拷贝和构造次数,是C++11中的新特性,主要适用于对临时对象的赋值。

在使用push_back函数往容器中增加新元素时,必须要有一个该对象的实例才行,而emplace_back可以不用,它可以直接传入对象的构造函数参数直接进行构造,减少一次拷贝和赋值操作。

例如以下学生类:

class stu_info {
private:
    string name;
public:
    stu_info(const string &name) {
        this->name = name;
        cout << "stu_info(): " << this->name << endl;
    }

    ~stu_info() {
        cout << "~stu_info(): " << this->name << endl;
    }

    stu_info(const stu_info &s) {
        this->name = s.name;
        cout << "copy constructor: " << this->name << endl;
    }
};

使用push_back插入元素的办法:

vector<stu_info> v;
v.push_back(stu_info("nginx"));

在push_back之前,必须使用stu_info实例一个临时对象传入才行,实例对象就必须要执行构造函数,然后拷贝到容器中再执行一次拷贝构造函数。

而emplace_back可以不用执行多余的拷贝构造函数了,它是直接在容器内执行对象的构造:

vector<stu_info> v;
v.emplace_back("redis");

两个函数的执行结果:

我以前确实是还不知道这个函数,要不是编译器提醒我,以后我可能也不会知道。不得不说IDE在某些方面也能帮助我们提高眼界

往vector中添加元素时,如果空间不够将会导致扩容。vector有两个属性:size和capacity。size表示已经使用的数据容量,capacity表示数组的实际容量,包含已使用的和未使用的。

vector扩容规则:

  1. 当数组大小不够容纳新增元素时,开辟更大的内存空间,把旧空间上的数据复制过来,然后在新空间中继续增加。
  2. 新的更大的内存空间,一般是当前空间的1.5倍或者2倍,这个1.5或者2被称为扩容因子,不同系统实现扩容因子也不同。
注意,扩容会导致迭代器失效。

在VS2017中,vector的扩容因子是1.5。可以追踪push_back的实现:

decltype(auto) emplace_back(_Valty&&... _Val)
{    // insert by perfectly forwarding into element at end, provide strong guarantee
    if (_Has_unused_capacity())
    {
        return (_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...));
    }

    _Ty& _Result = *_Emplace_reallocate(this->_Mylast(), _STD forward<_Valty>(_Val)...);
    // ...
}

函数中先通过_Has_unused_capacity函数判断是否有还有未使用的空间,如果有未使用的空间,直接在未使用的空间上新增。如果没有未使用的空间了,就需要执行_Emplace_reallocate重新扩容:

pointer _Emplace_reallocate(const pointer _Whereptr, _Valty&&... _Val)
{    
    // ..
    const size_type _Newsize = _Oldsize + 1;
    const size_type _Newcapacity = _Calculate_growth(_Newsize);
    // ..
}

函数中先判断新长度,然后继续调用_Calculate_growth扩容,这个函数才是真正用来扩容的函数。

忽略过其他判断逻辑,_Calculate_growth的实现为:

size_type _Calculate_growth(const size_type _Newsize) const
{    
    // ...
    const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;
    // ...
}

新的扩容大小等于当前容量再加上当前容量的一半,即按照1.5倍扩容。

在GCC的实现中,vector扩容是2倍扩容的。

1.5倍扩容和2倍扩容的区别

  1. 扩容因子越大,需要分配的新内存空间越多,越耗时。空闲空间较多,内存利用率低。
  2. 扩容因子越小,需要再分配的可能性就更高,多次扩容耗时。空闲空间较少,内存利用率高。

因此,小规模数组,添加元素不频繁的,建议使用扩容因子更小的。当数据规模越大,插入更频繁,大扩容因子更适合。

示例代码

#include <iostream>
#include <vector>

using namespace std;

int main() {
    int i;
    vector<int> v;

    cout << v.capacity() << " ";
    for (i = 0; i < 10; i++) {
        v.push_back(1);
        cout << v.capacity() << " ";
    }

    return 0;
}

以上代码通过循环插入了十个元素,并打印出每次插入后vector的容量:

0 1 2 3 4 6 6 9 9 9 13

能看出的增长规律为:0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 9 -> 13,而同样的代码通过G++编译后的输出结果为:

0 1 2 4 4 8 8 8 8 16 16

一、迭代器失效

向容器添加或者删除元素可能会导致指向容器的指针、引用或者迭代器失效。使用已经失效的指针、引用或者迭代器将会导致程序出现异常,编码过程中一定要时刻注意迭代器失效的场景。

例如,以vector为例:

int main() {
    vector<int> v{1, 2};
    vector<int>::iterator it;

    for (it = v.begin(); it != v.end(); it++) {
        v.push_back(*it);
    }
    
    return 0;
}

执行以上代码会导致段错误:

原因:在循环中新增了元素,并且重新分配了内存空间,导致迭代器失效。使用已经失效的迭代器会导致程序出现段错误。

迭代器失效,主要有两个层面的意思:

  1. 无法通过迭代器++或--操作遍历整个stl容器,记作第一层失效
  2. 无法通过迭代器存取迭代器所指向的内存,记作第二层失效

二、失效场景

vector和string

如果增加或删除元素导致内存空间重新分配了,那么指向容器的迭代器都会失效(第二层失效)。如果存储空间未重新分配,指向删除元素之前的所有迭代器还有效(第一层失效),但是删除元素之后的所有迭代器都无效了(第二层失效)。

deque

插入到除首尾元素之外任何位置都会导致迭代器失效(第二层失效)。如果插入到首尾元素,迭代器会失效,但是指向已存在元素的指针和引用不失效(第一层失效)。

删除除首尾元素之外的元素,所有迭代器失效(第二层失效)。如果删除的是首尾元素,首前和尾后迭代器失效,其他元素的引用、指针和迭代器不会失效。

map和set

删除和添加元素会导致内部结构调整,迭代器失效,但是引用和指针任然有效(第一层失效)。

list

添加元素不会导致迭代器失效,但是删除元素会导致删除元素后面的所有迭代器失效(第一层失效)。list删除元素永远都会导致尾后迭代器失效(第二层失效)。

三、避免迭代器失效

避免迭代器失效的几种方法:

  1. 减小迭代器的使用范围,不保存迭代器的值。
  2. 避免在遍历迭代器的过程中修改容器。
  3. 不要保存首前和尾后指针。

vector避免删除失效

在遍历vector的过程中删除元素,会导致后面的迭代器失效。如果希望删除后还能继续使用迭代器,要使用erase方法,并接收返回值作为下一个迭代器使用。

正确的使用方式:

int main() {
    vector<int> v{1, 2, 3, 4, 5};
    vector<int>::iterator it;

    for (it = v.begin(); it != v.end();) {
        if (*it == 2 || *it == 4) {
            // 接收返回值作为下一个迭代器
            it = v.erase(it);
            continue;
        }
        cout << *it << endl;
        it++;
    }
}

set/map避免迭代器失效

set和map也和vector一样:

int main() {
        set<int> s{1, 2, 3, 4, 5};
    set<int>::iterator it;

    for (it = s.begin(); it != s.end();) {
        if (*it == 2 || *it == 4) {
            it = s.erase(it);
            continue;
        }
        cout << *it << endl;
        it++;
    }
    return 0;
}

一、vector迭代器失效

vector是先行存储的,大部分时候的插入删除操作都有可能导致迭代器失效。失效场景:

  • 执行插入操作时,end指针失效,如果此时重新分配内存了,所有的迭代器失效,否则其他迭代器可能不会失效。
  • 执行删除操作时,所有的迭代器都会失效

示例代码:

vector<int> data;
vector<int>::iterator it;
data.push_back(100);
data.push_back(200);
data.push_back(300);
data.push_back(400);

for (it = data.begin(); it != data.end(); it++) {
    cout << *it << endl;
    if (*it == 300)
        data.erase(it); // 删除之后将会失效
}

二、list迭代器失效

失效规则:

  • 插入操作(insert)和接合操作(splice)不会造成原有的list迭代器失效
  • 删除操作(erase)只有指向被删除元素的那个迭代器失效,其他迭代器不受影响

list不是线性存储的,链式存储的好处就是方便增加和删除,不会影响原有的内存空间。

三、deque迭代器失效

失效规则:

  • 在deque容器首部或者尾部插入元素不会使得任何迭代器失效,vs环境下测试会失效,但是g++没有失效
  • 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效
  • 在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效
// 首尾添加元素,在vs环境下测试失效,g++正常
void deque_f() {
    deque<int> q;
    deque<int>::iterator it;

    q.push_back(100);
    q.push_back(200);
    q.push_back(300);
    q.push_back(400);

    for (it = q.begin(); it != q.end(); it++) {
        cout << *it << endl;
        if (*it == 200) // 不会失效
            q.push_front(50);
        if (*it == 300) // 不会失效,且最后会输出500
            q.push_back(500);
    }

}

四、map和set

map和set底层都是红黑树,不是线性存储,它们的失效规则:

  • 执行插入操作时,原有的迭代器不会失效
  • 删除操作时,其他的迭代器不会失效,被删除元素的迭代器失效

STL中的mapset默认时不支持存结构体的,如果要添加结构体的支持,必须手动重载<运算符。

原因:map和set底层都是通过红黑树实现的,红黑树搜索树的一种,插入数据时要比较大小,所以结构体必须重载小于号

示例:

#include <iostream>
#include <string>
#include <set>

using namespace std;

typedef struct stu_st {
    string name;
    int age;
}stu_t;

int main() {
    set<stu_t> stu_infos;

    stu_t a, b;
    a.name = "xiaoming";
    a.age = 20;

    b.name = "xiaohua";
    b.age = 21;

    stu_infos.insert(a);
    stu_infos.insert(b);

    cout << stu_infos.size() << endl;

    return 0;
}

以上代码在vs下编译报错:

问题很明确,没有重载<符号,添加上以下代码即可:

bool operator<(const stu_t& a, const stu_t& b) {
    return a.name < b.name;
}

使用map要添加头文件#include <map>,命名空间using namespace std

初始化一个map:

map<int, bool> m1; 
map<int, const char *> m2; 

对于C++11,还可以在初始化时设定一系列初始值:

map<const char *, int> age_map = {
    pair<const char *, int>("maqian", 22),
    pair<const char *, int>("xiaobai", 1)
};

基本用法:

// 插入元素
insert();
// 删除元素
erase();
// 元素是否存在
count():
// 得到k的值
map[k];

注意的是获取key的值的时候,如果不存在这个元素,map将会自动增加一个当前key的元素。

int main() {
    map<int, bool> m1;
    map<int, const char *> m2;

    map<const char *, int> age_map = {
        pair<const char *, int>("maqian", 22),
        pair<const char *, int>("xiaobai", 1)
    };

    age_map.insert(
        pair<const char *, int>("zhouzhou", 3)
    );

    printf("count: %d, name: %s, age: %d\n", age_map.size(), "maqian", age_map["maqian"]);

    age_map.erase("maqian");
    printf("count: %d, name: %s, age: %d\n", age_map.size(), "maqian", age_map["maqian"]);


    return 0;
}
count: 3, name: maqian, age: 22
count: 3, name: maqian, age: 0
因此,注意不要通过map[x]来判断元素是否存在,使用count()方法。

STL中的string类有两个方法size()length()用来返回字符串的长度。 两者在实现上没有区别:

> sed -n 907,918p /usr/include/c++/7/bits/basic_string.h 
      // Capacity:
      ///  Returns the number of characters in the string, not including any
      ///  null-termination.
      size_type
      size() const _GLIBCXX_NOEXCEPT
      { return _M_string_length; }

      ///  Returns the number of characters in the string, not including any
      ///  null-termination.
      size_type
      length() const _GLIBCXX_NOEXCEPT
      { return _M_string_length; }

reserve方法用来给vector预留空间,预留的空间只会改变capacity的大小,不会改变size大小。resize方法表示重新调整数组大小,capacity和size都会改变。

使用reserve后,不能直接使用下标来增加元素,虽然内存是已经分配了直接使用不会报错,但是直接通过下标来复制会导致其他参数得不到更新(如size),会导致意想不到的错误。如以下代码:

int i;
vector<int> v;
v.reserve(10);
cout << "cap: " << v.capacity() << ", size: " << v.size() << endl;
for (i = 0; i < 10; i++) {
    v[i] = i;
}
cout << "cap: " << v.capacity() << ", size: " << v.size() << endl;

输出:

cap: 10, size: 0
cap: 10, size: 0

通过下标给数组中的每个元素复制,实际上本身数组的长度并没有得到增长,一旦再执行push_back就会导致前面的数据被覆盖。正确的方式是使用push_back或者insert方法插入元素。

STL标准类型vector(一):vector的基本用法

一、vector介绍

标准库类型vector用来表示对象的集合,其中所有对象的类型都相同且不固定长度,常被称为“动态数组”。

它并不是一个标准的数据类型,而是一个类模板用来实例其他对象,也被称为容器

使用它需要包含一下头文件和使用声明:

# include <vector>
using std::vector

vector可以包含任意数据类型,但是因为引用不是一个对象,所以vector不能实例引用对象。

vector<int> iVec; // int对象
vector<string> sVec; // string对象
// vector中的对象可以是vector对象
vector<vector<int>> arrVec;

在早期的C++标准中,如果vector的元素还是vector类型的对象,必须在最外层vector对象的右尖括号前添加一个空格,即vector<vector<int> >

因为>>是C++中的操作符,如果使用vector<vector>,最后边的>>会被编译器当作输出操作符。

二、定义和初始化vector对象

定义vector对象的常用方法:

语法说明
vector<T> v1v1是一个空的vector,元素类型为T,每个元素都会被默认初始化
vector<T> v2(v1), vector<T> v2 = v1使用v1初始化v2,v2中包含所有v1的副本
vector<T> v3(n, val)v3包含n个重复的元素val
vector<T> v4(n)v4包含n个元素,每个元素的值都被默认初始化
vector<T> v5{a, b, c}, vector<T> v5 = {a, b, c}使用列表初始化,包含a, b, c三个元素

初始化时要十分小心的地方是(){}的区别:使用圆括号表示用多少个值来构造初始化,而使用方括号一般表示使用什么值来列表初始化

vector<int> v1(10); // 10个元素,每个都初始化为0
vector<int> v2{10}; // 一个元素,值是10

vector<int> v3(10, 2); // 10个元素,每个元素的值为2
vector<int> v4{10, 2}; // 有两个元素分别为10和2

一种例外的情况是,如果使用了花括号但是又不能使用列表初始化的时候,会考虑使用构造的方式来初始化。

vector<string> v1{"h1"}; // 一个元素h1
vector<string> v2("h2"); // 错误,不能使用字面值构造初始化

vector<string> v3{10};  // 10是int类型,不能初始化string对象
                        // 此时会进行构造初始化,v3包含10个空字符串
vector<string> v4{10, "h1"};  // 包含10元素,值为"h1"

三、vector操作

vector的常用操作:

语法说明
v.empty()判断是否为空
v.push_back(t)在最后添加一个元素t
v.pop_back()删除最后一个元素
v.size()返回容器大小
v[n]访问第n个元素
v1 = v2赋值操作,用v2的元素替换v1的元素
v1 == v2, v1 != v2判断相等和不等
<, <=, >=, >比较大小

vector的empty()size()方法和string完全一致,一个判断是否为空,一个判断大小。

但是vector的size()返回值和string不同,string是string::size_type,而vector是vector<T>::size_typeT指元素类型。

3.1 push_back

push_back()用于在末尾添加元素,类似于python里面的append方法。

一个简单的例子:

string word;
vector<string> text;
while(cin >> word)
{
    text.push_back(word);
}

C++标准要求vector能在运行的时候高效地添加元素,因此在定义对象是指定大小是没有必要的,事实上这么做性能可能更差。

3.2 比较

vector进行比较时要求值必须能够比较,也就是重载了相应的运算符才行。

比较时只有两个vector的长度相同且对应位置元素也相同两者才相等,如果长度不等会根据位置从头开始比较下去,直到一方的结束或者某个位置的元素大小不一致时才会会返回。

四、下标索引及遍历

4.1 使用下标遍历

vector也能通过下标进行索引,和数组以及string类型里面的下标一致。

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

int main(){
    const vector<int> v1(10, 1);
    int i = 0;
    for (; i < v1.size(); i++){
        cout << v1[i] << endl;
    }
    return 0;
}

可以使用下标访问元素,但是不能使用下标添加元素,也就是不能使用下标访问不存在的元素。

3.2 使用范围for循环遍历

C++11的新特性范围for循环遍历

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

int main(){
    vector<int> v1;
    for (int i = 0; i < 10; i ++)
        v1.push_back(i);
    for (auto &i : v1)
        i *= i;
    for (auto i : v1)
        cout << i << " ";
    cout << endl;

    return 0;
}
> g++ main.cpp 
> ./a.out 
0 1 4 9 16 25 36 49 64 81

4.2 使用迭代器遍历

把上面的范围for循环输出改成使用迭代器遍历,也能输出同样的结果:

int main(){
    ...
    vector<int>::iterator it;
    for (it = v1.begin(); it != v1.end(); it++)
        cout << *it << " ";
    cout << endl;
}