济南 规划 网站,网校课程,建设银行网站用户权限,wordpress个人中心页虎牙C技术面经
1、虚函数底层
在C中#xff0c;虚函数的实现涉及到虚函数表#xff08;Virtual Table#xff09;的概念。每个含有虚函数的类都会有一个对应的虚函数表#xff0c;其中存储着指向各个虚函数的地址。当一个对象被创建时#xff0c;编译器会将该对象的虚函…虎牙C技术面经
1、虚函数底层
在C中虚函数的实现涉及到虚函数表Virtual Table的概念。每个含有虚函数的类都会有一个对应的虚函数表其中存储着指向各个虚函数的地址。当一个对象被创建时编译器会将该对象的虚函数表地址保存在对象的内存布局中。
通过使用指针或引用来访问对象时可以根据对象所属类型找到相应的虚函数表并通过虚函数表中存储的地址调用正确的虚函数。
具体来说在底层实现上编译器通常为每个类生成一个隐藏的成员变量——指向虚函数表的指针即vptr。当调用一个基类指针或引用上的虚成员函数时实际执行过程是首先根据 vptr 找到相应的虚函数表然后通过偏移量找到正确位置上存储着目标成员函数地址并进行调用。
这种方式使得在派生类中重写override基类中定义的虚函数成为可能派生类可以通过修改自己对应的虚函数表中相应项来改变默认行为。这也是 C 多态性polymorphism特性实现之一。
细节补充
虚函数表的位置 虚函数表通常位于类的静态存储区域。每个类的实例都包含一个指向其虚函数表的指针。纯虚函数 如果一个类包含至少一个纯虚函数即带有 0 的虚函数那么该类将变为抽象类无法被实例化。抽象类的虚函数表中包含指向纯虚函数的指针。虚析构函数 如果基类的析构函数是虚的那么它可以确保在通过基类指针删除指向派生类对象的时候调用正确的析构函数。多重继承 对于多继承每个基类都有自己的虚函数表派生类会包含指向这些表的指针。在多重继承的情况下可能会有虚拟继承的概念以避免产生二义性。vptr 和 vtable 的命名 虚指针vptr和虚函数表vtable的具体命名可能会有所不同取决于编译器和平台。动态类型识别 运行时类型信息RTTI也与虚函数密切相关可以通过 typeid 运算符来获得对象的实际类型。
#include iostream
#include typeinfo
class Base {
public:virtual void foo() {std::cout Base::foo() std::endl;}virtual ~Base() default; // 虚析构函数
};
class Derived : public Base {
public:void foo() override {std::cout Derived::foo() std::endl;}
};
int main() {Base* basePtr new Derived; // 运行时类型识别if (typeid(*basePtr) typeid(Derived)) {std::cout basePtr指向Derived类对象 std::endl;}// 删除对象时会调用正确的析构函数delete basePtr;return 0;
}2、Vector 动态扩容底层
在 C 中std::vector 是一个动态数组容器它会根据需要自动扩展内部的存储空间。当元素数量超过当前容量时vector 会重新分配一块更大的内存并将现有元素拷贝到新的内存区域中。
具体实现上vector 通常使用动态分配的连续内存来存储元素。它在底层使用了指针和动态内存分配函数如malloc()或new[]来管理内存。
当需要扩展容量时vector 会创建一个更大的缓冲区并将原有数据拷贝到新缓冲区中。然后释放旧缓冲区所占用的内存空间。
这种动态扩容的机制使得 vector 可以高效地处理不确定大小的数据集合并且支持随机访问、快速插入和删除操作。
细节补充
内存分配策略 预留容量 vector 通常会预留一些额外的容量以避免每次插入操作都触发动态扩容。这个额外的容量可以通过 capacity() 函数查询。预留容量的目的是减少频繁扩容的次数提高性能。 扩容策略 翻倍扩容 为了均摊插入操作的代价vector 通常采用翻倍扩容的策略。也就是说当容量不足时它会创建一个原容量两倍大小的新缓冲区并将元素从旧缓冲区拷贝到新缓冲区然后释放旧缓冲区。这个策略确保了插入操作的平摊复杂度是 O(1)。增量步长 有些实现可能选择采用增加一个固定步长的方式进行扩容而不是翻倍。这可以减小内存的浪费但可能会导致插入操作的均摊代价略高。 移动语义 C11 引入的优化 C11 引入了移动语义使得在动态扩容时可以对元素进行移动而非拷贝。这在对象较大且开销较高的情况下可以提高性能。 内存碎片 碎片问题 尽管 vector 的动态扩容机制带来了灵活性但在频繁插入和删除操作时可能导致内存碎片。这可能影响内存的利用率特别是对于大量小对象的情况。 swap 惯用法 释放不需要的内存 在预知要插入大量元素之前可以使用 vectorT().swap(v) 的惯用法来释放不需要的内存。这个操作会构造一个临时 vector 并与原 vector 进行交换最终原 vector 会获得一个较小的容量。这在需要最小化内存占用的情况下是一种常见的做法。
3、两个 vector 一个放普通数据类型一个放指针扩容有什么区别
当一个 vector 存放普通数据类型如int、float等而另一个 vector 存放指针时在扩容过程中会有一些区别。
对于存放普通数据类型的 vector扩容时会重新分配更大的连续内存空间并将原有数据拷贝到新的内存区域中。这是因为普通数据类型是值语义直接复制即可。
而对于存放指针的 vector扩容时仅会复制指针本身并不会复制指针所指向的对象。这是因为指针只是保存了对象地址而不拥有对象本身。因此在扩容过程中原有指针的值被简单地复制到新内存区域中而并没有复制指针所指向的实际对象。
需要注意的是在存在多个 vector 同时持有同一个对象或者多个对象之间存在依赖关系比如共享同一块动态分配内存时对其中一个 vector 进行扩容可能导致其他 vector 中的指针失效或引起潜在错误。在这种情况下需要谨慎处理和管理指针以避免问题发生。
细节补充 指针不负责内存管理 存放指针的 vector 本身并不负责指针所指向的内存的生命周期管理。这意味着在扩容的过程中指针的值会被简单地复制但指针所指向的实际对象并不会被复制。如果指针所指向的内存是动态分配的需要确保在不再需要的时候正确释放内存以防止内存泄漏。 潜在的野指针问题 如果在扩容后没有正确处理指针的所有权和生命周期可能会导致悬空指针或野指针的问题。当一个 vector 扩容时其元素的指针值被复制到新的内存区域而老的内存区域被释放。如果其他地方还在使用老的内存区域就会出现悬空指针问题。 共享同一块内存 如果多个 vector 共享同一块内存那么在扩容时需要确保所有相关的 vector 都被正确处理。否则一个 vector 的扩容可能会导致其他 vector 中的指针失效或指向无效内存。 深拷贝和浅拷贝 如果指针所指向的对象需要深度拷贝那么在扩容时需要自行实现深拷贝操作。否则多个 vector 可能会指向相同的对象从而导致对一个对象的修改影响其他对象。
总体而言在存放指针的 vector 中进行扩容需要特别小心确保正确地管理指针和与指针相关的内存。使用智能指针如std::shared_ptr或std::unique_ptr可以帮助减少一些潜在的问题因为它们具有自动内存管理的特性。
4、进程通信共享内存如何实现进程安全
在使用共享内存进行进程间通信时确保进程安全是非常重要的。以下是一些常见的方法来实现进程安全
互斥锁Mutex 使用互斥锁可以确保同一时间只有一个进程可以访问共享内存区域。在读写共享内存之前进程需要先获得互斥锁完成操作后释放锁。这样可以防止多个进程同时修改数据导致冲突。信号量Semaphore 通过信号量来控制对共享内存的访问。使用计数型的信号量可以限制对资源的并发访问数量。当一个进程需要访问共享内存时它必须首先检查信号量计数值如果大于0则允许访问并将计数减1如果为0则表示当前没有可用资源进程需要等待其他进程释放资源。文件锁File Locking 利用操作系统提供的文件锁机制在访问共享内存之前获取一个特定文件上的独占锁或共享锁。只有获得了文件锁的进程才能执行相应操作其他进程则需要等待。读写锁Read-Write Lock 当多个进程需要同时读取共享内存而不涉及修改时可以使用读写锁进行优化。读写锁允许多个进程同时读取但只有一个进程能够进行写操作。这样可以提高读操作的并发性能。原子操作Atomic Operation 某些特定情况下共享内存中的数据可以通过原子操作进行修改。原子操作是不可被中断的单个指令保证了在多线程或多进程环境中的一致性和完整性。适当的数据结构设计 在共享内存中使用适当的数据结构设计可以降低竞态条件的发生。例如使用原子操作的计数器而不是共享的计数器可以减少竞态条件的可能性。事务性操作 将对共享内存的一系列操作组织成事务要么全部成功执行要么全部回滚。这通常需要使用类似数据库事务的机制确保在一系列操作中任何一步出错都能够回滚到一致的状态同步工具 利用同步工具如条件变量确保一个进程在进入关键代码段之前等待某个条件的发生从而避免竞态条件。锁层次 如果多个共享资源需要访问考虑使用锁层次结构按照顺序获取锁释放锁的顺序相反。这样可以降低死锁的概率。仔细的错误处理 需要在共享内存的读写过程中处理可能发生的错误以防止错误的状态导致不一致性。写时复制Copy-on-Write 对于大块数据的写操作可以考虑使用写时复制策略即在写操作时先复制一份再修改。这样可以减少写操作的冲突。
5、malloc和 free如何知道释放内存具体大小
在使用 malloc 动态分配内存时系统会在分配的内存块之前记录其大小。这样当你调用 free 释放内存时系统可以根据这个大小信息来确保正确地释放相应的内存。
具体而言在大部分情况下malloc 会在分配的内存块之前留出一定大小的空间用于保存大小信息。这个大小信息通常是以字节为单位并且依赖于系统和编译器的实现。
对于 free 函数它能够根据传入指针所指向的内存地址找到相关的大小信息并将相应的内存块归还给系统。
需要注意的是在使用动态分配函数时如 malloc、calloc、realloc我们不能修改返回指针所指向的数据区域之前或之后额外添加任何数据。如果这样做可能会导致未定义行为。
细节补充 头部信息 通常malloc 分配的内存块前面会有一个头部信息用于存储分配的大小。这个头部信息的大小和具体内容取决于系统和编译器的实现。这个头部信息通常包含了分配的内存块的大小以字节为单位。malloc 返回的指针指向的是用户可用的内存块的起始地址而不是头部信息的地址。 对齐要求 有些系统和编译器对内存的对齐有特殊要求因此分配的内存块的实际大小可能会比用户请求的大小大一些。这是为了满足平台的对齐需求。 内部碎片 malloc 会根据系统的内存分配策略如首次适应、最佳适应等分配一块足够大的内存这可能导致一些内部碎片即实际分配的内存块大小可能比用户请求的稍大。 多线程安全 在多线程环境下一些实现可能需要额外的空间来存储线程相关的信息以确保 malloc 和 free 的多线程安全性。
6、线程池
线程池是一种用于管理和复用多个线程的机制它通过维护一定数量的线程并将任务分配给这些线程来提高应用程序的性能和效率。以下是线程池的基本原理和实现过程
创建线程池首先需要创建一个线程池对象其中包含固定数量的工作线程也称为工作者线程。接收任务当有新任务需要执行时可以将任务提交给线程池。任务可以以函数、方法或其他可执行单位的形式表示。任务队列线程池维护一个任务队列用于存储待执行的任务。当有新任务提交时将其加入到队列中。工作者线程在初始化阶段所有工作者线程都处于空闲状态等待从任务队列中获取可执行的任务。任务调度当有空闲的工作者线程时它会从任务队列中取出一个待执行的任务并开始执行该任务。处理结果在任务完成后可以选择返回结果或通知相关方。线程复用一旦某个工作者线程完成了当前分配的任务它会再次进入空闲状态并准备接受下一个任务。这样就避免了频繁创建和销毁线程带来的开销。线程池管理线程池还可以提供一些管理功能例如动态调整线程数量、设置最大线程数、超时处理等。
使用线程池的好处是避免了反复创建和销毁线程的开销提高了任务执行的效率。此外它还能够限制并发线程数量避免资源过度占用并提供更好的任务调度和管理机制。
7、基类析构函数为什么是虚函数
基类析构函数为虚函数的主要目的是实现多态性的正确销毁。
当基类指针指向派生类对象时如果基类析构函数不是虚函数则在使用 delete 操作符释放这个对象时只会调用基类的析构函数而不会调用派生类的析构函数。这就导致派生类中可能存在资源没有正确释放造成内存泄漏或其他问题。
通过将基类析构函数声明为虚函数在使用 delete 操作符释放一个指向派生类对象的基类指针时会先调用派生类的析构函数再调用基类的析构函数。这样可以确保每个继承层次上的析构过程都得到正确执行从而避免了潜在的资源泄漏和错误。
8、堆区和栈区的区别
堆区和栈区是计算机内存中两个不同的存储区域主要用于管理变量和对象的内存分配。
栈区Stack
栈区是由编译器自动管理的具有自动分配和释放内存的特性。存放函数调用时的局部变量、函数参数等。栈内存由系统自动分配和回收速度较快。存储空间有限一般较小。
堆区Heap
堆区是由程序员手动管理的需要显式地申请和释放内存。用于存储程序运行时动态分配的对象或大块数据。堆内存分配通过 malloc、new 等操作实现释放通过 free、delete 等操作实现。堆内存空间较大可以灵活地进行动态分配。
细节补充 生命周期 栈区中的变量生命周期由其作用域决定函数执行结束时栈上的局部变量会自动被销毁。堆区中的对象生命周期由程序员控制需要手动分配和释放没有明确的作用域概念需要注意防止内存泄漏。 大小限制 栈区的大小是有限的通常较小。栈空间是有限的通常几 MB 到几十 MB 具体取决于系统和编译器。堆区的大小较大理论上受制于计算机的虚拟内存限制可以动态地分配和释放较大的内存块。 碎片问题 栈区由于是连续分配的可能会发生碎片问题即频繁的压栈和出栈可能导致栈内存出现零散的空间不能被充分利用。堆区由于是动态分配的可能会有内存碎片问题但通过各种内存管理策略如内存池可以缓解这一问题。 访问速度 栈区的存取速度较快因为它是线性的、有序的变量的创建和销毁只涉及移动栈指针。堆区的存取速度相对较慢因为需要动态分配和释放而且可能存在内存碎片问题。 使用场景 栈区适合存放局部变量、函数调用等适用于生命周期较短、较小的数据。堆区适合存放动态分配的大量数据如动态数组、对象等适用于需要灵活管理内存的情况。
总体而言栈和堆的选择取决于数据的生命周期、大小以及对内存管理的要求。
9、宏定义放在哪里
可读性 将宏定义放在开头可以让其他人更容易理解代码中使用的宏提高代码的可读性。作用域 如果宏定义放在源文件或头文件的顶部则该宏定义对整个源文件或包含了该头文件的所有源文件都可见确保了正确地应用于需要的范围内。预处理效率 将宏定义集中放置预处理器可以更快速地进行替换和展开提高预处理效率。
然而在特定情况下也可以将宏定义局部化只在特定作用域内起作用。例如在某个函数内部使用一个简单的宏定义来提高代码可读性或减少重复输入。
细节补充 括号的使用 在宏定义中对于参数的使用要特别小心确保用括号括起来以避免由于运算符优先级导致的问题。例如MAX(a, b) 中的括号是必要的因为在宏中可能包含表达式。 #define MAX(a, b) ((a) (b) ? (a) : (b))副作用 宏中的参数可能会被多次求值因此在传递有副作用的表达式时要小心。 #define SQUARE(x) ((x) * (x))int a 5;
int result SQUARE(a); // 此时 a 被修改两次名称空间 宏定义没有名称空间的概念因此可能会发生命名冲突。为了减少冲突的可能性可以使用较长或者具有特殊前缀的名字。 #define MY_PROJECT_MAX(a, b) ((a) (b) ? (a) : (b))内联函数 在 C 中宏定义可以被内联函数替代内联函数更安全而且通常更易读。 // 宏定义
#define SQUARE_MACRO(x) ((x) * (x))// 内联函数
inline int square_inline(int x) {return x * x;
}条件编译 宏定义常用于条件编译但要注意使用 #ifdef、#ifndef、#else、#endif 来确保只在需要时进行编译。 #ifdef DEBUG
#define LOG(msg) std::cout msg std::endl;
#else
#define LOG(msg)
#endif宏定义在一些情况下可以提高代码的灵活性和可读性但过度使用可能会导致代码的可维护性降低。在现代 C 中许多情况下都能使用 const、constexpr、内联函数或模板来替代宏定义。
10、qt 信号链接的方式
在 Qt 中信号与槽是一种常用的通信机制。以下是几种常见的连接方式
使用 QObject::connect() 函数这是最常见的连接方式。可以使用该函数将一个信号与一个槽函数进行连接。示例代码如下 QObject::connect(senderObject, SIGNAL(signalName()), receiverObject, SLOT(slotName()));使用 Lambda 表达式如果你使用 C11 或更高版本可以使用 Lambda 表达式来连接信号和槽。示例代码如下 QObject::connect(senderObject, SenderClass::signalName, receiverObject, [](parameters) {// 槽函数实现});使用 Qt5 新语法从 Qt5 开始引入了新的连接语法它使用了更安全和类型检查的方法来连接信号和槽。示例代码如下 QObject::connect(senderObject, SenderClass::signalName, receiverObject, ReceiverClass::slotName);无论选择哪种方式都需要确保发送者对象、接收者对象和信号/槽函数正确地定义和声明并且满足相应的访问限制要求。
还有其他一些高级特性可用于信号与槽的连接例如使用 Qt 的元对象系统查询、使用多个参数等。具体使用方式可以参考 Qt 文档以及相关教程和示例。
11、智能指针三种底层实现和应用场景
智能指针是一种 C 中的智能内存管理工具用于自动化地管理动态分配的内存资源防止内存泄漏和悬空指针等问题。以下是三种常见的智能指针底层实现和它们的应用场景
shared_ptrshared_ptr使用引用计数的方式来管理资源即通过记录有多少个智能指针共享同一个资源并在没有任何引用时释放该资源。这种底层实现适用于多个智能指针需要共享同一个资源的情况例如在多线程环境下共享数据、循环引用等。unique_ptrunique_ptr使用独占所有权的方式来管理资源即每个资源只能由一个unique_ptr拥有并且不能进行复制或拷贝操作。这种底层实现适用于需要独占某个资源且不需要共享所有权的情况例如管理原始数组、显式拥有对象等。weak_ptrweak_ptr是一种辅助性智能指针它可以与shared_ptr配合使用。与shared_ptr相比weak_ptr不增加引用计数也不拥有所指向的资源。其主要作用是解决shared_ptr可能导致的循环引用问题在需要观察但不拥有某个对象时使用。
应用场景举例
shared_ptr在多个对象之间共享同一资源如共享数据结构、共享缓存等。unique_ptr管理独占性资源如动态分配的内存块、原始指针等。weak_ptr解决shared_ptr循环引用问题如观察者模式、缓存回收等。
需要根据具体的需求和设计来选择适合的智能指针类型及其底层实现方式。
12、预防内存泄漏方式
使用智能指针使用 C 中的智能指针如 shared_ptr 、 unique_ptr 等可以自动管理动态分配的内存资源避免手动释放忘记或错误释放导致的内存泄漏。遵循 RAII资源获取即初始化原则在对象构造时申请资源在析构时释放资源。通过使用栈上对象或成员对象来管理资源生命周期确保在不再需要时及时释放资源。避免无效指针赋值在将指针变量赋值为 nullptr 之前应始终检查其是否已经被删除或释放。同时避免野指针和悬空指针的出现。清理不再使用的对象定期检查并清理不再使用的对象和数据结构确保它们被正确地删除或释放。谨慎使用动态内存分配尽量减少对堆内存的直接动态分配并且在必要时使用合适大小和作用域的内存块进行动态分配以避免过多频繁地申请和销毁内存。使用工具进行内存泄漏检测可以借助一些工具来进行静态代码分析或运行时检测以帮助发现潜在的内存泄漏问题如 Valgrind 、 LeakSanitizer 等。
13、调试工具用什么
GDBGDB 是一个功能强大的命令行调试器适用于 C、C 等语言。它允许你在程序运行时进行断点设置、变量查看、堆栈跟踪等操作。Visual Studio Debugger对于使用Visual Studio开发的项目Visual Studio提供了内置的集成调试器可以方便地进行单步执行、变量查看、条件断点等操作。Xcode Debugger Xcode 是苹果公司提供的集成开发环境在 Mac 上进行 iOS 和 macOS 开发时常用。Xcode 提供了强大的调试功能包括代码断点、变量监视、内存查看等。Eclipse Debugger Eclipse 是一个流行的Java开发环境并且支持多种编程语言。Eclipse提供了内置的调试功能适用于Java和其他语言。Valgrind Valgrind 是一款用于内存错误检测和性能分析的工具套件。它可以检测到内存泄漏、不正确的内存访问以及其他潜在问题。Chrome DevTools针对 Web 开发在 Chrome 浏览器中使用 DevTools 可以进行 JavaScript 调试和性能分析包括断点设置、网络请求监控、内存分析等功能。
14、互斥锁和条件变量的使用
互斥锁Mutex和条件变量Condition Variable是多线程编程中常用的同步机制。它们通常一起使用来实现线程间的协调与同步。
互斥锁主要用于保护共享资源确保在任意时刻只有一个线程可以访问该资源。以下是互斥锁的基本使用方式
初始化互斥锁在使用互斥锁之前需要先进行初始化。加锁当一个线程需要访问共享资源时它会尝试获取互斥锁。如果互斥锁已经被其他线程持有则该线程将进入阻塞状态直到获得锁为止。访问共享资源一旦某个线程成功获得了互斥锁它就可以安全地访问共享资源了。解锁当某个线程完成对共享资源的操作后应该及时释放互斥锁以便其他等待获取该资源的线程可以继续执行。
条件变量用于在线程之间发送信号和等待信号。以下是条件变量的基本使用方式
初始化条件变量在使用条件变量之前需要先进行初始化。等待信号当某个线程需要等待某个条件满足时在加锁的情况下它会调用条件变量的等待函数进行等待。此时线程会释放互斥锁并进入阻塞状态。发送信号当某个条件满足时可以通过条件变量的发送信号函数通知正在等待的线程。该线程将从阻塞状态中被唤醒并重新获取互斥锁以继续执行。
需要注意的是条件变量和互斥锁一起使用时通常先对互斥锁加锁然后再操作条件变量。