❥ 高考结束已然啦,祝愿学弟学妹们能达到自己想要的成就 ❥
对于日后从事计算机专业的同学来说,C++毋庸置疑是主流之一(毕竟大多入学都要学C),这次博主将会给大家分享我春招时期的C++面试高频宝典,给即将入门或者即将秋招的小伙伴们提供一些力所能及的帮助!
❥ 对于经验不足的校招生或者找实习的同学来说,可能并不是那么容易的就能获取面试相关的一些资源。对此,我们可以在牛客、力扣或者一些比较有名的公众号上去获取很多信息。在进行投递的时候,最好通过官网投递,招聘软件上比较容易踩坑,如果想要招聘季各公司官网链接的excel表的话,可以在下方评论区评论,私发给有需要的小伙伴!
❥ 计算机专业的小伙伴们在找工作的时候,常常提到八股。那么什么是八股呢?下面博主就和大家说一说自己的看法~
❥ 作为一名软件工程专业在校生,八股就是在笔试或者面试过程中经常遇到的知识点,通用的有 网络原理、网络编程、操作系统、计算机组成原理、数据库 这些,特定的就是 岗位强相关的技术和面对对象语言。
❥ 对于互联网前后端而言,Java 一般是强相关的语言(在此基础上,字节青睐 Golong,腾讯青睐 C++);对于游戏研发来说,C++ / C# / Lua 就是强相关的语言,强相关的技术一般有 Unity / Unreal / Cocos / 计算机图形学 / OpenGL...;对于测试开发来说,测试开发相关的 自动化测试 / 黑盒测试 / 白盒测试 以及一些测试开发工具(比如 Jmeter / Postman / Selenium / 禅道)就是强相关的。
❥ 小虾在这次的春季招聘中,只投递了喜欢的游戏开发领域,经历了包括(腾讯天美、腾讯魔方、网易雷火、阿里巴巴灵犀互娱、吉比特雷霆、完美世界、三七互娱、字节跳动朝夕光年、剑心互娱、飞鱼科技)等名厂的面试,也收获到了核心项目组的offer和不错的薪资待遇。
❥ 下面就和大家分享分享我所遇到的重点八股!之前分享了Unity篇,这次和大家分享更为通用的C++篇,希望给大家带去帮助~【
为了方便记忆,简化处理了不少说法,对大家可能有点拗口,见谅】
☀️动态合批与静态合批
动态物体共用相同的材质时,Unity自动进行批处理。
区别:动态批处理一切都是自动的,而且物体是可以移动的,但是限制很多。静态批处理自由度很高,限制很少,缺点可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了。
☀️虚拟内存托管
使程序认为它拥有一个连续完整的地址空间,而实际上通常是被分隔成多个物理内存碎片,部分暂存在外部磁盘存储器上,在需要时进行答数据交换。
☀️对象池思想
需要频繁创建/销毁的对象,首先从对象池中找有没有可用的对象,没有就创建对象使用,当一个对象不使用的时候,将它设置为不激活的状态并放入对象池中,等待需要使用的时候再去对象池中寻找,并把它激活。
☀️线程 进程 协程
1)进程是资源分配的最小单位,线程是CPU处理调度的最小单位。
2)进程有独立的栈和堆,由操作系统调度;线程有独立栈和共享堆,标准线程由操作系统调度;协程和线程一样,独立栈,共享堆。
3)一个进程一般由1个主线程+若干辅线程,线程里开启携程让程序在特定时间运行,协程避免无意义调度,提高性能,失去多CPU的能力,程序员承担调度责任。
4)协程定义:在主线程运行时同时开启另一段逻辑处理,协助当前程序执行。换句话说,开启协程就是开启一个可以与程序并行的逻辑。可以用来控制运动、序列以及对象的行为。
5)协程本身是异步的一种实现,线程同步异步都有,同步是运行等待,异步是回调,多线程异步,线程同步要加锁。
☀️协程和线程的区别和特点
(1)开销。协程执行效率高,协程是单个线程执行,以子程序中断的形式切换,没有多线程切换的开销。(2)协程不需要多线程的锁机制。不存在同时写变量冲突。
☀️Inline内联函数的作用和缺点
1)作用:编译器在函数调用点上展开函数,避免函数调用的开销;
2)inline可能造成代码膨胀,尤其是递归,会造成大量内存开销,占用CPU资源;不方便调试,每次修改会重新编译头文件,增加编译时间。
☀️Inline内联函数和define宏
1)define在预处理阶段对命令进行替换,inline在编译阶段在函数调用点处直接展开函数,节省了函数调用的开销;
2)define不会检查参数的类型,会出现类型安全问题,比如定义一个max命令,但是传递的时候可能会传递一个整数和一个字符串,就会出错,但是内联函数在编译阶段会进行类型检查;
3)使用宏的时候可能要添加很多括号,比较容易出错
☀️内存/字节对齐 原因与方式
指C++结构体中数据成员的内存地址是不是对齐字节大小的倍数。关键在于CPU存取数据的效率。为提高效率,计算机按照固定长度从内存取数据。内存对齐可以节省内存/提升数据读取的速度(若不进行对齐,要取出两块地址中的数据,进行掩码和移位等操作,导致效率很低)
对齐原则:结构体变量的 首地址/总体大小 能够被最宽基本类型成员的对齐值整除;每个成员相对起始地址的偏移量能被该变量的大小整除;若不满足这些条件,编译器就会填充(padding)。因此声明时,依次声明字节对齐的数据。然后组合小成员,省去浪费的空间,不参杂声明在字节对齐的数据之间。
☀️堆和栈的区别
1)堆中内存需要手动申请/释放,栈中内存是由OS自动申请/释放;
2)堆分配的内存较大(4G),栈能分配的较小(1M);
3)堆中分配/释放内存会产生内存碎片,栈不会产生内存碎片;
4)堆的分配效率低,栈的分配效率高;
5)堆地址从低向上,栈由高向下。
☀️浅拷贝和深拷贝
深拷贝是逻辑意义上的,程序员定义,要拷贝构造函数,要申请新的内存空间;而浅拷贝是物理意义上的,复制比特位就行。
☀️钻石/菱形 继承的问题与解决
二义性的问题,两个父类会对公共基类的数据和方法产生一份拷贝,因此对于子类来说读写一个公共基类的数据或调用一个方法时,不知道是哪一个父类的数据和方法,也会导致编译错误。可以采用虚继承的方法解决这个问题(父类继承公共基类时用virtual修饰),这样就只会创造一份公共基类的实例,不会造成二义性。
☀️Struct结构与联合union的区别/联系
(1). 都由多个不同数据类型组成,任何同一时刻,联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值不存在, 而对于结构的不同成员赋值互不影响。
☀️指针和引用的区别
1)指针本质是一个地址,有自己的内存空间,引用只是一个别名;
2)引用必须被初始化,指针不必。引用初始化以后不能被改变,指针可以改变所指的对象;
3)指针可以初始化为nullptr,而引用必须被初始化为一个已有对象的引用;
4)指针可以是多级指针,引用只能是一级。
☀️指针与数组的区别
1)数组在静态存储区被创建,而指针在栈上被创建。
2)指针可以随时指向任意类型的内存块,数组不行
3)sizeof可计算数组的容量(字节数),指针得到的是指针变量的字节数,而不是p所指的内存容量。C++/C 没有办法知道指针所指的内存容量,除非在申请内存时记住它。
4)当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
☀️sizeof和strlen的区别
sizeof返回所占总空间,数组要算上 ,指针就返回4;
strlen不区分是数组/指针,读到 为止返回长度,且strlen是不把 计入长度
☀️全局/局部变量 的区别和特点
作用域:整个程序/当前函数或循环 内存存储方式:全局数据区/栈区。
生命期:和主程序一样,随程序销毁而销毁;随函数的退出或循环退出不存在。
使用方式:声明后程序的各个部分都可以用到;只在局部使用。函数内部优先使用局部变量
☀️C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
☀️main函数执行之前,会执行什么?
全局对象的构造函数
☀️重载、重写和隐藏
1)重载指同一个名字的函数,具有不同的参数列表(参数类型/个数)或 不同的返回类型,根据参数列表和返回类型决定调用哪一个函数;
2)重写/覆盖 指派生类中的函数重写了基类中的虚函数,重写的基类的中函数必须被声明为virtual,并且返回值,参数列表和基类中的函数一致;
3)隐藏指派生类中的同名函数把基类中的同名函数隐藏了,即基类同名函数被屏蔽掉;此时基类函数不能声明为virtual。
从实现原理上来说:重载时,编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。如有两个同名函数,对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定)。重写时,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出),因此在运行期绑定的(晚绑定)。
☀️内存分配的方式以及区别
1)从静态存储区域分配。内存在程序编译的时候就分配好,在程序的整个运行期间都存在。
2)在栈上创建。执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3)从堆上分配,即动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
☀️static作用
1)函数体内作用范围是函数体,不同于auto,内存只被分配一次,因此值在下次调用时维持上次的值;
2)模块内的static变量可被模块内所用函数访问,但不能被模块外其它函数访问;
3)模块内的static函数只可被这一模块内的其它函数调用,函数的使用范围被限制在声明它的模块内;
4)类的static成员变量属于整个类,对类的所有对象只有一份拷贝;
5)类的static成员函数属于整个类,不接收this指针,只能访问类的static成员变量。
☀️const关键字作用
1)阻止一个变量被改变。定义const变量时,通常需要初始化,之后不能改变
2)对指针来说,放在左/右/两边修饰作用不同
3)函数声明中,const可修饰形参,表明输入参数,函数内部不能改变值;
4)类的成员函数,若指定为const类型,则是一个常函数,不能修改类的成员变量;
5)类的成员函数,有时候必须指定其返回值为const类型,使其返回值不为左值。
☀️const与#define相比,有何优点
1)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
2)有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
☀️struct和class(结构体和类)的区别()
基本是通用的,有几个细节不同:
1)class类中的成员默认private属性;struct结构体中的成员默认都是public。
2)class继承默认是private继承,而struct继承默认是public继承
3)class可以使用模板,struct不能
☀️New/delete和malloc/free间的关系
都可用于 申请/释放 动态内存。C使用malloc/free(库函数),C++使用new/delete(运算符)。对自定义对象,malloc/free无法调用其构造/析构函数;new/delete可以,还能完成对象的空间分配/初始化/销毁和释放。
因此具体区别:(1)new分配内存空间无需指定分配内存大小,malloc需要;(2)new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型;(3)new是从自由存储区获得内存,malloc从堆中获取内存;(4)对于类,new会调用构造函数和析构函数,malloc不会【创建对象的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数】
☀️初始化列表的初始化顺序
初始化次序按照成员变量在类中的声明次序执行,而非按初始化列表列出的顺序
☀️必须使用初始化列表的情况
- 成员是个引用。
- 成员是个const类型成员。
- 这个类继承一个基类,并且基类中有构造函数,这个构造函数里边还有参数。
- 成员变量类型是某个类类型,而这个类的构造函数带参数。
☀️初始化列表的优势
除了必须用初始化列表的场合,就是提高程序的运行效率。尤其是类类型成员变量放到初始化列表中能够比较明显的看到效率的提升。写程序的时候,成员变量尽量放在初始化列表里。
☀️this指针调用成员变量时,堆栈发生什么变化
当类的非静态成员函数访问非静态成员时,编译器自动将对象地址作为隐含参数传递给函数,隐含参数就是this指针。即使没写this,编译器在链接时也会加上,对各成员的访问都通过this。
例如建立了类的多个对象,在调用类的成员函数时,并不知道具体是哪个对象在调用,此时可以通过查看this指针来查看具体是哪个对象在调用。This指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。
☀️多态、虚函数和纯虚函数。
多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;在程序编译时多态性体现在函数和运算符的重载上;
虚函数:在基类中冠以关键字 virtual 的成员函数。它提供了一种接口界面。允许在派生类中对基类的虚函数重新定义。
纯虚函数的作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。作为接口而存在的纯虚函数不具备函数的功能,一般不能直接被调用。
从基类继承来的纯虚函数,在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。
虚函数表:主是要一个类的虚函数的地址表,解决了继承、覆盖的问题,保证其容真实反应实际的函数
抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类是必须用作派生其他类的基类,而不能用于直接创建对象实例。但仍可使用指向抽象类的指针支持运行时多态性。
注:定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。定义一个函数为纯虚函数,才代表函数没有被实现。
☀️基类的析构函数不是虚函数,会带来什么问题
派生类的析构函数用不上,会造成资源的泄漏。
☀️野指针
指针变量未初始化/释放后未置空/操作超越变量作用域 导致,解决方法就是置空
☀️智能指针
主要解决内存泄露,以自动地释放内存空间。本身是一个类,当函数结束的时候会调用析构函数,并由析构函数释放内存空间。智能指针分为共享指针shared_ptr, 独占指针unique_ptr和弱指针weak_ptr
1)shared_ptr:多个共享指针可以指向相同的对象,引用计数机制,当最后一个引用销毁时,释放内存空间;
2)unique_ptr:保证同一时间段内只有一个智能指针能指向该对象(move操作传递)
3)weak_ptr:解决shared_ptr相互引用时的死锁问题(若相互引用,这两个指针的引用计数永远不可能下降为0,资源永远不会释放)。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
☀️shared_ptr实现过程&是否线程安全
1)构造函数:将指针指向该对象,引用计数置为1;
2)拷贝构造函数:将指针指向该对象,引用计数++;
3)赋值运算符:=号左边的shared_ptr的引用计数-1,右边的shared_ptr的引用计数+1,如果左边的引用技术降为0,还要销毁shared_ptr指向对象,释放内存空间。
shared_ptr的引用计数本身是安全且无锁的,但是它指向的对象的读写则不是,因此可以说shared_ptr不是线程安全的。
☀️为什么不用raw ptr来解决shared_ptr的循环引用问题
一个weak_ptr绑定到shared_ptr之后不会增加引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使weak_ptr指向对象,也还是会释放;raw指针,当对象销毁之后会变成悬浮指针。
☀️auto和deltype的作用和区别
答:用于实现类型自动推导,让编译器来操心变量的类型;auto不能用于函数传参和推导数组类型,但deltype可以解决这个问题。
☀️左值和右值&右值引用
1)左值具有可寻址的存储单元,并且能由用户改变其值的量,具有持久的状态,直到离开作用域才销毁;右值表示即将销毁的临时对象,具有短暂的状态。
2)右值引用就是必须绑定到右值的引用,可以通过&&(两个取地址符)来获得右值引用;右值引用只能绑定到即将销毁的对象,因此可以自由地移动其资源;
3)右值引用是为了支持移动操作而引出的一个概念,它只能绑定到一个将要销毁的对象,使用右值引用的移动操作可以避免无谓的拷贝,提高性能。使用std::move()函数可以将一个左值转换为右值引用。
☀️Delete和Delete[ ]的区别,[ ]如何知道要delete多少次,类成员函数中能否Delete This?
1)若是基本类型,delete和delete[]效果是一样的,系统会自动记录分配的空间,然后释放;对于自定义数据类型(比如类)不行,delete仅仅释放数组第一个元素的内存,且仅调用了第一个对象的析构函数,但delete[]会调用数组所有元素的析构函数,并释放所有内存空间;如果用delete时没用括号,delete就会指向单个对象,否则指向一个数组。
2)在new []一个对象数组时,需要保存数组的维度,C++在分配数组空间时多分配了4个字节的大小,专门保存数组大小,这个数据存在这个分配返回的指针周围,在 delete[]时就可以取出这个保存的数,就知道了需要调用析构函数多少次了;
3)类的成员函数可以调用delete this,并且delete this之后还可以调用该对象的其他成员,但是有个前提:被调用的方法不涉及这个对象的数据成员和虚函数。当一个类对象声明时,系统会为其分配内存空间。在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。
☀️类对象的大小 受什么影响
1)类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
2)内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
3)虚函数的话,会在类对象插入vptr指针,加上指针大小;
4)当类是某类的派生类,那么派生类继承的基类的数据成员也会存在在派生类中的空间中,对派生类进行扩展。
☀️程序使用动态内存的原因
1)程序不知道自己需要使用多少对象
2)程序不知道所需对象的准确类型
3)程序需要在多个对象间共享数据
4)若使用get()返回的指针,当最后一个对应的智能指针销毁后,指针就变为无效了
5)若智能指针管理的资源不是new分配的内存,传递给它一个删除器
☀️各种排序算法的原理和时间复杂度?
1)快排:一轮划分,选择一个基准值,小于该基准值的元素放到左边,大于的放在右边,此时该基准值在整个序列中的位置就确定了,接着递归地对左边子序列和右边子序列进行划分。时间复杂度o(nlogn),最坏的时间复杂度是o(n2);
2)堆排序:利用完全二叉树性质构造的一个一维数组,用数组下标代表结点,则一个结点的左孩子下标为2i+1,右孩子为2i+2,一个结点的父节点为(i-1)/2。堆排序的思想就是,构造一个最大堆或者最小堆,以最大堆为例,那么最大的值就是根节点,把这个最大值和最后一个结点交换,然后在从前n-1个结点中构造一个最大堆,再重复上述的操作,即每次将现有序列的最大值放在现有数组的最后一位,最后就会形成一个有序数组;求升序用最大堆,降序用最小堆。时间复杂度O(nlogn);
3)冒泡排序:从前往后两两比较,逆序则交换,不断重复直到有序;时间复杂度O(n2),最好情况O(n);
4)插入排序,类似打牌,从第二个元素开始,把每个元素插入前面有序的序列中;时间复杂度O(n2),最好情况O(n);
5)选择排序,每次选择待排序列中的最小值和未排序列中的首元素交换;时间复杂度O(n2);
6)归并排序,将整个序列划分成最小的>=2的等长序列,排序后再合并,再排序再合并,最后合成一个完整序列。时间复杂度O(nlogn)。
7)希尔排序,是插入排序的改进版,取一个步长划分为多个子序列进行排序,再合并(如135一个序列,246一个序列),时间复杂度O(n1.3),最好O(n),最坏O(n2);
8)桶排序,将数组分到有限数量的桶里。每个桶再个别排序,最后依次把各个桶中的记录列出来记得到有序序列。桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM),M为桶的数量。最好的情况下为O(N)。
☀️求序列中前k最大/最小的数
1)基于快排,每轮划分选择一个基准值,把比它小的数放在左边,大的放在右边,函数返回基准值的位置,如果该位置恰好是K,就说明了这是第K小的数,所以从0-基准值位置的数是序列中的前K小数。若返回基准值的位置小于或者大于K,再进行相应调整:如果返回的基准值大于k,在基准值左边序列查找,如果大于,在基准值右边进行查找。递归地进行快排,直到返回的结果=K;时间复杂度为O(n)。
2)基于堆排序,求前K个最小的数用最大顶堆,求前K个最大的数用最小顶堆。以最大顶堆为例,要维护一个大小为K的顶堆,就是先将K个数插入堆中,随后,对每一个数,与堆顶的最大元素比较,若该数比堆顶元素小,则替换掉堆顶元素,然后调整堆,若大于堆顶元素,则不管,那么将所有元素比较和插入后,该堆维护的就是最小的K个数。求前k小的数用最大顶堆的目的(原理):这是一种局部淘汰的思想,尽量的把小的数都放在堆中,最后使得即使堆中最大的数,也比外界的所有数都小,就达到了目的。
☀️尽快在一个序列中移除指定条件的元素,并不改变其他元素的相对顺序
比如一个序列{1,3,2,5,2,2,6,7}移除序列中所有的2?
先找到第一个符合条件的元素的位置,然后从这个位置开始,往后扫描不符合条件的元素,逐个的安插到这个位置以及之后的位置,无论是什么元素都直接覆盖(可以用一个指针指向这个要安插和覆盖的位置,往后步进)。该算法时间复杂度为O(n)。
☀️哈希表?如何处理冲突?怎么删除一个元素?
1)哈希表是一种根据关键码值直接访问数据的数据结构,它通过把关键码值映射到表中的一个位置来访问元素,以加快查找的速度。这个映射函数叫做哈希函数;
2)哈希表的长度使用质数,可以降低发生冲突的概率,使哈希后的数据更加均匀,如果使用合数,可能会导致很多数据集中分布到一个点上,造成冲突;
3)解决冲突的办法有开放定址法和拉链法,开放定址法包括线性测探、平方测探法;
4)线性测探法并不会真正的删除一个元素,而是做一个标记,否则可能会导致正常的查找出错(利用线性探测法解决hash冲突)
☀️如何判断链表是否有环?
快慢指针(最快方法)
☀️缓存(Cache)?如何提高缓存的命中率?缓存是不是最快的?
(1)Cache即CPU的高速缓冲存储器,是一种是用于减少处理器访问内存所需平均时间的部件;
(2)由于CPU的计算速度远远大于从CPU向内存取数据的速度,如果每次都让CPU去内存取数据,会导致CPU计算能力的浪费,所以人们设计了缓存,CPU通过读写缓存来获取操作数,结果也通过缓存写入内存;
(3)注意程序的局部性原理,在遍历数组时按照内存顺序访问;充分利用CPU分支预测功能,将预测的指令放到缓存中执行;此外缓存的容量和块长是影响缓存效率的重要因素。
(4)缓存不是最快的,寄存器更快。
☀️缓存一致性 的定义与如何保证
(1)在多核CPU中,每个核有自己的缓存,在两个核进行独自修改缓存中的数据的时候,就可能会造成数据不一致的问题,就是缓存的一致性问题;
(2)一个是在总线中加锁,一个是采用缓存一致性协议。
☀️缓存块的大小,读取内存字段
如果缓存没有命中(即读取一个数据没有在缓存中),不仅需要把该字(数据)从主存中取出,还需要从主存中将它所在的整个字块一次调入缓存中。缓存线(块)的长度是64B。
在笔试面试的时候,博主主要是在算法方面建立了比较多的优势,在八股方面略有欠缺,这篇文章有纰漏的地方欢迎大家在评论区进行补充QAQ



