面试知识总结——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. Protected和Private继承的区别,什么时候会用?

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

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

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

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

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

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

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

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

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

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

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

  • 类成员函数前面添加virtual关键字以后,该函数被称为虚函数,定义一个成员函数为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 类中创建的虚函数的地址会存放在一个虚函数表中,只要我们在类中定义了virtual函数,那么我们在定义对象的时候,C++编译器会在对象中存储一个vptr指针指向这个表的首地址。
  • 虚函数表放在全局数据区
    面试知识总结——C++/C#_第1张图片
    参考:继承下虚函数表的的变化

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更安全

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

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

13. Sizeof相关的问题?

  • 一个空类sizeof 答案是(1)因为如果定义对空的类或者结构体取sizeof()的值为0,那么该空的类或结构体实例化出很多实例时,在内存地址上就不能区分该类实例化出的实例,所以,为了实现每个实例在内存中都有一个独一无二的地址,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节。
  • 一个类中有static对象的sizeof(1)
  • 下面Sizeof(MyStruct)=16
    面试知识总结——C++/C#_第3张图片

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有三种形式,其中一种称为placement new,可以实现在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适配器,代码适配原来的相机移动最后调用的是原来的移动,现在加了适配器继承里面放了当前引擎的摄像机,然后覆盖原来摄像机的移动逻辑。
  • Observer,一个对象绑定多个观察者,然后这个对象一旦有消息就立刻公布给所有的观察者,观察者可以动态添加或删除。在UE4里面,行为树任务节点请求任务后进入执行状态,然后会立刻注册一个观察者observer到行为树(行为树本身就相当于前面提到的那个对象)的observer数组里面同时绑定一个代理函数。行为树tick检测消息发送给所有观察者,观察者收到消息执行代理函数。

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修饰的局部变量在栈区
  • 程序代码区:存放源程序二进制代码

32. 为什么要内存对齐?

内存对齐的主要作用是:

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

33.内存泄漏如何定位?

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

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

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

35.右值引用的作用?std::move的实现原理?

  • 在C++11前,有很多的不必须要的拷贝,因为在某些情况下,对象拷贝完之后就下来就销毁了。C++11标准引入了移动操作,减少了很多的复制操作,而右值引用是正式为了支持移动操作而引入的新的引用类型。
  • C++11引入右值引用,并且提供了move函数,用来获得绑定到左值上的右值引用。调用move之后,必须保证除了对原对象复制或销毁它外,我们将不再使用它,在调用move之后,我们不能对移动源后对象做任何假设。

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

  • shared_ptr:多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。shareptr在实现上有两个核心的成员,一个是指向资源对象的指针变量,另一个是指向引用计数的指针变量。第一个参数不言而喻,第二个参数为什么也是指针?因为多个shared_ptr对象指向同一资源时,引用计数是需要同步更新的,所以需要一个共享的存储区域来保存这个引用计数。

37.如下图

面试知识总结——C++/C#_第4张图片
第一种写法效率更高,主要是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#的闭包

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

你可能感兴趣的:(C++,面试知识总结)