C++校招常见问题汇总

网友投稿 708 2022-09-04

C++校招常见问题汇总

C++校招常见问题汇总

文章目录

​​1. this指针干什么用的?​​​​2. new和delete,什么时候用new[]申请,可以用delete释放?​​​​3. static关键字的作用(从elf结构、链接过程回答)?​​​​4. C++的继承有什么好处?​​​​5. 讲一下C++的多态​​​​6. 空间配置器allocator?​​​​7. vector和list的区别?​​​​8. map和multimap的区别​​​​9. C++如何防止内存泄漏?智能指针详述​​​​10. C++如何调用C语言函数接口?​​​​11. 那些情况下可能出现访问越界​​​​12. C++中类的初始化列表​​​​13. C和C++的区别以及内存分布​​​​14. `int* const p`和`const int* p`的区别​​​​15. malloc和new的区别​​​​16. map和set的实现原理​​​​17. shared_ptr的引用计数在哪存放​​​​18. STL各个容器底层数据结构?​​​​19. vector里empty()和size()的区别?​​​​20. 迭代器的实效问题​​​​21. C++中struct和class的区别​​​​22. 初始化的全局变量和未初始化的全局变量的区别​​​​23. 堆和栈的区别​​​​24. 构造函数和析构函数可不可以是虚函数​​​​25. 构造函数和析构函数是否可以抛异常​​​​26. 宏和内联函数的区别​​​​27. 局部变量存放在哪​​​​28. 拷贝构造函数为什么传引用&,不传值​​​​29. 内联函数和普通函数的区别(反汇编角度)​​​​30. 如何实现一个不可继承的类​​​​31. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪?​​​​32. 说一下C++中的const​​​​33. const与static的区别​​​​34. 四种强制类型转换​​​​35. deque底层数据结构​​​​36. 虚函数​​​​37. 多态​​​​38. C++异常处理原理​​​​39. 早绑定(静态)和晚绑定(动态)​​​​40. 指针和引用的区别(汇编角度)​​​​41. 交叉引用的问题如何解决​​​​42. C++函数重载的原理​​​​43. 编写一个C++程序需要注意什么​​​​44. 设计模式知道哪些?具体讲一下​​​​45. 构造函数抛出异常可能会导致内存泄漏,如何解决?​​

1. this指针干什么用的?

一个class可以定义很多对象,每个对象都有自己的成员变量以及成员方法,this指针在编译后作为成员方法的第一个参数

在成员方法里访问成员变量的时候,this指针用于区分访问哪个对象的成员变量

2. new和delete,什么时候用new[]申请,可以用delete释放?

new和delete是重载的运算符,实际上调用的是​​operator new​​​和​​operator delete​​

如果是自定义类型,而且提供了析构函数,用​​new[]​​​申请的内存空间就一定要用​​delete[]​​​释放,因为​​new[]​​除了开辟用户需要使用的内存空间,还多开辟了4字节存放对象的个数。因为编译器知道自己总共分配了多少空间,记录对象的个数后,可以在内存中准确划分出每个对象的首地址。

如果没有提供析构函数, ​​new[]​​开辟空间的起始地址和返回给用户的地址相同,也就是没有存放对象个数的4字节空间。

胡乱猜测:没有提供析构函数,delete不执行析构,分配多少空间就回收多少空间,不用4字节记录对象个数。

可参考:​​C++运算符重载​​

3. static关键字的作用(从elf结构、链接过程回答)?

从面向过程角度来说:

符号表中被static修饰的全局变量、函数就从global变成了local,也即从整个项目可见变成当前文件可见

static修饰的局部变量放在数据段(.data或.bss),由于是在数据段,程序一开始就要开辟空间,在执行到相应语句的时候初始化变量。局部变量本身不产生符号,通过​​ebp-偏移量​​进行访问,但是被static修饰后由于放在数据段,这时候就需要产生符号。

从面向对象角度来说:

static可以修饰成员变量,修饰成员方法,不再产生this指针,变成归类所有,通过类作用域调用

4. C++的继承有什么好处?

继承处于类和类之间的关系,好处如下:

代码复用通过继承,在基类里面给所有派生类保留统一的纯虚函数接口,派生类进行重写。通过基类指针或者引用指向派生类对象实现多态(指针指向谁就访问谁的方法),也更符合软件开发的“开闭原则”(只要是派生类,基类指针都可指向,不用修改)

可参考:​​C++的继承和多态​​

5. 讲一下C++的多态

分为静多态(编译时期)和动多态(运行时期)。

静多态的表现形式包括函数重载和类模板,在编译时期就要确定下来。

继承结构中,Base类指针(引用)指向Derive类对象,通过该指针(引用)调用同名覆盖方法(虚函数),该指针指向哪个Derive对象的覆盖对象,就调用哪个Derive类方法

可参考:​​C++的继承和多态​​

6. 空间配置器allocator?

allocator主要是给容器使用,作用是将开辟空间和构造对象分开,以及将回收空间和析构对象分开。 对于容器而言,如果首先就要开辟空间,而不是放入对象,这个时候需要用到空间配置器。容器删除元素的时候,只是析构对象,而不是像delete一样析构对象且回收空间。

7. vector和list的区别?

vector底层是数组,内存连续。插入删除为O(n),随机访问O(1),尾部的插入删除都是O(1)

list底层是双向链表,内存不连续。中间增加删除O(n),首尾增加删除O(1),访问O(n)

可参考:​​C++STL总结​​

8. map和multimap的区别

map是一个映射表[key-value],不允许key重复,底层实现是红黑树(一种二叉排序树),主要是对key进行查找比较,快速找到key,时间复杂度为O(log2n)

multimap允许key重复,底层实现是红黑树

红黑树的5个性质:

每个节点要么是黑色,要么是红色。根节点是黑色。叶子节点是黑色。每个红色节点的两个子节点一定都是黑色, 不能有两个红色节点相连。任意一节点到每个叶子节点的路径都包含数量相同的黑色结点。

性质5又可以推出: 如果一个节点存在黑色子节点,那么该结点肯定有两个子节点,不然走另一条路就会少一层黑色结点。

红黑树插入有3种情况(最多旋转2次),删除有4种情况(最多旋转3次)

9. C++如何防止内存泄漏?智能指针详述

内存泄漏:分配的堆内存(没有名字,只能用指针指向)没有释放

智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源

利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。所以,智能指针一般都是定义在栈上的

可参考:​​C++智能指针​​

10. C++如何调用C语言函数接口?

// 使用C语言的方式生成函数符号extern "C" { // 函数符号function_C,而不是function_C_int int function_C();}

可参考:​​C和C++的区别​​

11. 那些情况下可能出现访问越界

数组访问越界STL容器访问越界字符串访问越界字符串没有添加​​\0​​,导致访问越界使用类型强转,使得大类型(派生类)的指针指向小类型(基类)的对象,指针解引用直接越界

12. C++中类的初始化列表

可以指定数据成员的初始化方式,尤其是指定成员对象的初始化(成员变量的初始化和定义的顺序有关,和初始化列表的顺序无关)

13. C和C++的区别以及内存分布

引用、函数重载、运算符重载、new/delete/malloc/free、const、inline、模板、OOP语言可使用设计模式、STL、异常处理、智能指针

可参考:​​C和C++的区别​​

14. ​​int* const p​​​和​​const int* p​​的区别

​​int* const p​​​表示不可改变p指针的指向,​​const int* p​​表示不可修改p指针指向的值

可参考:​​C和C++的区别​​

15. malloc和new的区别

malloc是按字节开辟空间的,new开辟内存时需要指定类型(new int()),malloc开辟内存返回的都是void*,而new返回的是对应类型的指针malloc负责开辟空间,new不仅有malloc的功能,还可以进行数据初始化,比如:new int(10)。new有开辟空间和构造的功能。malloc开辟内存失败返回nullptr,而new则会抛出bad_alloc异常我们调用new实际上是调用的operator new

可参考:​​C++运算符重载​​

16. map和set的实现原理

set是集合,里面只存放key。map是映射表,存储[key,value]键值对。

底层数据结构都是红黑树,然后需要答一些红黑树的性质,相关算法

17. shared_ptr的引用计数在哪存放

存放在堆内存

可参考:​​C++智能指针的enable_shared_from_this和shared_from_this机制​​

18. STL各个容器底层数据结构?

顺序容器: vector(数组)、deque(二维数组)、list(双向循环链表)容器适配器: stack(deque)、queue(deque)、priority_queue(vector)关联容器: set系列(红黑树)、map系列(红黑树)

可参考:​​C++ STL总结​​

19. vector里empty()和size()的区别?

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(_My_data._Mylast - _My_data._Myfirst);}

20. 迭代器的实效问题

迭代器不允许一边读一边修改

当容器调用erase方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)

当容器调用insert方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)

insert来说,如果引起容器内存扩容,原来容器的所有的迭代器就全部失效

当通过迭代器更新容器元素以后,要及时对迭代器进行更新,insert/erase都会返回新位置的迭代器

可参考:​​C++运算符重载​​

21. C++中struct和class的区别

定义类的时候,struct的访问限定默认是public,class的访问限定默认是private继承时,​​class Derive:Base​​​表示私有继承,​​struct Derive:Base​​表示公有继承C语言里struct定义的空结构体是0字节,C++里struct定义的空类是1字节为了兼容C语言里的struct,C++里struct定义的类对象初始化也可以用C语言中结构体的初始化方式,如:​​Data data = {10,20}​​C++支持​​template​​​,不支持​​template​​

22. 初始化的全局变量和未初始化的全局变量的区别

​​.data​​​:存放初始化且初始化值不为0的数据​​​.bss​​:存放未初始化和初始化为0的数据

23. 堆和栈的区别

堆内存的大小 >> 栈内存的大小堆内存需要手动开辟、释放,栈区开辟函数栈帧,自动清退堆内存从低地址到高地址分配,栈从高地址(栈底)到低地址(栈顶)

24. 构造函数和析构函数可不可以是虚函数

构造函数不能是虚函数,析构函数可以是虚函数

构造函数不能实现成virtual,构造函数构造完成,对象才产生,才有vfptr,才能访问虚函数表。

如果构造一个派生类对象,派生类构造需要先构造基类,基类的构造是一个虚函数,此时发生动态绑定,需要访问派生类的前4个字节vfptr,然后这个时候派生类对象还没产生,访问出错。

基类的指针指向堆上的派生类对象时,​​delete ptr_base​​调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual。

若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数。

可参考:​​C++中的继承和多态总结​​

25. 构造函数和析构函数是否可以抛异常

主要考虑内存泄漏、资源释放的问题

构造函数不能抛出异常,若抛出异常则表示对象创建失败,不能调用析构函数,资源无法释放

析构函数只能在释放完所有资源后抛出异常

26. 宏和内联函数的区别

​​#define​​​在预编译时期处理,​​inline​​在编译时期处理​​#define​​​就是字符串替换,release版本中​​inline​​在函数调用点把函数代码展开,节省函数的调用开销​​#define​​​可以定义很多,比如常量、代码段、函数等,​​inline​​只能用于修饰函数​​#define​​​不能调试,​​inline​​在Debug版本下可以调试,这时产生函数调用开销

27. 局部变量存放在哪

局部变量通过 栈底指针​​ebp​​偏移访问,存放在栈区

局部变量不产生符号,不属于数据,属于指令的一部分(若是在VS Debug版本下查看反汇编会发现依然有符号,这是VS优化后展示给用户的结果,实际上应该是​​ebp-偏移量​​)

28. 拷贝构造函数为什么传引用&,不传值

class Test{ // 会产生编译错误 Test(const Test t){ ... }};int main(){ Test t1; Test t2(t1); return 0;}

这时用t1拷贝构造t2,如果传值,则需要用t1初始化形参t,这个时候也需要调用Test的拷贝构造函数(​​t1.Test(t)​​),而调用拷贝构造函数的时候,仍然需要实参初始化形参,再次调用拷贝构造函数,陷入了死循环。

而实际上,编译器也会检查,直接发生编译错误,无法运行

29. 内联函数和普通函数的区别(反汇编角度)

inline函数

编译期间在代码调用的地方展开(release版本),有逻辑性的进行文本替换,因此不产生函数符号能够调试,在debug版本(需要调试)inline函数和普通的函数表现一致,只有在release版本才会真正在调用点展开因为需要在编译期间展开,而编译期间针对的是单文件。所以inline函数的作用域只在本文件,debug版本生成local符号有类型检测,安全类体内实现的成员方法直接是inline

核心: 函数调用开销

开辟栈帧

栈帧清退

普通函数

不展开能调试作用域在全局,生成global符号有类型检测,安全

可参考:​​C和C++的区别​​

30. 如何实现一个不可继承的类

派生类构造的过程:先是执行基类构造函数,然后执行派生类构造函数。

我们将基类的构造函数私有化,此时基类的构造函数对派生类是不可见的,所以无法继承。

31. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪?

virtual void fun() = 0;

用纯虚函数定义的类是抽象类,无法实例化对象,可以定义指针和引用

纯虚函数定义在基类里面,基类不代表实体,它的主要任务之一就是 给所有的派生类保留统一的纯虚函数接口,让派生类进行重写。重写以后就 可以使用多态,基类指针指向派生类对象,指向哪个对象就调用哪个对象的方法

虚函数表是在编译阶段产生,运行时加载到​​.rodata​​段

可参考:​​C++继承和多态总结​​

32. 说一下C++中的const

编译时期值替换

const定义的叫常量,它的编译方式是把出现常量名字的地方,用常量的值进行替换

const int a = 10; int* p = (int*)&a; *p = 100; cout<

常变量

若用立即数初始化,则编译阶段可以直接用立即数进行替换。 若用变量初始化,则退化为和C语言中一模一样的常变量(只是不能作为左值,编译方式同普通变量)

定义常成员方法 this指针由​​​Test* this​​​变成​​const Test* this​​,普通对象和常对象都可以调用

可参考:​​C和C++的区别​​

33. const与static的区别

面向过程

const修饰:全局变量、局部变量、形参变量 static修饰:全局变量、局部变量、函数(本文件可见)

面向过程

const 常方法(普通对象和常对象都可以调用,只能对成员进行读不能写)、常成员变量(不能被修改,必须在构造函数的初始化列表初始化)

34. 四种强制类型转换

​​const_cast​​:去掉常量属性

​​static_cast​​:类型安全的转换

​​reinterpret_cast​​:C风格的类型转换,没有安全可言,随意转换

​​dynamic_cast​​:支持RTTI识别的类型转换。基类指针转成相应的派生类对象指针的时候,dynamic_cast会识别该指针是否能够进行转换

35. deque底层数据结构

deque是双端队列容器,底层数据是动态扩容的二维数组。一维数组从2开始,以2倍的方式进行扩容,每次扩容后原来第二维的数组从新的第一维数组的下标​​oldsize/2​​开始存放,上下留出相同的空间,方便deque首尾入队。

由于是双端队列,所以最开始的时候,​​first​​​ 和 ​​last​​其实是指向相同地方的

扩容后:

36. 虚函数

一个类如果有虚函数表,那在编译阶段就会产生vfptr,指向虚函数表,该表运行时加载到​​.rodata​​

用指针或引用调用虚函数的时候,首先访问对象前4个字节vfptr,然后找到虚函数表取出函数入口地址,进行动态绑定

37. 多态

使用基类的指针或引用指向不同的派生类对象,指向哪个对象就调用哪个对象的方法。

增加新功能或删除已有功能的时候,函数接口不需要改变。不管基类有多少个派生类,接口一套就够了,不需要对每一个对象都设计一套接口。

OOP里面的设计模式离不开多态,达到高内聚、低耦合的目标

38. C++异常处理原理

try{ 可能抛出异常的代码}catch(const string& error){ 捕获相应异常类型的对象,进行处理,处理完成后继续向下执行}

C++中如果在当前函数栈帧上没有找到相应的catch块处理,就会把异常抛给调用方函数。处理了就继续运行,没处理就继续向调用方抛出,直到main函数还没有处理,就向系统抛出,系统发现进程有异常未处理,就直接终止进程。

39. 早绑定(静态)和晚绑定(动态)

早绑定:编译时期完成(汇编代码是​​call 函数名​​​),用对象调用虚函数。 晚绑定:运行时确定(汇编代码是​​​call 寄存器​​),用指针或引用调用虚函数。确定指针类中被调用函数的类型,是虚函数则找到对象前4个字节的vfptr,从vftable拿到函数入口地址

可参考:​​C++继承和多态总结​​

40. 指针和引用的区别(汇编角度)

lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。而mov指令则恰恰相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。

可参考:​​C和C++的区别​​

41. 交叉引用的问题如何解决

交叉引用问题:无论是定义对象还是使用对象都用shared_ptr,导致交叉引用问题

定义对象时候使用强智能指针shared_ptr,引用的时候使用弱智能指针weak_ptr。当通过weak_ptr访问对象成员时,需要调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr,然后再进行对象成员调用。

可参考:​​C++智能指针​​

42. C++函数重载的原理

C++生成函数符号是依赖函数名+参数列表

43. 编写一个C++程序需要注意什么

首先要分析需求,然后进行概要设计、详细设计。考虑设计的函数、接口、类是否运行在多线程环境、考虑线程安全。考虑代码的可移植性使用现有的代码框架、设计模式等

44. 设计模式知道哪些?具体讲一下

可参考:​​C++设计模式总结​​

45. 构造函数抛出异常可能会导致内存泄漏,如何解决?

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 p1; unique_ptr p2;}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【复习笔记】操作系统之内存管理
下一篇:PHP实现用户注册、验证邮箱激活功能示例(php 邮箱验证)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~