技术面应该注意的问题:
- 当面试官问问题时,不要着急做答,适当停顿,整理逻辑思路
- 对于简单问题回答不要照本宣科,找准问题回答的角度/层次,争取简单问题回答的比较有亮点
- 对于相对复杂的问题,比较难以阐述的问题,思考上要花一些时间,整理好逻辑顺序,以及问题大致描述的顺序,如果是现场面试,最好用纸笔边画边讲。如果是电话面试,回答问题中,需要和面试官经常沟通,不要自顾自的滔滔不绝。
- 对于面试中自己不知道的问题,回答要饱满,切不可说不知道。
- 你还有什么问题?比如:我将来在公司用那些技术,我现在掌握的是C和C++,需不需要转型;请面试官对我的技术面试做一个点评,提供一些宝贵的经验。
1. C++this指针是干什么用的?
一个类型定义的对象都有各自的成员变量,但是他们共享一套成员方法,在成员方法里访问哪个对象的成员变量需要this指针来区分。成员方法一经编译,方法的参数都会添加一个this指针,用来接收调用该方法的对象的地址。所以在成员方法里访问的其他成员变量或调用其他的成员方法前面都会默认加this指向。
2. C++的new和delete,什么时候用new[]申请,可以用delete释放?
new和delete是运算符重载,delete是先调用析构函数,再free释放内存。实际上调用的是operator new和operator delete
如果是自定义类型,而且提供了析构函数,用new[]申请的内存空间就一定要用delete[]释放,因为new[]除了开辟用户需要使用的内存空间,还多开辟了4字节存放对象的个数。因为编译器知道自己总共分配了多少空间,记录对象的个数后,可以在内存中准确划分出每个对象的首地址。
如果没有提供析构函数, new[]开辟空间的起始地址和返回给用户的地址相同,也就是没有存放对象个数的4字节空间。
3. C++的static关键字的作用?
- 从面向过程:
static可以修饰全局变量、函数、局部变量,只在当前文件可见。因为对于全局变量和函数被static修饰以后,在符号表中,符号的作用域就从global变成local;对于static修饰局部变量,局部变量的内存就存放在.data、.bss数据段,由于在数据段,程序一开始就要开辟空间,在执行到相应语句的时候初始化变量。局部变量本身不产生符号,通过ebp-偏移量来访问,但是被static修饰以后由于放在数据段,这时候就需要产生符号。 - 从面向对象
static可以修饰成员变量、修饰成员方法。static修饰成员变量,成员变量就从私有变成对象共享。static修饰成员方法,这些成员方法不再产生this指针,就不需要用对象调用,直接通过类作用域来调用。
4. C++继承的好处?
继承(a kind of)是类和类之间的关系,类和类之间的关系除了继承还有组合(a part of)。
- 代码复用
- 通过继承,在基类里面给所有的派生类可以保留统一的虚函数接口,等待派生类重写,通过使用多态,可以通过基类的指针访问不同派生类对象的同名覆盖方法。(不用提供很多接口,每一个接口对应一个派生类对象,只需要提供一个接口就行,指针指向谁就访问谁),更符合软件开-闭原则。
5. C++继承多态,空间配置器,vector和list的区别,map,多重map,红黑树?
- vector底层是一个内存可以2倍扩容的数组,它提供了尾部的增删操作(push_back和pop_back),都是O(1)操作,其余地方的增删操作为O(n)。所以vector适合随机访问元素,因为内存是连续的,比如优先级队列是基于vector实现,不适合增删,因为增删操作O(n)。
- list是一个循环的双向链表,适合增删,每个节点都是new出来的不连续,但是由于它是链表,push_back和pop_back,push_front和pop_front时间都是O(1);
-
map和multimap的区别 map是一个映射表,里面存的元素是[key-value]键值对,不允许key重复,底层实现是一颗红黑树,红黑树也是一颗二叉排序树,特点是进出的元素要进行比较,左孩子<父节点<右孩子,他对于传入的key-value主要是对key做比较,通过快速的查找key来对应的value,时间复杂度为O(log2n)。 multimap允许key重复,其他和map都一样。 -
红黑树 5个性质:
- 每个节点要么是黑色,要么是红色;
- 根节点必须是黑色;
- 叶子节点是黑色;
- 每个红色节点的两个叶子结点一定都是黑色,不能有两个红色节点相连;
- 任意一节点到每个叶子结点的路径都包含数量相同的黑色节点。
性质5又可以推出:如果一个节点存在黑色子节点,那么该节点肯定有两个子节点,不然走另一条路就会少一层黑色节点。 红黑树插入有三种情况(最多旋转两次),删除有四种情况(最多旋转三次)
6. C++如何防止内存泄漏?智能指针详述?
内存泄漏就是分配的内存没有释放,也再没有机会释放了,一般指堆内存(因为堆内存没有名字,只能用指针来指向)。
智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。所以,智能指针一般都是定义在栈上的。
可以再说说不带引用计数的智能指针(auto_ptr、scoped_ptr和unique )和带引用计数的智能指针(shared_ptr和weak_ptr ),再说说推荐使用哪个为什么,还有带引用计数的好处、强弱智能指针有什么好处、为什么不只是用强智能指针(交叉引用问题,如何解决)以及线程安全方面。
参考:智能指针
7. C++如何调用C语言语句?
C和C++生成符号的方式不同,C和C++语言之间的API接口是无法直接调用的,C++调用C语言:C语言的函数声明必须扩在extern "C" 里面
8. 那些情况下可能出现访问越界
访问的内存超过系统所分配的内存
- 访问数组元素越界
- STL容器访问越界
- 字符串访问越界
- 字符串没有加
\0 ,导致访问越界 - 使用类型强转,使得大类型(派生类)的指针指向小类型(基类)的对象,指针解引用直接越界
9. C++中类的初始化列表
初始化列表可以指定对象成员变量的初始化方式,尤其是指定成员对象的构造方式。成员变量的初始化和定义的顺序有关,和初始化列表的顺序无关。
10. C和C++的区别?C和C++的内存分布有什么区别
- 引用
C++中有引用,是一种更安全的指针,比指针方便; - 函数重载
C++支持函数重载,可以定义函数名相同函数列表不同的函数。 - new/delete/malloc/free
- const,inline,带默认值参数的函数
- 模板
可以写一套代码用任何类型去实例化,产生处理相应类型的代码 - 类和对象
C++中的类和对象使组织软件设计更丰富,可使用设计模式 - STL
- 异常处理
- 智能指针
- 运算符重载
- 内存分布
C和C++的内存分布没有区别
11. int* const p和int const *p区别?
int* const p 表示不可改变p指针的指向,const int* p 表示不可修改p指针指向的值
12. malloc和new区别
- malloc是按字节开辟空间的,new开辟内存时需要指定类型(new int()),malloc开辟内存返回的都是void*,而new返回的是对应类型的指针
- malloc负责开辟空间,new不仅有malloc的功能,还可以进行数据初始化,比如:new int(10)。new有开辟空间和构造的功能。
- malloc开辟内存失败返回nullptr,而new则会抛出bad_alloc异常
- 我们调用new实际上是调用的operator new,malloc是C的库函数
13. map&set容器的实现原理
set:集合,里面只放key map:映射表,存储[key,value]键值对 底层数据结构都是红黑树,再说一些红黑树的性质,相关算法。
14. shared_ptr引用计数存在哪里
参考:C++智能指针的enable_shared_from_this和shared_from_this机制
15. vector里的empty()和size()的区别?
_NODISCARD bool empty() const noexcept {
auto& _My_data = _Mypair._Myval2;
return _My_data._Myfirst == _My_data._Mylast;
}
_NODISCARD size_type size() const noexcept {
auto& _My_data = _Mypair._Myval2;
return static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst);
}
16. STL中迭代器失效的问题
迭代器是不允许一边读一边修改的。 当通过迭代器插入一个元素,所有迭代器就失效了; 当通过迭代器删除一个元素,当前删除位置的后面所有元素的迭代器就都失效了。
所以当通过迭代器更新容器元素后,要及时对迭代器更新,insert/erase方法都会返回新位置的迭代器。
17. STL中哪些底层是由红黑树实现的
set、multiset、map、multimap
18. struct和class的区别
- 定义类的时候区别,struct访问限定默认是public,class默认是private;
- 继承时,class Derive:Base表示私有继承,struct Derive:Base表示公有继承;
- struct在C语言中定义的空结构体是0,在C++中是1;
- 对于C++11,还提供了用struct定义的类生成的对象和class定义的类生成的对象不同的初始化,C++里struct定义的类对象初始化也可以用C语言中结构体的初始化方式,如:struct Data{int ma,int mb} Data data = {10,20};
- class在template还可以定义模板类型参数。
19. vector和数组的区别
直接使用数组的话,就会考虑到数组内存是否够用、是否需要扩容以及扩容代码如何写,将会和使用数组业务逻辑的代码混到一起,看起来很乱。vector是数组的一个面向对象表示,把数组封装起来了。我们想用一个可以自动扩容的数组直接用vector就行,想添加就添加,想删除就删除,永远也不用担心数组是否越界以及内存够不够。
20. STL(standard template libaray)标准容器的分类,各容器底层实现?
- 顺序容器
vector:底层是一个可扩容的数组 deque:是一个动态开辟的二维数组 list:双向循环链表 - 容器适配器
容器适配器没有自己的数据结构,也没有迭代器。 stack:依赖deque适配的,底层还是deque方法,但它把deque方法封装了,给我们提供了push、pop操作。 queue:依赖deque priority-queue:优先级队列,实现的是一个大根堆,通过下标的计算访问每个元素,所以基于vector。 - 关联容器
有序关联容器:set、multiset、map、multimap,底层用红黑树 无序关联容器:unordered_set和unordered_map,底层用链式哈希表。
21. 编译链接全过程?
参考:编译和链接
22. 初始化全局变量和未初始化全局变量有什么区别?
初始化且初始值不为0的在.data 未初始化或初始化为0的在.bss
23. 堆和栈的区别
- 堆内存的大小远远大于栈内存,所以在分配比较大的内存时尽量分配到堆上而不是栈上。
- 堆是通过malloc/new开辟,特点是必须手动free/delete,否则内存会泄露;而栈内存就是函数的运行、函数的局部变量需要在栈上分配栈帧,栈内存无需手动回收,运行时系统会自动给函数分配栈帧,函数出}运行完后系统会自动回收函数栈帧
- 堆分配是由低地址到高地址;栈分配是由高地址到低地址。
24. 构造函数和析构函数可不可以是虚函数,为什么?
虚函数必须把函数地址放在虚函数表里面,虚函数表是通过虚函数指针来访问,虚函数指针在对象里面。
构造函数不可以是虚函数 因为构造函数调用时对象还不存在,就没有虚函数指针,也就无法访问虚函数表。 如果构造一个派生类对象,派生类构造需要先构造基类,基类的构造是一个虚函数,此时发生动态绑定,需要访问派生类的前4个字节vfptr,然后这个时候派生类对象还没产生,访问出错。
析构函数可以是虚函数 基类的指针指向堆上的派生类对象时,delete ptr_base调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual。 若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数。
25. 构造函数和析构函数能不能抛出异常,为什么?
主要考虑内存泄漏、资源释放的问题。 构造函数不能抛出异常,如果构造函数抛异常就说明对象创建失败,编译器就不会调用对象的析构函数了,因为可能构造函数是前面创建资源代码都成功了,在中间抛出异常由于构造函数抛出异常对象创建失败,析构函数就无法调用,所以释放资源的代码无法执行,涉及了资源泄露。 把分配的堆内存用智能指针代替。
析构函数不能抛异常,如果在前面抛异常,后面代码就无法执行了,涉及资源泄露。
26. 宏和内联函数的区别
- 宏(#define)是预编译阶段处理,内联(inline)是编译阶段处理。
- 宏是字符串替换,至于{}有没有对齐,for有没有包括相应的代码块都不管;内联是在函数调用点,通过函数的实参把函数代码直接展开调用,节省了函数的调用开销。
- 宏可以定义很多,比如常量、代码段、函数等,内联只能用于修饰函数。
- 宏不能调试,内联在debug版本下可以调试,这时产生函数调用开销。
27. 局部变量存在哪里
局部变量存在栈上,通过ebp指针偏移来访问,不产生符号,所以局部变量不是数据,是指令的一部分(若是在VS Debug版本下查看反汇编会发现依然有符号,这是VS优化后展示给用户的结果,实际上应该是ebp-偏移量)
28. 拷贝构造函数,为什么传引用而不传值?
class Test
{
public:
Test(const Test t)
{
}
}
int main()
{
Test t1;
Test t2(t1);
}
这时用t2拷贝构造t1,如果传值,则需要用t1初始化形参t,这个时候也需要调用Test()的拷贝构造函数(t1.Test(t)),而调用拷贝构造函数的时候,仍然需要实参初始化形参,再次调用拷贝构造函数,陷入了死循环。 而实际上,编译器也会检查,直接发生编译错误,无法进行。
29. 内联函数和普通函数的区别
核心:函数调用的开销 开辟栈帧 栈帧清退
inline函数(内联函数)
-
在编译过程中,就没有函数的调用开销,在函数的调用点直接把函数的代码进行展开处理了。 -
不再生成相应的函数符号 -
inline只是建议编译器把这个函数处理成内联函数,但不是所有的inline函数都会被编译器处理成内联函数—递归(因为内联要在编译期展开,而在编译期间无法获得递归的终止条件) -
debug版本上,inline是不起作用的;inline只有在release版本下才能出现 -
因为需要在编译期间展开,而编译期间针对的是当前文件,所以inline函数的作用域只在本文件 -
有类型检测
普通函数
- 不展开
- 能调试
- 作用于在全局,生成global符号
- 有类型检测,安全
参考:C和C++的区别
30. 如何实现一个不可以被继承的类
派生类的初始化过程是:先是基类构造,然后再是派生类构造。 所以如果要实现一个不可以被继承的类,就可以把基类的构造函数私有化,此时基类的构造函数对派生类是不可见的,派生类无法继承。
31. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪里?
纯虚函数:virtual void fun()=0; 有纯虚函数的类叫做抽象类(不能实例化对象,但是可以定义指针和引用)
一般定义在基类里面,基类并不代表任务实体,它的主要作用之一就是给所有的派生类保留统一的纯虚函数接口,让派生类进行重写,方便的使用多态机制。因为不需要实例化,所以它的方法也就不知道该怎么去实现。
虚函数表是在编译阶段产生的,运行时加载到.rodata段。所有类定义的对象它的虚函数指针指向的是同一张该类型的虚函数表。
32. 手写单例模式
参考:设计模式
33. 说一说C++中的const,const和static的区别?
C++定义的const叫常量,它的编译方式是编译过程中,把出现常量名字的地方,用常量的值进行替换。
const int a=10;
int *p=(int*)&a;
*p=20;
cout<<a<<" "<<*p<<endl;
const和static区别:
- 从面向过程来说,const只能修饰全局常量、局部变量、形参变量;但是static只能修饰全局变量和局部变量。
- const不能修饰函数;static可以修饰函数。
- 从面向对象角度来说
const:修饰的是常方法(普通对象和常对象就都可以调用了,但是只能对成员进行读操作不能进行写操作)和成员变量(不能被修改,必须在构造函数的初始化列表初始化),要依赖对象; static:修饰的是静态方法和成员变量,不依赖对象,直接通过类作用域来访问。
- 静态方法:该方法没有this指针,不依赖于对象,通过类作用域访问
- 静态成员变量:静态成员变量属于类,不属于某个具体的对象,实现多个对象共享数据的目的。static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。即没有在类外初始化的 static 成员变量不能使用。
34. 四种强制类型转换
-
const_cast:去掉(指针或者引用)常量属性的一个类型转换 -
static_cast:提供编译器认为安全的类型转换(没有任何联系的类型之间的转换就被否定) -
reinterpret_cast:类似于C风格的强制类型转换 -
dynamic_cast:主要用在继承结构中,可以支持RTTI类型识别的上下转换 参考:四种强制类型转换
35. 详细解释deque的底层原理
deque底层是一个动态开辟的二维数组,里面有两个宏定义#define MAP_SIZE 2 、#define QUE_SIZE(T) 4096/sizeof(T) 。
由于deque是双端队列,所以两端都有队头和队尾,都可以插入删除,O(1)。
扩容:一维数组从2开始,以2倍的方式进行扩容,每次扩容后原来第二维的数组从新的第一维数组的下标oldsize/2 开始存放,上下留出相同的空间,方便deque首尾入队 。 由于是双端队列,所以最开始的时候,first 和 last 其实是指向相同地方的 扩容后 deque的内存利用率好,因为它的第二维是分段连续的,每一个二维都是单独new出来的,而且刚开始就有一段内存可以供使用。所以在容器适配器里面stack和queue都是依赖的deque。
36. 虚函数 多态
37. 异常机制怎么回事?
C++中的异常
try
{
可能会发生异常的代码
}
catch(const string &err)
{
捕获相应异常类型的对象,完成后,代码继续向下运行
}
C++中如果在当前函数栈帧上没有找到相应的catch块处理,就会把异常抛给调用方函数。处理了就继续运行,没处理就继续向调用方抛出,直到main函数还没有处理,就向系统抛出,系统发现进程有异常未处理,就直接终止进程。异常机制的好处就是可以把代码中所有的异常抛到统一的地方进行处理。
38. 早绑定和晚绑定
早绑定(静态绑定):普通函数的调用,用对象调用虚函数,汇编代码是call 函数名 晚绑定(动态绑定):用指针或者引用调用虚函数的时候,都是动态绑定,汇编代码是call寄存器 ,从指针访问的对象的前四个字节取vfptr,再从vfptr里访问vftable,再从vftable里面取virtual addr,最后把addr放到寄存器里,call寄存器。
39. 指针和引用的区别?
- 汇编角度
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。而mov指令则恰恰相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。 - 定义角度
(1)引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针; (2)引用必须初始化,指针可以不初始化,所以引用是一种更安全的指针。
40. 智能指针交叉引用问题怎么解决
交叉引用问题:无论是定义对象还是使用对象都用shared_ptr ,导致交叉引用问题
解决·:定义对象的时候用强智能指针shared_ptr ,而引用对象的时候用弱智能指针weak_ptr (强智能指针会引起引用计数的改变,弱智能指针不会)。当通过weak_ptr访问对象成员时,需要先调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr,再进行对象成员调用。
强弱智能指针一块使用还能监测对象是否已经析构,当引用计数为0则已经析构,提升失败,不为0对象还在则提升成功。
参考:智能指针
41. C++重载,虚函数的底层实现
- 重载
重载是因为C++生成函数符号,是依赖函数名字+参数列表 编译到函数调用点时,根据函数名字和传入的参数(个数和类型),和某一个函数重载匹配的话,那么就直接调用相应的函数重载版本(静态的多态,在编译阶段处理的)。 - 虚函数底层实现
一个类有虚函数,编译阶段就会给这个类产生一张虚函数表,虚函数表运行时加载到.rodata段 ,当用指针或引用调用一个虚函数的时候,就要进行动态绑定,用指针或引用访问对象的vfptr,进而访问虚函数表,在虚函数表里面取虚函数地址,进行call寄存器 指令的调用。
42. 编写一个C++程序需要注意什么
- 首先要分析需求,然后进行概要设计、详细设计。
- 考虑设计的函数、接口、类是否运行在多线程环境、考虑线程安全。
- 考虑代码的可移植性
- 使用现有的代码框架、设计模式等
43. 设计模式知道哪些?具体讲一下
参考:设计模式
44. 构造函数抛出异常可能会导致内存泄漏,如何解决?
class Test{
public:
Test(){
p1 = new int();
p2 = new int();
throw "xxxx"
}
~Test(){
delete p1;
delete p2;
}
private:
int* p1;
int* p2;
}
改写成
class Test{
public:
Test(){
p1 = new int();
p2 = new int();
throw "xxxx"
}
~Test(){ }
private:
unique_ptr<int> p1;
unique_ptr<int> p2;
}
|