【复习整理归纳】| C++面经(基础概念、线程、进程)
【复习整理归纳】| C++面经(函数相关)
【复习整理归纳】| C++面经(内存管理)
【复习整理归纳】| C++面经(STL及项目)
====》C++【STL】 | 仿函数的应用以及如何规范要求
====》STL【tuple】| tuple源码刨析,为什么能传入自定义个数参数?
====》C++【STL】 | STL Effective C++
- 数组,固定大小,连续内存,随机访问;
- string使用了引用计数,消除不必要的内存分配和不必要的拷贝动作;
- 动态数据,连续存储空间,随机访问;
- 插入删除慢;
- 空间动态变化,两倍扩张;
- 可作为关联容器的替代,在运行时间和空间上优于关联容器;
- vector和string能够自动调用析构函数释放包含元素的内存;
- 使用swap技巧除去多余的容量;
- vector 的reserve增加了vector的capacity,但是它的size没有改变!
而resize改变了vector的capacity同时也增加了它的size;- insert、push_back、erase可能会引起内存扩张
- 【底层数据结构】:vector是动态数组、deque是动态的二维数组,第二维是固定长度的数组空间,第一维是按照
2倍扩容,故vector在扩容方面速率会低;
- 【前中后插入删除时间复杂度】:vector为O(n),deque在头尾为O(1)、在中间为O(n);
- 【对内存的使用效率】:vector内存空间必须是连续的,deque可分块进行数据存储,不需要内存空间是连续的;
- 在中间进行insert或erase,vector的效率要好一点,因为deque在第二维内存空间不是连续的,元素的移动可能会比
较慢;
关联容器的性能取决于散列函数;
- 对于已排序的vector能够使用binary_search、lower_bound、equal_range等;
- 在大小上,关联容器使用红嘿树实现,具有左右父节点多了3个指针;
- 在时间上,已序的vector性能会更好;
- 但vector需要自定义函数,用于排序、查找;
typedef pair<string, int> Data;
class DataCompare {
public:
/* 用于排序 */
bool operator()(const Data& lhs, const Data& rhs) const {
return keyLess(lhs.first, rhs.first);
}
/* 用于查找 */
bool operator()(const Data& lhs, const Data::first_type& k) const {
return keyLess(lhs.first, k);
}
/* 第二种查找 */
bool operator()(const Data::first_type& k, const Data& rhs) const {
return keyLess(k, rhs.first);
}
private:
bool keyLess(const Data::first_type& k1, const Data::first_type& k2)const {
return k1 < k2;
}
};
- 双端队列,动态大小,随机访问;
- 用于多次插入头部、尾部;
- 在deque容器的任何其他位置进行插入或删除操作都将使指向该容器元素的所有迭代器失效;
- 一维数组以2开始,以两倍的方式扩容,每次扩容后将第二维的数组,从第一维数组的下表oldsize/2开始存放,上下都预留了相同的空行,方便支持首尾元素的添加;
========》deque容器《========
- 双向链表,可在任何未知插入、删除;
- 不能随机访问、内存不连续;
vector:增加删除O(n),查询O(n),随机访问O(1),size和empty直接用头尾指针相减;
list:增加删除O(1);list的empty与size两个其中有一个不能为线性的
底层的数据结构不同为双向链表;
- 当删除或者移动链表中的节点时,只需要断开节点之间的指针即可,即为常数时间的操作;若需要在该操作中记录链表的大小,则需要遍历其中的个数,进而会增加操作的时间使之成为线性时间操作;但其size()函数则可通过常数时间获取其长度;相反,若在删除或移动等操作中不对size进行记录,则该操作为常数时间,则size为线性时间;而empty即可直接通过是否右结点来判断,此时empty的性能即比size高;
- insert将list对象的副本插入到目标区域;
- splice将对象直接移到目标地址,剪切;
- 先进先出队列;
- 大根堆;
单链表
是序列容器,允许在序列中的任何位置进行恒定时间的插入和擦除操作;
map中的[]操作符提供了更新和插入的功能;
- 当map中存在该对象将返回一个已有值对象的引用,
- 当没有时将会默认构造新对象,作为当前索引的值,在将其做为引用返回,在赋值;【开销,临时对象的创建、析构、赋值】
- 若在插入的操作中使用insert,将会提高程序的效率,节省上述的开销;
template<typename MapType, typename KeyArgType,
typename ValueArgType>
typename MapType::iterator AddOrUpdate(MapType& m, const KeyArgType& k,
const ValueArgType& v) {
/* 先确定在什么位置 */
typename MapType::iterator lb = m.lower_bound(k);
/* 是否存在 */
if(lb != m.end() && !(m.key_comp()(k, lb->first))) {
lb->second = v;
return lb;
}
else{ // 不存在,则插入
typedef typename MapType::value_type MVT;
return m.insert(lb, MVT(k, v));
}
}
- 一般使用红黑树来实现;
- 用于查询居多;
map
- 无序映射是存储由key和映value组合形成的元素的关联容器,并且允许基于键快速检索单个元素素;
- 采用哈希桶的数据结构;
- 桶是内部哈希表中的一个槽,元素根据哈希函数计算的哈希值分配给该槽;
- 桶桶的数量直接影响哈希表的负载因子——碰撞的概率;
- 桶增加桶的数量时都会导致重新散列;
set
- 无序容器,支持快速检索;
- 采用哈希桶:
- 桶是内部哈希表中的一个槽,元素根据哈希函数计算的哈希值分配给该槽;
- 桶的数量直接影响哈希表的负载因子——碰撞的概率;
- 增加桶的数量时都会导致重新散列;
- key唯一标识,只能删除和插入,修改会影响数据结构;
- 访问速度比set快;
- 即迭代器指向错误的元素或无效地址;
- vector:删除的元素迭代器之后的迭代器都会失效;
- deque:区分中间、前后删除元素的迭代器会失效;
- list:删除操作只会引起被删除节点的迭代器失效;
- map/set:删除操作只会引起被删除节点的迭代器失效;
迭代器不允许边读边写修改;
当迭代器插入一个元素时,所有的迭代器都失效;
当迭代器删除一个元素时,当前删除位置到后面所有元素都失效;
当通过迭代器更新容器后,要及时对迭代器进行更新,insert、erase都会返回新位置的迭代器;
- 若要任意位置插入,序列式容器;
- 元素是否排序,哈希容器;
- 随机访问,vector/deque/string;
- 避免删除或插入时移动容器元素,则选择基于节点容器;
- 数据布局是否与C兼容,考虑vector;
- 查找速度,哈希容器、sort的vector、关联容器;
- 不使用带引用计数的string,vector;
- 插入删除出现异常需要回滚,选择list;
- 防止使用swap则其迭代器、指针、引用变得无敌,避免string;
- 容器存放该基类的指针类型来防止被切割;
- 区间函数优于对应单元素成员函数(更多的调用拷贝);
- 若插入时,会不断扩张内存,拷贝等操作降低效率;
- 如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉;
- 切勿创建包含auto_ptr的容器对象,会在排序或其他操作中改变内容;
- 删除元素的方法:一般不使用遍历的方法进行删除,若错误使用会导致迭代器变得无效
- 当删除连续内存的容器的最好方法即
erase-remove/remove_if
;- list直接使用remove、remove_if;
- 关联容器删除erase;
- 分配子:
- 该分配子为一个模板,模板参数代表你为它分配内存的对象的类型;
- 提供类型定义pointer(T*)和reference(T&);
- 分配子不能带有非静态数据成员;
- 传给allocate成员函数是要求内存的对象个数,而不是字节数,且返回值为T*;
- 一定要提供嵌套的rebind模板,由于标准容器依赖;
- 如何安全修改关联容器种的元素:
- 先查找到该元素;
- 为将要修改的元素做一份拷贝,且不要将其修改为const;
- 修改该拷贝;
- 将该元素在容器种删除;
- 将新值插入到容器内;
该方法将不会受移植性的限制;- 对包含指针的容器使用remove这一类算法时要特别小心
- 当容器中存放指针类型,在删除时容易造成资源泄漏;
- 在要remove前,我们应该将其指针置空,然后再删除;
- v.erase(remove(begin, end, static_cast
(0)), end); - 当然,如果使用智能指针即可直接删除;
- v.erase(remove_if(begin, end, not1(mem_fun(&class::icVarify))), end);
// 分配子
class Heap1 {
public:
static void* alloc(size_t numBytes, const void* memoryBlockToBeNear);
static void dealloc(void* ptr);
};
template<class T, class H>
class SpecificHeapAllocator {
public:
typedef T value_type;
typedef size_t size_type;
typedef T* pointer;
typedef T& reference;
pointer allocate(size_type numObjs, const void* localiHint = 0) {
return static_cast<pointer>(H::alloc(numObjs * sizeof(T), localiHint));
}
void deallocate(pointer ptrToMemory, size_type numObjects) {
H::dealloc(ptrToMemory);
}
};
void test2() {
vector<int, SpecificHeapAllocator<int, Heap1>> v;
}
无序关联容器:链式哈希表 增删查O(1);
有序关联容器:红黑树 增删查O(log2n);
- 适配器底层没有自己的数据结构,是另外一个容器的封装,由底层依赖的容器进行实现的;
- 没有实现自己的迭代器;
为什么queue和stack底层不依赖vector
- vector的内存使用效率太低了,从1开始2 被增长;而deque直接使用4096/sizeof(int);
- 对于queue来说需要支持尾部插入,头部删除,O(1),若使用vector则出队效率低;
- vector需要大片的连续内存,而deque只需要分段的内存,当存储大量数据时,则内存的
利用率好点;
priority_queue底层依赖vector
底层默认把数据组成一个大根堆结构,在一个内存连续的数组上构建一个大根堆或小根堆
可通过下表(idx*2+1)取查询;
【泛型算法 = template + 迭代器 + 函数对象】
- 泛型算法的参数接收都是迭代器;
- 泛型算法的参数还可接收函数对象;
【绑定器 + 二元函数对象 => 一元函数对象】
- bind1st:把二元函数对象的operator()第一个形参绑定起来;
- bind2nd:把二元函数对象的operator的第二个形参绑定;
给容器使用,主要把对象的内存开辟和对象构造分开,把对象的析构和内存释放分开;由于,我们在初始化一个容器的时候应该只有内存不应
该有对象,当删除元素的时候应该只把相应的对象析构掉,而不是将内存也释放掉,这个内存需要能够再次使用以此来提高内存使用效率;
========》算法【树】| 红黑树总结《========