元戎启行(软件工程师 - 仿真 - 一面)C++ 秋招面经1. 类成员函数后加 const 和 final 关键字分别代表什么?
- const:表示该成员函数不会修改类的任何成员变量(即不会改变对象的状态)。在成员函数声明后加 const,可以使得编译器在编译期间进行类型检查,确保该函数不会修改对象的状态。
- final:表示该成员函数是最终的,不允许在派生类中被重写。在成员函数声明后加 final,可以确保该函数的行为在派生类中保持一致,防止被意外修改。
2. 如果基类析构函数没有声明为虚函数会造成什么问题?
如果基类析构函数没有声明为虚函数,当通过基类指针删除派生类对象时,会导致派生类的析构函数不会被调用,从而造成内存泄漏和资源未正确释放的问题。因此,通常建议将基类的析构函数声明为虚函数,以确保在删除对象时能够正确调用派生类的析构函数。
3. 浅拷贝和深拷贝的区别(举例说明)?
- 浅拷贝:只复制对象的指针,而不复制指针所指向的数据。例如,当复制一个包含动态分配内存的类对象时,如果采用浅拷贝,则两个对象将共享同一块内存。
- 深拷贝:不仅复制对象的指针,还复制指针所指向的数据,从而生成两个完全独立的对象。例如,当复制一个包含动态分配内存的类对象时,如果采用深拷贝,则两个对象将拥有各自独立的内存块。
4. 重写和重载的区别?
- 重写(Override):是指在派生类中重新定义基类中的虚函数。重写要求函数名、参数列表和返回类型都与基类中的虚函数相同。重写用于实现多态性。
- 重载(Overload):是指在同一作用域内,允许存在多个同名函数,但这些函数的参数列表必须不同(参数个数或类型不同)。重载用于提高函数的灵活性和可读性。
5. 静态库 .a 和动态库 .so 的区别?
- 静态库 .a:在编译时,将库中的代码复制到最终的可执行文件中。因此,静态库生成的可执行文件体积较大,但运行时不需要额外的库文件。
- 动态库 .so:在编译时,只将库的引用信息添加到最终的可执行文件中。运行时,需要从系统中加载动态库。因此,动态库生成的可执行文件体积较小,但需要额外的库文件支持。动态库可以实现代码的共享和更新。
6. 从数据结构角度分析堆、栈、队列的区别?
- 堆:是一种特殊的树形数据结构,通常用于实现优先队列。堆中的元素没有固定的顺序,但每个节点的值都大于或等于(或小于或等于)其子节点的值。
- 栈:是一种后进先出(LIFO)的数据结构。栈中的元素按照后进先出的顺序进行访问和删除。栈通常用于实现函数调用和表达式求值等。
- 队列:是一种先进先出(FIFO)的数据结构。队列中的元素按照先进先出的顺序进行访问和删除。队列通常用于实现任务调度和消息传递等。
7. map 和 unordered_map 的区别?
- map:是基于红黑树实现的有序关联容器。map中的元素按照键的排序顺序进行存储和访问。因此,map支持高效的查找、插入和删除操作,并且可以按照键的顺序遍历元素。
- unordered_map:是基于哈希表实现的无序关联容器。unordered_map中的元素按照哈希值进行存储和访问。因此,unordered_map支持高效的查找、插入和删除操作,但不支持按照键的顺序遍历元素。
8. LeetCode:无序数组的中位数(类似 <数据流的中位数 No. 295>、要求 O(n) 时间复杂度、提示:快速排序)
可以使用快速选择算法(Quickselect)来解决这个问题。快速选择算法是快速排序算法的一种变形,用于在无序数组中找到第 k 小的元素。通过调整快速排序的划分过程,可以在 O(n) 的时间复杂度内找到中位数。
9. 分割等和子集(No. 416)
这是一个典型的动态规划问题。可以使用一个布尔数组 dp 来记录每个子集和是否可以通过选取数组中的元素得到。通过遍历数组并更新 dp 数组,最终可以判断是否存在一个子集和等于数组总和的一半。
百度(C++/PHP/GO 研发工程师 - 小度 - 一面)C++ 秋招面经1. vector 和 list 的区别以及查找、插入、删除的时间复杂度?
- vector:是一个动态数组,支持随机访问,但插入和删除操作(特别是在中间位置)的时间复杂度较高,为 O(n)。
- list:是一个双向链表,不支持随机访问,但插入和删除操作(特别是在中间位置)的时间复杂度较低,为 O(1)。
2. 向 vector 末尾 push_back 元素时底层做了哪些操作?
当向 vector 末尾 push_back 元素时,如果当前容量不足以容纳新元素,vector 会分配一块更大的内存空间(通常是当前容量的两倍),然后将原有元素复制到新的内存空间中,并添加新元素。
3. vector push_back 和 emplace_back 的区别以及使用场景?
- push_back:将元素复制到 vector 的末尾。适用于已知要添加的元素值的情况。
- emplace_back:在 vector 的末尾直接构造元素。适用于需要在 vector 中直接构造复杂对象的情况,可以减少一次不必要的复制或移动操作。
4. 如何清空一个 vector?
可以使用 vector.clear() 方法来清空一个 vector。该方法会删除 vector 中的所有元素,并将 vector 的大小设置为 0。但需要注意的是,该方法不会释放 vector 的容量,如果需要释放容量,可以使用 vector.shrink_to_fit() 方法(C++11 及以上版本)。
5. vector 是线程安全的吗?如何解决 vector 线程不安全的情况?
vector 本身不是线程安全的。在多线程环境中,如果多个线程同时访问和修改同一个 vector,可能会导致数据竞争和未定义行为。为了解决这个问题,可以使用互斥锁(如 std::mutex)来保护对 vector 的访问。或者使用 C++11 提供的线程安全容器(如 concurrent_vector,但这不是标准库的一部分,需要第三方库支持)。
6. 介绍一下模板的工作原理?
模板是 C++ 中实现泛型编程的一种机制。模板允许程序员编写与类型无关的代码,然后在编译时根据具体的类型生成相应的代码。模板的工作原理是:在编译时,编译器会根据模板参数的类型生成相应的函数或类代码。这样,就可以使用相同的模板代码来处理不同类型的数据。
7. 虚函数和纯虚函数的区别以及使用场景?
- 虚函数:是在基类中声明为 virtual 的成员函数。虚函数允许派生类重写基类的函数,从而实现多态性。
- 纯虚函数:是在基类中声明为 virtual 并赋值为 0 的成员函数。纯虚函数没有函数体,它要求派生类必须提供该函数的实现。纯虚函数用于定义抽象基类,使得派生类必须实现某些特定的功能。
8. 虚函数表存放在内存的哪个区(常量区)?
虚函数表通常存放在程序的常量区(也称为数据段或只读数据段)。常量区用于存储程序的常量数据,包括字符串常量、常量表达式和虚函数表等。需要注意的是,虚函数表的具体存放位置可能因编译器和操作系统的不同而有所差异。
9. 如何避免内存泄漏(RAII / 智能指针)?
- RAII(Resource Acquisition Is Initialization):是一种管理资源的技术,它利用对象的生命周期来自动管理资源。在 RAII 中,资源(如内存、文件句柄等)的获取和释放与对象的构造和析构绑定在一起。当对象被构造时,它会自动获取所需的资源;当对象被析构时,它会自动释放资源。这样可以有效地避免内存泄漏。
- 智能指针:是 C++ 中用于自动管理动态内存分配的类模板。智能指针通过封装指针的创建、使用和销毁过程,来确保动态内存的正确释放。C++ 标准库提供了几种智能指针类型,如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr 等。使用智能指针可以简化内存管理,并减少内存泄漏的风险。
10. 介绍一下智能指针的几种类型?
- std::unique_ptr:独占所有权的智能指针。它保证同一时间内只有一个 std::unique_ptr 可以指向某个对象。当 std::unique_ptr 被销毁时,它所指向的对象也会被自动销毁。
- std::shared_ptr:共享所有权的智能指针。它允许多个 std::shared_ptr 实例共享同一个对象。当最后一个指向该对象的 std::shared_ptr 被销毁时,对象才会被自动销毁。std::shared_ptr 通过引用计数来管理对象的生命周期。
- std::weak_ptr:是一种不控制对象生命周期的智能指针。它主要用于解决 std::shared_ptr 之间的循环引用问题。std::weak_ptr 可以指向一个由 std::shared_ptr 管理的对象,但它不会增加对象的引用计数。因此,当对象的最后一个 std::shared_ptr 被销毁时,即使还有 std::weak_ptr 指向该对象,对象也会被销毁。