C++打怪升级(十)- STL之vector

~~~~

  • 前言
  • 1. vector 是什么
  • 2. 见见vector的常用接口函数吧
    • 构造函数
      • 无参构造函数
      • 使用n个val构造
      • 拷贝构造
      • 使用迭代器范围构造
      • 初始化形参列表构造
    • 析构函数
    • 赋值运算符重载函数
    • 元素访问
      • []运算符重载函数访问
      • at函数访问
      • front函数
      • back函数
    • 迭代器相关
      • 正向迭代器
      • 反向迭代器
    • 容量相关
      • size函数
      • capacity函数
      • empty函数
      • reserve函数
      • resize函数
    • 增删查改相关
      • push_back函数
      • pop_back
      • assign函数
      • insert函数
      • erase函数
      • clear函数
      • swap函数
      • emplace函数
    • 非成员函数 - 关系运算符重载函数
      • ==
      • !=
      • <
      • <=
      • >
      • >=
    • 非成员函数 - 其他函数
      • swap函数
  • 3. 揭开vector的真面目-模拟实现吧
    • 底层成员变量
    • 构造函数
      • 默认无参构造
      • 使用n个val构造
      • 使用迭代器范围构造
    • 拷贝构造函数
    • 赋值运算符重载
    • 析构函数
    • 迭代器相关
      • 迭代器的实现 - 原生指针
      • 正向迭代器
    • 元素访问
      • operator[]
    • 容量相关
      • size函数
      • capacity函数
      • reserve函数
      • resize函数
      • empty函数
    • 增删查改相关
      • push_back函数
      • insert函数
        • 插入一个元素
        • insert的迭代器失效问题
        • 插入多个元素
      • erase函数
        • erase的迭代器失效问题
      • assign函数
    • 非成员函数 - swap函数
  • 4. vector的一些深拷贝问题
  • 结语

在这里插入图片描述

前言

本节将要介绍STL容器中的vector的常用接口函数,简单模拟实现一下vector。


1. vector 是什么

vector可变大小的顺序容器,类似于数组,存放元素的空间是连续的,所以可以实现类似于数组的随机访问;
相对于其他容器deque、list、forward_list,vector的优势:

  • 尾插和尾删操作相对高效,访问元素时也是相当高效;

劣势:

  • 头插和中间插入元素、头删和中间删除元素的操作由于涉及到大量元素的移动,效率很低;
  • vector的空间可以动态的开辟,开辟空间是存在系统资源的消耗的,所以vector不是每新增一个元素就重新开辟空间,而是再空间满时开辟适当的空间,具体开辟多少空间取决于不同版本实现者的考虑。虽然每次开辟的空间没有具体要求,但是需要保证开辟空间是以对数间隔进行的,以便于每次在结束位置插入数据时vector能提供均摊的常数时间复杂度;
  • 与string的元素类型是char不同,vector是一个类模板,这意味着它可以接受任意类型的参数,存放任意类型的元素;

2. 见见vector的常用接口函数吧

构造函数

无参构造函数

函数声明

vector();

例子

vector<int> v;

注意不能写成vector<int> v();,定义初始化时,如果是无参构造或是全缺省的构造不加括号(),否则编译器会把这句代码识别成无参、返回类型是vector<int>的函数声明;只有这句代码并不会报错,但当你把v当做vector<int>类型的变量去使用时就会出现问题,因为编译器认为v是一个函数指针;

使用n个val构造

声明

vector(size_t n, const value_type& val = value_type());

eg

vector<char> v1(3, 'x'); // 结果为 xxx,没有'\0'
vector<int> v2(3, 7) 	 // 结果为 777

拷贝构造

声明

vector (const vector& x);

例子

// 5个9构造vector<int>类型对象 v1
vector<int> v1(5, 9);

vector<int> v2(v1);

使用迭代器范围构造

声明
两个参数first和last都是迭代器类型,且该函数时函数模板,迭代器类型可以指定,即可以使用其他顺序容器的迭代器构造vector;

template <class InputIterator>
 vector (InputIterator first, InputIterator last);

eg

vector<int> v1(5, 6); // 66666

vector<int>v2(v1.begin(), v1,begin() + 3); // 666

初始化形参列表构造

声明
initializer_list是一个类模板,使用由大括号括起来的以逗号分隔的元素构造initializer_list对象;

vector (initializer_list<value_type> il);

eg

vector<int> v1{1,2,3,4,5}; // 1 2 3 4 5
vector<int> v2 = {3,4,5,6,7); // 3 4 5 6 7

析构函数

声明
在vector对象销毁之前调用,释放vector对象申请的资源;

~vector();

赋值运算符重载函数

声明

vector& operator= (const vector& x);

eg
直接vector v2 = v1; 使用会被编译器当做定义初始化而调用拷贝构造函数,而不是赋值运算符重载函数;

vector<int> v1(5, 6); // 66666
vector<int> v2;

v2 = v1; // 66666

元素访问

[]运算符重载函数访问

声明
[]运算符重载函数会在内部对传入的坐标参数进行严格检查,超出有效坐标范围将会报错,这与系统的抽查不同;
[]运算符重载函数既需要满足读也要满足写,所以实现了非const版本和const版本;

reference operator[] (size_type n);
const_reference operator[] (size_type n) const;

eg

非const

vector<int> v{ 1,2,3,4,5 };
for (int i = 0; i < v.size(); ++i) {
    v[i]++;
    cout << v[i] << " "; // 结果为2 3 4 5 6
}
cout << endl;

const

const vector<int> v{ 1,2,3,4,5 };
for (int i = 0; i < v.size(); ++i) {
    //v[i]++; 不能修改const对象成员
    cout << v[i] << " "; // 结果为1 2 3 4 5
}
cout << endl;

at函数访问

声明
at也会对传入的坐标参数进行判断,超出坐标范围后会抛异常,而不是报错;

reference at (size_type n);
const_reference at (size_type n) const;

eg

非const

vector<int> v{ 1,2,3,4,5 };
for (int i = 0; i < v.size(); ++i) {
    v.at(i)++;
    cout << v.at(i) << " ";
}
cout << endl;

const

const vector<int> v{ 1,2,3,4,5 };
for (int i = 0; i < v.size(); ++i) {
    cout << v.at(i) << " ";
}
cout << endl;

front函数

声明
返回第一个元素的引用

reference front();
const_reference front() const;

eg

vector<int> v{1,2,3,4};
v.first(); // 结果为 1

back函数

声明
返回最后一个元素的引用

reference back();
const_reference back() const;

eg

vector<int> v{1,2,3};
v.back(); // 结果为 3

迭代器相关

迭代器是像指针一样的类型,使用时需要解引用*

正向迭代器

定义
包括非const和const迭代器;

返回vector对象内元素起始位置的迭代器

iterator begin() noexcept;
const_iterator begin() const noexcept;

返回vector对象内元素结束位置的下一个位置的迭代器

iterator end() noexcept;
const_iterator end() const noexcept;

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int>::iterator it = v1.begin();
while (it != v1.end()) {
    cout << *it << " "; // 结果为 1 2 3 4 5
    it++;
}
cout << endl;
const vector<int> v2{ 6,7,8,9,0 };
vector<int>::const_iterator cit = v2.begin();
while (cit != v2.end()) {
    cout << *cit << " "; // 结果为6 7 8 9 0
    cit++;
}
cout << endl;

反向迭代器

定义

返回vector对象内最后一个元素的所在位置的迭代器

reverse_iterator rbegin() noexcept;
const_reverse_iterator rbegin() const noexcept;

返回vector对象内第一个位置的元素的前一个理论位置的迭代器

reverse_iterator rend() noexcept;
const_reverse_iterator rend() const noexcept;

eg

容量相关

size函数

声明
返回vector对象的元素个数

size_type size() const noexcept;

eg

vector<int> v{1,2,3,4,5};
v.size(); // 结果为 5

capacity函数

声明
返回vector对象的引用

size_type capacity() const noexcept;

eg

vector<int> v{1,2,3,4,5};
v.capacity(); // 结果为 5

empty函数

声明
判断vector对象是否为空,是则返回true;否则返回false

bool empty() const noexcept;

eg

vector<int> v1; // 空
vector<int> v2(2, 3); // 33

v1.empty(); // true
v2.empty(); // false

reserve函数

声明
参数size_type n,表示要申请开辟的容量大小;
改变vector对象的容量capacity,当n小于等于当前vector对象的容量capacity时,reserve函数什么也不做;
只有当n大于vector对象的容量时,reserve函数才会向系统申请开辟空间;即reserve函数一般只进行扩容处理,不缩容;

void reserve (size_type n);

eg

vector<int> v{ 1,2,3,4,5 }; // capacity为 5

v.reserve(3); // capacity为 5

v.reserve(5); // capacity为 5

v.reserve(10); // capacity为 10

resize函数

声明
参数size_t n表示vector对象有效元素的个数;
如果n小于vector原有的size,相当于删除元素直到size为n;
如果n等于vector原有size就什么也不做;
如果n大于vector原有size且小于等于vector的原有capacity,相当于尾插元素直到size为n;
如果n大于vector原有的capacity,相当于先扩容使容量增大,再尾插元素直到size为n;

void resize (size_type n);
void resize (size_type n, const value_type& val);

eg

vector<int> v2{1,2,3,4,5};
	v2.reserve(10);
	cout << v2.size() << endl;
	cout << v2.capacity() << endl;
	print(v2);

	v2.resize(3);
	cout << v2.size() << endl;
	cout << v2.capacity() << endl;
	print(v2);

	v2.resize(8, 1);
	cout << v2.size() << endl;
	cout << v2.capacity() << endl;
	print(v2);

	v2.resize(15, 2);
	cout << v2.size() << endl;
	cout << v2.capacity() << endl;
	print(v2);

image.png

增删查改相关

push_back函数

声明
在vector对象内最后一个元素的末尾增加一个元素,增加的元素参数val的拷贝;

void push_back (const value_type& val);
void push_back (value_type&& val);

eg

vector<int> v;
for (int i = 0; i < 5; ++i) {
    v.push_back(i);
}
// v的内容为 0 1 2 3 4 

pop_back

声明
删除vecrot的最后一个元素,但是debug模式下,当vector为空时调用pop_back尾删元素时程序会出错;

void push_back (const value_type& val);
void push_back (value_type&& val);

eg

vector<int> v;

for (int i = 0; i < 5; ++i) {
    v.push_back(i);
}
// v的内容为 0 1 2 3 4 
for (int i = 0; i < 5; ++i) {
    v.pop_back();
}
// v的内容为 空

assign函数

声明
使用参数迭代器first和last表示元素的起始位置和结束位置;
使用迭代器范围内的元素为vector赋值,同时会把vector原先的内容去除;
这个范围是左闭右开的区间[first, last]

// range (1)
template <class InputIterator>
void assign (InputIterator first, InputIterator last);

eg

vector<int> v1{ 1,2,3,4 };
vector<int> v2;
v2.assign(v1.begin(), v1.end()); // 结果为 1 2 3 4

声明
使用size_t类型的nvalue_type类型的val为vector赋值;

// fill (2)	
void assign (size_type n, const value_type& val);

eg

vector<int> v1{ 1,2,3,4 };
vector<int> v2;
v2.assign(3, 5); // 结果为 555

声明
使用初始化列表对vector对象进行赋值

// initializer list (3)	
void assign (initializer_list<value_type> il);

eg

vector<int> v2;
v2.assign({ 1,2,3,4,5 }); // 结果为 1 2 3 4 5

insert函数

声明
参数const_iterator pos是vector内的元素对应位置的迭代器;
参数const value_type是待插入的值;
在迭代器位置pos之前插入一个元素,该元素是val的拷贝;

// single element (1)
iterator insert (const_iterator pos, const value_type& val);

eg

vector<int> v3{ 1,2,3,4,5 }; 
v3.insert(v3.begin() + 3, 9); // 结果为 1 2 3 9 4 5

声明
参数const_iterator pos是vector内的元素对应位置的迭代器;
参数size_type n是插入元素的个数;
参数const value_type是待插入的值;
在迭代器位置pos之前插入n个元素;

// fill (2)	
iterator insert (const_iterator pos, size_type n, const value_type& val);

eg

vector<int> v3{ 1,2,3,4,5 }; 
v3.insert(v3.begin() + 3, 59); // 结果为 1 2 3 9 9 9 9 9 4 5

声明
参数const_iterator pos是vector内的元素对应位置的迭代器;
参数first、last是迭代器范围,first是起始位置,last是结束位置;
在迭代器pos位置之前依次插入[first, last)范围内的所有元素;

// range (3)	
template <class InputIterator>
iterator insert (const_iterator pos, InputIterator first, InputIterator last);

eg

vector<int> v1{ 1,2,3,4,5 };

vector<int> v2{ 1,2,3,4,5 };
v4.insert(v2.begin()+ 4, v1.begin(), v1.begin() + 3); // 结果为 1 2 3 4 1 2 3 5

声明

// initializer list (5)	
iterator insert (const_iterator pos, initializer_list<value_type> il);

eg

vector<int> v{ 1, 2, 3, 4 };
v.insert(v.begin(), { 2, 3 ,3 }); // 结果是 1 2 3 2 3 3 4

erase函数

声明
参数const_iterator pos表示vector内元素所在位置的迭代器;
删除pos位置的元素;

iterator erase (const_iterator pos);

eg

vector<int> v{ 1,2,3,4,5 };
v.erase(v.begin() + 2);

声明
参数first是待删除范围的起始元素的位置;
参数last是待删除范围的最后一个元素的下一个位置;
删除迭代器范围内的所有元素;

iterator erase (const_iterator first, const_iterator last);

eg

vector<int> v{ 1,2,3,4,5 };
v.erase(v1.begin() + 1, v.begin() + 4);

clear函数

声明
清空vector对象,删除所有元素;

void clear() noexcept;

eg

vector<int> v2{ 9, 9, 9, 9, 9 }; // size = 5, capacity = 5

v2.clear(); // size = 0, capacity = 5

swap函数

声明
与另一个vector对象进行内容的交换,也包括size和capacity;

void swap (vector& x);

eg

vector<int> vv1{ 1,2,3,4,5 };   // size=5, capacity=5;
vector<int> vv2{ 6, 6, 6 }; 	// size=3, capacity=3;

vv1.swap(vv2); 
// vv1 -> 内容:6 6 6       size=3, capacity=3; 
// vv2 -> 内容:1 2 3 4 5   size=5, capacity=5

emplace函数

声明
参数pos是vector的元素对应位置的迭代器
参数args是待插入的元素
pos位置之前插入元素,类似于insert

template <class... Args>
iterator emplace (const_iterator pos, Args&&... args);

eg

vector<int> vvv1{ 1, 2, 3, 4, 5 };
vvv1.emplace(vvv1.begin() + 3, 233); // 1 2 3 233 4 5
vvv1.emplace(vvv1.end(), 1145); 	 // 1 2 3 233 4 5 1145 

非成员函数 - 关系运算符重载函数

==

声明
比较两个vector对象v1, v2的内容是否相同;
比较过程:先比较v1和v2的size是否相等,不相等直接不相等;相等就继续顺序比较二者的元素,如果存在一对不相等的元素就是不相等;元素都相等时才是相等;
不比较二者的cappacity;

template <class T>
bool operator==(const vector<T>& v1, const vector<T>& v2);

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
v1 == v2;		// true
v1.reserve(10);
v1 == v2;       // true
v1.resize(3);
v1 == v2;       // false

image.png

!=

声明

template <class T>
bool operator!=(const vector<T>& v1, const vector<T>& v2);

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
v1 != v2;		// false

<

声明

template <class T>
bool operator<(const vector<T>& v1, const vector<T>& v2);

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
v1 < v2; // false

<=

声明

template <class T>
bool operator<=(const vector<T>& v1, const vector<T>& v2);

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
v1 <= v2; // true

>

声明

template <class T>
bool operator>(const vector<T>& v1, const vector<T>& v2);

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
v1 > v2; // false

>=

声明

template <class T>
bool operator>=(const vector<T>& v1, const vector<T>& v2);

eg

vector<int> v1{ 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
v1 >= v2; // true

非成员函数 - 其他函数

swap函数

声明
与另一个vector对象进行内容的交换,也包括size和capacity;
与成员函数swap相比,第一个参数不再是隐式的this了;

template <class T>
  void swap (vector<T>& x, vector<T>& y);

eg

vector<int> v1{6, 6, 6, 6};   // size=4, capacity=4
vector<int> v2{1, 2, 3, 4, 5} // size=5, capacity=5
swap(v1, v2);
// v1 -> 1,2,3,4,5  size=5, capacity=5
// v2 -> 6,6,6,6    size=4, capacity=4

3. 揭开vector的真面目-模拟实现吧

底层成员变量

vector类模板的底层设计中包含三个T类型的指针:
_start指向空间的起始地址,也是第一个元素的地址;
_finish指向最后一个元素的下一个位置;
_end_of_storage指向空间的结尾位置的下一个位置;

template<class T>
class vector {
public:
private:
	T* _start;
	T* _finish;
	T* _end_of_storage;
};

image.png

构造函数

默认无参构造

不传参数时把三个指针都初始化为nullptr

vector()
    :_start(nullptr)
    ,_finish(nullptr)
    ,_end_of_storage(nullptr){ }

使用n个val构造

new申请的空间大小是n*sizeof(T),别忘记类型T的大小;

vector(size_t n, const T& val = T()) {
    _start = new T[n * sizeof(T)];
    _end_of_storage = _finish = _start + n;
    for (int i = 0; i < _finish - _start; ++i) {
        _start[i] = val;
    }
}

与上面的函数都是使用n个val构造,只是上面函数n的类型是size_t,而本函数n的类型是int
对于vector<int> v(5, 1)这句代码,编译器将会调用vector(InputIterator first, InputIterator last)函数,而不是vector(size_t n, const T& val = T())
首先模板参数T实例化为int,而5和1默认作为int类型参数处理,两个参数类型一致,编译器会把vector(InputIterator first, InputIterator last)的InputIterator模板参数实例化为int,即vector(int first,int last)从而更符合参数5和1的类型,但是5和1不是迭代器范围,编译时报错;
所以需要额外实现函数vector(int n, const T& val = T())可以直接匹配5和1参数;
对于vector<int> v(5, 'a')等情况则仍将会正常调用vector(size_t n, const T& val)函数的;

vector(int n, const T& val = T()) {
    _start = new T[n * sizeof(T)];
    _end_of_storage = _finish = _start + n;
    for (int i = 0; i < _finish - _start; ++i) {
        _start[i] = val;
    }
}

使用迭代器范围构造

template<class InputIterator>
vector(InputIterator first, InputIterator last) {
    size_t n = last - first;
    reserve(n);
    iterator it = begin();
    while (first != last) {
        (*it) = (*first);
        it++;
        first++;
    }
    _finish = _start + n;
}

拷贝构造函数

vector(const vector<T>& v)
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr) {
    reserve(v.capacity());
    iterator it = begin();
    const_iterator cit = v.begin();
    while (cit != v.end()) {
        *it = *cit;
        ++it;
        ++cit;
    }
    _finish = it;
}

image.png

vector(const vector<T>& v) 
    :_start(nullptr)
    ,_finish(nullptr)
    ,_end_of_storage(nullptr){
    T* tmp = new T[v.capacity()];
    for (int i = 0; i < v.size(); ++i) {
        tmp[i] = v[i];
    }
    _start = tmp;
    _finish = _start + v.size();
    _end_of_storage = _start + v.capacity();
}

赋值运算符重载

现代写法,提高了代码书写效率
开空间交给了拷贝构造函数,而不是我们再次实现;

vector<T>& operator=(vector<T> v) {
    swap(v);
    return *this;
}

传统写法

vector<T>& operator=(const vector<T>& v) {
    // 不能自己给自己赋值
    if (*this != v) {
        T* tmp = new T[v.capacity()];
        for (int i = 0; i < v.size(); ++i) {
            tmp[i] = v[i];
        }
        //
        delete[] _start;
        _start = tmp;
        _finish = _start + v.size();
        _end_of_storage = _start + v.capacity();
    }
    return *this;
}

析构函数

~vector() {
    if (_start) {
        delete[] _start;
        _start = _finish = _end_of_storage = nullptr;
    }
}

迭代器相关

迭代器的实现 - 原生指针

typedef T* iterator;
typedef const T* const_iterator;

迭代器的返回类型,传值返回还是传引用返回?
对于非const版本,都可以,一般传值返回即可;
对于const版本,只能传值返回;因为引用返回会导致权限的放大,this指针类型是vector<T>*const,是指针常量,const修饰时变为vector<T> const * const,常量指针常量,函数返回类型const_iterator&const T* &,返回指针变量_start是this指针指向的内容,故类型是T* const,所以由T*constconst T *将会出错;

正向迭代器

非const版本

begin返回指向开始元素的迭代器,即_start
end返回指向最后一个元素的下一个位置的迭代器,即_finish

image.png

iterator begin() {
    return _start;
}
iterator end() {
    return _finish;
}

const版本

const_iterator begin() const{
    return _start;
}
const_iterator end() const {
    return _finish;
}

元素访问

operator[]

访问pos位置的元素,注意需要对pos进行检查,防止越界访问;

非const版本

T& operator[](size_t pos) {
    assert(pos < size());
    return _start[pos];
}

const版本

const T& operator[](size_t pos) const {
    assert(pos < size());
    return _start[pos];
}

容量相关

size函数

vector没有直接记录元素个数的变量,通过_finish - _start指针相减计算;

size_t size() const {
    return _finish - _start;
}

capacity函数

vector没有直接记录空间容量的变量,通过_finish - _start指针相减计算;

size_t capacity() const {
    return _end_of_storage - _start;
}

reserve函数

改变vector对象的容量capacity,
n小于等于当前vector对象的容量capacity时,reserve函数什么也不做;
只有当n大于vector对象的容量时,reserve函数才会向系统申请开辟空间;即reserve函数一般只进行扩容处理,不缩容;

void reserve(size_t n) {
    if(n <= capacity()) return;
    T* tmp = new T[n];
    for (int i = 0; i < size(); ++i) {
        tmp[i] = _start[i];
    }
    // 保存旧空间,防止_start更新后直接调用size()函数导致计算错误,
    // 此时_start是新的,而_finish还是旧的;
    int oldsize = size();
    delete[] _start;
    _start = tmp;
    _finish = _start + oldsize;
    _end_of_storage = _start + n;
}

resize函数

如果n小于vector原有的size,相当于删除元素直到size为n,_finish更新为_start + n即可;
如果n等于vector原有size就什么也不做;
如果n大于vector原有size且小于等于vector的原有capacity,相当于尾插元素直到size为n,即每次把val拷贝给_finish指向位置的元素,再++_finish直到_finish等于_start+n
如果n大于vector原有的capacity,相当于先扩容使容量增大,再尾插元素直到size为n;

void resize(size_t n, const T& val = T()) {
    // 小于 
    if (n < size()) {
        _finish = _start + n;
    }
    else if (n < capacity()) {
        while (_finish < _start + n) {
            *_finish = val;
            ++_finish;
        }
    }
    else {
        reserve(n);
        while (_finish < _start + n) {
            *_finish = val;
            ++_finish;
        }
    }
}

empty函数

bool empty() const {
    return size() == 0;
}

增删查改相关

push_back函数

dd

void push_back(const T& val) {
    if (_finish == _end_of_storage) {
        int newcapacity = _end_of_storage - _start == 0 ? 4 : 2 * (_end_of_storage - _start);
        reserve(newcapacity);
    }
    *_finish = val;
    _finish++;
}

insert函数

再pos迭代器位置之前插入元素

插入一个元素

image.png

iterator insert(iterator pos, const T& val) {
    assert(pos >= _start && pos < _finish);
    int len = pos - _start;
    if (_finish == _end_of_storage) {
        int newcapacity = capacity() == 0 ? 4 : 2 * capacity();
        reserve(newcapacity);
    }
    // update pos
    pos = _start + len;
    // move
    iterator end = _finish;
    while (end > pos) {
        *end = *(end - 1);
        end--;
    }
    
    *pos = val;
    ++_finish;
    return pos;
}
insert的迭代器失效问题

例子引入

vector<int> v(4, 3);
vector<int>::iterator pos = v.begin() + 1;
v.insert(pos, 9);
(*pos)++; // 野指针
vector<int> v(4, 3);
vector<int>::iterator pos = v.begin() + 1;
pos = v.insert(pos, 9);
(*pos)++; // 正确访问

传值传入插入位置的迭代器变量pos,在insert内部vector对象会先进行容量检查:

  • 如果容量满了就会进行扩容,这将会导致_start指向的空间变为扩容后新申请的空间,而pos还是指向旧空间的位置,此时pos是野指针,我们不能使用旧的pos了,需要pos指向新空间对应元素的位置后,再使用pos才能正确访问到元素;而外部调用insert函数后,作为参数的外部pos也失效了,其也指向了旧空间的位置,直接使用就是访问已经释放的空间,是非法访问内存,解决方法是外部pos接受insert的返回值新pos
  • 如果容量没满就无需扩容,内部pos不会失效,外部pos也不会失效;

为什么insert函数的参数iterator pos不使用引用类型iterator& pos

  • 诚然,使用引用后,外部的pos可以随着内部pos的更新而更新,不会失效了;但是此时形参pos只能是iterator的引用,遇到匿名对象等具有常性的迭代器时就会报错,导致权限放大问题即iterator 引用了 const_iterator对象,于是乎前人考虑的种种情况下,选择了insert更新后的pos作为返回值为外部提供,这样当外部想继续使用pos之前需要接收insert的参数即可。
插入多个元素
iterator insert(iterator pos, size_t n, const T& val) {
    assert(pos >= _start && pos < _finish);
    int len = pos - _start;
    if (size() + n > 2 * capacity()) {
        // 2
    }
    else if (size() + n > capacity()) {

    }
    pos = _start + len;
    // move
    iterator end = _finish;
    while (end > pos) {
        *(end + n - 1) = *(end - 1);
        end--;
    }

    iterator begin = pos;
    while (begin < pos + n) {
        *begin = val;
        ++begin;
    }
    _finish += n;
    return pos;
}

erase函数

删除pos位置的元素

iterator erase(iterator pos) {
    assert(pos >= _start && pos < _finish);
    iterator begin = pos + 1;
    while (begin < _finish) {
        *(begin - 1) = *begin;
        begin++;
    }
    _finish--;
    return pos;
}
erase的迭代器失效问题

例子引入:

vector<int> v = {1, 2, 3, 4}; //
vector<int>::iterator pos1 = v.end() - 1;
v.erase(pos1);
(*pos1)++; // 

vector<int>::iterator pos2 = v.begin();
v.erase(pos2);
(*pos2)++; //

这个例子的容器v分别执行了erase操作之后,迭代器pos1pos2你是否认为失效了呢?
这在g++和vs2019编译器下有着不同的结果:

  • g++编译器认为没有失效,erase操作之后pos2仍然指向第二个位置,此时该位置的元素是3,g++版本的STL的vector底层实现上迭代器是一个原生的指针即typedef T* iterator,即使我们使用了失效的迭代器也无法检查出来;
  • vs编译器会对迭代器进行强制检查,认为pos2是失效的,程序会直接报错。这是因为vs下STL的实现方式更加复杂,对于vector容器的迭代器来说,不是一个原生的指针,而是被进一步封装为一个类,在使用迭代器时会先进行检查是否失效,如果失效了程序就会报错,反之就正常执行;
    当前我们实现的版本更加接近g++的vector版本,是一个原生的指针,也就是说我们实现的版本在erase操作之后也无法对失效的迭代器进行有效检查;

那么到底该不该认为erase操作之后的迭代器失效呢?

  • 我的答案是不管编译器是否认为erase之后的迭代器是否失效,都认为迭代器是失效的,即每次erase之后及时更新迭代器,这样不管在g++编译器还是vs编译器下我们写的程序都是统一能跑的。

assign函数

为vector对象分配一个新值

一个迭代器范围[first, last)作为参数

template <class InputIterator>
void assign(InputIterator first, InputIterator last) {
    assert(first);
    assert(last);
    assert(first < last);
    if(_start) delete[] _start;
    _start = _finish = _end_of_storage = nullptr;
    while (first != last) {
        push_back(*first);
        first++;
    }
}

nval作为参数;
有两个版本,功能完全相同,只有参数n的类型不同,目的是为了防止特殊情况下导致的问题:
传入(5, 1)时期望是用5个int类型的1赋值给vector对象,但是在没有assign(int,const T&)函数时将会优先匹配assign(InputIterator first, InputIterator last)函数导致其内部解引用产生非法访问内存错误。

void assign(size_t n, const T& val) {
    vector<T> tmp(n, val);
    swap(tmp);
}
void assign(int n, const T& val) {
    vector<T> tmp(n, val);
    swap(tmp);
}

非成员函数 - swap函数

交换两个vector对象的内容

template <class T>
void swap(vector<T>& x, vector<T>& y) {
	x.swap(y);
}

4. vector的一些深拷贝问题

reserve函数是进行扩容的函数,其中涉及到开辟新空间,再把旧空间内容拷贝到新空间,最后释放旧空间的过程。
对于把内容从旧空间拷贝到新空间的方式,对于内置类型进行值拷贝就行;对于自定义类型特别是涉及资源管理的自定义类型来说,不能简单的进行值拷贝如使用memcpy函数,而是需要进行深拷贝,调用赋值运算符重载函数,前提是该自定义类型内部已经显式实现了深拷贝功能。
image.png
使用memcpy函数拷贝包含资源管理的自定义类型时发生的问题
以vector类为例,string类内部包含的指针指向了堆上的可变的字符数组,是需要管理的空间资源。其作为vector的元素在扩容时将会涉及到vector本身的深拷贝和string内部的深拷贝,也就是两层深拷贝。
image.png
image.png
image.png
image.png
解决方法:赋值操作,当是自定义类型时将会调用该自定义类型的赋值运算符重载函数完成深拷贝操作
image.png


结语

本节主要介绍了STL容器部分的vector,对于接口函数的学习先使我们整体了解vector如何使用。之后深入探究vector底层的实现方式,对比的是SGI版本,其中需要着重注意的是使用inserterase导致的迭代器失效问题,实现vectorresere函数时考虑到涉及到资源管理的自定义类型需要深拷贝的情况。


T h e The The E n d End End

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/123955.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Head First Java 第二版

不管你的程序有多大&#xff0c;一定都会有一个main()来作为程序的起点。Java是强类型语言。float f23.5f 如果不加上f&#xff0c;就会被Java当做double处理。对于任意一个Java虚拟机来说&#xff0c;所有的引用大小都一样&#xff0c;但是不同的Java虚拟机可能会以不同的方…

多元高斯分布

下面我们来看一下多元高斯分布&#xff0c;叫做 multivariative 高斯分布&#xff0c;也就是目前的情况是向量的形式&#xff0c;也就是说我的 x 它是一个向量&#xff0c;那这个情况下我们的高斯分布应该怎么去表示&#xff1f;我们这里面重点还是来看一下它的一个表示的方法&…

golang 2018,go 1.19安装Gin

GOPROXYhttps://mirrors.aliyun.com/goproxy/ 一致提示URL不能有点&#xff0c;给我整郁闷了&#xff0c;换了这个地址好了 但是一致提示zip的包问题&#xff0c;最后还是不行又换回七牛 NEWBEE&#xff01; [GIN-debug] Environment variable PORT is undefined. Using por…

BIM、建筑机器人、隧道工程施工关键技术

一、BIM简介 &#xff08;一&#xff09;BIM概念 BIM&#xff08;Building Information Modeling&#xff09;&#xff0c;建筑信息模型。该技术通过数字化手段&#xff0c;在计算机中建立虚拟建筑&#xff0c;该虚拟建筑提供从单一到完整、包含逻辑关系的建筑信息库。信息库…

ZZ308 物联网应用与服务赛题第B套

2023年全国职业院校技能大赛 中职组 物联网应用与服务 任 务 书 &#xff08;B卷&#xff09; 赛位号&#xff1a;______________ 竞赛须知 一、注意事项 1.检查硬件设备、电脑设备是否正常。检查竞赛所需的各项设备、软件和竞赛材料等&#xff1b; 2.竞赛任务中所使用的…

Android codec2 视频框架 之输入buffer

文章目录 输入端的内存管理输入数据包buffer结构体的转换 主要的流程如上&#xff0c; 申请内存在CCodecBufferChannel&#xff0c;申请之后回调到MediaCodec。然后应用从MediaCodec获取 将解码数据放到buffer中&#xff0c;CCodecBufferChannel在将这块buffer 送到componet模块…

IS-LM模型:从失衡到均衡的模拟

IS-LM模型&#xff1a;从失衡到均衡的模拟 文章目录 IS-LM模型&#xff1a;从失衡到均衡的模拟[toc] 1 I S − L M 1 IS-LM 1IS−LM模型2 数值模拟2.1 长期均衡解2.2 政府部门引入2.3 价格水平影响2.4 随机扰动因素 1 I S − L M 1 IS-LM 1IS−LM模型 I S − L M IS-LM IS−LM是…

51单片机汇编-点亮一个led

文章目录 前言1.打开IDE2.设置编辑器3.设置输出4. 原理图5.编写代码6 编译7.下载8.其它代码1.LED闪烁2.跑马灯 前言 51单片机基础 51汇编实战 本章主要介绍打开一个led,具体采用51汇编 1.打开IDE 选择STC89C52RC 后缀是.asm 2.设置编辑器 3.设置输出 4. 原理图 5.编写代码 …

HR人才测评,采用线上测评做春招秋招

从人力资源管理的工作&#xff0c;已经有好些年了&#xff0c;我只想说这不是一个有创意和创造性的工作&#xff0c;因为大部分时间我都在从事数据方面的工作。关于公司内部的文案工作先且不说&#xff0c;这里分享下我做招聘工作的过程。 每年春秋两季的校招&#xff0c;算是…

通过51单片机控制SG90舵机按角度正反转转动

一、前言 本文介绍如何通过51单片机控制SG90舵机实现角度的正反转转动。SG90舵机是一种常用的微型舵机&#xff0c;具有体积小、重量轻、结构简单等特点&#xff0c;被广泛应用于机器人、遥控模型和各种自动控制系统中。 使用51单片机&#xff08;STC89C52&#xff09;作为控…

5-爬虫-打码平台、打码平台自动登录打码平台、selenium爬取京东商品信息、scrapy介绍安装、scrapy目录结构

1 打码平台 1.1 案例 2 打码平台自动登录打码平台 3 selenium爬取京东商品信息 4 scrapy介绍安装 5 scrapy目录结构 1 打码平台 # 1 登录某些网站&#xff0c;会有验证码---》想自动破解-数字字母&#xff1a;python模块&#xff1a;ddddocr-计算题&#xff0c;成语题&#xf…

CSS3 边框、圆角、背景

CSS3是最新的CSS标准。CSS3被拆分为“模块”。一些最重要的CSS3模块如下&#xff1a;选择器、盒模型、背景和边框、文字特效、2D/3D转换、动画、多列布局、用户界面。 一、CSS3边框&#xff1a; 用CSS3&#xff0c;可以创建圆角边框、添加阴影框&#xff0c;并作为边界的形象而…

【单目测距】单目相机测距(三)

文章目录 一、前言二、测距代码2.1、地面有坡度2.2、python代码2.2.1、旋转矩阵转角度2.2.2、角度转旋转矩阵2.2.3、三维旋转原理 (Rotation 原理)2.2.4、完整代码 2.3、c 代码 一、前言 上篇博客【单目测距】单目相机测距&#xff08;二&#xff09; 有讲到当相机不是理想状态…

17.复制字符串 ,包括\0

#include<stdio.h> #include <cstring>int main(){int len1,len2;char s1[44];char s2[33];scanf("%s",s1);scanf("%s",s2);len1strlen(s1)1;printf("先s1的字符长度为&#xff1a;%d\n",len1) ;strcpy(s1,s2) ;printf("复制字…

前端AJAX入门到实战,学习前端框架前必会的(ajax+node.js+webpack+git)(三)

知者乐水&#xff0c;仁者乐山。 XMLHttpRequest AJAX原理 - XMLHttpRequest 前面与服务器交互使用的不是axios吗&#xff1f; ajax并不等于axios 我们使用的axios的内部&#xff0c;实际上对XHR对象/原理 的封装 为什么还要学习ajax&#xff1f; ①在一些静态网站项目中…

UPLOAD-LABS1

less1 (js验证) 我们上传PHP的发现不可以&#xff0c;只能是jpg&#xff0c;png&#xff0c;gif&#xff08;白名单限制了&#xff09; 我们可以直接去修改限制 在查看器中看到使用了onsubmit这个函数&#xff0c;触发了鼠标的单击事件&#xff0c;在表单提交后马上调用了re…

【Android】Dagger2 框架设计理念和使用方式详解

文章目录 Dagger 框架作用基本使用方法引入依赖创建 Object创建 Module创建 Component向 Activity 注入对象 Component 内部单例全局单例自定义 Scope关于单例作用域的理解注入多种同类型对象Component 依赖Component 继承传递 Activity Dagger 框架作用 这里&#xff0c;我们…

verilog 每日一练- 移位寄存器

module shift_1x64 (clk, shift,sr_in,sr_out,);input clk, shift;input sr_in;output sr_out;reg [63:0] sr;always(posedge clk)beginif (shift 1b1)beginsr[63:1] < sr[62:0];sr[0] < sr_in;endendassign sr_out sr[63];endmodule 这个Verilog模块 shift_1x64 实现了…

Leetcode—226.翻转二叉树【简单】

2023每日刷题&#xff08;二十四&#xff09; Leetcode—226.翻转二叉树 实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* …

分享vmware和Oracle VM VirtualBox虚拟机的区别,简述哪一个更适合我?

VMware和Oracle VM VirtualBox虚拟机的区别主要体现在以下几个方面&#xff1a; 首先两种软件的安装使用教程如下&#xff1a; 1&#xff1a;VMware ESXI 安装使用教程 2&#xff1a;Oracle VM VirtualBox安装使用教程 商业模式&#xff1a;VMware是一家商业公司&#xff0c;而…