STL库(1)
- vector
- vector介绍
- vector使用
- 初始化
- 元素访问
- 内存扩容
- 插入删除
- list
- list介绍
- 初始化,元素访问
- 插入
- 删除元素
- vector和list区别
vector
vector介绍
- vector是可以改变大小的数组的容器。
- 其内存结构和数组一样,使用连续的存储空间,也就可以使用指针指向其元素,通过偏移量来访问存储空间中的元素。
- 和数组不同之处在于vector的大小可以动态的变化,容器可以自动扩容存储空间。
- vector使用一个动态分配的连续存储空间来存储元素,在插入新元素的时候也可能需要重新分配存储空间,也就意味着每次扩容都需要将其元素重新移动到新的存储空间中,很显然这效率是非常低的,为此不会每次像容器中添加元素都重新分配。
- 容器可以分配一些额外的存储空间以适应添加的对象,其每次扩容以原本的1.5或2倍来扩容。
- 与其他的容器相比,vector可以更加高效的访问其他元素,并且可以高效的从尾部添加或者删除元素。对于节位意外的位置插入删除效率较低。
vector使用
初始化
int main() {
vector<int> iar;
vector<double> dar(12,10,0);
vector<int> ibr={1,2,3,4,5,6};
vector<Int> Iar;//处理自定义类型
vector<int*>par;//尽量别这么使用,因为其可能指向动态申请的内存,其不会主动释放。
}
元素访问
1.at访问(返回的是引用,可以进行修改)
2.下标访问
3.data()返回首地址,通过首地址偏移量进行解引用操作。
4.迭代器
5.范围for
int main() {
vector<int> iar={1,2,3,4,5};
int n=iar.size();
for(int i=0;i<n;i++) {
ibr.at(i)+=10;
cout<<iar.at(i)<<" "<<iar[i]<<endl;
}
cout<<iar.back()<<endl;
cout<<iar.front()<<endl
int* p=ibr.data();
for(int i=0;i<n;++i) {
cout<<p[i]<<endl;
//*(p+i);
}
for(auto &x:iar) {
cout<<x<<" ";
}
vector<int>::iterator it=iar.begin();
vector<int>::const_iterator cit=iar.begin();
vector<int>::reverse_iterator rit=iar.rbegin();//逆向迭代器
for(;rit!=rend();) {
cout<<*rit<<endl;
}
vector<int>::const_reverse_iterator it=iar.rbegin();
for(;it!=iar.end();){
(*it)-=100;
cout<<*it<<endl;
}
}
内存扩容
int main() {
vector<int> var ;
for (int i = 0; i < 100; i++) {
var.push_back(i);
cout << "size:" << var.size() << endl;;
cout << "capacity:" << var.capacity() << endl;
}
}
运行上面代码我们观察输出结果:
其容量分别是:1,2,3,4,6,9,13,19…其扩容分别按照原本的1.5倍扩容,如果其1.5倍和原本一样就对容量进行+1操作。
这里我们用的是内置类型,如果是我们自己定义的类呢,就会发生这样的过程,比如我们定义的是ptr类,类中存在构造,拷贝构造,移动构造,移动赋值,析构等函数此处不做编写。
扩容等操作我们来具体理解以下:
int main() {
std::vector<Ptr> ar;
for(int i=0;i<100;i++) {
ar.push_back(Ptr(i));
cout << "size:" << ar.size() << endl;;
cout << "capacity:" << ar.capacity() << endl;
}
}
首先观察第一张图,容量为1,大小为1,在首先会调用缺省构造函数来构造ptr的无名对象,其为右值,然后使用移动构造来将无名对象的资源移动到新的对象中,该对象就存在于容器中,然后析构无名对象。紧接着当再次添加对象的时候需要进行扩容处理,其扩容就是重新申请一块内存,将原本内存中的资源拷贝一份放入新的内存中,然后释放旧的资源。为此我们可以看到其调用拷贝构造函数,创建新对象来放入新内存中,然后析构掉原本的对象,然后创建新添加的对象,移动构造来移动无名对象的资源,最后析构无名对象。这就是其扩容的内部操作。很明显效率很低,在不断的构建对象和析构对象。
为此呢我们可以使用reserve()函数。
int main() {
std::vector<Ptr> ar;
ar.reserve(200);
//ar.resize(200);
//ar.assign(10,Ptr(10));
for(int i=0;i<100;i++) {
ar.push_back(Ptr(i));
cout << "size:" << ar.size() << endl;;
cout << "capacity:" << ar.capacity() << endl;
}
}
该函数可以直接申请够200个对象的内存,不会进行反复的扩容和拷贝构造,和这个函数相仿的还存在一个resize()函数,该函数与其不同之处在于,这个函数在申请内存之后会创建100个对象,为此加入对象的时候会从第100个后面进行添加。还存在一个assign()函数,该函数页会创建对新象,不过其需要指定创建的对象。
插入删除
int main() {
std::vector<Ptr> ar;
ar.reserve(10);
ar.push_back(Ptr(1));
ar.push_back(Ptr(2));
ar.push_back(Ptr(3));
ar.push_back(Ptr(4));
ar.push_back(Ptr(5));
vector<Ptr>::operator it=ar.begin();
ar.insert(it,Ptr(6));
for(auto &x:ar){
x.Print();
}
ar.pop_back();
}
很明显vector的插入删除一般都是在尾部插入删除,而通过迭代器和插入函数头部插入时,必然将后面所有的元素都要向后移动,效率大幅度降低,当我们使用了迭代器之后然后尾删的时候很明显出现了程序崩掉的现象,这是为什么呢?因为我们对迭代器进行操作之后迭代器失效了。为什么会失效呢?迭代器实际上是和对象绑定的,
我们的迭代器是和对象绑定的,例如迭代器此时指向首元素1,然后进行了头删,那么头结点内存释放了,对象丢失了为此迭代器也就丢失了。而扩容依然是如此,重新申请了内存,拷贝了资源,那么原本的对象就丢失了,迭代器也就丢失了。
list
list介绍
- list是序列容器,允许在序列中任何位置执行O(1)时间的插入和删除,并在两个方向上进行迭代。
- 其底层结构是双链表,将每个元素存储在不同的存储位置,每个结点通过next,prev指针连结成的顺序表。
- list与其他容器相比,可以在任何位置插入和删除,获得迭代器的情况下时间复杂度为O(1).
- 不能通过下标访问,需要通过迭代器找到位置才可以访问,需要遍历的时间开销。
- 存储密度低,使用一些额外的内存空间(next,prev指针)来保持每个元素的关联性,从而导致存储小元素的列表存储密度低。
初始化,元素访问
数组初始化,范围for遍历
int main() {
std::list<int> arlist={1,2,3,4,5,6,7,8};
cout<<arlist.back()<<endl
cout<<arlist.front()<<endl;
for(auto& x:arlist) {
cout<<x<<" ";
}
}
插入
int main() {
std::list<Ptr> arlist;
for(int i=0;i<5;i++) {
//arlist.push_back(Ptr(i));
arlist.emplace_back(i);
}
for(const auto &x:arlist) {
x.Print();
}
}
因为其list容器结构是双链表结构,所以我们进行头插尾插的效率都是一样的,不过push_back插入我们知道是先创建对象,然后进行移动构造来插入数据,效率较低,为此在list中存在emplace_back函数,他和push_back不同之处在于他是原位构造,直接在申请的内存上构造对象,不会进行移动构造然后析构对象。范围for遍历时也最好用常引用,如果不是引用便会调用拷贝构造构造对象来调用Print函数输出,使用引用就可以不在调用拷贝构造函数,大大节省了时间和空间。而加入const可以保证容器中的元素不发生改变。
同vector一样,也list容器中最好不要使用指针,为什么呢?
int main() {
std::list<Ptr*> arlist;
for(int i=0;i<5;i++) {
//arlist.push_back(Ptr(i));
arlist.emplace_back(new Ptr(i));
}
for(const auto &x:arlist) {
x->Print();
}
}
我们使用上面代码的时候很明显其没有析构对象,因为容器中是指针,其不能判断内部是不是动态申请了内存而释放他,所以呢就不会进行析构,为此最好不要使用指针,要析构就要在范围for中使用delete析构。或者使用智能指针。
删除元素
- erase():删除指定位置的元素,也可以删除某个区间的多个元素。
- clear():删除所有元素。
- remove(val):删除所有等于val的元素。
- unique():删除容器中相邻的重复元素。
int main() {
list<int> ilist={1,2,3,4,5,1,2,3,4,5};
ilist.sort();
ilist.unique();
for(auto &x :ilist) {
cout<<x<<" ";
}
}
unique()删除通过上面代码就可以展示出来。
list中的sort排序底层是快排,而当数据量足够大的时候呢就会存在一个阈值,高于这个值就会使用归并排序。
vector和list区别
vector | list | |
---|---|---|
底层实现 | 连续存储的容器,动态数组,对上分配空间 | 动态双向链表,堆上分配空间 |
空间利用率 | 连续空间,不易造成内存碎片化,空间利用率高 | 节点不连续,容易造成内存碎片化,小元素使结点密度低,空间利用率低 |
查找元素 | 下标,at,find,binary_search() | find O(n) |
插入 | push_back(val);O(1)//空间足够 | O(1) |
迭代器 | 随机迭代器,检查越界,支持++,–,==,+=,… | 双向迭代器,检查越界,支持++,–,==,!= |
迭代器失效 | 插入删除都会导致迭代失效 | 插入元素不会导致迭代器失效,删除会导致迭代器失效,不影响其他迭代器 |
两者适用情况:
- 需要高效得随机存储,不在乎插入删除效率(很少使用插入删除),选用vector
- 需要大量得插入删除,苏哦系取值很少使用,选用list。