秋招日寄|嵌入式模拟面试拷打|20240918
1. push_back()
左值和右值的区别是什么?
左值:当传递一个左值给 push_back()
时,push_back
会调用拷贝构造函数,将对象复制到 vector
的最后一个位置。
右值:当传递一个右值给 push_back()
时,如果存在右值引用版本的 push_back()
,它会调用移动构造函数,避免不必要的拷贝,从而提高性能。
深入分析:左值和右值的处理方式是 C++ 的核心特性之一,特别是在资源管理和性能优化时,移动语义的引入极大提高了容器类(如 vector
)的效率。对资源占用较高的对象使用右值引用和移动构造,可以避免深拷贝。
2. std::move
底层是如何实现的?
std::move
是一个模板函数,它将对象强制转换为右值引用。这并不会真正移动对象的内容,只是通过将左值转换为右值引用,使该对象可以绑定到右值引用参数中。
深入分析:std::move
的核心作用是配合移动语义使用,它不会改变对象的状态或位置,只是将对象的类型转换为右值,以便可以调用相关的移动操作。在资源管理中,std::move
能减少不必要的内存开销。
3. 完美转发的原理是什么?
完美转发允许函数模板将参数原封不动地传递给另一个函数。其核心在于 std::forward
,它结合模板参数推导和右值引用,通过判断传入参数是左值还是右值,确保传递时不改变参数的值类别。
深入分析:完美转发的实现依赖于引用折叠规则。通过 std::forward
保持参数的原始类型,避免不必要的拷贝或移动。它是泛型编程中非常有用的工具,能够在编写高效库代码时发挥重要作用。
4. 空类中有什么函数?
空类中默认包含以下函数:
- 默认构造函数
- 拷贝构造函数
- 移动构造函数(C++11 及以上)
- 拷贝赋值运算符
- 移动赋值运算符(C++11 及以上)
- 析构函数
深入分析:即便空类没有显式定义成员函数,编译器仍会为其生成默认的构造函数和赋值操作符。C++11 后引入的移动构造函数和移动赋值运算符极大提高了对象的灵活性。
5. explicit
用在哪里?有什么作用?
explicit
关键字用于构造函数和转换运算符,它的作用是防止隐式转换,以避免意外调用构造函数或类型转换。
深入分析:隐式转换可能导致代码中发生未预期的行为。通过显式声明构造函数为 explicit
,可以减少这种不必要的错误,特别是在复杂的类层次中防止意外的自动转换。
6. 成员变量初始化的顺序是什么?
成员变量的初始化顺序是按照它们在类定义中的声明顺序进行,而不是按照初始化列表中的顺序。
深入分析:编译器严格遵循声明顺序来初始化成员变量,而不是构造函数的初始化列表顺序。这可能会在成员变量依赖于其他变量的情况下引发问题,因此需要谨慎管理初始化顺序。
7. 指针占用的大小是多少?
指针的大小取决于平台的位数。在 32 位系统上通常为4 字节,在 64 位系统上为8 字节。
深入分析:指针大小与系统架构直接相关,在处理跨平台代码时需要注意,尤其在内存对齐和大小计算时。指针大小也会影响到数据结构和程序性能。
8. 什么是野指针和内存泄漏?如何避免?
野指针: 指向已经释放或未分配内存的指针。内存泄漏: 分配的内存没有被释放,导致该内存无法再被使用。
避免方法:
- 初始化指针
- 使用智能指针(如
std::unique_ptr
和std::shared_ptr
) - 确保在合适的地方释放内存
深入分析:野指针和内存泄漏是 C++ 开发中常见的内存管理问题。智能指针的引入使得内存管理变得更加安全,避免了手动管理的复杂性。
9. malloc
和 new
的区别是什么?
malloc
只分配内存,但不调用构造函数,必须手动指定内存大小。new
不仅分配内存,还会调用构造函数,并且自动计算所需大小。
深入分析:malloc
和 new
的最大区别在于后者还涉及对象的构造。现代 C++ 中更推荐使用 new
和智能指针,而不再手动使用 malloc
,这能避免手动管理对象生命周期的问题。
10. 多线程会发生什么问题?线程同步有哪些手段?
问题:
- 数据竞争
- 死锁
- 饥饿
- 竞态条件
同步手段:
- 互斥锁(
std::mutex
) - 条件变量(
std::condition_variable
) - 原子操作(
std::atomic
) - 信号量
深入分析:线程同步是保证多线程环境下数据一致性的关键,合理使用同步机制可以避免数据竞争和死锁等问题。同时,应尽量减少锁的使用频率,以提高程序性能。
11. 什么是 STL?
STL(标准模板库)是 C++ 标准库的一部分,提供了常用的数据结构、算法和迭代器,为开发者提供了一套通用、复用性强的组件。
深入分析:STL 的引入大大提高了 C++ 的开发效率,开发者可以通过 STL 提供的容器、算法等组件快速实现高效的代码,同时保证代码的安全性和可维护性。
12. 对比迭代器和指针的区别
迭代器: 更通用,能够访问不同容器的元素,并提供丰富的操作接口,如 ++
、--
、*
等。指针: 是一种特化的迭代器,通常用于数组或原生指针操作,功能相对较单一。
深入分析:迭代器抽象了容器操作,提供统一接口,支持 STL 中所有容器。而指针仅限于操作原始内存块。迭代器的灵活性使得其在泛型编程中不可或缺。
13. 线程有哪些状态?线程锁有哪些?
线程状态:
- 新建
- 就绪
- 运行
- 阻塞
- 终止
线程锁:
- 互斥锁(
std::mutex
) - 读写锁(
std::shared_mutex
) - 自旋锁
深入分析:多线程编程中,线程的状态和锁的选择直接影响程序的执行效率和数据一致性。自旋锁适合短时间锁定的场景,而读写锁适合读多写少的场景。
14. map
和 unordered_map
的区别
map
是基于红黑树实现的有序关联容器,键按顺序排列。unordered_map
是基于哈希表实现的无序关联容器,通过哈希值访问键值对。
深入分析:map
提供有序性和对数复杂度的查找,而 unordered_map
则提供平均 O(1) 的查找效率。选择哪种容器取决于具体应用中是否需要键的有序性。
15. push_back()
和 emplace_back()
的区别及使用场景
push_back()
:将元素的副本添加到vector
中。emplace_back()
:在vector
尾部直接构造元素,避免不必要的拷贝或移动操作。
深入分析:emplace_back()
更加高效,适合在性能要求较高的场景下使用,尤其是当对象的构造复杂或不希望额外的拷贝时。