C++/ UE C++/ C#/ 引擎开发相关

1. Malloc和new的区别?

  • malloc和new都是在堆上开辟内存的,malloc只负责开辟内存,没有初始化功能,需要用户自己初始化;new不但开辟内存,还可以进行初始化。
  • malloc的返回值需要强转成指定类型的地址;new是运算符,开辟内存需要指定类型,返回指定类型的地址,因此不需要进行强转。
  • malloc失败会返回NULL,new失败会抛出异常。

2. STL中的vector,list,map,unordered_map的底层实现是什么?

数据结构底层实现
vector动态数组
list双向链表
map 、set、multiset、multimap红黑树
unordered_map、unordered_set哈希表

3. STL里面的sort函数的实现原理?

STL中的sort并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据量小于某个阀值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。

4. C++里面多重继承可能带来哪些问题?

在这里插入图片描述

  • 还有就是多重继承会令类型转换变得更加复杂,因为指针的实际地址会随转换的目标基类而改变。

5. public、protected和private继承的区别,什么时候会用?默认是使用哪个?

  • public继承不改变基类成员的访问权限,private继承使得基类所有成员在子类中的访问权限变为private,protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
  • 如果是“is-a”关系,也即是用到多态,那就public继承,private继承意味着“根据某物实现出”,尽量用复合替代private继承,protected继承很少用到,大致就是在包含的基础上允许访问被包含类的protected成员。

不加限定词,默认是private继承

6. 解释成员函数的重载、覆盖和隐藏。

  • 重载(overload)是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数。特别注意:重载不关心函数返回类型。
  • 隐藏是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。(有的地方称为overwrite“重写”,有点奇怪)
  • 重写(覆盖) (override)是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

区分覆盖和隐藏:
覆盖指的是子类覆盖父类函数(被覆盖),特征是:
1.分别位于子类和父类中
2.函数名字与参数都相同
3.父类的函数是虚函数(virtual)
隐藏指的是子类隐藏了父类的函数(还存在),具有以下特征:
1.子类的函数与父类的名称相同,但是参数不同,父类函数被隐藏
2.子类函数与父类函数的名称相同,参数也相同,但是父类函数没有virtual,父类函数被隐藏
判别流程如下图:
在这里插入图片描述

7. 构造函数和析构函数可以是虚函数吗?

构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。

  • 首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区,所以构造函数只能作为普通函数存放在类所指定的代码区中。
  • 那么为什么析构函数推荐最好设置为虚函数呢?如文章开头的例子中,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类base的析构函数。而当继承的时候,通常派生类会在基类的基础上定义自己的成员,此时我们当时是希望可以调用派生类的析构函数对新定义的成员也进行析构啦。

8. 析构函数可以抛出异常吗?

不可以。假设有一个vector,析构第一个元素抛出异常,析构第二个元素又抛出异常,两个同时作用的异常会导致程序提前结束或者导致不明确的行为。

9. C++的多态是如何实现的?如何用C语言实现多态?

C++使用虚函数机制来实现多态,虚函数是通过一张虚函数表来实现的,C语言可以利用函数指针来创建虚函数表,从而实现多态。

10. 解释虚函数,虚函数表的概念。虚函数表放哪里?虚函数表是在什么时期建立的?虚函数表里面放的是什么东西?

  • 类成员函数前面添加virtual关键字以后,该函数被称为虚函数,定义一个成员函数为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 类中创建的虚函数的地址会存放在一个虚函数表中,只要我们在类中定义了virtual函数,那么我们在定义对象的时候,C++编译器会在对象中存储一个vptr指针指向这个表的首地址。
  • C++中虚函数表位于C++内存模型中的常量区,由于虚函数表不需要更改,因此只需要放在只读存储器中即可;而虚函数则位于C++内存模型中的代码区
  • 虚函数表是在编译期建立的。参考 C++中的虚函数表是什么时期建立的?
    在这里插入图片描述
    参考继承下虚函数表的的变化

11. static_cast和dynamic_cast的区别?各自的用途?为什么用dynamic_cast的地方不用Static_cast?

  • static_cast:任何明确定义的类型转换,只要不包含底层const,都可以使用。static_cast(x)的语义差不多是这样的:以x为参数构造一个T类型的返回值,这个转型的过程必需是在编译期可以确定的。
  • dynamic_cast主要用于类层次间的上行转换和下行转换,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

补充:

dynamic_cast < type-id > ( expression)

该运算符把expression转换成type-id类型的对象。Type-id可以是类的指针、类的引用或者void*。如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理, 即会作出一定的判断dynamic_cast在将父类cast到子类时,父类必须要有虚函数,否则编译器会报错。

12. 讲一下函数调用栈的变化过程?

  • 首先,要清楚一个概念“栈帧”。栈帧(stack frame):机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实是两个指针寄存器,寄存器ebp为帧指针(指向该栈帧的最底部),而寄存器esp为栈指针(指向该栈帧的最顶部)。
  • 然后我们再简单描述一下函数调用的机制,每个函数有自己的函数调用地址,里面会有各种指令操作(这端内存位于“代码段”部分),函数的参数与局部变量会被创建并压缩到“栈”的里面,并由两个指针分别指向当前帧栈顶和帧栈尾。当进入另一个子函数时候,当前函数的相关数据会被保存到栈里面,并压入当前的返回地址。子函数执行时也会有自己的“栈帧”,这个过程中会调用CPU的寄存机进行计算,计算后再弹出“栈帧”相关数据,通过**“栈”里面之前保存的返回地址**再回到原来的位置执行前面的函数。参考下图在这里插入图片描述

13. Sizeof相关的问题?为什么结构体要内存对齐呢?

  • 一个空类sizeof 答案是(1)因为如果定义对空的类或者结构体取sizeof()的值为0,那么该空的类或结构体实例化出很多实例时,在内存地址上就不能区分该类实例化出的实例,所以,为了实现每个实例在内存中都有一个独一无二的地址,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节。
  • 一个类中有static对象的sizeof(1)
  • 一个类中有且仅有一个虚函数,那么sizeof结果是一个虚函数指针的大小
  • 下面Sizeof(MyStruct)=16
    在这里插入图片描述
    结构体内存对齐的原因:
    在这里插入图片描述

14. 如何限制只在堆上/栈上创建对象?

  • 把析构函数声明为private的,那只能在堆上创建对象,因为栈上就得自动调用构造析构函数。
  • 重载new运算符并声明为private的,就不能调用new了,只能在栈上创建对象。

15. 解释一下new operator 和 operator new的区别?

  • new operator是指我们在C++里通常用到的关键字,比如A* a = new A(),而operator new是一个操作符,并且可被重载(类似加减乘除的操作符重载)。new operator分为三步调用operator new (sizeof(A))->调用A:A()->返回指针
  • operator new有三种形式,包括operator new (sizeof(A))是否抛出异常的两个版本,第三种称为placement new,多接受一个void*指针ptr,可以实现在ptr所指地址上构建一个对象(通过调用其构造函数),这在内存池技术上有广泛应用。

16. 了解const么?哪些时候用到const?与宏定义有什么差异?

  • 简单理解,const的目的就是定义一个“不会被修改的常量”,可以修饰变量、引用、指针,可以用于函数参数、成员函数修饰。成员变量。使用const可以减少代码出错的概率,我们通常要注意的是区分常量指针(指向常量的指针)和指针常量(地址是常量,指针指向的地址不变)以及合理的在函数参数里面使用。
  • 宏定义的名称不会进入记号表,不方便调试。而const定义的变量会进入记号表,更方便调试。另外对于浮点常量,预处理器盲目替换的行为可能导致目标码出现多份这一常量,而const浮点常量避免了这个问题。

17. reference和pointer的区别?哪些情况使用pointer?

  • 指针可以为空,而引用不可以指向空值。
  • 指针可以不初始化,引用必须初始化。
  • 指针可以随时更改指向的目标,而引用初始化后就不可以再指向任何其他对象。

18. final和override的作用,以及使用场合?

  • final:禁止继承该类或者覆盖该虚函数
  • override:必须覆盖基类的匹配的虚函数
  • 使用场合(final):不希望这个类被继承,比如vector,编码者可能不够了解vector的实现,或者说编写者不希望别人去覆盖某个虚函数,顾名思义,final就是最终么
    场合(override):重写父类的方法时,编译器可以给你验证 override 对应的方法名是否是你父类中所有的,如果没有则报错.

19. C++11有哪些你使用到的新特性?

  • auto,有一些迭代器或者map嵌套类型,遍历时比较麻烦,auto写起来很方便。
  • 类内初始值问题,总是需要放到构造函数里面初始化,初始化列表倒是不错,但是初始化数据太多就不行了。
  • nullptr,C++11前的NULL一般是是这样定义的 #define NULL 0,这可能会导致一些函数参数匹配问题。而nullptr可以避免这个问题。
  • 智能指针shareptr,一定程度上解决内存泄露问题。
  • 右值引用,减少拷贝开销。
  • lambda function,简化那些结构简单的函数代码

20. Delete数组的一部分会发生什么?为什么出现异常?

单一对象和数组的内存布局不一样,数组需要记录数组大小。delete数组一部分会导致未定义行为。(VS2019编译通过,运行未报错,但编辑器会提示用法出错)

21. C++编译器有哪些常见的优化?听说过RVO(NRVO)么?

常见优化比如:

  • 常量替换如int a = 2; int b = a; return b;可能会优化为 int b=2; return b; 进一步会优化为return 2;
  • 无用代码消除比如函数返回值以及参数与该表达式完全无关,直接会优化掉这段代码
  • 表达式预计算和子表达式提取常量的乘法会在编译阶段就计算完毕,相同的子表达式也会被合并成一个变量来进行计算
  • 某些返回值为了避免拷贝消耗,可能会被优化成一个引用并放到函数参数里面,如RVO,NRVO。

RVO:函数返回的对象如果是新构造的值类型就直接通过一个引用作为参数来构造,进而避免创建一个临时的“temp”对象。
NRVO:相比RVO进一步优化。对于RVO,如果函数在返回前创建了一个临时变量,这个临时变量还是会被构造的,参考下面代码

Point3d Factory()
{
    Point3d po(1,2, 3);
    return po;
}
//RVO优化后
void Factory(Point3d &_result)
{
    Point3d po(1,2,3);
    _result.Point3d::Point3d(po);
    return;             
}
//NRVO优化后
void Factory(Point3d &_result)
{
   _result.Point3d::Point3d(1, 2, 3);  
    return;           
}

NRVO则直接跳过临时对象的构造。

22. 描述一下C/C++代码的编译过程?

预处理——编译——汇编——链接。预处理器先处理各种宏定义,然后交给编译器;编译器编译成.s为后缀的汇编代码;汇编代码再通过汇编器形成.o为后缀的机器码(二进制);最后通过链接器将一个个目标文件(库文件)链接成一个完整的可执行程序(或者静态库、动态库)。

23. 了解静态库与动态库么?说说静态链接与动态链接的实现思路?

  • 静态库:任意个.o文件的集合,程序link时,被复制到output文件。这个静态库文件是静态编译出来的,索引和实现都在其中,可以直接加到内存里面执行。
    对于Windows上的静态库.lib有两种,一种和上面描述的一样,是任意个.o文件的集合。程序link时,随程序直接加载到内存里面。另一种是辅助动态链接的实现,包含函数的描述和在DLL中的位置。也就是说,它为存放函数实现的dll提供索引功能,为了找到dll中的函数实现的入口点,程序link时,根据函数的位置生成函数调用的jump指令。(Linux下.a为后缀)
  • 动态库:包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。(Linux下.so为后缀)

24. 知道内部链接与外部链接么?

  • 内部链接:如果一个名称对于他的编译单元是局部的,并且在链接时不会与其他的编译单元中同样的名字冲突,那么这个名称就拥有内部链接。
  • 外部链接:一个多文件的程序中,一个实体可以在链接时与其他编译单元交互,那么这个实体就拥有外部链接。换个说法,那些编译单元(.cpp)中能想其他编译单元(.cpp)提供其定义,让其他编译单元(.cpp)使用的函数、变量就拥有外部链接。

25. 用过或很熟悉的设计模式有哪些?

  • 工厂模式,通过简单工厂生成NPC对象,简单处理的话可通过“字符串匹配”动态创建对象。如果有“反射机制”就可以直接传class来实现。当然可以进一步使用抽象工厂,处理不同的生产对象。
  • 单例,实现全局唯一的一个对象。构造函数、静态指针都是私有的,使用前提前初始化或者加锁来保证线程安全。
  • Adaptor适配器,代码适配原来的相机移动最后调用的是原来的移动,现在加了适配器继承里面放了当前引擎的摄像机,然后覆盖原来摄像机的移动逻辑。
  • 观察者模式,一个对象绑定多个观察者,然后这个对象一旦有消息就立刻公布给所有的观察者,观察者可以动态添加或删除。在UE4里面,行为树任务节点请求任务后进入执行状态,然后会立刻注册一个观察者observer到行为树(行为树本身就相当于前面提到的那个对象)的observer数组里面同时绑定一个代理函数。行为树tick检测消息发送给所有观察者,观察者收到消息执行代理函数。

25-补充问题:使用单例类和只创建一个static类(只包含static方法的类)有什么区别?有构造函数为什么还要设计抽象工厂模式?

  • 单例类和static类之间最本质的区别就是,一个代表对象,而另一个代表方法。如果你的单例类并没有保存任何状态性质的属性,而只是提供了一些公共方法,那么就应该考虑包含静态方法的类
  • 如果你需要维护一些状态信息,那么单例比静态类更合适。因为在静态类中维护状态信息很容易导致细微的 bug 。
  • 单例对象可以被延迟初始化。而静态类总是在类被加载的时候就初始化。
  • 工厂模式可以隐藏创建过程的复杂性。

参考 单例模式VS静态方法
参考 工厂设计模式有什么用?

26. 虚函数的优缺点?

  • 好处:简单来讲就是为了实现多个基类特有的功能。
  • 缺点:菱形继承;二义性

27. 类的内存布局是什么样的?考虑有虚函数、多继承、虚继承几种情况。

简单总结一下就是类只有成员变量占用内存(静态成员不占类内部内存,函数不占内存)。如果有虚函数,每个类对象都会有一个虚函数指针Vptr(占用一个指针大小的内存),vptr指向一个虚函数表,表里面记录了各项标记virtual的函数,子类如果覆盖父类虚函数,对应虚表位置的虚函数会被子类的替换(虚表在运行时其位置与大小就被决定了,一个类只有一个虚表),详细参考C++继承内存对象模型

28. 空类自带六个函数,哪六个?

  • 缺省构造函数。
  • 缺省拷贝构造函数。
  • 缺省析构函数。
  • 缺省赋值运算符。
  • 缺省取址运算符。
  • 缺省取址运算符 const。

29. static的作用?

把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。static局部变量只被初始化一次,下一次依据上一次结果值。对于函数来讲,static的作用仅限于隐藏(改变了作用域)。

30. inline关键字作用?什么情况编译器会拒绝inline?

使用内联函数可以避免函数调用的开销,缺点是直接导致可执行程序变大。三种情况编译器会拒绝inline:虚函数、带有循环或者递归的调用、通过函数指针调用inline函数。

31. C/C++ 内存分区是怎样的?

C/C++中内存分5大区:栈,堆,全局/静态存储区,常量存储区,代码区。

  • 栈(stack):指那些由编译器在需要的时候分配,不需要时⾃动清除的变量所在的存储区,效率高,分配的内存空间有限,形参和局部变量分配在栈区,栈是向地地址生长的数据结构,是一块连续的内存
  • 堆(heap):由程序员控制内存的分配和释放的存储区,是向高地址生长的数据结构,是不连续的存储空间,堆的分配(malloc)和释放(free)有程序员控制,容易造成二次删除和内存泄漏
  • 静态存储区(static):存放全局变量和静态变量的存储区,初始化的变量放在初始化区,未初始化的变量放在未初始化区。在程序结束后释放这块空间
  • 常量存储区(const):存放常量字符串的存储区,只能读不能写,const修饰的局部变量存储在常量区(取决于编译器),const修饰的局部变量在栈区
  • 程序代码区:存放源程序二进制代码

以上内容整理自网络,仅供参考,接下来是《游戏引擎架构》P115更靠谱的总结,本问题和“进程的数据段”、“可执行文件的数据段”都是一样的
总结起来就是 “堆、栈、静态、常量、代码”

  • :由malloc或new动态分配的内存
  • :即程序堆栈,主要保存3类数据:调用函数的返回地址、相关CPU寄存器的内容、函数里所有的局部变量
  • 静态存储区:存放全局变量和静态变量的存储区,初始化的变量放在初始化区(数据段),未初始化的变量放在未初始化区(BSS段)。
  • 常量存储区:存放const的全局变量。注意整数常量会被视为明示常量插入机器码中,而不是存放于常量存储区!注意const的局部变量放在栈区!
  • 代码区:存放程序的二进制可执行机器码,

32. 为什么要内存对齐?

内存对齐的主要作用是:

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:经过内存对齐后,CPU的内存访问速度大大提升

33.内存泄漏如何定位?

使用CRT调试功能来检测内存泄漏

34.STL迭代器失效的情况?

当使用一个容器的insert或者erase函数通过迭代器插入或删除元素"可能"会导致迭代器失效,因此我们为了避免危险,应该获取insert或者erase返回的迭代器,以便用重新获取的新的有效的迭代器进行正确的操作

35.右值引用的作用?std::move的实现原理?介绍一下转发和完美转发?

  • 在C++11前,有很多的不必须要的拷贝,因为在某些情况下,对象拷贝完之后就下来就销毁了。C++11标准引入了移动操作,减少了很多的复制操作,而右值引用是正式为了支持移动操作而引入的新的引用类型。
  • C++11引入右值引用,并且提供了move函数,用来获得绑定到左值上的右值引用。调用move之后,必须保证除了对原对象复制或销毁它外,我们将不再使用它,在调用move之后,我们不能对移动源后对象做任何假设。
  • 所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。在universal references(即T&&)和std::forward的合作下,能够实现完美的转发。

可参考 我理解的右值引用、移动语义和完美转发

36.三个智能指针说一下?weak_ptr如何判断是否失效?sizeof(shared_ptr)多少?

  • shared_ptr:多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。shareptr在实现上有两个核心的成员,一个是指向资源对象的指针变量,另一个是指向引用计数的指针变量。第一个参数不言而喻,第二个参数为什么也是指针?因为多个shared_ptr对象指向同一资源时,引用计数是需要同步更新的,所以需要一个共享的存储区域来保存这个引用计数。
  • unique_ptr实现独占式拥有,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

    不能使用weak_ptr直接访问对象,而必须调用lock函数来访问。
    在这里插入图片描述

    8(两个指针,参考深入理解智能指针之shared_ptr

37.如下图

在这里插入图片描述
第一种写法效率更高,主要是Cache机制导致的,参考C/C++遍历二维数组,列优先(column-major)比行优先(row-major)慢,why?

38.如何重载new操作符?重载new有什么意义?

参考这篇博客整理一下《C++ Primer》和《Effective C++》

39.extern "C"的作用?

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。
参考extern “C”的作用详解

40.C#的反射

反射的概念:反射是.NET中的重要机制,通过反射,可以在运行时获得程序或程序集中每一个类型(包括类、结构、委托、接口和枚举等)的成员和成员的信息。有了反射,即可对每一个类型了如指掌。另外我还可以直接创建对象,即使这个对象的类型在编译时还不知道。

41.C#的委托

在C#中,委托(delegate)是一种引用类型,在其他语言中,与委托最接近的是函数指针,但委托不仅存储对方法入口点的引用,还存储对用于调用方法的对象实例的引用。简单的讲委托(delegate)是一种类型安全的函数指针。C/C++函数指针是通过寻找函数的入口来调用一个函数,C#委托是把函数名当做一个参数传入一个委托对象当中,委托是类型,函数指针是指针。

42.C#的闭包

闭包是指有权访问另一个函数作用域中的变量的函数。注意,闭包这个词本身指的是一种函数。而创建这种特殊函数的一种常见方式是在一个函数中创建另一个函数。

43.联合体所占空间

union U
{
    char s[9];
    int n;
    double d;
};

以上联合体所占空间大小为16字节。
参考关于C语言中联合体union占用内存的情况

44. new和malloc创建的变量或对象存放在虚拟内存还是物理内存上?

在进行动态内存分配时,例如malloc()函数或者其他高级语言中的new关键字,操作系统会在硬盘中创建或申请一段虚拟内存空间,并更新到页表(分配一个PTE,使该PTE指向硬盘上这个新创建的虚拟页)。

参考 虚拟内存的那些事儿

45. i++和++i的区别

i++ 与 ++i 的主要区别有两个:

i++ 返回原来的值,++i 返回加1后的值。
i++ 不能作为左值,而++i 可以。
int i = 5;
int *p1 = &(++i); //正确,++i可通过取地址&运算符获得内存地址
int *p2 = &(i++); //错误,i++不可通过取地址&运算符获得内存地址
++i = 1; //正确,++i可作为左值
i++ = 5; //错误,i++不可作为左值
前缀形式:
int& int::operator++() //这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用。
{//函数本身无参,意味着是在自身空间内增加1的
*this += 1; // 增加
return *this; // 取回值
}
后缀形式:
const int int::operator++(int) //函数返回值是一个非左值型的,与前缀形式的差别所在。
{//函数带参,说明有另外的空间开辟
int oldValue = *this; // 取回值
++(*this); // 增加
return oldValue; // 返回被取回的值
}
//如上所示,i++ 最后返回的是一个临时变量,而临时变量是右值。

45.C++基本数据类型所占字节数

类型32位操作系统64位操作系统
char11
int44
long44
float44
double88
char*48

46.构造函数和析构函数可以调用虚函数吗?

不可以。参考《Effective C++》第9条款。

47. 野指针和悬空指针的区别

野指针:野指针就是没有被初始化过的指针。
悬空指针:悬空指针是指针最初指向的内存已经被释放了的一种指针。

48.重载operator new操作符

重载的必要性:C++标准库提供的new和delete操作符,是一个通用实现,未针对具体对象做具体分析
存在分配器速度慢、小型对象空间浪费严重等问题,不适用于对效率和内存有限制的应用场景

重载的好处

  • 灵活的内存分配控制
  • 提高和改善内存使用效率
  • 检测代码内存错误
  • 获取内存使用统计数据

参数要求:参数个数可以任意,只需保证第一个参数为size_t类型,饭后void *类型即可,其他操作符重载时参数需保持严格一致

参考:C++ 重载new和delete操作符

49.vector扩容为啥是2倍?

用2是出于对空间和时间的权衡。简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多。但vector扩容不一定是2倍,用2倍导致之前的空间无法使用,cache不友好,但平摊时间复杂度低。参考
C++ STL 中 vector 内存用尽后, 为什么每次是 2 倍的增长, 而不是 3 倍或其他值?

50.栈内存和堆内存的区别?为什么要区分?函数里的变量是哪一种内存?

栈内存由编译器分配和释放,堆内存由程序员手动申请和释放。
函数里的变量属于栈内存。
在这里插入图片描述

51. 模板的声明和定义为什么不能分开写?什么情况只能分开写?要想分开写该怎么做?

简单来说就是编译器只有遇到模板调用的时候才会使用模板参数去生成模板类或者模板函数的定义,这就要求此时编译器可以访问到模板的定义。如果模板声明在A.h而实现在A.cpp,那么在B.cpp使用一个具体类型定义模板类或者模板函数的时候,此时编译器正在编译b单元,无法访问到A单元中的实现。 参考 模板 第35.12问答。

当模板的声明头文件中包含全局变量时,为了解决此头文件被多次包含后导致的重定义问题,需要把模板声明和定义分开写。

通过在cpp中显示实例化模板类或模板函数并在h文件中添加实例化的模板声明,可以实现模板的声明和定义分离。参考 一种将C++函数模板定义和声明分开的方法

52.介绍一下 模板的特化、全特化、偏特化

模板特化可以只让特定类型使用函数模板。
通过全特化一个模板,可以对一个特定参数集合自定义当前模板,类模板和函数模板都可以全特化。(这样就使得只有这个参数集合类型才能使用这个模板了)
类似于全特化,偏特化也是为了给自定义一个参数集合的模板,但偏特化后的模板需要进一步的实例化才能形成确定的签名。
值得注意的是函数模板不允许偏特化,多数情况下函数模板重载就可以完成函数偏特化的需要
参考 C++模板的偏特化与全特化

53.UE如何实现的委托?

委托可绑定到任何的c++可调用对象, 全局函数(静态成员函数), 类的非静态成员函数,Lambda 以及 按Name 绑定到UFunction.尔后可在恰当的时机调用它。
由于不同的可调用对象的调用方式差别巨大,所以我们需要把这些可调用对象统一成一个东西, 即Delegate, 不同的Delegate之间仅仅是所代表的可调用对象的参数和返回值类型不同。而具体的调用方式, 则根据你绑定的可调用对象的形式而定。
(个人理解:就是实现了一个类,把全局函数,类的非静态成员函数,lambda等的函数指针调用统一了起来。这些不同函数的委托实现原理不一样,比如全局函数只需要使用简单的函数指针,但类的非静态成员函数则需要结合成员函数指针这个特性去实现
参考 UE4-深入委托Delegate实现原理

53-补充: 介绍一下C++“指向类成员的指针”和“指向成员函数的指针”

  • 指向类成员的指针并不指向某一个具体对象的内存地址。它指向的是一个类的特定成员,而不是指定某一个特定对象的成员。“指向类成员的指针” 的实现方式是获取该类成员在类中的偏移值,实际上是一种整数类型。“指向类成员的指针” 不能指向 static 成员,这是由于 “指向成员的指针” 只是该成员在类内的偏移量,为了访问位于那个偏移量的子对象 (数据成员),则我们需要该类 (A) 的一个对象的地址。
  • 一个指向成员函数指针的实现自身必须存储一些信息,比如它所指向的函数是虚函数还是非虚函数,如何找到适当的虚函数表指针,所以通常指向类成员函数的指针的实现为一个小型的结构来存储这些关键的信息。指向类成员函数的指针” 是具有两个 i64 对象的结构。 该结构的第一个成员用来存储成员函数的普通函数指针,第二个用来存储 this 指针的偏移(因为在类的继承层次里,数据成员的位置并不是绝对的,而是相对于基类的相对位置)。

参考 C++ 中指向类成员的指针并非指针

54.UE中的反射有什么作用?UE是如何实现反射系统的?

  • 反射,是指程序在运行时进行自检的的能力,在编辑器的属性面板、序列化、GC 等方面非常有用。但是 C++ 语言本身不支持反射特性,UE 在 C++ 的语法基础上通过 UHT 实现了反射信息的生成,从而实现了运行时的反射的目的。
  • UE通过C++的指向类成员的指针来实现根据反射信息访问类成员
  • UE 中成员函数实现反射,并没有依赖 C++ 的指向成员函数的指针,它完全依赖 UHT 生成了一个统一原型的 Thunk 函数,在 Thunk 函数中调用真正执行的函数(包括从栈上取数据等操作)。并且会为反射的函数生成用于传递给 ProcessEvent 函数的参数结构,以及每个参数、返回值生成属性的反射信息(它们的内存偏移都是相对于 UHT 生成的参数结构的)。

参考 UE 反射实现分析:C++ 特性
参考 UE 反射实现分析:基础概念

55.UBT、UHT的作用是什么?

  1. UBT
  •  扫描solution中的模块和插件
    
  •  判断哪些模块需要重写编译
    
  •  调用UHT去解析C++头文件
    
  •  根据.Build.cs 和 .Target.cs创建编译器和链接器选项
    
  •  执行各平台特定的编译器
    
  1. UHT
  •  解析包含UClass的C++头文件
    
  •   为UClass和UFunction生成胶水代码
    
  •   在Intermediates文件夹中存储生成的文件
    

参考 Build flow of the Unreal Engine4 project

56. 平时写的宏有什么用,比如UCLASS代表什么?

UCLASS/USTRUCT/UFUNCTION/UPROPERTY 等可以在 () 中添加很多的标记值以及 meta 参数,用于指导 UHT 来生成对应的反射代码。

57.UE的GC系统是如何实现的?如果派生自UObject的对象不想使用引擎的这套GC,该怎么做?如果非UObject的自定义类型想使用这套GC,该怎么做?

  • List item

58.UE的智能指针和C++ 11的智能指针有什么区别和联系?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值