C++面经汇总
技术面试过程中,回答问题应该注意的事项
-
当面试官提出问题后,不要着急作答,应该适当停一下,整理一下逻辑思路。
-
对于简单问题的回答,尽量不要照本宣科,找准问题回答的角度/层次,争取简单问题回答的比较有亮点。
-
对于相对复杂的问题,比较难以阐述的问题,思考上要花一些时间,整理好逻辑思路,以及问题大致描述的顺序。
如果是现场面试(视频面试),最好用纸笔边画边讲;如果是电话面试,回答问题的过程中,需要和面试官经常沟通,不要自顾自的滔滔不绝。
-
对于面试中,被提问到自己不知道的内容,尽量把自己知道的相关知识都说一说,回答得饱满一些,让面试官了解我们。
-
你还有什么问题?
我将来在公司可能接触到的技术有哪些?
我在公司会用到什么技术,我现在掌握的C和C++去公司需不需要转型?
请面试官能不能对我的技术面试进行点评,给我提出一些宝贵的经验?
1. C++this指针是干什么用的?
- 每个非静态成员函数只会产生一份函数实例,也就是说多个同类型的对象共用一块代码,C++通过提供
this
指针来解决如何区分哪个对象调用了这个非静态的成员方法。this
指针指向的就是被调用的成员函数所属的对象。 - 当形参和成员变量同名时,可用
this
指针来区分。 - 在类的非静态成员函数中返回对象本身,可使用
return *this;
。
2. C++的new和delete,什么时候用new[]申请,可以用delete释放?
如果说自定义类型,而且提供了析构函数,那么使用new []
申请内存时,就一定需要用delete[]
释放内存,除此之外,在用new[]申请内存时,可以用delete释放。
3. C++的static关键字的作用?(我从elf结构,链接过程来回答)
- 从面向过程的角度来说,
static
修饰全局变量或者函数时,被static
修饰以后,在符号表中,符号的作用域就从g
变成l
;static
修饰局部变量时,静态局部变量存储在.data
或.bss
段,局部变量本身不产生符号的,通过ebp-偏移量
来进行访问,而此时会产生符号,符号的作用域是l
。 - 从面向对象的角度来说,
static
可以修饰成员变量,也可以修饰成员方法,不再产生this
指针。既可以通过对象,也可以通过类名直接访问静态成员变量。
4. C++的继承有什么好处?
- 代码的复用,通过继承可以把基类的成员直接复用到派生类中。
- 通过继承,可以在基类里面给所有派生类保留统一的纯虚函数接口,等待派生类进行重写,使用多态,可以通过基类的指针访问不同派生类对象的同名覆盖方法。
5. C++的继承多态、空间配置器、vector和list的区别、map、多重map?
- 继承的好处:1. 代码的复用,通过继承可以把基类的成员直接复用到派生类中。2. 通过继承,可以在基类里面给所有派生类保留统一的纯虚函数接口,等待派生类进行重写,使用多态,可以通过基类的指针访问不同派生类对象的同名覆盖方法。
- 多态分为静态多态和动态:1. 静态(编译时期)多态:函数重载、运算符重载和模版。2. 动态(运行时期)多态:在基类中定义虚函数,通过基类的指针或引用指向派生类对象。
- 空间配置器
allocator
:给容器使用的,主要作用是把对象的内存开辟和对象构造分开,把对象析构和内存释放分开。这样做的目的在于:当初始化容器时,只需要给容器底层开辟空间,并不需要在空间上构造无效的对象,所以不能使用new
;当从容器删除对象时,只需要把对象析构掉,并不需要释放对象的内存;当容器出作用域时,只需要把容器中有效的对象析构再释放内存,而如果使用delete
,则会把容器底层所有的空间都当做对象析构掉再释放内存。 - vector和list的区别参考基础课程。
map
和multi_map
是一个映射表,存储的元素是键值对key-value
,底层的实现是红黑树。map
不允许key
重复,而multi_map
允许key
重复。- 红黑树的5个性质:1. 节点是红色或者是黑色;2. 根结点必须是黑色;3. 每个叶子结点必须是黑色;4. 每个红色节点的两个子节点都是黑色的(也就是说从根节点到每个叶子结点的路径上,不允许出现连续的红色节点);5. 从任一节点到其每个叶子结点的所有路径都包含相同数目的黑色节点。
- 红黑树插入的3种情况
- 红黑树删除的4种情况
6. C++如何防止内存泄漏?智能指针详述?
- 由于堆内存没有名字,只能用指针来指向。当分配的堆内存没有释放时,如果程序抛出异常、提前返回、指针重定向,那么也就再也没有机会释放对应的堆空间,产生内存泄漏,
- 通过在程序中使用智能指针来指向堆上的空间,可以有效的防止内存泄漏。
7. C++如何调用C语言函数接口?
C 和 C++生成符号的方式不同,C 和 C++语言之间的API接口是无法直接调用的,C 语言的函数声明必须括在extern "C"{}
里面。
8. C++中类的初始化列表?
- 可以指定当前对象成员变量的初始化方式。
- 自定义了一个构造函数,编译器就不会再产生默认构造了。
- a类中包含成员对象时,如果成员对象的类型b中自定义了构造函数,也就没有默认构造函数,那么需要通过a类的构造函数的初始化列表来对成员对象进行初始化,会调用成员对象的自定义构造函数。
- 成员变量的初始化和它们定义的顺序有关,和构造函数初始化列表中出现的先后顺序无关。
9. C 和 C++ 的区别?
C++中引入了引用、函数重载、运算符重载、new\delete
、const
、inline
、带默认值参数的函数、模版、类和对象、STL、异常、智能指针等。
10. malloc 和 new 的区别?
-
malloc 称作C的库函数,而 new 被称作运算符。
-
new 不仅可以开辟内存空间,还可以对内存做初始化操作;而 malloc 只负责开辟内存,不负责初始化。
-
由于 new 在开辟内存时是指定数据类型的,所以返回值不需要进行类型的转换;而 malloc 只是按字节数开辟内存的,返回值永远是 void* 类型,因此需要对返回值进行类型的转换。
-
malloc 开辟内存失败,返回 nullptr 指针;而 new 开辟内存失败,是通过抛出 bad_alloc 类型的异常来判断的。
11. map 和 set 容器的实现原理?
map
称为映射表,存储[key, value]
键值队,两者的底层数据结构都是红黑树。- 对红黑树进行阐述。
12. shared_ptr引用计数存在哪里?
引用计数是在堆上分配的,一个资源只能对应一个引用计数,引用同一个资源的所有智能指针是共享这个引用计数的,才不至于在增减资源引用计数时导致错乱。
13. STL 中的迭代器失效的问题?
迭代器不允许一边读一边修改。
-
哪些情况会导致容器迭代器出现失效?
- 当容器调用
erase
方法后,当前位置到容器末尾元素的所有的迭代器全部失效了。 - 当容器调用
insert
方法后,当前位置到容器末尾元素的所有的迭代器全部失效了。 - 当容器调用
insert
方法并且引起了容器内存扩容,那么原来容器的所有的迭代器全部失效了。 - 不同容器的迭代器是不能进行比较运算的,否则将导致迭代器失效。
- 当容器调用
-
迭代器失效了以后,问题该如何解决?
对插入将删除点的迭代器进行更新操作。
通过迭代器对容器中的元素进行插入/删除操作时,成员方法
insert
和erase
会返回一个更新后的迭代器。
14. STL 中哪些底层是由红黑树实现的?
set
、multiset
、map
、multimap
15. struct 和 class 的区别?
16. STL的容器分类,各容器底层实现?
STL容器分为
顺序容器:vector(底层为可扩容的数组)、list(底层为双向循环链表)、deque(底层为可动态扩容的二位数组)
容器适配器:stack(底层使用deque容器实现)、queue(底层使用deque容器实现)、priority_queue(底层是一个大根堆,使用vector容器实现)
关联式容器:有序容器set、multiset、map、multimap(底层采用红黑树实现)
无序容器unordered_set、unordered_map、unordered_multiset、unordered_multimap (底层采用链式哈希表实现)
17. 编译链接的全过程?
程序编译链接需要经过以下几个阶段:
- 预编译(处理以#开头的指令、过滤所有注释、添加行号和文件名标识等,生成以
.i
结尾的文件) - 编译(对程序进行词法分析、语法分析、语意分析、代码优化、生成汇编代码,即
.s
文件、汇编代码的优化以及生成符号表) - 汇编(将汇编代码转变为机器指令,生成二进制可重定向的目标文件,即
.o
文件) - 编译(将所有的
.o
文件和静态库文件链接生成可执行文件,即.out
文件。在链接阶段,首先会将所有.o
文件段合并,符号表合并后,进行符号解析,符号解析成功后,给所有符号分配虚拟地址,然后对符号进行重定向,完成后生成可执行文件。)
18. 初始化全局变量和未初始化全局变量有什么区别?
.data
段存储初始化不为0的全局变量,.bss
段存储未初始化的全局变量以及初始化为0的全局变量。
19. 堆和栈的区别?
-
堆内存的大小远远大于栈所占内存的大小
-
堆上的内存空间需要手动开辟和释放,而栈上的空间不需要手动开辟和释放,函数运行时,系统会自动为函数的局部变量在栈上分配空间,局部变量出作用域时,系统自动回收栈帧。这也导致堆上的资源和栈上的资源生命周期也不相同。
-
堆是从低地址向高地址分配堆内存,而栈是从高地址向低地址分配栈空间。
20. 构造函数和析构函数可不可以是虚函数,为什么?
-
构造函数不可以是虚函数。由于构造函数完成后,才会产生对象,因此不能实现成虚函数。构造函数中调用任何函数,都是静态绑定的,即使调用虚函数,也不会发生动态绑定。
-
析构函数可以是虚函数。当基类的指针(引用)指向堆上
new
出来的派生类对象,delete pb
(pb
为基类的指针),它调用析构函数的时候必须发生动态绑定,否则会导致派生类的析构函数无法调用。
21. 构造函数和析构函数中能不能抛出异常,为什么?
- 构造函数不能抛异常。如果构造函数抛出异常,则对象创建失败,就不会调用对象的析构函数了,将导致内存泄漏。
- 析构函数不能抛异常。当抛出异常后,后面释放资源的代码就不会执行了,也会导致内存泄漏。
22. 宏和内联函数的区别?
-
#define
是在预编译时处理,对字符串进行替换;而
inline
函数是在运行时才处理,在函数调用点,通过函数的实参把函数代码直接展开调用,节省了函数的调用开销。 -
内联函数有类型检查更加的安全,宏定义没有类型检查。
-
内联函数在运行时可调试,而宏定义不可以。
-
#define
可以定义常量、代码块以及函数块,而inline
只是修饰函数。
23. 局部变量存放在哪里?
局部变量存放在栈上,通过ebp
指针偏移来访问的,不产生符号。
24. 拷贝构造函数,为什么传引用而不传值?
如果使用值传递,创建对象t2
时,调用拷贝构造函数,将 t1
作为实参进行传递时,首先会把t1
传递给形参,此时就需要拷贝构造创建形参,当值传递时,需要再次创建新的形参,这个过程是层层循环,无穷调用,导致程序编译错误。
25. 内联函数和普通函数的区别?
- inline内联函数在编译过程中,没有函数的调用开销,在函数调用点直接把函数的代码进行展开处理;普通函数在调用时,参数压栈、函数栈帧的开辟和回退过程等都会产生开销
- inline内联函数不会生成相应的函数符号
- inline只是建议编译器把函数处理成内联函数,但是不是所有的inline都会被编译器处理成内联函数,比如递归函数
- 在进行函数调试时,debug版本上,inline是不起作用的,inline只有在release版本下才能出现
26. 如何实现一个不可以被继承的类?
- 派生类在进行初始化时,首先需要调用基类构造函数对基类成员进行初始化,然后在调用派生类构造函数。因此,如果要把一个类实现为不可以被继承的类,可以将基类的构造函数私有化,此时子类就无法访问基类的构造函数。
27. 什么是纯虚函数?为什么需要有纯虚函数?虚函数表存放在哪里?
- 形如
virtual void func() = 0;
的函数就是纯虚函数。拥有纯虚函数的类称为抽象类,抽象类不能实例化对象,但是可以定义指针和引用变量。 - 纯虚函数一般定义在基类中,定义基类的初衷并不是为了让基类抽象某个实体的类型,定义基类主要是为了让派生类可以通过继承基类直接复用基类中的成员变量,同时给所有派生类保留统一的覆盖/重写接口。因为基类不需要实例化对象,它的方法也就不知道如何实现,所以把这些方法定义为纯虚函数。
- 虚函数表是在编译阶段产生的,在运行时,虚函数表加载到
.rodata
段。
28. 说一下C++中的const,const 与static 的区别?
-
const 修饰的变量不能够再作为左值,初始化完成后,值不能被修改。
-
不能把常量的地址泄露给一个普通的指针或者普通的引用变量。
-
在 C++ 代码中,所有出现 const 常量名字的地方,在编译阶段就被常量初始化的值替换了。
-
定义const常量时,必须对其进行初始化。
-
const 与static 的区别:
-
面向过程:
-
const可以修饰全局变量、局部变量以及形参变量,而static只能修饰全局变量和局部变量。
-
const不能修饰函数,static可以修饰函数。
-
-
面向对象:
- const可以修饰成员方法和成员变量,并且依赖于对象。
- static可以修饰成员方法和成员变量,不依赖于对象,可通过类作用域直接访问。
-
29. C++语言级别提供的四种类型转换方式
-
const_cast:去掉(指针或引用)常量属性的一种类型转换。
-
static_cast:提供编译器认为安全的类型转换。(没有任何联系的类型之间的转换就被否定了)
-
reinterpret_cast:类似于C风格的强制类型转换。
-
dynamic_cast:主要用在继承结构中,可以支持RTTI类型识别的上下转换。
30. 详细解释deque的底层原理
- 动态开辟的二维数组
- 定义了两个宏
#define MAP_SIZE 2
、#define QUE_SIZE(T) 4096/sizeof(T)
- 第一维数组的初始化大小为
MAP_SIZE(T*)
,第二维数组默认开辟的大小为QUE_SIZE(T)
- 由于deque是一个双端队列,在两端都可以进行插入删除
- 当需要扩容时,把第一维数组按2倍的方式进行扩容,扩容之后,会把原来的第二维数组从新的第一维数组的第
oldsize/2
开始存放
31. 早绑定和晚绑定
- 早绑定(静态绑定):普通函数的调用,用对象调用虚函数
- 晚绑定(动态绑定):用指针或引用调用虚函数的时候,都是动态绑定
32. 智能指针交叉引用的问题怎么解决?
定义对象的时候,使用强智能指针;引用对象的时候,使用弱智能指针。
当通过weak_ptr
访问对象成员时,需要先调用weak_ptr
的lock
方法,把weak_ptr
提升为shared_ptr
强智能指针,再进行对象成员调用。
33. 为什么C++能进行函数重载,虚函数的底层实现是什么?
因为C++生成的函数符号依赖函数名称和参数列表,当程序编译到函数调用点时,如果函数名称和传入实参的个数和类型与某一个函数重载版本匹配,则直接调用相应的函数重载版本(静态的多态,在编译阶段处理)。
虚函数的底层实现:当一个类中含有虚函数成员方法时,在编译阶段会产生虚函数表,虚函数运行时加载到rodata
段。一个类里面定义了虚函数,那么这个类定义的对象,其运行时,在内存中的开始部分,多存储了一个虚函数指针 vfptr
,指向相应类型的虚函数表 vftable
,在虚函数表中获取到相应虚函数的地址 。一个类型定义的多个对象,它们的虚函数指针 vfptr
指向的是同一张虚函数表。
34. avl 和 rbtree有什么区别?
-
AVL树是一种自平衡二叉查找树,它的每个节点都有一个平衡因子,当某个节点的平衡因子小于1时,需要进行旋转操作来保持平衡。而红黑树是一种特殊的二叉查找树,它通过保持节点的红色或黑色来保持平衡。
-
红黑树不要求绝对平衡,需要对节点着色;而AVL树要求绝对平衡,需要通过平衡因子计数,旋转次数可能会比红黑树多。数据量比较大的情况下,使用红黑树的效率更高。
35. 假如map的键是类类型,那么map底层是如何调整的?
map的底层是通过红黑树实现的,每个数据存储为键值对,默认是对key进行小于<的比较操作,因此,当map的键是类类型时,需要提供operator<()运算符重载函数。
36. 讲一下红黑树以及它的特性
37. 如果让你实现一个内存池,要求获取资源和插入资源时间花费O(1),应该如何设计?
设计成SGI STL二级空间配置器的内存池的实现就可以了。然后展开说一下具体的实现细节。