101-【八股】编程语言
C++
多态与虚函数
- 什么是多态?C++的多态是如何实现的?
答:所谓多态,就是同一个函数名具有多种状态,或者说一个接口具有不同的行为;C++的多态分为编译时多态和运行时多态,编译时多态也称为为静态联编,通过重载和模板来实现,运行时多态称为动态联编,通过继承和虚函数来实现。
- 虚函数的实现机制是什么?
答:虚函数是通过虚函数表来实现的,虚函数表包含了一个类(所有)的虚函数的地址,在有虚函数的类对象中,它内存空间的头部会有一个虚函数表指针(虚表指针),用来管理虚函数表。当子类对象对父类虚函数进行重写的时候,虚函数表的相应虚函数地址会发生改变,改写成这个虚函数的地址,当我们用一个父类的指针来操作子类对象的时候,它可以指明实际所调用的函数。
- 虚函数是存在类中还是类对象中(即是否共享虚表)?
答:存在类中,不同的类对象共享一张虚函数表(为了节省内存空间)。
- 在基类的构造函数和析构函数中调用虚函数会怎么样?
答:语法上没有问题,但从效果上看,不能实现多态;因为调用构造函数的时候,是先进行父类成分的构造,再进行子类的构造。在父类构造期间,子类的特有成分还没有被初始化,此时下降到调用子类的虚函数,使用这些尚未初始化的数据一定会出错;同理,调用析构函数的时候,先对子类的成分进行析构,当进入父类的析构函数的时候,子类的特有成分已经销毁,此时是无法再调用虚函数实现多态的。
- 虚函数关键字virtual能加到构造函数和析构函数前面吗?
构造函数不行。当构造函数被调用时,基类的构造函数已经开始执行,但派生类的构造函数尚未执行。因此,虚函数的动态绑定机制并不会生效。
析构函数可以。将析构函数声明为虚函数可以确保当通过基类指针删除对象时,派生类的析构函数会被正确调用(而不是调用基类虚构函数),确保派生类的资源被清理。
关键字
new和malloc
C使用malloc/free,C++使用new/delete,前者是C语言中的库函数,后者是C++语言的运算符,对于自定义对象,malloc/free只进行分配内存和释放内存,无法调用其构造函数和析构函数,只有new/delete能做到,完成对象的空间分配和初始化,以及对象的销毁和释放空间,不能混用,具体区别如下:
(1)new分配内存空间无需指定分配内存大小,malloc需要;
(2)new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型;
(3)new是从自由存储区获得内存,malloc从堆中获取内存;
(4)对于类对象,new会调用构造函数和析构函数,malloc不会。
static
static即静态的意思,可以对变量和函数进行修饰。分三种情况:
(1)当用于文件作用域的时候(即在.h/.cpp文件中直接修饰变量和函数),static意味着这些变量和函数只在本文件可见,其他文件是看不到也无法使用的,可以避免重定义的问题。
(2)当用于函数作用域时,即作为局部静态变量时,意味着这个变量是全局的,只会进行一次初始化,不会在每次调用时进行重置,但只在这个函数内可见。
(3)当用于类的声明时,即静态数据成员和静态成员函数,static表示这些数据和函数是所有类对象共享的一种属性,而非每个类对象独有。
(4)static变量在类的声明中不占用内存,因此必须在.cpp文件中定义类静态变量以分配内存。文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量在第一次使用时分配内存并初始化。
智能指针
内容
智能指针主要解决一个内存泄露的问题,它可以自动地释放内存空间。因为它本身是一个类,当函数结束的时候会调用析构函数,并由析构函数释放内存空间。智能指针分为共享指针(shared_ptr), 独占指针(unique_ptr)和弱指针(weak_ptr):
(1)shared_ptr ,多个共享指针可以指向相同的对象,采用了引用计数的机制,当最后一个引用销毁时,释放内存空间;
(2)unique_ptr,保证同一时间段内只有一个智能指针能指向该对象(可通过move操作来传递unique_ptr);
(3)weak_ptr,用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
只要使用智能指针,一定不会发生内存泄漏吗
在某些情况下,即使使用智能指针,也可能发生内存泄漏。以下是几种可能导致内存泄漏的情况:
循环引用(Cyclic References):当两个或多个
shared_ptr
相互持有对方的引用时,它们之间会形成一个循环引用,这样就会导致它们的引用计数永远不会归零,从而无法释放内存。为打破循环引用,可以使用
std::weak_ptr
。weak_ptr
不参与引用计数,因此不会增加对象的引用计数,避免了循环引用的发生。shared_ptr
的不当使用:手动管理一个对象的内存,或者shared_ptr
被错误地拷贝或赋值,可能会造成预期外的行为。忘记释放资源(管理其他资源):智能指针管理的是内存,但如果对象管理了其他资源(如文件句柄、网络连接等),智能指针并不会自动管理这些资源,可能会导致资源泄漏。
对于非内存资源,可以通过自定义删除器(deleter)来确保这些资源在智能指针生命周期结束时被正确释放。
编译链接原理
答:包括四个阶段:预处理阶段、编译阶段、汇编阶段、连接阶段。
(1)预处理阶段处理头文件包含关系,对预编译命令进行替换,生成预编译文件;
(2)编译阶段将预编译文件编译,生成汇编文件(编译的过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码);
(3)汇编阶段将汇编文件转换成机器码,生成可重定位目标文件(.obj文件)(汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可);
(4)链接阶段,将多个目标文件和所需要的库连接成可执行文件(.exe文件)。
内存问题
内存分区
(1)堆,使用malloc、free动态分配和释放空间,能分配较大的内存;
(2)栈,为函数的局部变量分配内存,能分配较小的内存;
(3)全局/静态存储区,用于存储全局变量和静态变量;
(4)常量存储区,专门用来存放常量;
(5)自由存储区:通过new和delete分配和释放空间的内存,具体实现可能是堆或者内存池。
内存对齐
内存泄漏
内存泄漏是 C++ 编程中常见且严重的问题,会导致程序性能下降、崩溃或系统资源耗尽。要解决内存泄漏问题,可以通过以下方式:
- 使用智能指针来自动管理内存。
- 小心指针的使用,避免丢失引用。
- 手动释放动态分配的内存。
- 使用工具如 Valgrind 和 AddressSanitizer 检查内存泄漏。
- 遵循 RAII 模式和避免循环引用。
其他内存问题
- 野指针:重复释放同一块内存;
Js
- 面试官:什么是防抖和节流?有什么区别?如何实现? | web前端面试 - 面试官系列 (vue3js.cn)
参考资料
- 【游戏开发面经汇总】- 计算机基础篇 - 知乎 (zhihu.com)