找回密码
 立即注册
查看: 211|回复: 0

游戏开发面试答案篇(一)-- C++篇

[复制链接]
发表于 2022-8-17 14:27 | 显示全部楼层 |阅读模式
游戏开发程序岗面试题答案版C++篇,后续继续更新游戏逻辑篇、unity篇、图形学篇,并整理成文档,可在公号【游戏君五尘】获取。
知乎排版较乱,原文链接
目录
一、基础语法   
二、面向对象   
三、内存   
四、STL
五、C++11新特征

一、基础语法
1、C 和 C++的区别
i.  C++是面向对象的的编程语言,C是面向过程的编程语言
ii. C++中的内存分配运算符是new/delete而C 中是malloc和free
iii. C++中有函数重载而C 中没有
iv. C++中新增了引用的概念而C 中只有值和指针

2、struct 和 class的区别
i. struct 一般用于描述一个数据结构集合而class是对一个对象数据的封装
ii. class 默认访问修饰符是私有的而struct 是公有
iii. 在继承方面 class默认是私有继承而struct 默认是公有继承

3、define宏定义 和 const 的区别
i. 首先 宏定义是在编译的预处理阶段起作用而const是在编译、运行时起作用
ii. 其次 宏定义它只做替换,并不会进行检查,很容易报错而const有数据类型,编译器会对它进行类型检查
iii. 最后 宏定义的数据没有分配内存,只是插入替换
而const定义的变量只是值不能变,但是会分配内存

4、define宏定义 和 line内联函数的区别
i. 首先 宏定义在预处理阶段起作用,只做简单的字符串替换,它没有返回值而 内联函数在编译阶段起作用,有返回值
ii. 然后 内联函数在编译时直接将函数代码嵌入到目标代码中,省去了函数调用的开销,从而提高性能,并且可以重载
iii. 最后 编译器会对内联函数进行类型检查以及语法判断
而宏定义不会

5、指针和引用的区别
i. 指针是一个变量,里面存放的是地址而引用是变量的别名,它和原来的变量实际上是同一个东西ii. 指针可以有多级而引用只能有一级
iii. 指针在初始化依然可以改变指向而引用初始化后就不能改变
iv. 对指针取地址,得到的是指针原本的地址
而对引用取地址,得到的是变量的地址

6、数组和指针的区别
i. 首先 数组是存储多个相同数据类型的集合。数组名是首元素的地址而指针是变量,用于存放其它变量在内存中的地址。指针名指向内存的首地址
ii. 其次 数组在内存中是连续存储的,通过数组下标进行访问,数组不是在静态区就是在栈上而指针的存储的存储空间不能确定iii. 最后 用sizeof计算数组,得到的是整个数组的大小
而指针得到的是该指针变量的大小

7、数组指针和指针数组的区别
数组指针:它是指向数组的指针,它的本质是指针,只不过指向数组中的某一个元素指针数组:它是存放指针的数组,其本质是数组,只不过其中存放的元素是指针
数组指针写做 int(*ptr) [4]
指针数组写做 int* ptr[4]

8、深拷贝和浅拷贝的区别
原因:在拷贝构造的过程中,导致两个对象指向同一块内存区域,这样再释放内存的过程中就会导致内存资源重复释放浅拷贝因为浅拷贝只是拷贝了一个指针,并没有新开辟一块内存区域所以就导致了两个对象指向了同一块地址最后会导致内存资源的重复释放深拷贝深拷贝是直接开辟了一个新的空间,新对象指向这个新的空间
这样 即使原对象被析构,也不会影响到新对象

9、移动构造和拷贝构造的区别
i. 首先 在拷贝构造函数中,如果涉及到指针,就需要用到深拷贝而在移动构造函数中,则用的是浅拷贝
ii. 最后 拷贝构造函数的参数是一个左值引用
而移动构造函数的参数是一个右值将亡值的引用

10、重载,重写,隐藏的区别
i. 首先,只有同一范围定义的同名函数才存在重载关系重载特点是函数名相同参数类型和数目不同
ii. 然后,重写是指子类覆盖父类中的同名函数,要求子类函数必须是虚函数,且与父类的虚函数有相同的参数类型,参数个数,以及返回值类型
iii. 所以,重写和重载的区别重载是函数之间的关系,重写是子类和父类的关系重载要求参数类型和数目不同,重载要求参数列表和返回值相同iv. 最后隐藏是指,子类中的函数屏蔽了父类中的同名函数。隐藏发生条件如下两者函数参数相同,但父类函数不是虚函数。与重写的区别在于父类是否为虚函数
两者参数不同,不管父类是不是虚函数,都会隐藏

11、指针常量和常量指针的区别
这里以以C++primer为准
指针常量:写作int const *p,指针本身是一个常量,它的值不能修改
常量指针:写作int* const p ,指针的指向是一个常量,它的指向不能修改

12、野指针和悬空指针区别
首先,它们都是指向无效内存区域的指针
野指针:指向的位置不确定
原因:指针定义时没有进行初始化
解决:定义时就进行初始化或置空悬空指针:指向的内存区域被释放
原因:指针指向的内存被释放,但指针没有及时置空
解决:内存释放后及时置空

13、静态类型和动态类型的区别
静态类型对象在声明时采用的类型,在编译期就已经确定动态类型
一个指针或引用目前所指对象的类型,在运行期时才能确定

14、源文本到文本可执行文件经历的过程
C++从源码到指向文件有四个过程:预处理、编译、汇编、链接
i. 预处理过程如下将所有的#define删除,并且展开所有的宏定义处理一些预编译指令,如#ifndef、#ifdef等处理#include预编译指令,将被包含的文件插入到该预编译指令的位置过滤所有的注释添加行号和文件名标识#ifndef、#ifdef的作用是防止重复包含头文件#include<> ,从标准库中寻找头文件。#include"",从当前目录开始寻找头文件。
ii. 编译过程分为6步词法分析:将源代码的字符序列分割成一系列的记号语法分析:对记号进行语法分析,产生语法树语义分析:判断表达式是否有意义代码优化生成汇编代码汇编代码优化
iii. 汇编:这个过程主要是将汇编代码转变成机器可以执行的指令
iv. 链接:将不同源文件产生的目标文件进行链接,从而形成一个可以执行的程序

15、递归和循环的区别
i. 递归时,每递归一层,就会在内存生成一个调用栈,来保存本次递归的信息,所以如果递归深度过深,就会有栈溢出的问题
ii. 循环是一次正向的过程,递归则需要回溯
iii. 在写法上,递归需要不断调用自身的函数,循环则不需要

16、i++ 和 ++i的区别
i. 首先 i++是先加后赋值而++i是先赋值后加
ii. 其次 前置返回一个引用,后置返回一个对象。而前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低。所以++i更快

17、const的四种作用
i. const修饰局部变量或全局变量,初始化后不能更改
ii. const修饰函数的参数,为了避免该参数被修改
iii. 用const修饰函数返回值,说明函数的返回类型是const的,则返回值不能被修改
iv. 用const修饰函数,则不能修改其成员变量的值

18、static的作用如果不考虑类的情况
i. 第一个作用是隐藏,不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,而加了之后,只能在该文件所在的编译模块中使用
ii. 然后 作用在局部变量上,可以提高其生命周期。它不会出代码块而销毁,而是存储在静态存储区中
iii. 最后 用static修饰的变量,默认初始化为0考虑类的情况static修饰成员变量或者成员函数
它就只与类进行关联,而不再属于栈上某个对象的数据,所有对象都共享这一块静态存储空间

二、面向对象1、面向对象的三大特性
i. 封装封装是把客观事物封装成抽象类。比如把公共的数据或方法用public修饰,把私有的数据或方法用private修饰
ii. 继承继承是让某个类对象获得另一个类对象的属性和方法。
iii. 多态多态是指同一事物表现出不同的能力多态性是允许将子类类型的指针赋值给父类类型
实现多态的方式有两种:重写(运行时多态)和重载(编译时多态)

2、类的默认成员函数
i. 无参的构造函数:用于完成对象的初始化工作
ii. 拷贝构造函数:用于复制本类对象
iii. 赋值运算符重载函数:同样也是负责本类的对象
iv. 析构函数:用于对象的清理

3、C++ 类对象的初始化顺序,有多重继承情况下的顺序
i. 父类的构造函数优先初始化,若父类中包含成员类对象,则再初始化成员类对象,最后再初始化子类对象
ii. 成员变量的初始化与声明顺序有关
iii. 析构顺序与构造顺序相反
可以简单的理解为"套娃问题",子类包含父类的属性和方法,那么子类一定比父类"大",所以我们要先初始化"小"的,再从外面套上"大"的

4、简述一下 C++ 中的多态
子类重写父类的方法,然后用父类引用或指针指向子类对象,最后当我们调用方法时会进行动态绑定,这就是多态静态多态和动态多态区别静态多态:在编译期就已经确定,函数的重载就属于静态多态动态多态:在运行时才能确定,并且还需要完成动态绑定的条件
i. 父类中必须要有虚函数,然后子类重写父类虚函数
ii. 通过父类的指针引用来调用这个虚函数

5、虚函数和纯虚函数的异同

i. 首先 含有纯虚函数的类被称为抽象类而只含有虚函数的类不能被称为抽象类。
ii. 其次 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用而而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义
iii. 最后 它们的定义形式也不同,虚函数是:virtual{},而纯虚函数是virtual{} = 0;
同:
i. 首先 虚函数和纯虚函数可以定义在同一个类
ii. 其次 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
iii. 最后 在虚函数和纯虚函数的定义中不能有static标识符,因为被static修饰的函数在编译时就要进行绑定,然而它们却是动态绑定的

6、为什么析构函数一般写成虚函数
如果析构函数不被声明成虚函数,则编译器执行的是静态绑定,在删除父类指针时,只会调用父类的析构函数而不调用子类的析构函数,这样就会造成子类对象析构不完全造成内存泄漏C++默认的析构函数不写成虚函数,是因为虚函数需要额外的虚函数表和虚函数指针,会占用额外的内存空间。所以我们对于不会被继承的类来说,是不会设置为虚函数的

7、构造、析构函数是否可以为虚函数
构造函数不可以为虚函数因为 虚函数所对应的虚函数表的地址是存储在对象的内存空间中的,而此时对象都还没实例化,就没有空间让虚函数表存储地址。所以这里就会冲突析构函数可以为虚函数
我们可以将需要被继承的父类的析构函数设置为虚函数。当我们父类类型的指针绑定到子类对象时,能够保证释放父类指针的时候 可以同时释放掉子类的空间,防止内存泄漏

三、内存管理
1、什么是内存对齐
系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。比如一个结构体中有一个int类型的数据和一个char类型的数据,它实际sizeof出来的大小占8个字节

2、为什么要进行内存对齐
为了提高数据读取的效率,程序分配的内存并不是连续存储的,而是按首地址为k的倍数的方式存储;这样就可以一次性读取数据,而不需要额外的操作结构体内成员按照声明顺序存储第一个成员地址和整个结构体地址相同
未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

3、栈和堆的区别
i. 首先 栈由操作系统自动分配释放,一般用于存放函数的参数,或局部变量堆由程序员通过new/delete关键字手动分配和释放
ii. 其次 栈使用的是一级缓存,通常是被调用时处于存储空间中,调用完毕立即释放而堆则存在二级缓存中,速度要慢一些
iii. 最后 栈的结果是先进先出,而堆是先进后出

4、内存模型
在C++中,内存分成5个区。分别是栈区、堆区、静态存储区、常量存储区、以及代码区
栈区:仅在定义的程序块运行时才存在。一般用于存储局部变量函数的形参
堆区:堆区的内存是通过new运算符动态分配的,也必须通过detele运算符手动销毁
静态存储区:内存在程序编译时就以及分配好,主要用于存放全局变量静态变量
常量存储区:这是一块比较特殊的存储区域,里面存放着常量。不允许进行修改
代码区:存放着程序的二进制代码

5、内存泄漏
简单地说就是申请了一块内存空间,使用完毕后没有释放掉比如:
i. new/malloc申请内存后,没有用delete/free释放
ii. 子类继承父类时,父类析构函数不是虚函数

6、避免内存泄漏
首先 可以用计数法,当我们使用new或者malloc的时候,计数就+1,而使用detele或free时,计数就-1。
最后打印这个计数,看看最后结果是否为0然后 一定要将父类的析构函数声明为虚函数最后要保证new/delete , malloc/free成对出现
解决方法:使用智能指针

7、new/delete 与 malloc/free的异同
相同点: 都可以用于内存的动态申请和释放
不同点:
i. 首先 new/delete 是C++中的运算符。而malloc/free是C/C++中的标准库函数
ii. 其次 new 在分配内存时会自动计算空间大小。而malloc需要手动计算
iii. 最后 new/delete 除了分配内存和回收内存以外,还具有调用构造函数和析构函数的功能。而malloc/free则只有会分配内存和回收内存

8、new和delete是如何实现的
new实现过程:
i. 先通过operator new()函数,它内部会调用malloc,在堆中分配一块内存
ii. 然后 将void类型的指针,转换为类类型的指针
iii. 最后再通过指针调用构造函数,用于初始化对象
delete实现过程:
i. 首先 调用析构函数删除内存中指针所指的数据
ii. 然后 通过operator delete()函数,它内部调用free去删除对象本身

四、STL
1、STL中迭代器的作用,有指针为何还要迭代器
i. 迭代器的作用用于指向顺序容器关联容器中的元素通过迭代器可以读取它指向的元素通过非const迭代器还可以修改其指向的元素
ii. 迭代器与指针的区别
迭代器是类模板,它只是表现的像指针。它模拟了指针的一些功能,重载了指针的一些操作符

2、vector 和 list的对比
vector:一维数组
特点:元素在内存连续存放,支持动态扩容,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。
优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)。
缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。
list:双向链表
特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。
优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)。
缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n),没有提供[]操作符的重载。

3、map 和 unordered_map 的区别以及底层实现
map:内部实现了一个红黑树,红黑树有自动排序的功能,因此map内部所有元素都是有序的。红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。
map中的元素是按照二叉排序树的方式存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来
unordered_map:内部实现了一个哈希表,通过把关键值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1)。因此它是无序的

4、请说说 STL 中常见的容器,并介绍一下实现原理
容器是可以用于存放各种类型数据的数据结构。可以分为顺序容器、关联式容器、容器适配器
三种类型顺序容器:容器中的元素没有进行排序,元素插入位置与元素的值无关。包含vector、list、deque
i. vector 的本质是动态数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
ii. list 的本质是双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。但不支持随机存取
iii. deque 的本质是双向队列。元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)
关联式容器:元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap(都是基于红黑树实现)
i. map 的底层容器是红黑树。map 的所有元素都是成对的,它的第一个元素被当成键,第二个元素被当成值。所有的元素都会根据元素的键值自动排序,且不允许键值重复
ii.set 的底层容器也是红黑树。但set中所有的元素只有键,没有值。不允许键重复,其中的元素会被自动排序,且不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的所以map和set的区别在于map的值不作为键,键和值是分开的容器适配器
封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下,变为一个具有stack功能的数据结构

5、vector扩容机制,以及1.5倍扩容因子的优点
vector有保留内存,当减少vector的大小后,内存并不会释放;只有新增大小大于当前大小时,才会开辟新的内存空间
扩容机制:首先 开辟1.5倍的内存空间然后 将旧数据拷贝到新的内存再 释放旧内存最后 指向新内存

五、C++ 11特性
1、简述一下移动构造函数
移动构造函数实现的是对象值真实的转移。比如从A移动到B,这说明将分配给A的内存转移给了B。而不是新开一块内存给B

2、智能指针是否线程安全
智能指针包括一个实际数据指针和一个引用计数指针,这两个操作不是一个指令可以完成的,因此多线程环境下,是会有问题的同一个shared_ptr被多个线程读,是线程安全的;
同一个shared_ptr被多个线程写,不是线程安全的;

3、左值和右值的概念左值
左值指既能够出现在等号左边,也能出现在等号右边的变量它是可寻址的变量,有持久性
左值引用:引用一个对象(平时指的引用一般是左值引用)
右值右值则是只能出现在等号右边的变量一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象短暂性
右值引用:C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用

4、智能指针的作用、原理、以及常用的智能指针
作用
让我们更方便的管理堆内存,若使用普通指针,则容易造成堆内存忘记释放、二次释放,程序发生异常时内存泄露等问题等。而使用智能指针能更好的管理堆内存
原理
智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。将动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源
常用的智能指针
i. shared_ptr采用引用计数器的方法,允许多个智能指针指向同一个对象每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1当计数为0的时候会自动的释放动态分配的资源。
ii. unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源转移一个 unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成 在结束时对同一内存指针多次释放而导致程序崩溃。
iii. weak_ptr首先 引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。所以需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入 的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
iv. auto_ptr(C++ 11弃用)主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存但auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发内存崩溃
而unique_ptr则无拷贝语义, 但提供了移动语义,避免这样的错误再次发生

5、C++ 智能指针实现引用计数的原理
i. 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
ii. 每次创建类的新对象时,初始化指针并将引用计数置为1
iii. 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
iv. 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则 删除对象),并增加右操作数所指对象的引用计数
v. 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)

6、C++11 中四种类型转换
C++中四种类型转换分别为const_cast、static_cast、dynamic_cast、reinterpret_cast
const_cast:将const变量转为非const
static_cast:最常用,可以用于各种隐式转换,比如非const转const。还可以用于类向上转换,但向下转换能成功但是不安全
dynamic_cast:只能用于含有虚函数的类转换,用于类向上和向下转换reinterpret_cast:可以做任何类型的转换,但不保证转换结果是否正确

原创声明:文章版权为作者所有,未经允许,禁止转载,抄袭
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 07:47 , Processed in 0.090895 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表