华为od 面试八股文_C++_02_含答案 女爷i 2024-04-20 14:13 69阅读 0赞 **目录** 1:虚函数是怎么实现的? 2:了解原子操作吗? 3:简单介绍下vector和list的底层原理以及对应的优缺点 3:说说unordered\_map的扩容过程 4:动态链接和静态链接的区别,动态链接的原理是什么? 5:构造函数是否能声明为虚函数?为什么? 6:如何处理内存泄漏问题?提供一些常见的内存管理技术。 7:列举并解释STL库中常用容器,例如vector、list、map等。 8:C++11引入了哪些新特性?请列举几个重要的特性并简要解释它们。 9:什么是智能指针?列举几种常见的智能指针类型,并解释其特点和适用场景。 10:请实现一个单例模式?列举几种实现方式并简要说明其优缺点。 -------------------- #### 1:虚函数是怎么实现的? #### 在C++中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针 虚函数表:每个包含虚函数的类都会生成一个虚函数表,其中存储着该类中所有虚函数的地址。虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。 虚函数指针:在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或虚表指针。这个指针指向该对象对应的虚函数表,从而让程序能够动态的调用虚函数。 当一个基类指针或引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。 在编译阶段生成,虚函数和普通函数一样存放在代码段,只是它的指针又存放在了虚表之中。 #### 2:了解原子操作吗? #### 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何切换到另一个线程。 原理是:在X86的平台下,CPU提供了在指令执行期间对总线加锁的手段,CPU中有一根引线\#HLOCK pin连接到北桥,如果汇编语言的程序在程序中的一条指令前面加上了前缀“LOCK”,经过汇编之后的机器码就使CPU在执行这条指令的时候把\#HLOCKpin的电平拉低持续到这条指令结束的时候放开,从而把总线锁住,这样别的CPU就暂时不能够通过总线访问内存了,保证了多处理器环境中的原子性。 #### 3:简单介绍下vector和list的底层原理以及对应的优缺点 #### Vector优点:可使用下标随机访问,尾插尾删效率高。 缺点:前面部分的插入删除效率低,扩容有消耗,可能存在一定的空间浪费。 底层是由一块连续的内存空间组成,由三个指针实现的分别是头指针(表示目前使用空间的头),尾指针(表示目前使用空间的尾)和可用空间尾指针实现 List优点:按需申请内存,不需要扩容,不会造成内存空间浪费。在任意位置的插入删除下效率高。 缺点:不支持下标随机访问 底层是由双向链表实现的 #### 3:说说unordered\_map的扩容过程 #### 当unordered\_map中的元素数量达到桶的负载因子(0.75)时,会重新分配桶的数量(通常会按照原有桶的数量\*2的方式进行扩容,但是具体的增长策略也可以通过修改容器中的max\_load\_factor成员变量来进行调整),并将所有的元素重新哈希到新的桶中。 ![bf05e2127c964f43ad1488d65e6e2444.png][] #### 4:动态链接和静态链接的区别,动态链接的原理是什么? #### 区别:他们的最大区别就是在于链接的时机不同,静态链接是在形成可执行程序前,而动态链接的进行则是程序执行时。 静态库:就是将库中的代码包含到自己的程序之中,每个程序链接静态库后,都会包含一份独立的代码,当程序运行起来时,所有这些重复的代码都需要占用独立的存储空间,显然很浪费计算机资源。 动态库:不会将代码直接复制到自己程序中,只会留下调用接口,程序运行时再去将动态库加载到内存中,所有程序只会共享这一份动态库,因此动态库也被称为共享库。 动态链接原理:是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件 #### 5:构造函数是否能声明为虚函数?为什么? #### 构造函数不能为虚函数,虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针指向,该指针存放在对象的内部空间之中,需要调用构造函数完成初始化,如果构造函数为虚函数,那么调用构造函数就需要去寻找vptr,但此时vptr还没有完成初始化,导致无法构造对象。 #### 6:如何处理内存泄漏问题?提供一些常见的内存管理技术。 #### 1. 显式释放内存:在使用动态分配的内存(如`new`、`malloc`)后,务必及时使用相应的释放操作(如`delete`、`free`)来手动释放已分配的内存。确保每次动态分配都有相应的释放操作与之对应。 2. 智能指针(Smart Pointers):使用智能指针可以自动管理内存资源,避免显式地调用释放操作。C++中提供了 `std::shared_ptr` 和 `std::unique_ptr` 两种智能指针,它们可以在对象不再被引用时自动释放相关内存。 3. RAII(Resource Acquisition Is Initialization):RAII 是一种编程范式,在对象构造函数中获取资源,在析构函数中进行资源的释放。通过利用栈上对象生命周期结束时自动调用析构函数的特性,可以确保资源得到正确和及时地释放。 4. 定期检查和测试:定期进行代码审查和测试,尤其关注内存分配和释放部分是否正确。使用工具或手动方法检测潜在的内存泄漏情况,并进行修复。 5. 使用容器类和标准库:使用现代化的容器类和标准库算法可以简化内存管理工作。例如,使用 `std::vector` 替代手动管理数组内存,使用 `std::string` 替代手动管理字符串内存等。 6. 遵循编码规范:良好的编码规范和设计原则有助于避免内存泄漏问题。例如,避免多层级的指针引用、避免过度复杂的嵌套结构、合理地处理异常情况等。 7. 内存分析工具:使用专门的内存分析工具(如Valgrind、AddressSanitizer)来检测和诊断程序中的内存泄漏问题。这些工具可以帮助发现潜在的资源未释放或访问无效内存等情况。 #### 7:列举并解释STL库中常用容器,例如vector、list、map等。 #### STL(标准模板库)是C++的一个重要组成部分,提供了一系列常用的容器类。下面是STL库中常用的几个容器及其简单解释: vector: vector 是一个动态数组,可以在运行时自动扩展和收缩大小。它以连续的内存块存储元素,支持随机访问、尾部插入和删除等操作。 list: list 是一个双向链表,每个节点包含指向前一个节点和后一个节点的指针。相比于 vector,list 在任意位置进行插入和删除操作更高效,但对于随机访问则较慢。 deque: deque(双端队列)也是一个动态数组,与 vector 类似,但支持在首尾两端进行高效插入和删除操作。 stack: stack 是一个后进先出(LIFO)的容器适配器,基于其他底层容器实现。它只允许在末尾进行元素插入和删除,并且只能访问最顶端的元素。 queue: queue 是一个先进先出(FIFO)的容器适配器,在尾部插入数据,在头部移除数据。与 stack 类似,它也基于其他底层容器实现。 map: map 是一种关联容器,存储一对键-值对。它根据键来进行排序和查找,具有较快的插入和删除操作。每个键在容器中是唯一的。 set: set 是另一种关联容器,存储唯一的值(不重复)。它自动将元素排序,并支持高效地插入、查找和删除操作。 unordered\_map: unordered\_map 是基于哈希表实现的关联容器,通过哈希函数来存储和访问元素。相比于 map,它的插入和查找操作通常更快,但不保证元素的顺序。 unordered\_set: unordered\_set 也是基于哈希表实现的集合容器,存储唯一的值并支持高效地插入、查找和删除操作。 ![ce1fb8c82bfc43a0bf0142325057de8a.png][] #### 8:C++11引入了哪些新特性?请列举几个重要的特性并简要解释它们。 #### Lambda表达式:Lambda表达式允许在需要函数对象的地方定义匿名函数,简化了代码,并支持捕获外部变量形成闭包。 自动类型推导(auto):使用auto关键字可以自动推导变量的类型,减少冗长的类型声明,提高代码可读性和灵活性。 智能指针:引入了shared\_ptr、unique\_ptr和weak\_ptr等智能指针,帮助管理资源的生命周期,避免内存泄漏和悬空指针问题。 范围基于for循环:通过简洁明确的语法,可以更便捷地遍历容器或其他序列中的元素。 移动语义(移动构造函数和移动赋值运算符):通过std::move和右值引用(&&)来实现资源的高效转移,提高程序性能。 列表初始化和统一初始化语法:引入了大括号\{\}进行列表初始化,并且扩展了构造函数的使用方式,使得初始化更加简单明了。 线程库(std::thread):标准库中添加了线程相关的头文件和类,方便开发并发程序。 异常处理改进:引入了新的异常规范机制(noexcept),使异常处理更加灵活和高效。 #### 9:什么是智能指针?列举几种常见的智能指针类型,并解释其特点和适用场景。 #### 智能指针是C++中的一个类模板,用于管理动态分配的资源(如堆上的对象),自动进行资源的释放,避免内存泄漏等问题。它们提供了一种更安全和方便的方式来操作动态分配的内存,并减少手动处理资源释放的工作。 以下是几种常见的智能指针类型及其特点和适用场景: 1. unique\_ptr:unique\_ptr 是独占所有权的智能指针,它保证在任意时刻只有一个 unique\_ptr 指向同一个对象或者没有对象。它在析构时会自动释放所管理的资源。适用于需要独占式拥有某个资源,并希望确保只有一个指针可以访问该资源的情况。 2. shared\_ptr:shared\_ptr 是共享所有权的智能指针,可以多个 shared\_ptr 共同拥有同一个对象,并且会对引用计数进行追踪。当最后一个 shared\_ptr 被销毁时,才会自动释放所管理的资源。适用于需要多个指针共享同一个资源,并且需要灵活地增加、减少共享拥有者数量的情况。 3. weak\_ptr:weak\_ptr 也是一种共享所有权的智能指针,但不会增加引用计数。weak\_ptr 可以被用来解决 shared\_ptr 的循环引用问题。适用于需要共享资源,但又希望避免循环引用导致的资源无法释放的情况。 4. auto\_ptr(在C++11中已被废弃):auto\_ptr 是一种独占所有权的智能指针,类似于 unique\_ptr,但它具有不完善的拷贝和赋值语义,并且存在一些潜在的问题。建议使用 unique\_ptr 替代 auto\_ptr。 这些智能指针类型都是通过 RAII(资源获取即初始化)技术实现的,在对象生命周期结束时自动释放所管理的资源。正确使用智能指针可以大大减少内存泄漏和悬挂指针等错误,并提高代码的可靠性和安全性。 #### 10:请实现一个单例模式?列举几种实现方式并简要说明其优缺点。 #### **懒汉式(Lazy Initialization):** * 优点:在需要时才进行初始化,节省内存空间。 * 缺点:线程不安全,多线程环境下可能创建多个实例。 class Singleton { private: static Singleton* instance; Singleton() {} public: static Singleton* getInstance() { if (instance == nullptr) { instance = new Singleton(); } return instance; } }; **饿汉式(Eager Initialization):** * 优点:线程安全,在程序启动时就进行初始化,保证只有一个实例。 * 缺点:占用内存空间,无论是否使用都会被创建。 class Singleton { private: static Singleton* instance; Singleton() {} public: static Singleton* getInstance() { return instance; } }; Singleton* Singleton::instance = new Singleton(); **双重检查锁定(Double-Checked Locking):** * 优点:延迟加载、线程安全。 * 缺点:代码复杂度较高。 #include <mutex> class Singleton { private: static std::mutex mtx; static volatile Singleton* instance; // 私有构造函数和拷贝构造函数 ... public: static volatile Singleton* getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(mtx); if (instance == nullptr) { // double-checked locking instance = new Singleton(); } } return instance; } }; volatile Singleton* Singleton::instance = nullptr; std::mutex Singleton::mtx; [bf05e2127c964f43ad1488d65e6e2444.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/090c0431cb4546fea7eb482477bc0867.png [ce1fb8c82bfc43a0bf0142325057de8a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/6309a41e20c842e78f61a8613e2c9cd4.png
还没有评论,来说两句吧...