1、resize和reserve的区别?
- resize()
调整容器的长度大小,使其能容纳n个元素。
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:
resize(n)用0来填充多出的元素空间,
resize(size_t n, char c)用字符c来填充多出的元素空间。
注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。 - reserve(size_t n):为容器预留n个空间,不改变有效元素个数,不进行初始化。当reserve的参数小于 容器的底层空间总大小时,reserver不会改变容量大小。
2、什么是深浅拷贝?
- 浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。
- 深拷贝:给每个对象都分配独立的资源,保证多个对象之间不会因为共享的资源而造成的多次释放产生程序崩溃问题。
3、写一个简单的string类的实现。
class String{
public:
构造函数 考虑str为空的情形
String(const char* str=""){
if (nullptr == str)
_str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//拷贝构造函数 通过临时对象的交换方式进行拷贝构造,构造对象借助之前实现的普通构造函数来构造对象,然后通过交换对象的内部指针的指向,从而达到拷贝构造对象
String(const String& s)
:_str(nullptr)
{
String tmp(s._str);
swap(_str, tmp._str);
}
//这种交换方式在赋值语句中,有利于预防内存泄漏,因为交换完成的临时对象,一旦生存作用域结 束,就会自动调用其析构函数,释放对应的资源
String& operator=(const String& s){
if(this != &s){
String tmp(s._str);
swap(_str, tmp._str);
}
return *this;
}
//析构函数,当_str不为空时才进行释放工作并且以数组方式进行释放
~String(){
if(_str){
delete[]_str;
_str = nullptr;
}
}
private:
char* _str;
};
4、vector是怎么增容的?只能扩大两倍吗?
vs下capacity是按1.5倍增长的,g++是按2倍增长的
触发扩容时,如果要插入的数据量比旧容量小,则按两倍扩容;如果要插入的数据量比原来的旧容量还要大,即表示即使按两倍扩容了,依然存不下要插入的数据,此时将会按照旧容量加要插入的数据量来扩容,保证一次扩容就能容下要插入的数据。
增长倍数不是绝对的,是根据具体需求确定的,增多了会造成空间浪费,增少了又会频繁增容,效率低。
5、什么是迭代器?
Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
6、什么是迭代器失效?
-
vector迭代器失效
在调用erase函数进行删除某一个元素或者迭代器的时候,当前位置及以后位置的迭代器会失效。
失效原因
在删除某一迭代器或者元素的时候,会将后面的元素向前移动,导致这个元素以后的迭代器都没有指向正确的位置,并且最后一个迭代器指向的空间内容无效。
解决方法
为erase设置一个返回值,返回当前结点的下一个位置的迭代器,即iterator it=erase(pos)。 -
list迭代器失效
在调用erase函数进行删除某一个结点时,导致这个结点的迭代器失效。
失效原因
在将结点删除以后,没有改变当前迭代器的指向。
解决方法
第一种:
为erase函数设置一个返回值,返回当前结点的下一个结点的迭代器,即 iterator it = erase (pos)。
第二种:
给erase传值时为 erase(pos++);这是因为当把当前位置传递给函数之后,自动指向下一个位置。 -
map等树结构迭代器失效
在STL中,底层为树的就是map ,set ,mulitimap ,mulitiset。
以map为例:
在调用erase删除某一结点时,当前结点的迭代器失效。
失效原因
在将结点删除以后,没有改变当前迭代器的指向。
解决方法
由于map的erase是没有返回值的,所以我们不能用返回值方式返回下一个位置的迭代器。
但是我们可以给erase传值时为 erase(pos++);这是因为当把当前位置传递给函数之后,自动指向下一个位置。
注:map的插入,查找操作都不会造成迭代器失效的问题。
7、vector和list的区别?
- Vector
连续存储的容器,动态数组,在堆上分配空间
底层实现:数组
两倍容量增长:
vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
性能:
访问:O(1)
插入:在最后插入(空间够):很快
在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
在中间插入(空间够):内存拷贝
在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
删除:在最后删除:很快
在中间删除:内存拷贝
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。 - List
动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
性能:
访问:随机访问性能很差,只能快速访问头尾节点。
插入:很快,一般是常数开销
删除:很快,一般是常数开销
适用场景:经常插入删除大量数据 - 区别:
1)vector底层实现是数组;list是双向链表。
2)vector支持随机访问访问效率为O(1),list不支持随机访问,效率为O(n)。
3)vector任意位置插入和删除效率低,需要搬移元素,时间复杂 度为O(N),插入时有可能需要增容,增容:开辟新空 间,拷贝元素,释放旧空间,导致效率更低,list任意位置插入和删除效率高,不 需要搬移元素,时间复杂度为 O(1)。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector底层为连续空间,不容易造成内存碎片,空间利用率 高,缓存利用率高;list底层节点动态开辟,小节点容易造成内存碎片,空间利用率低, 缓存利用率低。
8、stack和queue。
- stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行 元素的插入与提取操作。如果没有为stack指定特定的底层容器, 默认情况下使用deque。
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端 提取元素。 默认情况下,如果没有为queue实例化指定容器类,则使用标 准容器deque。
9、 priority_queue
- 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
- 默认情况下,如果没有为特定的priority_queue类实例化指 定容器类,则使用vector。
10、deque
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组。
缺点: deque不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到 某段小空间的边界,导致效率低下。