小鹏汽车一面面经,项目问题只问了一个,觉得最难的是什么?且是怎么解决的?其他的都是八股内容,包含C++,python,数据库,很多问题都是只要答出一个关键点,面试官就默认你懂,就进入下一个问题了,并且每一个问题不仅要知其然,还要知其所以然,比如vector是连续的,那么它在内容是为什么是连续的?数据库中索引有什么优缺点,为什么会有这样的缺点?等等等很底层的知识点,就是要看你掌握的深度。
整体面下来感觉挺好的,并且结果非常快,上午11.15面试上午11.45出结果。
# 小鹏汽车嵌入式开发一面面经
# 1. 面向对象三大特征是什么?谈谈你对这三大特征的理解?
答案:
面向对象编程的三大基本特征是:封装、继承、多态。
- a.封装
将数据(属性)和操作数据的方法(行为)捆绑在一起,并对外部隐藏对象的内部实现细节,访问只能通过公开的接口 pubilc 进行。
我的理解: 就像一台电视机,我们不需要知道内部复杂的电路是如何工作的,只需要通过遥控器(公开的接口)上的几个按钮(如开关、音量键)来使用它。
封装使得代码模块化,使用更简单,也更安全。
- b.继承
允许一个类(子类/派生类)基于另一个已存在的类(父类/基类)来创建,子类会自动获得父类的属性和方法,并可以添加自己特有的属性和方法。
我的理解: 就像生物学上的分类,“动物”是父类,它有“吃”、“睡”等行为。
“猫”和“狗”是子类,它们继承了“动物”的所有特性,同时又有自己特有的行为,比如“猫会爬树”,“狗会看家”。
继承提高了代码的复用性。
- 多态
指同一个操作(方法)作用于不同的对象,可以有不同的解释,产生不同的执行结果,通常通过继承和接口实现。
我的理解: 还用“动物”举例。父类“动物”有一个“叫()”的方法。子类“猫”和“狗”都重写了这个方法。当我们调用“猫”的“叫()”方法时,输出“喵喵喵”;调用“狗”的“叫()”方法时,输出“汪汪汪”。这就是多态。
它允许我们以统一的接口处理不同类型的对象,提高了代码的灵活性和可扩展性。
# 2. 为什么要封装、多态、继承?(深入)
这个问题是追问三大特征存在的目的和价值。
为什么要封装?
隐藏实现细节,降低复杂度:使用者只需关注接口,无需关心内部实现,使得代码更易理解和维护。
提高安全性:通过将数据设置为私有,可以防止外部代码随意修改内部数据,只能通过受控的方法进行访问和修改,保证了数据的一致性和有效性。
降低耦合度:只要公开的接口不变,内部实现的修改不会影响使用该对象的其他代码。
为什么要继承?
代码复用:这是最直接的目的。
子类可以复用父类已经定义好的属性和方法,避免了重复编写相同的代码。
建立类之间的层次关系:通过继承,可以清晰地表示现实世界中“是一类”的关系,使代码结构更符合逻辑,更易于扩展。
为什么要多态?
增强程序的可扩展性:这是多态最重要的价值。
假设我们有一个函数,它接收一个“动物”类型的参数,在不修改这个函数的情况下,我们可以传入任何“动物”的子类对象(如猫、狗、牛),函数都能正确调用该对象特有的“叫()”方法。
如果要新增一个“鸭子”类,只需让新类“鸭子”继承“动物”并重写“叫()”方法即可,之前的函数完全不需要改动。
接口统一:允许用父类的指针或引用来操作子类对象,使代码更通用、更简洁。
总结: 封装是基础,用于隐藏细节,保证安全;继承是扩展,用于代码复用,建立层次;多态是目标,基于继承实现,用于提高代码的灵活性和可扩展性。
三者共同构成了面向对象编程的强大能力。
# 3. 如何判断链表中是否有环?
答案: 最经典的方法是使用 快慢指针,也称为 Floyd 判圈算法。
代码随想录原题,这里就不展开了
# 4. 虚函数表的机制是什么?
答案:
虚函数表是 C++ 实现运行时多态(动态绑定)的核心机制。
当一个类中含有virtual关键字声明的虚函数时,编译器会为该类生成一个虚函数表,这是一个函数指针数组,数组中的每个元素指向一个虚函数的实际代码地址。
同时,编译器会隐式地在类的每个对象实例中添加一个指针,称为虚函数表指针,它指向该类的虚函数表。
当发生继承时,子类会继承父类的虚函数表,如果子类重写了父类的虚函数,那么子类虚函数表中对应的函数指针会被更新为指向子类自己的函数实现。
当通过基类指针或引用调用虚函数时,程序会通过以下步骤找到正确的函数:
a. 通过对象内部的vptr找到该对象所属类的虚函数表。
b. 在虚函数表中查找被调用虚函数的地址。
c. 调用该地址指向的函数。
# 5. C++内存管理常用方法有哪些?
答案:
静态内存分配: 在编译期确定大小和生命周期。
例如全局变量、static变量,内存在程序启动时分配,结束时释放。
栈内存分配: 函数内的局部变量、函数参数等。
内存的分配和释放由编译器自动管理,遵循后进先出的原则,效率高,但容量有限。
堆内存分配: 使用new/malloc等操作符在运行时动态申请任意大小的内存,生命周期由程序员手动控制,使用 delete/free释放,非常灵活,但管理不当易产生内存泄漏或野指针。
智能指针: 这是现代 C++ 推荐的管理堆内存的方,它是类模板,通过 RAII 机制将堆内存封装在对象中,利用栈上对象的析构函数自动释放内存。
主要类型有:
std::unique_ptr: 独占所有权,同一时间只能有一个 unique_ptr指向一个对象。
std::shared_ptr: 共享所有权,通过引用计数管理多个指针指向同一个对象。
std::weak_ptr: 配合 shared_ptr使用,解决循环引用问题,不增加引用计数。
# 6. vector和list有什么区别?核心区别?底层实现?
答案:
核心区别:底层数据结构的连续性,这直接导致了它们在随机访问和中间插入/删除性能上的根本差异。
| 特性 | vector(动态数组) | list(双向链表) |
|---|---|---|
| 底层数据结构 | 一段连续的内存空间 | 非连续的节点,每个节点包含数据和指向前后节点的指针 |
| 随机访问 | 支持,效率高 O(1) | 不支持,效率低 O(n) |
| 插入/删除 | 在尾部快 O(1),在中间或头部慢 O(n) | 在已知位置插入/删除快 O(1) |
| 内存使用 | 内存紧凑,额外开销小 | 每个节点都需要额外空间存储指针 |
| 迭代器类型 | 随机访问迭代器 | 双向迭代器 |
| 缓存友好性 | 好,数据局部性原理 | 差,内存不连续 |
# 追问,为什么vector内存连续,list不连续?
vector的设计目标是提供快速的随机访问,连续内存布局意味着可以通过简单的基地址+偏移量的计算(start + n * sizeof(type))在常数时间内访问任何元素,这完全契合数组的特性。
这也是为什么它被称为“动态数组”。
list的设计目标是提供高效的任意位置插入和删除,如果使用连续内存,在中间插入一个元素,需要移动其后所有元素,成本是 O(n)。
而链表只需修改相邻节点的指针,成本是 O(1),为了实现这个目标,它必须牺牲内存的连续性。
# 7. 如何反转一个单链表?
答案: 常用迭代法 算法思路: 初始化三个指针:prev指向 NULL,curr指向头节点 head,next用于临时存储。 遍历链表,在每一步中: a. 保存 curr的下一个节点:next = curr->next。 b. 将 curr的 next指针指向前一个节点 prev,完成反转。 c. 将 prev和 curr都向后移动一位:prev = curr, curr = next。 当 curr为 NULL时,遍历结束,此时 prev指向的就是新的头节点。
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
ListNode* next = nullptr;
while (curr != nullptr) {
next = curr->next; // 保存下一个
curr->next = prev; // 反转指针
prev = curr; // 移动prev
curr = next; // 移动curr
}
return prev; // 返回新头节点
}
2
3
4
5
6
7
8
9
10
11
12
13
# 8. Python列表和元组的区别?
答案:
| 方面 | 列表(list) | 元组(tuple) |
|---|---|---|
| 定义语法 | 方括号 [],如 [1, 2, 3] | 圆括号 (),如 (1, 2, 3) |
| 可变性 | 可变,可以动态增、删、改元素 | 不可变,一旦创建,内容不能修改 |
| 用途 | 用于存储可变的数据集合 | 用于存储固定的数据集合,常作为字典的键或函数返回值 |
| 可操作性 | 提供 append(), insert(), pop(), remove()等方法 | 没有修改自身的方法,只有查询方法如 count(), index() |
| 性能 | 由于可变,开销稍大 | 由于不可变,内存更小,创建和访问速度更快 |
| 哈希性 | 不可哈希,不能作为字典的键 | 如果所有元素都可哈希,则元组本身可哈希,可作为字典的键 |
核心区别:可变性,这决定了它们的使用场景和性能特征。
# 9. 深拷贝和浅拷贝的区别是什么?
答案: 这个问题主要针对复合对象(即对象中包含其他对象的引用,如列表、字典)。
- 浅拷贝:
只拷贝对象本身,而不拷贝对象内部所引用的子对象。
新旧对象中,对于可变子对象(如列表)的引用指向的是同一个内存地址。
结果: 修改原始对象中的可变子对象,会影响到拷贝后的对象。 Python示例: copy.copy()
- 深拷贝:
递归地拷贝对象本身以及它引用的所有子对象,直到最底层。
新旧对象完全独立,没有任何共享的数据。 结果: 对其中一个对象的任何修改都不会影响另一个。
Python示例:copy.deepcopy()
import copy
list1 = [1, 2, [3, 4]]
# 浅拷贝
list2 = copy.copy(list1)
# list1 和 list2 是兩個不同的列表对象,
# 但它们内部的第三个元素([3,4]这个列表)是共享的。
list1[2].append(5)
print(list2) # 输出 [1, 2, [3, 4, 5]], list2也被影响了!
# 深拷贝
list3 = copy.deepcopy(list1)
# list1 和 list3 以及它们内部的所有元素都是独立的副本。
list1[2].append(6)
print(list3) # 输出 [1, 2, [3, 4, 5]], list3不受影响。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 10. 什么是哈希表?
答案:
哈希表是一种使用哈希函数来实现高效数据访问的数据结构。
核心思想: 通过一个哈希函数,将键映射到数组中的一个位置,从而可以在平均O(1)时间复杂度内完成插入、删除和查找操作。
工作流程:
插入: 计算键的哈希值,根据哈希值确定在数组(称为哈希桶)中的存储位置,然后将值存入该位置。
查找: 计算键的哈希值,找到对应的位置,检查该位置的值是否是要查找的键值对。
关键问题: 哈希冲突,即不同的键可能被映射到同一个数组位置上。
# 11. 哈希映射产生冲突了该怎么办?
答案:
- 方案1 开放定址法:
当冲突发生时,通过某种探测方法(如线性探测、平方探测)在哈希表中寻找“下一个”空位置。
优点:所有数据都存储在同一个数组中,结构简单。 缺点:容易产生“聚集”现象,删除操作麻烦。
- 方案2 链地址法:
这是最常用的方法。哈希表的每个位置(桶)不再直接存储一个元素,而是存储一个链表(或红黑树)的头指针。
当冲突发生时,将新的键值对插入到对应桶的链表中。
优点: 处理简单,无聚集现象,易于删除。
缺点: 需要额外的指针空间。如果某个链表过长,会退化为 O(n) 的查找。
哈希表数组: [0] -> NULL
[1] -> (key1, value1) -> (key2, value2) -> NULL // 冲突,用链表连接
[2] -> NULL
...
2
3
4
# 12. 数据库的索引?优点和缺点?
答案:
是什么: 数据库索引就像一本书的目录,它是一种数据结构,帮助数据库系统快速定位和访问表中的特定数据,而无需扫描整个表。
常见的索引结构是B+Tree。
优点:
极大提高数据检索速度:这是最主要的目的,特别是对带有WHERE子句的查询。
保证数据的唯一性:唯一索引可以确保某一列或多列组合的值是唯一的。
加速表连接:在连接操作时,如果连接字段上有索引,会大大提高效率。
缺点:
占用额外的存储空间:索引本身也是一种数据结构,需要占用磁盘和内存空间。
降低数据增、删、改的速度:当对表中的数据进行插入、删除和更新时,数据库不仅需要操作数据本身,还需要维护索引结构,这会带来额外的开销。
# 追问:针对缺点追问“为什么有这个缺点”?
为什么占用空间?因为索引是独立于数据之外的附加结构。
例如,一个 B+Tree 索引包含了键值、指针等信息,这些都需要存储。
为什么降低增删改速度? 因为数据的变更需要同步更新所有相关的索引,以保持索引和数据的一致性。
例如,插入一条新记录,除了插入到数据页,还需要在索引树的正确位置插入一个新的键值,这个过程可能涉及树的平衡操作。
评论
验证登录状态...