这个博客系列会分为C++ STL-面经、常考公式推导和SLAM面经面试题等三个系列进行更新,基本涵盖了自己秋招历程被问过的面试内容(除了实习和学校项目相关的具体细节)。在知乎和牛客(某些文章上会附上内推码)也会同步更新,全网同号(lonely-stone或者lonely_stone)。
关于高频面试题和C++ STL面经,每次我会更新10个问题左右,每次更新过多,害怕大家可能看了就只记住其中几个点。(在个人秋招面试过程中,面试到后面,发现除了个人项目和实习经历外,个人所记录的内容基本能涵盖面试官能问到的)
(另外个人才疏学浅,如果所分享知识中出现错误,请大家指出,避免误导其他人)
1. thread线程中的join和detach方法
- join:比如在主线程里申明了线程t1,现在调用t1.join()就是要等待该子线程t1终止
- detach:作用是将子线程和主线程的关联分离,也就是说detach()后子线程在后台独立继续运行,主线程无法再取得子线程的控制权,即使主线程结束,子线程未执行也不会结束。当主线程结束时,由运行时库负责清理与子线程相关的资源。
2. 条件变量condition_variable
头文件<condition_variable>
condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable
通知线程:
- 获得 std::mutex (典型地通过 std::lock_guard )
- 在保有锁时进行修改
- 在 std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知操作保有锁)
等待线程:
- 获得 std::unique_lockstd::mutex ,在与用于保护共享变量者相同的互斥上
- 执行 wait 、 wait_for 或 wait_until ,等待操作自动释放上一步中锁上的互斥mutex,并悬挂线程的执行,等待条件变量
- condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。
std::condition_variable 只可与 std::unique_lockstd::mutex 一同使用;此限制在一些平台上允许最大效率。 std::condition_variable_any 提供可与任何基础可锁 (BasicLockable) 对象,例如 std::shared_lock 一同使用的条件变量。
3. c++:reserve和resize简介和区别
简介:首先要知道capacity和size的概念,举一个例子,这里有半瓶1L的矿泉水。那么1L就是我们说的容量,也就是capacity,表示指容器在内存中开辟的存储空间的总大小。 半瓶,也就是500ML,也就是size,表示已经使用的空间大小。
reserve:void reserve( size_type n) ;
- 如果n>capacity,那么就在内存空间重新分配一块更大的连续空间,然后将容器内所有的有效元素从旧空间的位置全部复制到新空间相应的位置,然后释放旧空间,指向新空间。
- 如果n值小于容器的现有容量,那么这个函数没有任何作用。
- 所以说:所以reserve函数的结果只是capacity变大,原本元素个数即size并没有变化。
resize:void resize (size_type n, const T& c = T());这里n是元素的个数,c是元素默认初始化的值。
- 如果n>size,则在容器的末尾插入n-size()个初始值为c的元素,若如果没有指定初始值,那就用元素类型的默认构造函数来初始化。这里有可能capacity变大。
- 如果n<size,则删除末尾的size()-n个元素,这样会导致size变小。但是在这种类型的容器在删除元素后并不会释放元素本身的内存空间,所以容器的容量capacity并没有改变。(一般不用这个函数进行删除操作)
- n值等于当前容器的大小时,什么也不做。
4. C++ 之 Lambda
表达式c++11引入了Lambda表达式,使得开发人员可以更方便的创建匿名函数。Lambda表达式是c++语言的一个重要特性,它可以作为函数对象使用,可以用来替代一些繁琐的函数声明和定义。
一般情况下 Lambda 表达式不会这样使用的,而是和 STL 的一些算法结合使用。C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。
使用 Lambda 表达式的两点优势:
- 函数对象定义在调用的地方,直接看到上下文,可读性更强,方便修改;
- Lambda 表达式可访问作用域内的任何变量,代码简洁。
当然,使用 Lambda 表达式的前提是 函数体足够简单,只有一两行。当函数体复杂时,还是推荐使用常规函数。
[captures](params) -> return_type { body }; [捕获列表] (函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
捕获列表是 Lambda 表达式最有意思的地方,这里重点介绍下捕获列表。
捕获列表,用来说明外部变量的访问方式,外部变量访问方式说明符可以是 = 或 & ,表示函数体中用到的、定义在外面的变量在函数体中是否允许被改变。= 表示值传递,不允许改变。& 表示引用传递,允许改变。
包括下面几种形式:
- 中括号[ ] 表示不捕获任何变量
- [=] 表示按值传递的方法捕获父作用域的所有变量
- [&] 表示按引用传递的方法捕获父作用域的所有变量
- [=, &a] 表示按值传递的方法捕获父作用域的所有变量,但按引用传递的方法捕获变量a
- [&, a] 表示按引用传递的方法捕获父作用域的所有变量,但按值传递的方法捕获变量a
比如,生成一个 vector,里面是随机数,一种是使用常规函数 my_rand()
5. C++智能指针
智能指针不是指针,是一个管理指针的类,用来存储指向动态分配(堆)对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。
智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件
6. 面试手撕~局部窗口滑动求中位数
原本的算法题是局部窗口滑动求最大值,变成了局部窗口滑动求中位数
在求最大值的时候是采用deque的方法,现在在求中位数的时候是在自定义类里面采用两个multiset实现的,大家可以自己模拟做一做。
7. 父类的私有成员可以被继承吗?
父类中的私有属性其实在子类中是可以继承的,因为在子类的对象中,实例数据也加载了父类中的私有属性,只是我们不能访问而已。
8. 如何防止内存泄露
内存泄露指的是在程序运行过程中,由于某种原因导致程序无法释放已经不再使用的内存,从而导致系统内存资源的浪费。防止内存泄露的方法包括:
使用引用计数机制:在程序中维护对象的引用计数,当引用计数为 0 时,表示对象不再被使用,可以将其回收。
- 使用垃圾回收机制:在程序中使用垃圾回收器,定期扫描内存中的对象,并回收不再使用的对象。
- 注意内存管理:在使用动态内存分配时,要记得在使用完后及时释放内存,避免出现内存泄露。
- 使用内存池:在程序中使用内存池,可以有效的避免内存泄露的问题。
- 使用调试工具:在程序开发过程中,使用调试工具(如 Valgrind)可以帮助我们检测内存泄露的问题。
9. 简述对原子变量了解
原子变量是一种特殊的变量,它可以在多线程环境中提供原子操作的保障。也就是说,在执行原子变量的操作时,不会被中断,从而保证操作的原子性。原子变量通常用于多线程环境中的计数器、计时器等场景,可以避免竞争条件的出现。
10. unordered_map桶大小相关
调用 rehash 函数会使 unordered_map 的桶数量增加,我们可以自己调用。
但是有几个注意事项:
- 调用rehash可能会使迭代器失效,需要重新进行获取迭代器
- 手动指定桶大小时,一般是选择2的幂次方,因为过小会导致哈希冲突过多,过大会导致内存浪费。
- rehash可以多次调用,但是频繁调用会影响性能。
11. c++性能优化手段
- 使用const:可以告诉编译器一个变量的值不应该被改变,这样可以提高性能,编译器可以进行一些优化,同时不变也能避免一些错误。
- 使用inline关键字:如果一个函数被内联,那么每次调用这个函数的时候,就会用函数体来替换调用语句,而不是跳转到函数所在的内存位置进行调用,这样减少函数调用的开销。
- 避免频繁进行内存分配和释放:对象池技术,是在内存中预先分配一块区域,用于存储特定类型的对象,当需要新的对象时,直接从对象池里获取,而不是用new分配新内存,当对象不再使用可以将其返回到对象池,而不是使用delete立即释放。这样可以减少内存分配时释放的频率,提高性能。
- 在某些情况下,使用引用而非直接的值:
- 如果函数要接受一个大的对象作为参数,那么使用引用而不是按值传递可以避免复制这个对象,提高性能。
- 此外,在函数要返回一个大的对象的时候,也使用引用的话可以避免复制这个对象
- 某些情况下,使用迭代器可能比使用指针更优:使用编译器自带的优化选项