向量 ( v e c t o r vector vector)是 C C C++ 标准模板库( S T L STL STL)中封装的顺序容器 ( S e q u e n c e C o n t a i n e r Sequence\ Container Sequence Container),是一个能够存放任意类型 的动态数组,能够增加和压缩数据。
研究数据的特定排序方式,以利于查找 或排序 或其他特殊目的,这一专门学科我们称为数据结构 ( D a t a S t r u c t u r e s Data\ Structures Data Structures)。几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。
S T L STL STL 容器即是将运用最广的一些数据结构实现出来:
a r r a y array array(数组 )、 l i s t list list(链表 )、 t r e e tree tree(树 )、 s t a c k stack stack(堆栈 )、 q u e u e queue queue(队列 )、 h a s h t a b l e hashtable hashtable(哈希表 )、 s e t set set(集合 )、 m a p map map(映射 ) ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ······ ⋅⋅⋅⋅⋅⋅ 等等。
2. 容器的分类
根据 " " " 数据在容器中的排列 " " " 特性,将这些数据结构分为序列式 ( s e q u e n c e sequence sequence)和关联式 ( a s s o c i a t i v e associative associative)两种:
序列式容器( S e q u e n c e C o n t a i n e r s Sequence\ Containers Sequence Containers)
关联式容器( A s s o c i a t i v e C o n t a i n e r s Associative\ Containers Associative Containers)
a r r a y array array【C C C++ 11 11 11】
R B − t r e e RB-tree RB−tree【非公开】
v e c t o r vector vector
s e t set set
h e a p heap heap【以算法形式呈现 ( x x x _ h e a p ) (xxx\_heap) (xxx_heap)】
m a p map map
p e i o r i t y _ q u e u e peiority\_queue peiority_queue
m u l t i s e t multiset multiset
l i s t list list
m u l t i m a p multimap multimap
f o r w a r d _ l i s t forward\_list forward_list【C C C++ 11 11 11】
h a s h t a b l e hashtable hashtable【C C C++ 11 11 11】
d e q u e deque deque
u n o r d e r e d _ s e t unordered\_set unordered_set【C C C++ 11 11 11】
s t a c k stack stack【配接器】
u n o r d e r e d _ m a p unordered\_map unordered_map【C C C++ 11 11 11】
q u e u e queue queue【配接器】
u n o r d e r e d _ m u l t i s e t unordered\_multiset unordered_multiset【C C C++ 11 11 11】
⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ······ ⋅⋅⋅⋅⋅⋅
u n o r d e r e d _ m u l t i m a p unordered\_multimap unordered_multimap【C C C++ 11 11 11】
各种容器之间存在内含( c o n t a i n m e n t containment containment)关系。例如:
h e a p heap heap 内含一个 v e c t o r vector vector
p r i o r i t y _ q u e u e priority\_queue priority_queue 内含一个 h e a p heap heap
s t a c k stack stack 和 q u e u e queue queue 都含一个 d e q u e deque deque
s e t / m a p / m u l t i s e t / m u l t i m a p set/map/multiset/multimap set/map/multiset/multimap 都内含一个 R B − t r e e RB-tree RB−tree
u n o r d e r e d _ x unordered\_x unordered_x 都内含一个 h a s h t a b l e hashtable hashtable
3. 序列式容器(sequence containers)
所谓序列式容器,其中的元素都可序( o r d e r e d ordered ordered),但未必有序( s o r t e d sorted sorted)。
C C C++ S T L STL STL 中主要提供了以下序列式容器:
a r r a y array array(定长数组)
v e c t o r vector vector(变长数组)
l i s t list list(双向链表)
f o r w a r d _ l i s t forward\_list forward_list(单向链表)
d e q u e deque deque(双端队列)
s t a c k stack stack(栈)
q u e u e queue queue(队列)
p r i o r i t y _ q u e u e priority\_queue priority_queue(优先队列)
其中, s t a c k stack stack 和 q u e u e queue queue 由于只是将 d e q u e deque deque 改头换面而成,技术上被归类为一种配接器 ( a d a p t e r adapter adapter),即其改变了容器的接口 ,被称为容器配接器 ( c o n t a i n e r a d a p t e r container\ adapter container adapter)。
a r r a y array array 和 f o r w a r d _ l i s t forward\_list forward_list 都是 C C C++ 11 11 11 才引入的新容器。
一、vector 的介绍
v e c t o r vector vector 其实就是顺序表 。在实际应用中非常的重要且非常的好用 。因此我们要熟悉其常用的接口,会使用,再到了解其底层,知其然且知其所以然。一些不常用的或者忘记的内容可以直接去查 C C C++ 官方文档 :v e c t o r vector vector 的文档介绍。
可以看出 v e c t o r vector vector 真正采用模板:
t e m p l a t e < c l a s s T , c l a s s A l l o c = a l l o c a t o r < T > > c l a s s v e c t o r ; template<class\ T,\ class\ Alloc = allocator<T>>class\ vector; template<class T, class Alloc=allocator<T>>class vector;
v e c t o r ( s i z e _ t y p e n , c o n s t v a l u e _ t y p e & v a l = v a l u e _ t y p e ( ) ) vector(size\_type\ n,const\ value\_type\&\ val = value\_type()) vector(size_type n,const value_type& val=value_type())
构造并初始化 n n n 个 v a l val val
v e c t o r ( c o n s t v e c t o r & x ) vector(const\ vector\&\ x) vector(const vector& x)(重点)
拷贝构造
v e c t o r ( I n p u t I t e r a t o r f i r s t , I n p u t I t e r a t o r l a s t ) vector(InputIterator\ first, InputIterator\ last) vector(InputIterator first,InputIterator last)
使用迭代器进行初始化构造
cpp复制代码
void test1()
{
//1.无参构造
vector<int> v1;
//2.带参构造
vector<int> v2(10);
//3.构造+初始化(10个1)
vector<int> v3(10, 1);
//4.迭代器构造
vector<int> v4(v2.begin() + 2, v2.end() - 3);
//5.拷贝构造
vector<int> v5(v3);
cout << "v1:";
for (auto i : v1) cout << i;
cout << endl << "v2:";
for (auto i : v2) cout << i;
cout << endl << "v3:";
for (auto i : v3) cout << i;
cout << endl << "v4:";
for (auto i : v4) cout << i;
cout << endl << "v5:";
for (auto i : v5) cout << i;
cout << endl;
}
c a p a c i t y capacity capacity 的代码在 v s vs vs 和 g g g++ 下分别运行会发现: v s vs vs 下 c a p a c i t y capacity capacity 是按 1.5 1.5 1.5 倍增长的, g g g++ 是按 2 2 2 倍增长的。
注意: v s vs vs 是 P J PJ PJ 版本 S T L STL STL, g g g++ 是 S G I SGI SGI 版本 S T L STL STL(具体增长多少是根据具体的需求定义的,思维不要固化)。
r e s e r v e reserve reserve 只负责开辟空间 ,如果确定知道需要用多少空间, r e s e r v e reserve reserve 可以缓解 v e c t o r vector vector 增容的代价缺陷问题。
r e s i z e resize resize 在开空间 的同时还会进行初始化 ,影响 s i z e size size。
注意: C C C++ 11 11 11 之后支持列表初始化:vector<int> v{ 1,2,3,4,5 };,非常方便。
三、vector 迭代器失效问题(重点)
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装 ,比如:v e c t o r vector vector 的迭代器 i t e r a t o r iterator iterator 就是原生态指针 T ∗ T^* T∗ (typedef T value_type; typedef value_type* iterator;)。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间 ,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
e r a s e erase erase 删除 p o s pos pos 位置元素后, p o s pos pos 位置之后的元素会往前搬移 ,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果 p o s pos pos 刚好是最后一个元素,删完之后 p o s pos pos 刚好是 e n d end end 的位置,而 e n d end end 位置是没有元素的,那么 p o s pos pos 就失效了。因此删除 v e c t o r vector vector 中任意位置上元素时, v s vs vs 就认为该位置迭代器失效了。
以下代码的功能是删除 v e c t o r vector vector 中所有的偶数,请问那个代码是正确的,为什么?
cpp复制代码
//要求删除所有的偶数
void test1()
{
vector<int> v{ 1,2,3,4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
v.erase(it);
++it;
}
for(auto i : v) cout << i << " ";
cout << endl;
}
t e s t 1 test\ 1 test 1 报错:
cpp复制代码
//要求删除所有的偶数
void test2()
{
vector<int> v{ 1,2,3,4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
it = v.erase(it);
else
++it;
}
for(auto i : v) cout << i << " ";
cout << endl;
}
t e s t 2 test\ 2 test 2 正常运行:
因此, e r a s e erase erase 在标准库中的实现是返回一个迭代器指向删除位置的下一个元 素,就是为了避免迭代器失效 的问题,所以如果以后要访问 p o s pos pos 位置元素,要更新一下再访问 :pos = erase(pos);。
注意: L i n u x Linux Linux 下, g g g++ 编译器对迭代器失效的检测并不是非常严格,处理也没有 v s vs vs 下极端。
从上述三个例子中可以看到: S G I S T L SGI\ STL SGI STL 中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果 i t it it 不在 b e g i n begin begin 和 e n d end end 范围内,肯定会崩溃的。
与 v e c t o r vector vector 类似, s t r i n g string string 在插入 + + + 扩容操作 + + + e r a s e erase erase 之后,迭代器也会失效
cpp复制代码
string s("hello");
auto it = s.begin();
// 放开之后代码会崩溃,因为resize到20会string会进行扩容
// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
// 后序打印时,再访问it指向的空间程序就会崩溃
//s.resize(20, '!');
while (it != s.end())
{
cout << *it;
++it;
}
cout << endl;
it = s.begin();
while (it != s.end())
{
it = s.erase(it);
// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
// it位置的迭代器就失效了
// s.erase(it);
++it;
}
虽然 s t r i n g string string 也会有迭代器失效的问题,但是我们一般使用 s t r i n g string string 都是通过下标访问,不经常用迭代器。
迭代器失效解决办法:在使用前,对迭代器重新赋值即可。
四、vector 的深度剖析
在 《 S T L STL STL源码剖析》 这本书中,根据 S G I S T L SGI\ STL SGI STL 源代码<stl_vector.h> 分析出 v e c t o r vector vector 的实现如下:
v e c t o r vector vector 所采用的数据结构非常简单:线性连续空间。
它以两个迭代器 s t a r t start start 和 f i n i s h finish finish 分别指向配置得来的连续空间中目前已被使用的范围 ,并以迭代器 e n d _ o f _ s t o r a g e end\_of\_storage end_of_storage 指向整块连续空间(含备用空间)的尾端:
cpp复制代码
template <class T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* iterator;
...
protected:
iterator start; //表示目前使用空间的头
iterator finish; //表示目前使用空间的尾
iterator end_of_storage; //表示目前可用空间的尾
...
};
运用 s t a r t start start, f i n i s h finish finish, e n d _ o f _ s t o r a g e end\_of\_storage end_of_storage 三个迭代器,便可轻松提供首尾标识begin()+end()、大小size()、容量capacity()、空容器判断empty()、注标运算子[] ··· 等机能:
这时就造成了这么一个问题 ------ m e m c p y memcpy memcpy 是浅拷贝 ,如果拷贝前的空间有指针指向其他的空间 (需要深拷贝 ),如: s t r i n g string string 类型,在拷贝完后原来指针所指向的空间会直接释放掉,因此拷贝到的指针指向的空间就被释放掉了。
m e m c p y memcpy memcpy 是内存的二进制格式拷贝 ,将一段内存空间中内容原封不动的拷贝到另外一段内存
空间中(浅拷贝)。
如果拷贝的是内置类型的元素, m e m c p y memcpy memcpy 既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时 (需要深拷贝 ),就会出错,因为 m e m c p y memcpy memcpy 的拷贝实际是浅拷贝。
结论:如果对象中涉及到资源管理时,千万不能使用 m e m c p y memcpy memcpy 进行对象之间的拷贝,因为 m e m c p y memcpy memcpy 是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
3. 动态二维数组理解
vector<T> 是模板类型 ,因此 T T T 不仅可以是内置类型 ,也可以是自定义类型 ,如:string、vector ⋅ ⋅ ⋅ ··· ⋅⋅⋅ 等。当 T T T 这个类型为 vector<T> v 的时候,vector<vector<T>> vv 就代表了 v v vv vv 里面每一个元素存放的都是一个 vector<T> v(地址),即可以理解为二维数组。
vector<vector<int>> vv(n) 构造一个 v v vv vv 动态二维数组, v v vv vv 中总共有 n n n 个元素,每个元素都是 v e c t o r vector vector 类型的,每行没有包含任何元素 ,如果 n n n 为 5 5 5 时如下所示:
v v vv vv 中元素填充完成之后,如下图所示:
二维数组的遍历( 3 3 3 种方式):
1. o p e r a t o r [ ] operator[\ ] operator[ ] 遍历:
vector<vector<int>> vv2(5, vector<int>(5, 2)); //匿名对象赋值
for (auto it = vv2.begin(); it != vv2.end(); ++it)
{
for (auto _it = it->begin(); _it != it->end(); ++_it)
{
cout << *_it << " ";
}
cout << endl;
}
cout << endl;
3. 范围 f o r for for 遍历:
cpp复制代码
vector<vector<int>> vv3(5, vector<int>(5, 3)); //匿名对象赋值
for (auto& v : vv3)
{
for (auto& i : v)
{
cout << i << " ";
}
cout << endl;
}
cout << endl;
运行结果如下:
五、vector 的模拟实现
因为 v e c t o r vector vector 隶属于 C C C++ S T L STL STL 容器 ,即标准模板 库。因此,由于模板不能分开写到两个文件里 ,所以之前对于类的声明和定义分离写到两个文件中是不可取的。从今往后,只要带模板的类,都只能写到一个文件中 ,但是声明和定义可以分离:声明写到类里面(类外要重新写模板参数 ),定义写到类外面,或者把所有东西都写到类里面(类内部默认为内联 i n l i n e inline inline)。
因此,总共分为两个文件: v e c t o r . h vector.h vector.h 用来存放 v e c t o r vector vector 的定义和实现; t e s t . c p p test.cpp test.cpp 用来测试。
注意:这里两个文件中的 v e c t o r vector vector 都是自己定义的,为了和 s t d : : v e c t o r std::vector std::vector 作区分,我们要单独创建一个命名空间,这里我用的是 n a m e s p a c e y b c namespace\ ybc namespace ybc 。
v e c t o r vector vector 是 S T L STL STL 封装好的一块能够动态开辟连续空间的模板类 。随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,v e c t o r vector vector 的运用对于内存的合理利用与运用的灵活性有很大的帮助 ,我们再也不必因为害怕空间不足而一开始就要求使用定长数组,或者遭受自己动态开辟释放空间的麻烦,我们可以安心使用 v e c t o r vector vector,用多少开辟多少。