2025春招整理-C++工程师-面试要点

C++的关键字

1. new 和 malloc 的区别

在C++中,newmalloc是两种不同的内存分配方式,它们之间有几个主要区别:

语法和类型安全:

new是C++的关键字,用于动态分配内存,并且可以自动调用构造函数来初始化对象。它返回一个指向正确类型的指针。
malloc是C语言的标准库函数,用于动态分配内存,但它只返回一个void*指针,需要显式地转换为正确的类型。malloc不会调用构造函数。

内存分配失败的处理:

new操作符无法分配足够的内存时,它会抛出一个std::bad_alloc异常。
malloc在内存分配失败时返回NULL

内存对齐:

new操作符会根据所分配对象的大小和对齐要求自动进行内存对齐。
malloc通常返回的内存是对齐的,但对齐方式依赖于实现和平台。

内存大小:

new操作符分配的内存大小由所请求的类型决定。
malloc需要显式地指定要分配的内存字节数。

构造函数和析构函数的调用:

new操作符在分配内存后会自动调用对象的构造函数。malloc不调用构造函数,因此如果分配的是对象,需要手动调用构造函数。同样,当使用new时,删除对象时会自动调用析构函数,而使用malloc时需要手动调用析构函数。malloc只是分配内存的一个函数。

数组分配:

C++中,new可以用来分配数组,它会为每个数组元素调用构造函数,使用delete[]才能销毁所有的数组元素。malloc分配数组时,不会调用构造函数,需要手动处理。

内存释放:

使用new分配的内存需要使用delete操作符来释放,在变量脱离作用域的时候,不使用delete关键字也会自动释放(一些情况除外:例如new分配的变量是一个特殊指针类型,指针指向的内存地址不会被释放)。使用malloc分配的内存需要使用free函数来释放,因为返回的一般是内存指针,所以free要求是必须的。

总的来说,new是C++中更安全、更方便的内存分配方式,因为它提供了类型安全、自动调用构造函数和析构函数等功能。而malloc则提供了更底层、更灵活的内存管理方式,但缺乏类型安全和自动调用构造函数/析构函数的特性。在C++编程中,通常推荐使用newdelete,除非有特定的理由需要使用mallocfree(直接分配内存适合在一些性能要求较高的场合使用,例如在内存池Memory Pool中使用并管理内存)。

STL容器

STL(Standard Template Library,标准模板库)是C++中提供的一系列模板类和函数,用于实现常见的数据结构和算法。其中,容器是STL的一个核心组成部分,它们提供了各种数据存储和操作的方式。下面是一些常见的STL容器及其使用场景:

vector(向量)

使用场景:当需要动态数组时,vector是一个很好的选择。它可以在运行时动态地调整大小,并且提供了随机访问迭代器,使得访问元素非常高效。
典型应用:存储一系列数据,如用户输入、文件内容等。

std::vector<T>(n) vec;
std::vector<std::vector<T>(n)>(m) vec<std::vector<T>(n)>; // 这是二维向量的声明语句(是否有点冗杂 

// 迭代器这么用
for(auto& it : vec) {
  // *it
}

list(双向链表)

使用场景:当需要频繁地插入和删除元素时,list是一个合适的选择。它提供了双向迭代器,可以在任意位置高效地插入和删除元素。索引是通过遍历寻找元素的,索引操作的效率不如std::vector。
典型应用:实现需要频繁操作元素的数据结构,如任务队列。

deque(双端队列)

使用场景:deque可以在两端高效地插入和删除元素,同时也可以随机访问元素。当需要同时从两端操作数据时,deque是最佳选择。
典型应用:实现广度优先搜索(BFS)算法、滑动窗口算法等。

实现滑动窗口:在处理数组或数据流时,deque可以用来维护当前窗口内的元素,高效地添加新元素和移除旧元素。
回文检测:deque可以从两端比较字符,用于检测字符串是否为回文。
双端队列操作:如排队系统,deque可以高效地处理队列两端的操作。

set/multiset(集合/多重集合 multi-代表元素可以重复)

使用场景:set和multiset基于红黑树实现,可以高效地插入、删除和查找元素。它们自动维护元素的有序性。
典型应用:去除重复元素、查找元素是否存在等。

map/multimap(映射/多重映射 multi-代表元素可以重复)

使用场景:map和multimap也是基于红黑树实现,它们存储键值对,可以高效地进行键查找、插入和删除操作。
典型应用:实现关联数组、数据索引等。

// map 储存的是键值对 std::pair<T1,T2>
for(std::pair& kvp : umap) {
    // kvp.first
    // kvp.second
}

stack(栈)

使用场景:stack实现了后进先出(LIFO)的数据结构,适用于需要后进先出操作的场合。
典型应用:函数调用栈、表达式求值等。

std::stack stk;
stk.push(); // 栈顶插入
stk.pop();  // 栈顶弹出

queue(队列)

使用场景:queue实现了先进先出(FIFO)的数据结构,适用于需要先进先出操作的场合。
典型应用:任务队列、消息队列、事件处理等。

std::queue que;
que.push();   // 队首插入
que.pop();    // 队尾弹出

priority_queue(优先队列)

使用场景:priority_queue允许根据元素的优先级(通常是元素的大小)来提取和插入元素。
典型应用:实现堆排序Dijkstra算法等。堆排序也可以直接通过std::vector来实现,但需要手动构建堆结构。

C++多态的实现机制

在C++中,多态性是一种重要的特性,它允许通过基类指针或引用来调用派生类中的函数。多态性主要分为两种:编译时多态(主要通过函数重载和模板实现)和运行时多态(主要通过虚函数实现)。

虚函数与虚表(Virtual Table, VTable)

  • 虚函数:在基类中声明为virtual的函数即为虚函数。这告诉编译器该函数的调用应该在运行时决定,而不是在编译时决定。
  • 虚表:对于包含虚函数的类,编译器会为这个类创建一个虚表(VTable)。虚表是一个包含函数指针的数组,每个指针对应一个虚函数的实现。

虚表指针(Virtual Table Pointer, VPtr)

  • 虚表指针:对于每个包含虚函数的类的对象,编译器都会在其内部添加一个虚表指针(VPtr)。这个指针指向对象的虚表。通过这个指针,我们可以在运行时找到正确的虚函数实现。

虚表指针的存储位置

  • 虚表指针的存储位置依赖于编译器的实现,但一般来说,对于单继承的类,虚表指针会被存储在对象的起始位置(或者紧跟在对象布局中的某些成员之后,如为了对齐考虑)。
  • 对于多重继承的类,虚表指针的存储会变得更加复杂,因为每个基类可能都有自己的虚表。在这种情况下,每个基类都会在其部分对象内存中放置一个虚表指针,并且对象的实际布局会根据编译器和具体的继承层次而变化。

虚析构函数的作用是什么?为什么基类需要虚析构函数?

  • 回答:虚析构函数允许通过基类指针删除派生类对象时,能够调用到派生类的析构函数,确保资源的正确释放。如果不将基类的析构函数声明为虚的,当通过基类指针删除派生类对象时,只会调用基类的析构函数,从而导致派生类部分的资源未能被正确释放,引发内存泄漏。

纯虚函数和抽象类的概念是什么?

  • 回答:纯虚函数是一种特殊的虚函数,它在基类中声明时不给出具体的实现(即使用= 0进行声明)。包含至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类使用,用于实现接口或规范一组派生类的共同行为。

钻石继承(Diamond Inheritance)/菱形继承(一种多继承)问题是什么?如何解决?

  • 回答:钻石继承问题是当多个派生类继承自同一个基类,而这些派生类又被另一个类继承时,可能会导致基类部分在派生类中被多次继承。这可能导致数据冗余和/或析构时资源被多次释放。一种解决方案是使用虚继承,虚继承会在所有通过虚路径继承该基类的派生类中共享基类的一个副本。class : virtual public Base {}

如何调试和观察虚表及其内容?

  • 回答:直接观察和修改虚表的内容是不安全的,因为它们是由编译器管理的。然而,一些编译器(如GCC)提供了扩展或工具(如GDB的特定命令),可以用来查看虚表指针和虚表的内容。另外,理解类的布局和虚函数的调用机制对于理解和调试多态性是非常重要的。

C++编译过程

C++的编译过程通常包括以下几个主要步骤:

  1. 预处理(Preprocessing)
    • 预处理器会处理源代码中的预处理指令,如#include#define等。
    • #include指令会将指定的头文件内容插入到源代码中。
    • #define指令会进行宏替换。
    • 预处理后的结果是一个扩展的源代码文件。
  2. 编译(Compilation)
    • 编译器将预处理后的源代码转换为汇编语言。
    • 这个过程涉及到语法分析、语义分析、生成中间代码、优化中间代码以及生成汇编代码。
  3. 汇编(Assembly)
    • 汇编器将汇编语言代码转换为机器语言代码,即二进制目标文件(.obj或.o文件)。
    • 每个源文件都会生成一个相应的目标文件。
  4. 链接(Linking)
    • 链接器将所有的目标文件以及库文件链接在一起,生成可执行文件(.exe文件)或共享库(.dll或.so文件)。
    • 在链接过程中,链接器会解决不同目标文件之间的符号引用问题,如函数调用、全局变量等。
  5. 加载(Loading)
    • 加载器将可执行文件加载到内存中,准备执行。
    • 这个步骤通常由操作系统在程序运行时自动完成。

编译过程分步的作用是什么

详细说明:

  • 预处理
    • 预处理器不会检查代码的语法错误,它只处理预处理指令。
    • 例如,#include <iostream>会将iostream头文件的内容插入到源代码中。
  • 编译
    • 语法分析:检查源代码的语法结构。
    • 语义分析:检查源代码的语义是否正确,例如类型检查。
    • 中间代码生成:将源代码转换为中间表示形式,便于优化。
    • 优化:对中间代码进行优化,以提高执行效率。
    • 汇编代码生成:将优化后的中间代码转换为汇编语言。
  • 汇编
    • 汇编器将汇编语言指令转换为机器语言指令。
    • 每个源文件生成的目标文件包含机器代码和符号表。
  • 链接
    • 链接器解决目标文件之间的符号引用,如函数调用、变量引用等。
    • 链接器还会合并相同的代码段和数据段,以减少最终可执行文件的大小。
    • 链接器可以处理静态链接和动态链接。静态链接将所有代码合并到可执行文件中,而动态链接则在运行时加载共享库。
  • 加载
    • 操作系统的加载器将可执行文件加载到内存中。
    • 加载器负责分配内存空间,设置程序执行入口点等。
posted @ 2025-12-15 21:13  北纬31是条纬线哦  阅读(1)  评论(0)    收藏  举报