1. C++ 中 deque 的原理?它内部是如何实现的?
deque,double-ended queue,是个双端队列,是一个STL容器,通过deque,我们在两端都能高效地插入和删除元素。
它内部的实现依靠一个分段连续的内存结构,而不是类似vector的单一连续块,因此在头部插入和删除操作的时间复杂度是O(1),更高效。
扩展知识:
deque的内部实现原理相比vector复杂很多:
1. 分段存储结构:
deque采用分段存储的方式。具体来说,它不是像vector一样直接分配一大块连续内存,而是分配若干小块的连续内存,并用一个映射表(类似指针数组)来管理这些小块内存。这种方式的好处在于,不需要频繁的内存拷贝和移动,尤其在头部插入或删除元素时。
2. 中央控制块:
deque有一个中央控制块,称为"map"或"指针数组"。这个数组的每个元素是指向一块子内存的指针,这些内存块称为"缓冲区"(buffer)。deque利用这个数组来记录所有的缓冲区,从而可以灵活管理内存。
3. 双端操作:
由于deque是双端队列,因此它提供了高效的头尾插入和删除操作。这些操作之所以高效,主要是中央控制块的设计:插入和删除元素时,仅需在这些小块内存上进行操作,而不需要像 vector那样在整个数组上操作。
4. 内存增长机制:
当 deque需要更多空间时,它会分配新的缓冲区,并更新中央控制块。与vector不同的是,当 deque扩展时,不需要移动现有的元素,因为每个缓冲区已经是独立分布的,小块内存的重新分配和地址调整在控制块中完成。
5. 缓存局部性(Cache Locality):
Deque 的分段存储结构可能影响缓存局部性,因为数据并不是存储在一块连续的内存中。当频繁访问大量元素时,它在缓存命中率方面不如vector,但是在插入和删除方面更加灵活和高效。
如果你既需要数组下标访问又需要在任意位置插入和删除,可以考虑使用deque。如果你只是在尾部插入删除,且大量下标访问元素,可以考虑使用vector。
2. C++ 中 map 和 unordered_map 的区别?分别在什么场景下使用?
两者都是常用的关联容器。但有一些区别:
1. 底层实现:
-
map:基于有序的红黑树(具体实现依赖于标准库)。 -
unordered_map:基于哈希表。
2. 时间复杂度:
-
map:插入、删除、查找的时间复杂度为O(logn)。 -
unordered_map:插入、删除、查找的时间复杂度为O(1)(摊销).
3. 元素顺序:
-
map:元素按键值有序排列。 -
unordered_map:元素无序排列。
4. 内存使用:
-
map:由于底层是红黑树,内存使用较少。
-
unordered_map:需要额外的空间存储哈希表,但在处理大量数据时,可能具有更好的表现。
场景选择
-
map:当需要按键值有序访问元素时,适合使用map,例如按顺序遍历键值对。 -
unordered_map:当主要关注查找速度、不关心元素顺序时,使用unordered_map会更高效,例如需要高效的键值存储和快速查找的场景。
3. C++ 中 list 的使用场景
数组和链表的区别想必大家都知道,而list就是双向链表。它适用于频繁插入和删除的场景,尤其是插入和删除:操作多于遍历操作的场景,插入和删除操作的时间复杂度:是O(1)。
扩展知识
1. 与其他容器比较:
list 与vector 和 deque 等其它容器各有优缺点。例如:
-
vector更适用于频繁访问和修改元素,但在中间插入和删除时效率较低。 -
deque特点是双端快速插入和删除,同时支持随机访问。 -
set和map之类的关联容器可以进行快速查找(基于平衡二叉树),但不适合频繁修改结构。
2. 排序:
注意list的排序应该使用list自己的类成员sort函数,不应该使用std::sort()函数。
3. 专用成员函数:
list还提供了一些独有的成员函数,比如splice、sort、reverse、merge等:
-
splice:可以快速将某段元素移动到另一个list 位置。 -
merge:合并两个有序链表。 -
reverse:反转链表元素。 -
sort:对链表进行排序。
4. 迭代器的使用:
由于list是双向链表,双向迭代器是最常用的迭代器类型,它可以向前和向后遍历容器。随机访问迭代器不能用于 list.
4. 什么是 C++ 中的 RAII ? 它的使用场景?
RAII, 全称是 "Resource Acquisition IsInitialization"(资源获取即初始化)。
它的核心思想是将资源的获取与对象的生命周期绑定,通过构造函数获取资源(如内存、文件句柄、网络连接等),通过析构函数释放资源。这样,即使程序在执行过程中抛出异常或多路径返回,也能确保资源最终得到正确释放,特别是可以避免内存泄漏。
5. C++ 中 lock_guard 和 unique_lock 的区别?
两者都是RAlI形式的锁管理类,用于管理互斥锁(mutex)。不过它们有一些关键区别:
-
lock_guard是一个简单且轻量级的锁管理类。在构造时锁定给定的互斥体,并在销毁时自动解锁。它不可以显式解锁,也不支持锁的转移。 -
unique_lock提供了更多的灵活性。它允许显式的锁定和解锁操作,还支持锁的所有权转移。unique_lock可以在构造时选择不锁定互斥体,并在稍后需要时手动锁定。
扩展知识
1. lock_guard
lock_guard 很简洁,它的唯一任务就是确保在作用域结束时自动释放互斥锁。因为它的简洁,所以性能上更有优势。下面是个简单的示例:
cpp
std::mutex mtx;
void example() {
std::lock_guard<std::mutex> lock(mtx);
// 互斥锁已经锁定,可以安全地访问共享资源
} // 作用域结束,mtx 自动解锁
2. unique_lock
unique_lock提供了更灵活的锁管理方式,适用于需要延迟锁定、显式解锁和锁所有权转移的场景。以下是一些特性和用法:
延迟锁定:你可以在构造 unique_lock时选择不锁定互斥锁,而在后续调用lock()方法时显式锁定。
cpp
std::mutex mtx;
void example()
{
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 在需要时显式锁定
lock.lock();
// 互斥锁已经锁定,可以安全地访问共享资源
} // 作用域结束,mtx 自动解锁
显示解锁:你可以在中间需要时显示解锁互斥锁,然后再次锁定。
cpp
std::mutex mtx;
void example() {
std::unique_lock<std::mutex> lock(mtx);
// 访问共享资源
lock.unlock();
// 互斥锁已解锁
// 其他不能并发访问的操作
lock.lock();
// 再次锁定共享资源
} // 作用域结束,mtx 自动解锁
锁所有权转移:unique_lock的所有权可以在不同作用域之间转移,这在一些需要精细控制锁生命周期的场景中非常有用。
cpp
std::mutex mtx;
void example()
{
std::unique_lock<std::mutex> lock1(mtx);
// 访问共享资源
std::unique_lock<std::mutex> lock2 = std::move(lock1);
// lock1 不再拥有互斥锁
// lock2 拥有互斥锁
} // 作用域结束,mtx 自动解锁(如果 lock2 尚未解锁)
总结 :lock_guard 适合简单的场合,不需要复杂的锁定/解锁逻辑,性能更好;而 unique_lock提供了更多的灵活性,适合更复杂的并发编程需求,性能相对一般。
6. C++ 中 thread 的 join 和 detach 的区别?
join和 detach是 std::thread 的成员方法:
-
join():阻塞当前的调用线程,直到子线程完成。这意味着主线程将等待子线程执行完毕后再继续执行。这种方法确保了子线程的完成。 -
detach():将子线程从调用线程中分离 开来,子线程在后台独立执行,不会阻塞调用线程。使用detach后,子线程的资源在它独立执行完成后自动释放,但主线程无法再与其通信或得到其执行结果了。
简单来说,join是一种同步机制,保证子线程完成后主线程再继续;而 detach 是一种让子线程独立执行的方式,主线程不再等待和管理它。
7. C++ 中 jthread 和 thread 的区别?
两者都是用于创建并发线程的类,但它们有些许区别:
-
自动资源管理:
std::jthread是C++20引入的,它通过RAII来管理线程生命周期,当std::jthread对象被销毁时,它所管理的线程会join。而需要std::thread程序员手动调用join()或detach()方法,否则在std::thread对象销毁时如果线程仍未join,会导致程序终止。 -
中断支持: 设计来更优雅地支持线程中
std::jthread断机制,而在std::thread中没有直接的中断支持,程序员需要自己实现中断逻辑。
8. C++ 中 memcpy 和 memmove 有什么区别?
两者都是用于内存拷贝的函数,但它们的主要区别在于处理内存重叠区域的能力。
-
memcpy:用于从源地址复制指定数量的字节到目标地址。如果源和目标地址重叠,行为是未定义的,因为memcpy不处理重叠。 -
memmove:也是用于从源地址复制指定数量的字节到目标地址,但与memcpy不同的是,它可以安全地处理源和目标地址的重叠情况。memmove保证重叠情况下的数据也是被正确地复制。
9. C++ 的 function、bind、lambda 都在什么场景下会用到?
三者都用于处理函数和可调用对象:
-
std::function:用于存储和调用任意可调用对象(函数指针、Lambda、函数对象等)。常用场景包括回调函数、事件处理、作为函数参数和返回值。 -
std::bind:用于绑定函数参数,生成函数对象,特别是当函数参数不完全时。常见于将已有函数适配为接口要求的回调、将成员函数与对象绑定。 -
Lambda表达式:用于定义匿名函数,通常在短期和局部使用函数时比如一次性回调函数、算法库中的自定义操。作等。
10. 请介绍 C++ 中使用模版的优点
模板的优缺点主要有:
优点:
-
代码重用性: 模板允许我们编写与数据类型无关的代码,减少了重复代码,提高代码可重用性,遵循
Don't repeat yourself原则。 -
**类型安全:**模板可以在编译时进行类型检查,避免了运行时错误,提高程序的安全性。
-
**效率高:**由于模板是在编译时生成具体类型的代码,避免了运行时类型检查,提升了运行效率。
-
**灵活性强:**模板可以用来实现泛型编程,可以处理各种数据类型和操作,实现更为通用的算法。
缺点:
-
**编译时间增加:**因为模板会在编译时生成具体类型的代码,可能会导致编译时间显著增加。
-
**错误信息复杂:**模板引起的错误往往信息量巨大且难以理解,新手在调试时可能会比较头疼。
-
**代码膨胀:**如果使用不当,模板可能会导致生成大量冗余代码,增加最终模块的尺寸。
-
**可读性和维护性:**由于模板代码的泛型特性,代码的可读性和维护性可能会下降,理解起来比较困难,比如标准库代码,真的比较难理解。