本文主要是介绍后端开发面经系列 --中望C++面经,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
中望C++面经,全部内容!
公众号:阿Q技术站
文章目录
- 中望C++面经,全部内容!
- 一面 8.15 时长45min
- 1、介绍项目相关
- 2、gdb怎么调试的?打断点用什么指令?
- 3、gcc的编译过程
- 4、cmake添加头文件搜索路径用什么指令?添加动态链接库呢?
- 5、什么是内核态,什么是用户态?
- 6、C++内存分区?
- 7、B-树、B树、B+树的演进?
- 8、B+树增删改查时间复杂度?
- 9、死锁的条件?
- 10、迭代器和指针有什么区别?
- 11、list和vector的迭代器有什么不一样的地方?
- 12、map的迭代器可以++吗?怎么实现的?
- 13、Deque怎么实现的?
- 14、STL的sort是怎么实现的?
- 15、Deque比Vector的优势在哪里,除了双端开口?
- 16、C++多态如何实现的?虚函数表指针存在哪里(哪个内存分区)?
- 二面 8.22 时长45min
- 1、聊项目
- 2、场景题:一个图片对其进行缩放操作,随着缩放次数的增加,用户的体验会越来越卡顿,你怎么优化?
- 3、最近在读哪些技术书籍?对你有什么帮助吗?
- 4、平时有什么兴趣爱好?
- 5、反问环节。
来源:https://www.nowcoder.com/discuss/550638389125541888
一面 8.15 时长45min
1、介绍项目相关
2、gdb怎么调试的?打断点用什么指令?
- 编译时添加调试信息:在编译程序时,需要使用
-g
选项,以便将调试信息嵌入可执行文件中。例如:
g++ -g -o my_program my_program.cpp
- 启动 GDB:在终端中执行以下命令:
gdb ./my_program
这将启动 GDB 并加载你的可执行文件。
- 设置断点:设置断点以在程序执行到指定位置时暂停。可以使用
break
命令:
break main
这将在main
函数的开头设置一个断点。你也可以使用文件名和行号设置断点。
- 运行程序:在 GDB 中执行
run
命令启动程序:
run
当程序执行到断点时,会暂停执行。
- 查看变量的值:使用
print
或简写的p
命令来查看变量的值:
print variable_name
- 单步执行:使用
step
命令来单步执行程序,进入函数内部:
step
- 下一步执行:使用
next
命令来执行下一行代码,不进入函数内部:
next
- 继续执行:使用
continue
或简写的c
命令来继续执行程序直到下一个断点:
continue
- 查看堆栈:使用
backtrace
或简写的bt
命令来查看函数调用堆栈:
backtrace
- 退出 GDB:使用
quit
或简写的q
命令退出 GDB:
quit
3、gcc的编译过程
- 预处理(Preprocessing):
- 输入文件: 源代码文件(通常以
.c
、.cpp
、.c++
、.h
等为扩展名)。 - 处理工具: 预处理器(
cpp
)。 - 过程: 预处理器会执行一系列的预处理操作,包括宏替换、文件包含、条件编译等。产生一个新的临时文件,通常以
.i
或.ii
为扩展名。
- 输入文件: 源代码文件(通常以
gcc -E input.c -o output.i
-
编译(Compilation):
-
输入文件: 预处理生成的文件(
.i
或.ii
)。 -
处理工具: 编译器(
cc1
)。 -
过程: 编译器将源代码翻译成汇编代码。产生一个汇编文件,通常以
.s
为扩展名。
-
gcc -S output.i -o output.s
-
汇编(Assembly):
-
输入文件: 编译生成的汇编文件(
.s
)。 -
处理工具: 汇编器(
as
)。 -
过程: 汇编器将汇编代码翻译成目标文件(机器码)。产生一个目标文件,通常以
.o
或.obj
为扩展名。
-
gcc -c output.s -o output.o
-
链接(Linking):
-
输入文件: 目标文件、库文件等。
-
处理工具: 链接器(
ld
)。 -
过程: 链接器将目标文件、库文件等合并生成最终的可执行文件。链接的过程包括地址解析、符号解析、重定位等。
-
gcc output.o -o executable
4、cmake添加头文件搜索路径用什么指令?添加动态链接库呢?
在 CMake 中,可以使用 include_directories
指令添加头文件搜索路径,使用 target_link_libraries
指令添加动态链接库。
- 添加头文件搜索路径:
include_directories(path/to/include)
这样就会将 path/to/include
目录添加到项目的头文件搜索路径中。
- 添加动态链接库:
target_link_libraries(your_target_name library_name)
这里 your_target_name
是你项目中的目标名称,library_name
是要链接的动态库名称。你可以指定多个库,它们之间用空格分隔。
如果库文件不在默认的系统库搜索路径中,你可能还需要使用 link_directories
指令指定库文件的搜索路径:
link_directories(path/to/library)
注意:尽量避免使用 link_directories
,而是将库文件路径传递给 target_link_libraries
,因为 link_directories
是全局的,可能影响其他目标。而 target_link_libraries
可以更精确地指定目标所需的库。
给个示例 CMakeLists.txt 文件:
# CMake 最低版本要求
cmake_minimum_required(VERSION 3.10)# 项目名称
project(MyProject)# 添加头文件搜索路径
include_directories(path/to/include)# 添加库文件搜索路径(可选)
# link_directories(path/to/library)# 添加可执行文件
add_executable(my_executable main.cpp)# 添加动态链接库
target_link_libraries(my_executable my_library)
5、什么是内核态,什么是用户态?
- 内核态(Kernel Mode):
- 在内核态执行时,程序具有较高的特权级别,可以执行任意指令,包括对硬件的直接访问。
- 内核态通常是操作系统的核心部分,用于执行敏感的、特权的操作,如管理硬件、执行系统调用等。
- 在内核态执行时,程序能够执行所有指令,访问所有内存区域,并能响应所有中断。
- 用户态(User Mode):
- 在用户态执行时,程序的特权级别较低,受到一定的限制,不能执行某些特权指令,也不能直接访问硬件资源。
- 大多数应用程序在用户态下执行,它们不能直接操作底层硬件,而是通过系统调用请求内核态的服务。
- 在用户态执行时,程序不能随意访问所有内存区域,也不能执行一些特权操作。
操作系统使用这两种特权级别的切换来保护系统的稳定性和安全性。当用户程序需要执行一些特权操作时,例如请求文件读写、申请内存等,它会通过系统调用陷入内核态,由操作系统内核来执行相应的特权操作。执行完毕后,再返回到用户态。
6、C++内存分区?
在C++中,内存分成5个区,他们分别是栈、堆、自由存储区、全局/静态存储区和常量存储区。
- 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
- 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
- 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
- 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
7、B-树、B树、B+树的演进?
- B-树(Balance Tree):
- B-树是一种平衡的多路搜索树,每个节点包含多个子节点。B-树的特点是每个节点包含关键字和子节点的信息,且所有叶子节点都在同一层次上,使得查询时能够更快速地定位到目标数据。
- B-树常用于文件系统和数据库索引结构,例如在数据库中作为索引的数据结构,可以加速检索操作。
- B树(Binary Tree):
- B树是B-树的一个特例,其每个节点最多包含两个子节点。B树的演进可以看作是对B-树的简化,特别适用于内存中的数据结构。B树的每个节点包含一个或多个关键字和对应的子树。
- B树在计算机科学中有广泛的应用,尤其在数据库和文件系统中,用于加速检索和存储操作。
- B+树:
- B+树是在B树的基础上进行的改进。与B树不同,B+树的非叶子节点不存储数据,只存储关键字和子节点的信息,而数据都存储在叶子节点上。
- B+树的叶子节点形成了一个有序链表,可以方便地进行范围查询,而且所有叶子节点都有一个指向下一个叶子节点的指针,形成了更加紧凑的结构。
- B+树在数据库索引中广泛应用,它更适合范围查询和顺序遍历。
8、B+树增删改查时间复杂度?
-
插入(Insert):
B+树的插入操作一般是从根节点开始,向下逐层查找要插入的位置,然后插入到叶子节点上。如果插入导致节点的关键字个数超过限制,可能需要进行节点的分裂操作,但分裂操作的影响是局部的,因此时间复杂度为 O(log n),其中 n 是B+树的高度。
-
删除(Delete):
B+树的删除操作也是从根节点开始,向下逐层查找要删除的关键字所在的位置,然后删除。删除可能导致节点关键字的减少,可能需要进行节点的合并或者借用邻近节点的关键字,但合并和借用的操作也是局部的,时间复杂度也为 O(log n)。
-
查找(Search):
B+树的查找操作从根节点开始,根据关键字逐层向下查找,最终定位到目标叶子节点。由于B+树的叶子节点形成有序链表,可以通过二分查找或顺序查找在叶子节点上找到目标关键字。整个查找过程的时间复杂度同样是 O(log n)。
-
更新(Update):
B+树的更新操作涉及到查找和修改,因此时间复杂度也是 O(log n)。
需要注意的是,虽然B+树的各项操作的最坏时间复杂度都是 O(log n),但由于B+树的节点结构相对于B树更加紧凑,节点关键字数量更多,因此实际上B+树的查询效率往往更高。此外,B+树的范围查询和范围删除等操作也更加高效。
9、死锁的条件?
-
互斥条件(Mutual Exclusion):
进程对所分配到的资源进行排他性控制,即在一段时间内某资源只由一个进程占用。如果一个进程申请到了某资源,其他进程就无法再申请到该资源。
-
请求与保持条件(Hold and Wait):
进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占用。因此,如果一个进程在等待其他资源的同时还保持了自己占有的资源,就可能导致死锁。
-
不可剥夺条件(No Preemption):
进程已获得的资源在未使用完之前不能被其他进程抢占,只能在使用完时自己释放。如果一个进程获得了某资源,其他进程不能强制抢占这个资源,只有等待该进程主动释放。
-
循环等待条件(Circular Wait):
存在一个进程等待队列
{P1, P2, ..., Pn}
,其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个循环等待链。
10、迭代器和指针有什么区别?
- 泛化性:
- 迭代器: 迭代器是一种更抽象的概念,它提供对容器中元素的访问方式,不仅限于数组。迭代器可以用于各种容器,如链表、数组、集合等。
- 指针: 指针是一种特定类型的变量,直接指向内存地址。通常,指针主要用于数组和动态内存分配。
- 语法和操作:
- 迭代器: 迭代器通过使用
begin()
和end()
等方法获得,并通过++
、--
等操作符进行遍历。 - 指针: 指针通过
&
获取地址,通过*
解引用,使用++
、--
进行遍历。
- 迭代器: 迭代器通过使用
- 容器独立性:
- 迭代器: 迭代器是与容器独立的,同一种迭代器接口可以应用于不同类型的容器。
- 指针: 指针的类型与指向的数据类型直接相关。
- 范围:
- 迭代器: 迭代器可以表示容器中的任意位置,包括容器的开头、结尾和中间。
- 指针: 指针通常表示数组中的某个位置。
- 安全性:
- 迭代器: 在一些情况下,迭代器可能提供更安全的访问,因为它们可以在编译时检测到类型不匹配等错误。
- 指针: 指针的使用可能更容易导致内存越界和类型不匹配的问题。
11、list和vector的迭代器有什么不一样的地方?
- 内部实现:
vector
: 使用动态数组实现,具有连续的内存存储。list
: 使用双向链表实现,每个元素在内存中可以位于任意位置。
- 迭代器失效:
vector
: 在插入或删除元素时,可能会导致迭代器失效,因为动态数组的大小可能会发生变化,地址可能会改变。list
: 插入或删除元素时,不会导致迭代器失效,因为链表中的元素位置不会改变。
- 随机访问性:
vector
: 提供随机访问能力,迭代器支持+
、-
运算符,可以跳跃式访问元素。list
: 不支持随机访问,只能通过递增或递减迭代器一个位置一个位置地访问元素。
- 插入和删除操作:
vector
: 在中间或开头插入或删除元素可能需要移动其他元素,效率相对较低。list
: 在任何位置插入或删除元素都是常数时间操作,不需要移动其他元素。
- 空间占用:
vector
: 动态数组可能会分配一块连续的内存,可能会浪费一些空间。list
: 链表节点可以在内存中分散,但可能会有额外的指针占用空间。
12、map的迭代器可以++吗?怎么实现的?
map
的迭代器是双向迭代器,支持前移和后移操作,但不支持随机访问。因此,map
的迭代器是可以进行 ++
(前移)和 --
(后移)操作的。
map
是基于红黑树实现的,红黑树是一种自平衡的二叉搜索树。在红黑树中,每个节点包含一个键值对,按键值的大小顺序排列。map
的迭代器实际上是红黑树的迭代器,通过树的中序遍历(左根右)来实现对键值对的有序遍历。
给个例子演示一下 map
的迭代器的++
操作:
#include <iostream>
#include <map>int main() {std::map<int, std::string> myMap;// 插入键值对myMap[1] = "One";myMap[2] = "Two";myMap[3] = "Three";// 使用迭代器进行遍历for (auto it = myMap.begin(); it != myMap.end(); ++it) {std::cout << it->first << ": " << it->second << std::endl;}return 0;
}
13、Deque怎么实现的?
std::deque
(双端队列)是一种支持在两端高效插入和删除操作的数据结构。它通常被实现为一系列固定大小的连续存储块,每个存储块被称为一个缓冲区(buffer),并通过指针链接起来。这种实现方式使得 std::deque
具有高效的两端插入和删除操作。
简单描述一下 std::deque
是如何实现的:
- 每个缓冲区是一个固定大小的数组。
std::deque
有一个控制块,其中包含指向第一个和最后一个缓冲区的指针。- 当在两端执行插入或删除操作时,如果当前缓冲区没有足够的空间,会分配一个新的缓冲区,并通过指针将其链接到已有的缓冲区链表中。
- 在两端插入和删除时,可以直接在控制块中更新指针,而不需要移动元素。
这样的实现使得 std::deque
具有近似于 std::vector
的随机访问性能,同时在两端插入和删除时仍然保持高效。这也是 std::deque
的一种典型的实现方式,但具体实现可能会有所不同。
14、STL的sort是怎么实现的?
STL中的 sort
算法通常是使用一种混合排序的方法来实现的。混合排序结合了多种排序算法,根据不同的情况选择最适合的排序算法。包括以下几个阶段:
- 快速排序:快速排序是一种高效的分治算法,在大多数情况下能够达到很好的性能。
std::sort
通常使用快速排序作为其基本排序算法。 - 插入排序:在数据量较小的情况下,插入排序可能比快速排序更有效。因此,当待排序的元素数量较小时,
std::sort
可能会切换到插入排序。 - 堆排序:堆排序是一种选择排序算法,可以在不占用额外空间的情况下进行排序。
std::sort
在某些情况下也可能使用堆排序。 - 三元素取中法:在快速排序的过程中,通常选择数组的中间元素作为枢轴(pivot)。为了提高算法的稳定性,
std::sort
可能会使用三元素取中法,即选择左端、中间和右端三个元素中的中间元素作为枢轴。
15、Deque比Vector的优势在哪里,除了双端开口?
- 动态内存块:
deque
的内部结构通常由多个连续的动态分配的内存块组成,而不是像vector
那样的单一内存块。这意味着在deque
的两端执行插入和删除操作时,不需要移动整个容器的元素,而是在对应的内存块中进行操作。这使得在两端进行插入和删除操作的开销相对较小。 - 更好的中间插入性能: 由于
deque
内部结构的特点,它对于在序列中间插入或删除元素的性能更好。在vector
中,如果在中间插入或删除元素,可能需要移动后续元素,而deque
可以在不涉及整体元素移动的情况下进行中间插入或删除。 - 更好的扩展性: 由于
deque
内部结构的动态性质,deque
的扩展更为灵活。它可以在两端或中间动态添加新的块,而不需要重新分配整个容器。 - 迭代器的稳定性:
deque
对于插入和删除操作后,迭代器的稳定性相对较好。在vector
中,插入或删除元素可能导致迭代器失效,而deque
的内部结构允许在两端和中间进行插入和删除操作而不影响迭代器的有效性。
16、C++多态如何实现的?虚函数表指针存在哪里(哪个内存分区)?
- 虚函数(Virtual Function): 在基类中声明为
virtual
的成员函数是虚函数。派生类可以覆盖(重写)基类中的虚函数。 - 虚函数表(Virtual Function Table): 虚函数表是一个存储虚函数地址的表格。每个包含虚函数的类都有一个虚函数表。当类包含虚函数时,每个对象都包含一个指向虚函数表的指针,通常称为虚指针(vpointer或vptr)。这个虚指针位于对象内存布局的开始位置。
- 虚指针(vptr): 虚指针是一个指向虚函数表的指针,它位于对象的内存布局的最开始位置。这个指针在对象的构造阶段被设置为指向正确的虚函数表。
- 动态绑定: 动态绑定是在运行时确定调用的函数,而不是在编译时确定。它允许通过基类的指针或引用调用派生类的虚函数。
给个例子:
#include <iostream>class Base {
public:virtual void show() {std::cout << "Base class\n";}
};class Derived : public Base {
public:void show() override {std::cout << "Derived class\n";}
};int main() {Base* ptr;Derived obj;ptr = &obj;// 虚函数的动态绑定ptr->show();return 0;
}
二面 8.22 时长45min
1、聊项目
2、场景题:一个图片对其进行缩放操作,随着缩放次数的增加,用户的体验会越来越卡顿,你怎么优化?
3、最近在读哪些技术书籍?对你有什么帮助吗?
4、平时有什么兴趣爱好?
5、反问环节。
这篇关于后端开发面经系列 --中望C++面经的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!