金山C++研发实习生面试准备
金山C++研发实习生面试准备
1.自我介绍
2.拷打项目
3.拷打八股:
4.多态的实现
5.静态多态和动态多态
静态多态是指在编译时多态,动态多态是指运行时多态
静态多态有函数重载(函数名相同,参数不同,返回值相同或不同),函数模板
动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
6.动态多态怎么实现的
动态多态
通过每个类的虚函数和动态绑定
派生类实现动态绑定
- 先拷贝基类的虚函数表
- 如果派生类重写了基类的某个虚函数,就用派生类的虚函数替换虚表同位置的基类虚函数
- 跟上派生类自己的虚函数
指向基类的指针或引用
动态多态需要通过指向基类的 指针或引用 操作派生类对象。如果直接操作派生对象,则是静态绑定,而非动态绑定。
7.介绍一下虚函数表和虚指针
虚函数表和虚指针是由编译器生成并使用的,它们是实现 动态绑定 和运行时多态的核心。
虚函数表是一种由编译器生成的 隐藏数据结构,它是一个函数指针数组,用来存储类中所有虚函数的地址。简单来说,虚函数表就是一个指针数组,每个元素指向一个虚函数的具体实现。
虚指针是一个 隐藏成员变量,存在于每个包含虚函数的类的对象中。虚指针用于指向当前对象所属类的虚函数表。
8.说一下构造函数和析构函数在父类和子类的执行顺序
- 构造函数顺序:
父类构造函数 → 成员对象的构造函数 → 子类构造函数 - 析构函数顺序:
子类析构函数 → 成员对象的析构函数 → 父类析构函数
9.析构函数的作用
析构函数用于释放对象占用的资源(如内存、文件句柄等),在对象生命周期结束时自动调用,确保资源不会泄漏。
10.为什么析构函数通常定义为虚函数;不调用析构函数会怎样
- 原因:
若父类指针指向子类对象,且父类析构函数非虚,则通过父类指针删除对象时,只会调用父类析构函数,导致子类资源泄漏。 - 后果:
不调用析构函数会导致资源泄漏(如内存、文件句柄未释放)。
11.结构体为何通常要进行内存对齐;一般是几字节对齐
为了提高访问效率,为了时内存命中访问效率更高
对齐原则
结构体的内存对齐通常遵循以下原则:
- 每个成员变量的起始地址必须是其自身大小的整数倍
- 结构体本身的大小必须是其最大对齐系数(对齐要求)的整数倍
12.new/delete和malloc/free的区别
| 特性 | new/delete |
malloc/free |
|---|---|---|
| 语言特性支持 | C++ 专有 | C 风格(C 和 C++ 均支持) |
| 类型安全 | 是 | 否(需要手动类型转换) |
| 初始化支持 | 自动调用构造函数和析构函数 | 不调用,需要手动处理 |
| 异常处理 | 内存分配失败抛出 std::bad_alloc |
内存分配失败返回 NULL |
| 灵活性 | 适合对象管理和复杂场景 | 适合简单的POD或跨语言 |
| 效率 | 稍有额外开销(构造/析构函数调用) | 相对高效 |
什么时候选择哪一个?
使用 new/delete:
- 在C++程序中,需要创建具有构造函数和析构函数的复杂对象。
- 希望利用类型安全和异常处理机制,提高代码的健壮性。
- 更偏向于面向对象的编程风格。
使用 malloc/free:
- 在C语言程序中,或者需要与 C 的库/代码接口。
- 需要跨语言兼容时(如 C 和 C++ 混合开发)。
- 只操作简单的内存(非对象)时,比如使用裸内存块或数组。
总结
new和delete是C++中的语法糖,功能更强大,更贴合现代编程需求。malloc和free是底层内存管理工具,适合裸内存分配。- 在C++开发中,通常推荐优先使用
new和delete,除非有特殊需要才选择malloc和free。
13.什么情况用mmap()
mmap() 是一个 POSIX 标准的内存映射函数,通常用于将文件或设备映射到进程虚拟内存空间中,使得我们可以通过内存访问文件内容,大大提高了 I/O 的效率。它的使用场景主要集中在对文件的高效访问、大文件处理、共享内存以及特殊的内存管理需求等。以下是 mmap() 的典型使用情况、优缺点以及示例解释。
1.高效文件访问,减少内核态和用户态之间的拷贝开销
2.处理大文件,将文件内容按需映射到虚拟内存中
3.共享内存(进程间通信)
mmap()支持使用MAP_SHARED标志创建共享内存,多个进程可以操作同一内存区域进行通信。- 使用共享内存比使用消息队列、管道等传统 IPC 机制有更高的效率。
4.文件修改或写入(内存作为缓冲区)适用于需要高效更新大文件的场景
5.实现内存映射I/O
6.异步I/O和并行处理
使用 mmap() 的场景主要是权衡性能与开销,以下是具体选择标准:
- 适合用
mmap()的场景:- 需要频繁随机访问大文件。
- 文件的内容按需加载,不需要一次性加载到内存。
- 多进程需要共享文件或内存。
- 操作设备文件或硬件寄存器。
- 文件访问频繁,且希望减少用户态与内核态之间频繁的数据拷贝。
- 不太适合用
mmap()的场景:- 操作小文件,或者一次性读取整个文件,不需要复杂的随机访问。
- 文件处理逻辑非常简单,而
mmap()的复用性和管理开销反而适得其反。 - 跨平台兼容需求非常高的场景。
14.动态链接库用的brk()还是mmap()
| 特性 | brk() |
mmap() |
|---|---|---|
| 内存范围 | 紧跟数据段末尾 | 任意空闲的虚拟地址空间 |
| 按需加载 | 不支持 | 支持 |
| 分页分配 | 不支持 | 支持,以页为单位分配和管理 |
| 碎片问题 | 容易引发堆碎片 | 避免堆碎片,内存管理灵活 |
| 共享内存 | 不支持 | 支持共享内存(MAP_SHARED 标志) |
| 动态链接库加载 | 不适用 | 是动态链接库的默认选择 |
因此,动态链接库的加载和运行使用 mmap() 是最佳选择,它为动态链接库的按需加载、多进程共享、独立管理以及性能优化提供了完美支持。同时,brk() 由于其限制,已不适用于动态库加载。
15.动态链接和静态链接的区别
| 特性 | 静态链接 | 动态链接 |
|---|---|---|
| 生成的可执行文件体积 | 较大,库代码直接嵌入 | 较小,仅包含对动态库的引用 |
| 运行时依赖 | 不依赖外部库,所有代码都在一个文件中 | 依赖外部库,运行时需要动态加载 |
| 运行时性能 | 加载速度快,无需加载外部库 | 加载速度稍慢,需要动态加载符号解析 |
| 内存使用 | 每个程序独立占用一份库的内存 | 多个程序共享同一个库,节省内存 |
| 更新方式 | 更新程序需要重新编译并链接静态库 | 可以通过更新库文件,无需重新编译程序 |
| 跨平台兼容性 | 静态库只与编译平台相关,适合封装依赖 | 动态库可能与运行时环境、平台设置相关 |
| 调试难度 | 相对简单,因为库已集成进可执行文件 | 调试更复杂,可能需要检查多个动态库 |
| 符号冲突问题 | 不会出现符号冲突,代码已固定 | 有可能发生符号冲突,需要小心处理 |
| 磁盘存储 | 每个可执行文件都包含完整的库代码 | 动态库存储独立,节省磁盘空间 |
| 维护与分发 | 更新程序和库需要重新分发完整的可执行文件 | 库文件可以单独更新,减少分发工作量 |
16.红黑树特性;有哪些应用
- 特性:
- 每个节点是红或黑。
- 根节点和叶子节点(NIL)是黑。
- 红节点的子节点必为黑。
- 从任一节点到其叶子节点的路径包含相同数量的黑节点。
- 应用:
C++ STLmap/set、JavaTreeMap、Linux 内核调度器等。
17.死锁的必要条件有哪些
- 互斥:资源只能被一个进程占用。
- 持有并等待:进程持有资源并等待其他资源。
- 不可抢占:资源只能由持有者主动释放。
- 循环等待:存在进程资源的环形等待链。
18.线程安全实现方式
- 互斥锁(Mutex):保证临界区代码原子性。
- 原子操作(Atomic):通过硬件支持的原子指令(如 CAS)。
- 无锁编程:如使用无锁数据结构。
- 线程局部存储(TLS):避免共享数据。
- 读写锁(Read-Write Lock):区分读/写操作。
- 条件变量(Condition Variable):线程间同步。