1.static修饰局部变量和全局变量会有什么效果
解题思路
静态全局变量:具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域。 静态局部变量:具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在。
收起解题思路
2.堆和栈的区别
解题思路
栈由系统分配,堆由程序员控制,例如C/C++的malloc函数 栈的大小较小,有默认上限,堆理论上可以申请整个虚拟内存的大小 栈向下增长,地址由高到低,堆向上增长,地址从低到高 栈的分配效率较高,保证函数执行跳转的效率,堆的分配更为复杂,容易产生碎片,需要回收 栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等;堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
收起解题思路
3.简述gcc编译过程
解题思路
预处理、编译、汇编和链接,一个hello.c的c语言程序如下。 预处理阶段:hello.c–>hello.i 编译阶段:hello.i–>hello.s 汇编阶段:hello.s–>hello.o 链接阶段:hello.o–>hello
收起解题思路
4.野指针出现情况、怎么解决
解题思路
1.指针变量声明时没有被初始化,可以在指针声明时初始化,可以是具体的地址值,也可让它指向NULL。 2.指针 p 被 free 或者 delete 之后,没有置为 NULL。指针指向的内存空间被释放后指针应该指向NULL。 3.指针操作超越了变量的作用范围,在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。
收起解题思路
5.说一说RISC和CISC的区别
解题思路
精简指令集RISC和复杂指令集CISC CISC处理的是不等长指令集,它必须对不等长指令进行分割,因此在执行单一指令的时候需要进行较多的处理工作。而RISC执行的是等长精简指令集,CPU在执行指令的时候速度较快且性能稳定。因此在并行处理方面RISC明显优于CISC,RISC可同时执行多条指令,它可将一条指令分割成若干个进程或线程,交由多个处理器同时执行。由于RISC执行的是精简指令集,所以它的制造工艺简单且成本低廉。
收起解题思路
6.谈一谈什么是系统声明周期,说说你对敏捷开发的理解以及和SDLC的关系
解题思路
SDLC:sdlc(系统生命周期,系统生存周期)是软件的产生直到报废的生命周期,是软件工程中的一种思想原则,包括: 问题定义及规划、需求分析、软件设计、程序编码、软件测试、运行维护 敏捷开发的核心是迭代开发(iterative development)。敏捷一定是采用迭代开发的方式。迭代开发将一个大任务,分解成多次连续的开发,本质就是逐步改进。一般采用"增量开发"(incremental development)划分迭代。所谓"增量开发",指的是软件的每个版本,都会新增一个用户可以感知的完整功能。 虽然敏捷开发将软件开发分成多个迭代,但是也要求,每次迭代都是一个完整的软件开发周期,必须按照软件工程的方法论,进行正规的流程管理。也就是说,敏捷开发的每一次迭代都需要一个完整的SDLC。
收起解题思路
7.说说对MMU及TLB的理解
解题思路
MMU是内存管理单元,它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线的仲裁以及存储体切换(bank switching,尤其是在8位的系统上)。 TLB(Translation Lookaside Buffer)传输后备缓冲器是一个内存管理单元用于改进虚拟地址到物理地址转换速度的缓存。TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。 TLB( Translation Look- aside buffer)专门用于缓存内存中的页表项,一般在MMU单元内部。TLB是一个很小的 cache,TLB表项( TLB entry)数量比较少,每个TLB表项包含一个页面的相关信息,例如有效位、虚拟页号、修改位、物理页帧号等。当处理器要访问一个虚拟地址时,首先会在TLB中查询。如果TLB表项中没有相应的表项,称为TLB Miss,那么就需要访问页表来计算出相应的物理地址。如果TLB表项中有相应的表项,那么直接从TLB表项中获取物理地址,称为TLB命中。
收起解题思路
8.芯片选型考虑哪些因素?
解题思路
1.根据功能设计需求、成本、供应商等主要因素初步确定几款合适的芯片。 2.普通I/O口,考虑数量、负载能力,还需要保证裕量,如果有迭代升级,还需要考虑兼容性 3.片上存储和外围存储,保证bootloader和程序image的容量;内存支持,由程序的RAM需求决定 4.主频及时钟,决定芯片的运行效率,响应和处理速度 5.电源及功耗,由板上电源和承载能力决定 6.如果是CPU芯片,考虑多核和多线程并发能力;如果是FPGA芯片,考虑逻辑单元资源数;如果是DSP芯片,考虑浮点计算能力 7.工作环境,如辐射、单粒子和温度范围 8.芯片成本和交付日期,是否可以购买到,以及购买周期与开发周期的平衡 总结,与软件相关的因素:I/O,内存,外存,处理器核和多线程,系统可移植性等
收起解题思路
9.有cache的CPU上使用DMA如何保证数据的一致性
解题思路
Cache是CPU和主存之间的缓冲,DMA是为了主存和I/O数据交互设计的,期间CPU不参与控制。那么如果数据在主存中被CPU修改但是仍在cache中,即尚未更新主存,此时DMA获取的将是旧的数据,导致数据的不一致性。因此DMA在访问主存时,应当先检查cache是否命中,如果命中的话,DMA需要从Cache读取数据而非内存。 在设计时可以使用总线监视技术或者nocache机制解决非一致性问题,在软件层次上,当DMA往主存写数据时,在DMA传输之前,可以invalid DMA Buffer地址范围的高速缓存。在DMA传输完成后,程序读取数据不会由于cache hit导致读取过时的数据。 相反,DMA将数据输出时,在DMA传输之前,可以clean DMA Buffer地址范围的高速缓存,clean的作用是写回cache中修改的数据。在DMA传输时,不会把主存中的过时数据发送到I/O设备。
收起解题思路
10.volatile关键字作用?
解题思路
操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
2) 多任务环境下各任务间共享的标志应该加 volatile;
3) 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;
收起解题思路
11.简述字节对齐?
解题思路
内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。 如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。因此,通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。此外,合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。 字节对齐最主要反映在结构体对齐,对齐的原则如下:
1) 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。
2) 结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。
3) 指定对齐值:#pragma pack (value)时的指定对齐值value。
4) 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
收起解题思路
12.宏函数和内联函数的区别
解题思路
内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数和返回值,只是展开,相对来说,内联函数会检查参数类型,所以更安全。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
收起解题思路
13.虚拟地址怎么转换到物理地址
解题思路
地址转换(Address Translation)负责将虚拟地址转换成物理地址。现代操作系统有很多实现手段: 最简单的base+bound技术,进程通过寄存器base和bound来转换内存,不能保证安全性,程序的不同内存区需要连续,不能动态管理。 段技术:硬件为每个进程分配一组Base和Bound寄存器,每一对Base和Bound控制虚拟地址空间的一部分内存,称为段。每一段的虚拟地址空间是连续的,转换得到物理地址空间也是连续的,各个段之间不需要连续。这个技术可以很好的管理不同内存区,但是对于长度掌控的管理开销较大,寻找一段长度适合的物理地址需要额外开销。 页技术:将虚拟内存空间和物理内存空间皆划分成大小相同的页面,例如4KB、8KB和16KB等。并将页作为内存空间的最小分配单位,一个程序的一个页面(虚拟页面)可以存放在任何一个物理页面中。一个程序发出的虚拟地址由虚拟页面号和页内偏移值两部分组成。 快表TLB通常和处理器在一起,查找速度非常快,包含多级TLB,第一级TLB容量小、速度快,第二级TLB容量大、速度比第一级慢一些。可以大大提高虚拟地址转换为物理地址的效率
收起解题思路
14.传引用和传指针区别
解题思路
指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。 而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
收起解题思路
15.请你说一说cache的作用
解题思路
Cache存储器,电脑中为高速缓冲存储器,是位于CPU和主存储器DRAM(Dynamic Random Access Memory)之间,规模较小,但速度很高的存储器,通常由SRAM(Static Random Access Memory 静态存储器)组成。它是位于CPU与内存间的一种容量较小但速度很高的存储器。CPU的速度远高于内存,当CPU直接从内存中存取数据时要等待一定时间周期,而Cache则可以保存CPU刚用过或循环使用的一部分数据,如果CPU需要再次使用该部分数据时可从Cache中直接调用,这样就避免了重复存取数据,减少了CPU的等待时间,因而提高了系统的效率。Cache又分为L1Cache(一级缓存)和L2Cache(二级缓存),L1Cache主要是集成在CPU内部,而L2Cache集成在主板上或是CPU上。
收起解题思路
16.动态库和静态库的区别,后缀格式,以及函数的相对地址区别
解题思路
区别 命名方式不同: 静态库libxxx.a:库名前加”lib”,后缀用”.a”,“xxx”为静态库名。 动态库libxxx.so:库名前加”lib”,后缀变为“.so”。 链接时间不同: 静态库的代码是在编译过程中被载入程序中。 动态库的代码是当程序运行到相关函数才调用动态库的相应函数 链接方式不同: 静态库的链接是将整个函数库的所有数据在编译时都整合进了目标代码。 动态库的链接是程序执行到哪个函数链接哪个函数的库。(用哪个链接哪个) 优缺点? 静态库: 优点是,在编译后的执行程序不再需要外部的函数库支持,运行速度相对快些; 缺点是,如果所使用的静态库发生更新改变,你的程序必须重新编译。 动态库 : 优点是,动态库的改变并不影响你的程序,所以动态函数库升级比较方便; 缺点是,因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。
收起解题思路
17.SPI是什么?有几条线?几种模式?
解题思路
SPI,是一种高速的,全双工,同步的通信总线,在芯片的管脚上只占用四根线。SPI总线有四种工作方式,通过行同步时钟极性和相位可以进行组合配置。
收起解题思路
18.逻辑地址、线性地址、物理地址、总线地址、虚拟地址的区别?
解题思路
逻辑地址指的是与内存段相关的偏移地址部分。线性地址:线性地址 = 逻辑地址 + 基地址。 如果启用了分页机制,那么线性地址使用页表项变换后就是物理地址。如果没有启用分页机制,那么线性地址就是物理地址。总线地址其实就是物理地址。虚拟地址是通过MMU内存管理虚拟映射出来的地址。
收起解题思路
19.硬链接与软链接的区别?
解题思路
硬链接与原文件公用一个inode号,他们是同一个文件,而软链接与原文件拥有不同的inode号,他们是两个不同的文件;在文件属性上软链接明确写出了是链接文件,而硬链接没有写 出来,因为在本质上硬链接文件和原文件是完全平等关系;软链接的链接数目不会增加,硬链接没增加一个,链接数目就会加1;硬链接文件显示的大小是跟原文件是一样的,软连接不一定。
收起解题思路
20.C语言中,static关键字的作用?
解题思路
在C中,static主要定义全局静态变量、定义局部静态变量、定义静态函数。
1、定义全局静态变量:在全局变量前面加上关键字static,该全局变量变成了全局静态变量。全局静态变量有以下特点。
a.在全局区分配内存。
b.如果没有初始化,其默认值为0. c.该变量在本文件内从定义开始到文件结束可见。
2、定义局部静态变量:在局部变量前面加上关键字static,其特点如下:
a.该变量在全局数据区分配内存。
b.它始终驻留在全局数据区,直到程序运行结束。
c. 其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束
收起解题思路
21.堆和栈得区别?
解题思路
堆和栈的主要区别有:1、栈由系统自动分配,而堆是人为申请开辟;2、栈获得的空间较小,而堆获得的空间较大;3、栈由系统自动分配,速度较快,而堆一般速度比较慢;4、栈是连续的空间,而堆是不连续的空间。
收起解题思路
22.什么是野指针,产生的原因是什么?
解题思路
野指针是指向位置随机的、不正确的指针。野指针产生的原因有: 1、创建指针时没有对指针进行初始化,导致指针指向一个随机的位置; 2、释放指针指向的内存后没有置空,从而指向垃圾内存;
收起解题思路
23.DMA有什么用
解题思路
DMA是在专门的硬件( DMA)控制下,实现高速外设和主存储器之间自动成批交换数据尽量减少CPU干预的输入/输出操作方式。主要作用就是减少CPU的负担。
收起解题思路
24.程序中的内存分配方法
解题思路
内存为程序分配空间有四种分配方式:
1、连续分配方式
2、基本分页存储管理方式
3、基本分段存储管理方式
4、段页式存储管理方式
25.c语言volatile作用和用法
解题思路
volatile关键字用来阻止编译器认为的无法“被代码本身”改变的代码进行优化。如在C语言中,volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
收起解题思路
26.编译有几个阶段 每个阶段做什么事情
解题思路
词法分析阶段:读入源程序,对构成源程序的字符流进行扫描和分解,识别出单词, 语法分析阶段:机器通过词法分析,将单词序列分解成不同的语法短语,确定整个输入串能够构成语法上正确的程序。 语义分析阶段:检查源程序上有没有语义错误,在代码生成阶段收集类型信息 中间代码生成阶段:在进行了上述的语法分析和语义分析阶段的工作之后,有的编译程序将源程序变成一种内部表示形式 代码优化:这一阶段的任务是对前一阶段产生的中间代码进行变换或进行改造,目的是使生成的目标代码更为高效,即省时间和省空间 目标代码生成:这一阶段的任务是把中间代码变换成特定机器上的绝对指令代码或可重定位的指令代码或汇编指令代码
收起解题思路
27.简单描述一下数组指针和指针数组
解题思路
数组指针是一个指针,指向一个数组。指针数组由n个指针类型的数组元素组成。数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。
收起解题思路
28.指针和引用的区别
解题思路
(1)指针是实体,占用内存空间;引用是别名,与变量共享内存空间。
(2)指针不用初始化或初始化为NULL;引用定义时必须初始化。
(3)指针中途可以修改指向;引用不可以。
(4)指针可以为NULL;引用不能为空。
(5)sizeof(指针)计算的是指针本身的大小;而sizeof(引用)计算的是它引用的对象的大小。
(6)如果返回的是动态分配的内存或对象,必须使用指针,使用引用会产生内存泄漏。
(7)指针使用时需要解引用;引用使用时不需要解引用‘*’。 (8)有二级指针;没有二级引用。
收起解题思路
29.说说内联函数和宏函数的区别
解题思路
相同点: (1)二者都是通过将函数调用替换成完整的函数体,相比函数调用的时间、空间开销而言,二者提高了效率。 不同点: (1)宏定义不是函数,而内联函数时函数,因此内联函数可以调试,宏定义不能。 (2)宏定义的代码展开阶段是预处理阶段,而内联函数在编译阶段,因此内联 函数有类型安全检查,宏定义没有 (3)内联函数作为类的成员函数时,可以访问类的所有成员(公有、保护、私有),宏定义不能。
收起解题思路
30.在FreeRTOS中,二值信号量和互斥量的区别?
解题思路
互斥型信号量必须是同一个任务申请,同一个任务释放,其他任务释放无效。同一个任务可以递归申请。二进制信号量,一个任务申请成功后,可以由另一个任务释放。
收起解题思路
31.在FreeRTOS中,任务通知的运行机制是怎么样的?
解题思路
任务通知的数据结构包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用。任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知,FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。只有在任务中可以等待通知,而不允许在中断中等待通知。如果任务在等待的通知暂时无效,任务会根据用户指定的阻塞超时时间进入阻塞状态,我们可以将等待通知的任务看作是消费者;其它任务和中断可以向等待通知的任务发送通知,发送通知的任务和中断服务函数可以看作是生产者,当其他任务或者中断向这个任务发送任务通知,任务获得通知以后,该任务就会从阻塞态中解除。
收起解题思路
32.什么情况下会栈溢出?如何避免?
解题思路
1.局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。 2.递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。 3.指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。 解决这类问题的办法有两个, 一是增大栈空间,二是改用动态分配,使用堆(heap)而不是栈(stack)。
收起解题思路
33.深复制和浅复制的区别?
解题思路
浅复制:被复制对象的所有变量都含有与原来对象相同的值,而所有其他对象的引用仍然指向原来的对象。深复制:被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制的新对象,而不再是原有的那些被引用的对象。
收起解题思路
34.从一个源文件到可执行文件的过程?
解题思路
主要包括4个过程: 1. 预处理;2.编译(产生.s文件,-s);3.汇编(产生.o或者.obj文件,-c);4. 链接
收起解题思路
35.简述一下快速排序的步骤
解题思路
1.从数列中挑出一个元素,称为 "基准";
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置;
3.递归地把小于基准值元素的子数列和大于基准值元素的子数列排序;
收起解题思路
36.头文件的两种包含方式的区别
解题思路
< >引用的是编译器的类库路径里面的头文件,#include <> 的查找位置是标准库头文件所在目录;" "引用的是你程序目录的相对路径中的头文件, #include "" 的查找位置是当前源文件所在目录。
收起解题思路
37..CAN通信介绍;CAN通信报文的标识符有几位?
解题思路
CAN 是ISO国际标准化的串行通信协议。CAN 控制器根据两根线上的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。通信报文标识符有标准格式和扩展格式两种格式。标准格式有11 个位的标识符(ID),扩展格式有29 个位的ID。
收起解题思路
38.C语言结构体怎么定义节省内存
解题思路
1.在保证值域足够的情况下,用小字节变量代替大字节变量,如用short替代int
2.将各成员按其所占字节数从小到大声明,以尽量减少中间的填补空间(字节对齐)。
3.可以取消字节对齐,#pragma pack(1),当然这会牺牲效率,谨慎采用。
收起解题思路
39.STM32 中断是怎么进入到中断服务程序的
解题思路
在STM32中,为了区分不同的中断,每个设备有自己的中断号。系统有0-255一共256个中断。系统有一张中断向量表,用于存放256个中断的中断服务程序入口地址。每个入口地址对应一段代码,即中断服务程序。
收起解题思路
40.malloc和new的区别
解题思路
1、申请的内存所在位置不同。new操作符从自由存储区上为对象动态分配内存空间,malloc函数从堆上动态分配内存。
2. 返回类型安全性不同。new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
3. 内存分配失败时的返回值不同。new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL。malloc分配内存失败时返回NULL。
4. 是否需要指定内存大小不同。使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。malloc则需要显式地指出所需内存的尺寸。
收起解题思路
41.为什么局部变量未赋值时,每次初始化的结果是不确定的?
解题思路
定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的,上次用完没清零的,所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的,是不确定的。
收起解题思路
42.介绍下常用的gdb命令
解题思路
quit:退出gdb,结束调试 list:查看程序源代码 reverse-search:字符串用来从当前行向前查找第一个匹配的字符串 run:程序开始执行 help list/all:查看帮助信息 break:设置断点 break get_sum:以函数名设置断点 break 行号或者函数名 if 条件:以条件表达式设置断点 watch 条件表达式:条件表达式发生改变时程序就会停下来 next:继续执行下一条语句 ,会把函数当作一条语句执行 step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
收起解题思路
43.C++ 什么情况下必须用初始化列表
解题思路
1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。 2.const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。
收起解题思路
44.简述一下 C++ 中的多态
解题思路
得分点 静态多态、动态多态、多态的实现原理、虚函数、虚函数表 标准回答 在现实生活中,多态是同一个事物在不同场景下的多种形态。在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为,与之相对应的编译时绑定函数称为静态绑定。所以多态分为静态多态和动态多态。 1. 静态多态 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。 2. 动态多态 动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。 加分回答 1. 动态多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。 2. 实现动态多态的条件: - 要有继承关系 - 要有虚函数重写(被 virtual 声明的函数叫虚函数) - 要有父类指针(父类引用)指向子类对象 3. 动态多态的实现原理 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构, 虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。
收起解题思路
45.简述一下什么是面向对象
解题思路
得分点 面向过程的思想、面向对象的思想、面向对象三大特征:封装、继承、多态 标准回答 面向对象思想是基于面向过程思想的,要说面向对象思想,先说说面向过程思想。 1. 面向过程思想 完成一个需求的步骤:首先是搞清楚要做什么,然后再分析怎么做,最后再通过代码体现。一步一步去实现,而具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,从而完成需求。在上面的每一个具体步骤中我们都是参与者,并且需要面对具体的每一个步骤和过程,这就是面向过程最直接的体现。 面向过程编程,其实就是面向着具体的每一个步骤和过程,把每一个步骤和过程完成,然后由这些功能函数相互调用,完成需求。 2. 面向对象思想 面向对象的思想是尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程,把客观世界中的实体抽象为问题域中的对象。面向对象以对象为核心,该思想认为程序由一系列对象组成。 面向对象思想的特点: - 是一种更符合人类思维习惯的思想 - 可以将复杂的问题简单化 - 将我们从执行者变成了指挥者 3. 面向对象的三大特征:封装、继承、多态 - 封装:将事物属性和行为封装到一起,也就是 C++ 中的类,便于管理,提高代码的复用性。事物的属性和行为分别对应类中的成员变量和成员方法。 - 继承:继承使类与类之间产生关系,能够提高代码的复用性以及可维护性。 - 多态:多态意味着调用成员函数时,会根据调用方法的对象的类型来执行不同的函数。 加分回答 面向过程和面向对象解决问题举例:以洗衣服为例。 1. 面向过程:接水到盆中 —— 放入衣服 —— 加入洗衣粉 —— 浸泡 —— 搓衣服 —— 过水 —— 拧干 —— 晾干 2. 面向对象:将衣服放入洗衣机 —— 加入洗衣粉 —— 开启 —— 晾干 通过例子可以发现面向对象的方式解决问题更加简单一些,但是面向对象还是基于面向过程的。
收起解题思路
46.简述一下面向对象的三大特征
解题思路
得分点 封装、继承、多态 标准回答 面向对象的三大特征是:封装、继承、多态。 1. 封装 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理:比如景区,如果人人都能随意进来,那么很容易造成问题,比如损坏公物。所以我们需要建一堵围墙将景区围起来,但是我们的目的不是不让别人进去,所以开放了售票通道,可以买票在合理的监管机制下进去游玩。 C++通过 private、protected、public 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。 - private 修饰的成员只能在本类中访问 - protected 表示受保护的权限,修饰的成员只能在本类或者子类中访问 - public 修饰的成员是公共的,哪儿都可用访问。 封装的好处:隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。 2. 继承 C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。一个 B 类继承于 A 类,或称从类 A 派生类 B。这样的话,类 A 成为基类(父类), 类 B 成为派生类(子类)。派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。 继承的好处:提高代码的复用性;提高代码的拓展性;是多态的前提。 3. 多态 在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。
收起解题思路
47.简述一下浅拷贝和深拷贝
解题思路
得分点 浅拷贝的问题、深拷贝的实现 标准回答 浅拷贝和深拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是“引用”。浅拷贝和深拷贝一般在拷贝构造函数和赋值运算符重载函数中涉及到。
1. 浅拷贝 浅拷贝又称为值拷贝,将源对象的值拷贝到目标对象中,如果对象中有某个成员是指针类型数据,并且是在堆区创建,则使用浅拷贝仅仅拷贝的是这个指针变量的值,也就是在目标对象中该指针类型数据和源对象中的该成员指向的是同一块堆空间。这样会带来一个问题,就是在析构函数中释放该堆区数据,会被释放多次。默认的拷贝构造函数和默认的赋值运算符重载函数都是浅拷贝。
2. 深拷贝 深拷贝在拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样指针成员就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了拷贝的目的,还不会出现问题,两个对象先后去调用析构函数,分别释放自己指针成员所指向的内存。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
收起解题思路
48.请你说说三种智能指针实现原理和使用场景,以及其线程安全
解题思路
标准回答
1. 智能指针实现原理 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更严格,unique_ptr 能够在编译期识别错误。 跟踪引用特定对象的智能指针计数,这称为引用计数(reference counting)。例如,赋值时,计数将加 1,而指针过期时,计数将减 1. 仅当最后一个指针过期时,才调用 delete。这是 shared_ptr 采用的策略。
2. 使用场景 如果程序要使用多个指向同一个对象的指针,应该选择 shared_ptr; 如果程序不需要多个指向同一个对象的指针,则可以使用 unique_ptr; 如果使用 new [] 分配内存,应该选择 unique_ptr; 如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。
3. 线程安全 shared_ptr 智能指针的引用计数在手段上使用了 atomic 原子操作,只要 shared_ptr 在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全的,所有 shared_ptr 智能指针在多线程下引用计数也是安全的,也就是说 shared_ptr 智能指针在多线程下传递使用时引用计数是不会有线程安全问题的。 但是指向对象的指针不是线程安全的,使用 shared_ptr 智能指针访问资源不是线程安全的,需要手动加锁解锁。智能指针的拷贝也不是线程安全的。
收起解题思路
49.请你说说 C++11、C++14、C++17、C++20 都有什么新特性
解题思路
标准回答 1. C++11 新特新 - static_assert 编译时断言 - 新增加类型 long long ,unsigned long long,char16_t,char32_t,原始字符串 - auto - decltype - 委托构造函数 - constexpr - 模板别名 - alignas - alignof - 原子操作库 - nullptr - 显示转换运算符 - 继承构造函数 - 变参数模板 - 列表初始化 - 右值引用 - Lambda 表达式 - override、final - unique_ptr、shared_ptr - initializer_list - array、unordered_map、unordered_set - 线程支持库 2. C++14 新特新 - 二进制字面量 - 泛型 Lambda 表达式 - 带初始化/泛化的 Lambda 捕获 - 变量模板 - [[deprecated]]属性 - std::make_unique - std::shared_timed_mutex、std::shared_lock - std::quoted - std::integer_sequence - std::exchange 3. C++17 新特新 - 构造函数模板推导 - 结构化绑定 - 内联变量 - 折叠表达式 - 字符串转换 - std::shared_mutex 4. C++20 新特新 - 允许 Lambda 捕获 [=, this] - 三路比较运算符 - char8_t - 立即函数(consteval) - 协程 - constinit
收起解题思路
50.请你说说 new 的实现原理,new 和 malloc 的区别
解题思路
标准回答 1. new 的实现原理: 如果是简单类型,则直接调用 operator new(),在 operator new() 函数中会调用 malloc() 函数,如果调用 malloc() 失败会调用 _callnewh(),如果 _callnewh() 返回 0 则抛出 bac_alloc 异常,返回非零则继续分配内存。 如果是复杂类型,先调用 operator new()函数,然后在分配的内存上调用构造函数。 2. new 和 malloc 的区别 - new 是操作符,而 malloc 是函数; - 使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc 则需要显式地指出所需内存的尺寸; - new 分配失败的时候会直接抛出异常,malloc 分配失败会返回 NULL; - 对于非简单类型,new 在分配内存后,会调用构造函数,而 malloc 不会; - new 分配成功后会返回对应类型的指针,而 malloc 分配成功后会返回 void * 类型; - malloc 可以分配任意字节,new 只能分配实例所占内存的整数倍数大小; - new 可以被重载,而 malloc 不能被重载; - new 操作符从自由存储区上分配内存空间,而 malloc 从堆上动态分配内存; - 使用 malloc 分配的内存后,如果在使用过程中发现内存不足,可以使用 realloc 函数进行内存重新分配实现内存的扩充,new 没有这样直观的配套设施来扩充内存。
收起解题思路
51.说一说 STL 中有哪些常见的容器
解题思路
得分点 顺序容器、关联式容器、容器适配器 标准回答 STL 中容器分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下: 1. 顺序容器 容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list。 - vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。 - deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。 - list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。 2. 关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。 - set/multiset set中不允许相同元素,multiset 中允许存在相同元素。 - map/multimap map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。map 和multimap 的不同在于是否允许相同 first 值的元素。 3. 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。 - stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。 - queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。 - priority_queue:优先级队列 内部维持某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列。
收起解题思路
52.STL 容器用过哪些,查找的时间复杂度是多少,为什么?
解题思路
标准回答 STL 中常用的容器有 vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set 等。容器底层实现方式及时间复杂度分别如下: 1. vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N) 2. deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N) 3. list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1) 4. map、set、multimap、multiset 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN) 5. unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N) 注意:容器的时间复杂度取决于其底层实现方式。
收起解题思路
53.请你说说 STL 中容器的类型,每种分别有哪些容器
解题思路
得分点 序列式容器、关联式容器、无序关联式容器、容器适配器 标准回答 STL 中容器的类型和每种类型的容器如下: 1. 序列式容器 array、vector、deque、list、forward_list 2. 关联式容器 map、multimap、set、multiset 3. 无序关联式容器 unordered_map、unordered_multimap、unordered_set、unordered_multiset 4. 容器适配器 stack、queue、priority_queue
收起解题思路
54.简述一下堆和栈的区别
解题思路
得分点 管理方式、空间大小、是否产生内存碎片、生长方向、分配方式、分配效率 标准回答 堆和栈主要有如下几点区别:管理方式、空间大小、是否产生内存碎片、生长方向、分配方式、分配效率。 1. 管理方式 对于栈来讲,是由编译器自动管理,无需手动控制;对于堆来说,分配和释放都是由程序员控制的。 2. 空间大小 总体来说,栈的空间是要小于堆的。堆内存几乎是没有什么限制的;但是对于栈来讲,一般是有一定的空间大小的。 3. 碎片问题 对于堆来讲,由于分配和释放是由程序员控制的(利用new/delete 或 malloc/free),频繁的操作势必会造成内存空间的不连续,从而造成大量的内存碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的数据结构,在某一数据弹出之前,它之前的所有数据都已经弹出。 4. 生长方向 对于堆来讲,生长方向是向上的,也就是沿着内存地址增加的方向,对于栈来讲,它的生长方式是向下的,也就是沿着内存地址减小的方向增长。 5. 分配方式 堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配,静态分配是编译器完成的,比如局部变量的分配;动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器实现的,无需我们手工实现。 6. 分配效率 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率很高。堆则是 C/C++ 函数提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率要比栈底的多。
收起解题思路
55.请你说说指针和引用的区别
解题思路
标准回答 指针和引用的区别有: 1. 定义和性质不同。指针是一种数据类型,用于保存地址类型的数据,而引用可以看成是变量的别名。指针定义格式为:数据类型 *;而引用的定义格式为:数据类型 &; 2. 引用不可以为空,当被创建的时候必须初始化,而指针变量可以是空值,在任何时候初始化; 3. 指针可以有多级,但引用只能是一级; 4. 引用使用时无需解引用(*),指针需要解引用; 5. 指针变量的值可以是 NULL,而引用的值不可以为 NULL; 6. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了; 7. sizeof 引用得到的是所指向的变量(对象)的大小,而 sizeof 指针得到的是指针变量本身的大小; 8. 指针作为函数参数传递时传递的是指针变量的值,而引用作为函数参数传递时传递的是实参本身,而不是拷贝副本; 9. 指针和引用进行++运算意义不一样。
收起解题思路
56.请你说说单例设计模式
解题思路
得分点 概念、优缺点、实现、种类 标准回答
1. 概念 单例设计模式(Singleton Pattern)是一种比较简单的设计模式。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。注意: - 单例类只能有一个实例。 - 单例类必须自己创建自己的唯一实例。 - 单例类必须给所有其他对象提供这一实例。
2. 单例设计模式的优缺点 优点: - 单例模式可以保证内存里只有一个实例,减少了内存的开销。 - 可以避免对资源的多重占用。 - 单例模式设置全局访问点,可以优化和共享资源的访问。 缺点: - 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。 - 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。 - 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
3. C++ 单例设计模式的实现 - 私有化构造函数、拷贝构造函数、赋值函数 - 定义一个私有的本类的静态对象成员 - 定义一个公共的访问该示例静态成员方法,返回该静态对象成员 4. 单例设计模式的种类 - 懒汉式:获取该类的对象时才创建该类的实例 - 饿汉式:获取该类的对象之前已经创建好该类的实例
收起解题思路
57.简述一下 C++ 的重载和重写
解题思路
得分点:概念、应用、区别
标准回答:
1. 重载
a. 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同(参数列表不同)。调用的时候根据函数的参数来区别不同的函数,函数重载跟返回值无关。
b. 重载的规则 - 函数名相同 - 必须具有不同的参数列表 - 可以有不同的访问修饰符
c. 重载用来实现静态多态(函数名相同,功能不一样)。
d. 重载是多个函数或者同一个类中方法之间的关系,是平行关系。
2. 重写
a. 重写(也叫覆盖)是指在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样。
b. 重写的规则: - 方法声明必须完全与父类中被重写的方法相同 - 访问修饰符的权限要大于或者等于父类中被重写的方法的访问修饰符 - 子类重写的方法可以加virtual,也可以不加
c. 重写用来实现动态多态(根据调用方法的对象的类型来执行不同的函数)。
d. 重写是父类和子类之间的关系,是垂直关系。
加分回答
1. C++中重载的实现 采用命名倾轧(name mangling)技术,编译时会将同名的函数或方法根据某种规则生成不同的函数或方法名(因为函数或方法的特征标不一样)。
2. C++中重写的实现 C++中重写可以用来实现动态多态,父类中需要重写的方法要加上 virtual 关键字。 虚函数实现的原理是采用虚函数表,多态中每个对象内存中都有一个指针,被称为虚函数指针,这个指针指向虚函数表,表中记录的是该类的所有虚函数的入口地址,所以对象能够根据它自身的类型调用不同的函数。
收起解题思路
58.请你说说重载,复写,隐藏的区别
解题思路
得分点
定义、作用域、有无 virtual、函数名、形参列表、返回值类型
标准回答
重载、重写、隐藏在定义、作用域、有无 virtual、函数名、形参列表、返回值类型等方面有区别。
1. 重载:在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载,与返回值类型无关。
2. 重写:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。
隐藏的实质是:在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。
3. 派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
收起解题思路
59.简述一下虚函数的实现原理
解题思路
得分点 多态、虚函数表、虚函数表指针 标准回答 1. 虚函数的作用 C++ 中的虚函数的作用主要是实现了动态多态的机制。动态多态,简单的说就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。 2. 虚函数实现原理 编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表(virtual function table,vtbl)。虚函数表中存储的是类中的虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址,如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。 加分回答 使用虚函数时,对于内存和执行速度方面会有一定的成本: 1. 每个对象都会变大,变大的量为存储虚函数表指针; 2. 对于每个类,编译器都会创建一个虚函数表; 3. 对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址。
收起解题思路
60.简述一下 GDB 常见的调试命令
解题思路
得分点 gdb 常见的调试命令 标准回答 gdb 常见的调试命令如下: 启动和退出:gdb 可执行程序 quit/q 给程序设置参数/获取设置参数:set args 10 20 show args GDB 使用帮助:help 查看当前文件代码:list/l (从默认位置显示)list/l 行号 (从指定的行显示)list/l 函数名(从指定的函数显示) 查看非当前文件代码:list/l 文件名:行号 list/l 文件名:函数名 设置显示的行数:show list/listsize set list/listsize 行数 设置断点:b/break 行号 b/break 函数名 b/break 文件名:行号 b/break 文件名:函数 查看断点:i/info b/break 删除断点:d/del/delete 断点编号 设置断点无效:dis/disable 断点编号 设置断点生效:ena/enable 断点编号 设置条件断点(一般用在循环的位置) b/break 10 if i==5 运行GDB程序:start(程序停在第一行) run(遇到断点才停) 继续运行,到下一个断点停:c/continue 向下执行一行代码(不会进入函数体):n/next 变量操作:p/print 变量名(打印变量值) ptype 变量名(打印变量类型) 向下单步调试(遇到函数进入函数体):s/step finish(跳出函数体) 自动变量操作:display 变量名(自动打印指定变量的值) i/info display undisplay 编号 查看 follow-fork-mode mode 选项的值:show follow-fork-mode 查看 detach-on-fork 选项的值:show detach-on-fork 设置 follow-fork-mode mode 选项的值:set follow-fork-mode [parent \ child] 设置 detach-on-fork 选项的值:show detach-on-fork [on \ off] 查看当前调试环境中有多少个进程:info inferiors 切换到指定 ID 编号的进程对其进行调试:inferior id 其它操作:set var 变量名=变量值 (循环中用的较多) until (跳出循环)
收起解题思路
61.说一说 C++ 和 C 中 struct 的区别以及和 class 的区别
解题思路
得分点 成员、权限、使用、用途 标准回答 C++ 和 C 中 struct 的区别: 1. C 的结构体不允许有函数存在,C++ 的结构体允许有内部成员函数,并且允许该函数是虚函数 2. C 的结构体内部成员不能加权限,默认是 public,而 C++ 的结构体内部成员权限可以是 public、protected、private,默认 public 3. C 的结构体是不可以继承,C++ 的结构体可以从其它的结构体或者类继承 4. C 中的结构体不能直接初始化数据成员,C++ 中可以 5. C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名后直接使用,而 C++ 中使用结构体可以省略 struct 关键字直接使用 struct student{ int age; string name; } typedef struct student student2; //C中取别 struct student stu1; // C 中正常使用 student2 stu2; // C 中通过取别名的使用 student stu3; // C++ 中使用,C 中直接使用编译不通过 ``` struct 和 class 的区别: 1. struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装 2. struct 中默认访问控制权限是 public,而 class 中默认的访问控制权限是 private struct A { int iNum; // 默认访问控制权限是 public } class B { int iNum; // 默认访问控制权限是 private } 3. 在继承关系中,struct 默认是公有继承,而 class 是私有继承 4. class 关键字可以用于定义模板参数,而 struct 不能 template<class t=""> int func(const T& t, const Y& y) { }</class>
收起解题思路
62.请你说说各数据类型 sizeof 是多少,sizeof 指针是多少,sizeof 原理
解题思路
标准回答
1. 各数据类型 sizeof 的结果其实就是该数据类型的字节数,不同类型的数据 sizeof 的结果是不一样的,并且不同的操作系统和编译器下同一数据类型的 sizeof 的结果也不一样,具体看编译器是如何实现的。以下是Microsoft C++ 中对常见内置类型 sizeof 的代码: #include <iostream> using namespace std; int mn() { cout << sizeof(bool) << endl; cout << sizeof(char) << endl; cout << sizeof(short) << endl; cout << sizeof(int) << endl; cout << sizeof(long) << endl; cout << sizeof(long long) << endl; return 0; } 运行结果: 1 1 2 4 4 8 2. 对指针变量进行 sizeof 运算,获得的是指针变量的大小,而无论是什么类型的指针,在同一平台下结果都是一样的。在 32 位平台下是 4 个字节,在 64 位平台下是 8 个字节。
3. sizeof 的原理:sizeof 是在编译的时候,查找符号表,判断类型,然后根据基础类型来取值。如果 sizeof 运算符的参数是一个不定长数组,则该需要在运行时计算数组长度。</iostream>
收起解题思路
63.为什么将析构函数设置成虚函数
解题思路
得分点 概念、防止内存泄露 标准回答 1. 概念 虚析构函数,是将基类的析构函数声明为 virtual class Base { public: Base() { } // 虚析构函数 virtual ~Base() { } } 2. 作用 虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄露。如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时就会造成内存泄露。
收起解题思路
64.请你说说 malloc 的实现原理
解题思路
得分点 隐式链表,显示空闲链表 标准回答 malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,即内存池。 malloc() 分配空间有一个数据结构,允许它来区分边界,区分已分配和空闲的空间,数据结构中包含一个头部信息和有效载荷,有效载荷的首地址就是 malloc() 返回的地址,可能在尾部还有填充,为了保持内存对齐。头部相当于该数据结构的元数据,其中包含了块大小和是否是空闲空间的信息,这样可以根据头地址和块大小的地址推出下一个内存块的地址,这就是隐式链表。 malloc() 基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会调用 sbrk() 推进 brk 指针来申请内存空间。搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。 - 首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。 - 下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。 - 最佳适配:对堆进行彻底的搜索,从头开始遍历所有块,使用数据区大小大于 size 且差值最小的块作为此次分配的块。 在释放内存块后,如果不进行合并,那么相邻的空闲内存块还是相当于两个内存块,会形成一种假碎片。所以当释放内存后,需要将两个相邻的内存块进行合并。 还有一种实现方式则是采用显示空闲链表,这个是真正的链表形式。在之前的有效载荷中加入了前驱和后驱的指针,也可以称为双向链表。维护空闲链表的的方式第一种是用后进先出(LIFO),将新释放的块放置在链表的开始处。另一种方法是按照地址的顺序来维护。
收起解题思路
65.请你说说 delete 和 free 的区别
解题思路
标准回答 delete 和 free 的区别:
1. delete 是操作符,而 free 是函数;
2. delete 用于释放 new 分配的空间,free 有用释放 malloc 分配的空间;
3. free 会不会调用对象的析构函数,而 delete 会调用对象的析构函数;
4. 调用 free 之前需要检查要释放的指针是否为 NULL,使用 delete 释放内存则不需要检查指针是否为 NULL;
收起解题思路
66.说一说什么是内存泄露,如何检测
解题思路
得分点 概念、避免内存泄露、检测 标准回答 1. 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 2. 避免内存泄露的方法主要就是要有良好的编码习惯,动态开辟内存空间,及时释放内存。也可以采用智能指针来避免内存泄露。 3. 可以采用静态分析技术、源代码插装技术等进行检测。常见的一些检测工作有:LCLink、ccmalloc、Dmalloc、Electric Fence、Leaky、LeakTracer、MEMWATCH、Valgrind、KCachegrind等等。
收起解题思路
67.说一说 vector 和 list 的区别,分别适用于什么场景?
解题思路
得分点 低层数据结构、内存顺序、是否支持随机访问 标准回答 1. 区别 - vector 底层实现是数组,list 是双向链表 - vector 支持随机访问,list 不支持 - vector 是顺序内存,list 不是 - vector 在中间节点进行插入删除会导致内存拷贝,list 不会 - vector 一次性分配好内存,不够时才进行扩容,list 每次插入新节点都会进行内存申请 - vector 随机访问性能好,插入删除性能差,list 随机访问性能差,插入删除性能好 2. 适用场景 - vecto r拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector。 - list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
收起解题思路
68.请你说说 map,unordered_map 的区别
解题思路
得分点 头文件、原理 标准回答 map、unordered_map 是 C++ STL 中的两个容器,它们的区别有: 1. 导入的头文件 map:#include \<map> unordered_map:#include \<unordered_map> 2. 原理及特点 map:内部实现了一个红黑树,该结构具有自动排序的功能,因此 map 内部的所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素,因此,对于 map 进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了 map 的效率。 unordered_map:内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的。</unordered_map></map>
收起解题思路
69.请你说一说虚拟内存与物理内存
解题思路
标准回答 操作系统有虚拟内存与物理内存的概念。 1. 物理内存 以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于 CPU 的地址线条数。比如在 32 位平台下,寻址的范围是 2^32 也就是 4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给 4G 物理内存,就可能会出现很多问题: - 因为物理内存是有限的,当有多个进程要执行的时候,都要给 4G 内存,很显然内存不够,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作效率很低 - 由于指令都是直接访问物理内存的,那么任何进程都可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的 2. 虚拟内存 由于物理内存有很多问题,所以出现了虚拟内存。虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
收起解题思路
70.请你说说 C 语言里面 volatile,可以和 const 同时使用吗
解题思路
标准回答 volatile 限定符是用来告诉计算机,所修饰的变量的值随时都会进行修改的。用于防止编译器对该代码进行优化。通俗的讲就是编译器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份。 const 和 volatile 可以一起使用,volatile 的含义是防止编译器对该代码进行优化,这个值可能变掉的。而 const 的含义是在代码中不能对该变量进行修改。因此,它们本来就不是矛盾的。
收起解题思路
71.请你说说分段和分页
解题思路
得分点
分段、分页、段页式
标准回答
1. 分段
将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,实现了离散分配。分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
2. 分页
用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。分页主要用于实现虚拟内存,从而获得更大的地址空间。
3. 段页式
页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方法结合起来,就形成了段页式存储管理方式。
段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,利用段表和页表进行从用户地址空间到物理内存空间的映射。
系统为每一个进程建立一张段表,每个分段有一张页表。段表表项中至少包括段号、页表长度和页表始址,页表表项中至少包括页号和块号。在进行地址转换时,首先通过段表查到页表始址,然后通过页表找到页帧号,最终形成物理地址。
72.说一说 static 关键字的作用
解题思路
得分点 关键字、作用域、生命周期、共享数据 标准回答 static 是一个关键字,可以用来修饰局部变量、全局变量、成员变量、函数和成员方法。主要作用有:限制数据的作用域、延长数据的生命周期、修饰成员可以被该类所有对象共享。 1. 限制数据的作用域(隐藏) 所有没有加 static 的全局变量和函数都具有全局可见性,其它源文件中也可以访问。被 static 修饰的全局变量和函数只能在当前源文件中访问,其它源文件访问不了,利用这个特性可以在不同的文件中定义同名变量和同名函数,而不必担心命名冲突。 2. 延长数据的生命周期 普通的局部变量出了作用域就会释放,而静态变量存储在静态区,知道程序运行结束才会释放。 3. 静态成员被该类所有对象共享 static 关键字可以修饰类中的成员变量和成员方法,被称为静态成员变量和静态成员方法,静态成员拥有一块单独的存储区,不管创建多少个该类的对象,所有对象都共享这一块内存。静态成员本质上属于类,可以通过类名直接访问。 加分回答 1. 静态变量默认初始化值为0,如果没有显示初始化静态变量或者初始化为0的静态变量会存储在BSS段,而初显示初始化的静态变量存储在DATA段。 2. 静态成员函数中不能访问普通的成员变量,只能访问静态成员变量,并且在静态成员函数中没有 this 指针。
收起解题思路
73.说一说什么是野指针,怎么产生的,如何避免
解题思路
得分点 未知内存、未初始化、置为nullptr 标准回答 1. 什么是野指针 野指针是指指向的位置是随机的、不可知的、不正确的。 2. 野指针产生的原因
a. 指针变量未初始化或者随便赋值:指针变量没有初始化,其值是随机的,也就是指针变量指向的是不确定的内存,如果对它解除引用,结果是不可知的。
b. 指针释放后未置空:有时候指针在释放后没有复制为 nullptr,虽然指针变量指向的内存被释放掉了,但是指针变量中的值还在,这时指针变量就是指向一个未知的内存,如果对它解除引用,结果是不可知的。
c. 指针操作超出了变量的作用域:函数中返回了局部变量的地址或者引用,因为局部变量出了作用域就释放了,这时候返回的地址指向的内存也是未知的。
3. 如何避免野指针
a. 指针变量一定要初始化,可以初始化为 nullptr,因为 nullptr 明确表示空指针,对 nullptr 操作也不会有问题。
b. 释放后置为 nullptr。
收起解题思路
74.说说 const 和 define 的区别
解题思路
得分点 作用、编译阶段、预处理阶段、简单替换、类型检查、内存 标准回答 const 在 C 语言中表示只读,编译器禁止对它修饰的变量进行修改,在 C++ 中增加了常量的语义。而 define 用于定义宏,而宏也可以用于定义常量。它们的区别有:
1. const 生效于编译阶段,而 define 生效于预处理阶段;
2. define只是简单的字符串替换,没有类型检查,而 const 有对应的数据类型,编译器要进行判断的,可以避免一些低级的错误;
3. 用 define 定义的常量是不可以用指针变量去指向的,用 const 定义的常量是可以用指针去指向该常量的地址的;
4. define 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大,const 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝;
5. 可以对 const 常量进行调试,但是不能对宏常量进行调试。
收起解题思路
75.请你说说 vector 的扩容机制,扩容以后,它的内存地址会变化吗
解题思路
得分点 申请空间、拷贝数据、释放旧空间 标准回答 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
收起解题思路
76.请你说说 unordered_map 实现原理
解题思路
得分点
哈希表、链地址法
标准回答
unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。整个存储结构如下图(其中,Pi 表示存储的各个键值对):
可以看到,当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。
不仅如此,在 C++ STL 标准库中,将图中的各个链表称为桶(bucket),每个桶都有自己的编号(从 0 开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:
1. 将该键值对中键的值带入设计好的哈希函数,会得到一个哈希值(一个整数,用 H 表示);
2. 将 H 和无序容器拥有桶的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;
3. 建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上。
另外,哈希表存储结构还有一个重要的属性,称为负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程序,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。如果设计的哈希函数不合理,使得各个键值对的键带入该函数得到的哈希值始终相同(所有键值对始终存储在同一链表上)。这种情况下,即便增加桶数是的负载因子减小,该容器的查找效率依旧很差。无序容器中,负载因子的计算方法为:负载因子 = 容器存储的总键值对 / 桶数
默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。这也就解释了,为什么我们在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。
收起解题思路
77.请你说说 unique_ptr 的实现原理及使用场景
解题思路
得分点 所有权、private、delete 标准回答 1. 实现原理 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权,这就是用于 unique_ptr 的策略。 unique_ptr 中把拷贝构造函数和拷贝赋值声明为 private 或 delete,这样就不可以对指针指向进行拷贝了,也就不能产生指向同一个对象的指针。 2. 使用场景 如果程序不需要多个指向同一个对象的指针,则可以使用 unique_ptr; 如果使用 new [] 分配内存,应该选择 unique_ptr; 如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。
收起解题思路
78.请你说说 C++ Lambda 表达式用法及实现原理
解题思路
标准回答 Lambda 表达式语法: [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 { 函数体; }; 其中各部分的含义分别为: 1. [外部变量方位方式说明符] [ ] 方括号用于向编译器表明当前是一个Lambda 表达式,其不能被省略。在方括号内部,可以注明当前 Lambda 函数的函数体中可以使用哪些“外部变量”。所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。[外部变量]的定义方式: 外部变量格式:功能 [] :空方括号表示当前 lambda 匿名函数中不导入任何外部变量。 [=]:只有一个 = 等号,表示以值传递的方式导入所有外部变量; [&]:只有一个 & 符号,表示以引用传递的方式导入所有外部变量; [val1,val2,...] :表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序; [&val1,&val2,...]:表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序; [val,&val2,...] :以上 2 种方式还可以混合使用,变量之间没有前后次序。 [=,&val1,...]:表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。 [this] : 表示以值传递的方式导入当前的 this 指针。 2. (参数) 和普通函数的定义一样,Lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略; 3. mutable 此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 Lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。对于以值传递方式引入的外部变量,Lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量。 4. noexcept/throw() 可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,Lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 Lambda 函数内部可以抛出的异常类型。 5. -> 返回值类型 指明 Lambda 匿名函数的返回值类型。如果 Lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略 -> 返回值类型。 6. 函数体 和普通函数一样,Lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。 编译器实现 Lambda 表达式大致分为一下几个步骤: 1. 创建一个未命名的类,实现构造函数,使用 Lambda 表达式的函数体重载 operator()(所以 Lambda 表达式 也叫匿名函数对象) 2. 创建未命名的类的对象 3. 通过对象调用 operator()
收起解题思路
79.说一说什么是大端、小端,如何判断大端和小端
解题思路
得分点 字节序
标准回答
大端和小端指的是字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序。字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。
1. 大端字节序:是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处
2. 小端字节序:是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处
3. 如何判断大端还是小端:可以定义一个联合体,联合体中有一个 short 类型的数据,有一个 char 类型的数组,数组大小为 short 类型的大小。给 short 类型成员赋值一个十六进制数 0x0102,然后输出根据数组第一个元素和第二个元素的结果来判断是大端还是小端。 #include <stdio.h> int mn() { union { short value; char bytes[sizeof(short)]; } test; test.value = 0x0102; if((test.bytes[0] == 1) && (test.bytes[1] == 2)) { printf("大端字节序\n"); } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) { printf("小端字节序\n"); } else { printf("未知\n"); } return 0; }</stdio.h>
收起解题思路
80.简述 C++ 的内存管理
解题思路
得分点 代码区、未初始化数据区(BSS)、已初始化数据区(DATA)、栈区(Stack)、堆区(Heap) 标准回答 C++ 的内存分区主要有:代码区、未初始化数据区(BSS)、已初始化数据区(DATA)、栈区(Stack)、堆区(Heap) 1. 代码区 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。 2. 未初始化数据区 加载的是可执行文件 BSS 段,位置可以分开也可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。 3. 已初始化数据区(全局初始化数据区/静态数据区) 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。 4. 栈区 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 5. 堆区 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序,用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
收起解题思路
81.什么是纯虚函数,有什么作用
解题思路
得分点 概念、格式、特点、抽象类 标准回答 1. 概念、格式 纯虚函数是一种特殊的虚函数,它的格式是:虚函数不给出具体的实现,也就是后面没有大括号实现体,而在后面加上 "=0" class 类名 { virtual 返回值类型 函数名(参数列表) = 0; } 2. 作用 很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。例如猫类和狗类的基类是动物类,动物类中有一个吃饭的函数 eat(),那这个 eat() 函数可以是纯虚函数,因为并不能够确定动物吃的东西是什么,具体吃的内容由不同的派生类去实现。 3. 特点 如果一个类中有纯虚函数,那么这个类也被称为抽象类。这种类不能实例化对象,也就是不能创建该类的对象。除非在派生类中完全实现基类中所有的纯虚函数,否则派生类也是抽象类,不能实例化对象。
收起解题思路
82.请你说说虚函数和纯虚函数的区别
解题思路
得分点 定义格式、特点、作用 标准回答 1. 格式 虚函数的定义格式为:virtual 返回值类型 函数名(参数列表) {} 纯虚函数的定义格式为:virtual 返回值类型 函数名(参数列表) = 0; 2. 特点 虚函数可以有具体的实现,纯虚函数没有具体的实现。 对于虚函数来说,父类和子类都有各自的版本,由多态方式调用的时候动态绑定。 有纯虚函数的类称为抽象类,有纯虚函数的类不能实例化,派生类必须实现纯虚函数才可以实例化,否则也是抽象类。 3. 作用 虚函数是 C++ 中用于实现动态多态的机制。 很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。
收起解题思路
83.简述 vector 的实现原理
解题思路
得分点 动态数组、连续存储空间、扩容 标准回答 vector 是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问,由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
收起解题思路
84.请你说说 deque 的实现原理
解题思路
得分点
分段连续内存、中控器
标准回答
deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是 deque的存储空间的主体。
收起解题思路
85.请你说说 map 实现原理,各操作的时间复杂度是多少
解题思路
得分点 红黑树 标准回答 1. map 实现原理 map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AV L是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。 2. 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
收起解题思路
86.shared_ptr 怎么知道跟它共享对象的指针释放了
解题思路
得分点 引用计数 标准回答 share_ptr 底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。仅当最后一个指针过期时,才调用 delete。
收起解题思路
87.请你说说写时拷贝
解题思路
标准回答 写时拷贝顾名思义就是“写的时候才分配内存空间”,这实际上是一种拖延战术。传统的 fork() 系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单并且效率低下,因为它拷贝的数据或许可以共享,或者有时候 fork() 创建新的子进程后,子进程往往要调用一种 exec 函数以执行另一个程序。而 exec 函数会用磁盘上的一个新程序替换当前子进程的正文段、数据段、堆段和栈段,如果之前 fork() 时拷贝了内存,则这时被替换了,这是没有意义的。 Linux 的 fork() 使用写时拷贝(Copy-on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候,大大提高了效率。
收起解题思路
88.请你说说 extern 的作用,extern变量在哪个数据段,为什么要 extern C
解题思路
得分点 声明外部变量和函数、静态存储区(全局区)、BSS、DATA、C/C++ 混合开发 标准回答 extern 的作用有: 1. extern 可以置于变量声明或者函数声明前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其它文件中寻找其定义。 2. extern 变量表示声明一个变量,表示该变量是一个外部变量,也就是全局变量,所以 extern 修饰的变量保存在静态存储区(全局区),全局变量如果没有显示初始化,会默认初始化为 0,或者显示初始化为 0 ,则保存在程序的 BSS 段,如果初始化不为 0 则保存在程序的 DATA 段。 3. extern "C" 的作用是为了能够正确的实现 C++ 代码调用 C 语言代码。加上 extern "C" 后,会指示编译器这部分代码按照 C 语言(而不是 C++)的方式进行编译。由于 C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名。 这个功能十分有用处,因为在 C++ 出现以前,很多代码都是 C 语言写的,而且很底层的库也是 C 语言写的,为了更好的支持原来的 C 代码和已经写好的 C 语言库,需要在 C++ 中尽可能的支持 C,而 extern "C" 就是其中的一个策略。
收起解题思路
89.简述一下 C++ 中的内存对齐
解题思路
得分点 什么是内存对齐、内存对齐的原因、内存对齐的规则 标准回答 1. 什么是内存对齐 现代计算机中内存空间都是按照 字节(byte)划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数 k(通常它为4或8)的倍数,这就是所谓的内存对齐。 2. 内存对齐的原因 - 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的。某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。 3. 内存对齐的规则 - 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。可以通过预编译命令 #pragma pack(n),n = 1,2,4,8,16 来改变这一系数。 - 有效对其值:是给定值 #pragma pack(n) 和结构体中最长数据类型长度中较小的那个,有效对齐值也叫对齐单位。 - 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。 - 结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
收起解题思路
90.请你说说 set 的实现原理
解题思路
得分点 红黑树、排序 标准回答 set 底层使用红黑树实现,一种高效的平衡检索二叉树。 set 容器中每一个元素就是二叉树的每一个节点,对于 set 容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。 对 set 进行插入删除操作 都不会引起迭代器的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对 set的插入删除并不会改变原有内存中节点的改变。 set 中的元素都是唯一的,而且默认情况下会对元素进行升序排列。不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。
收起解题思路
91.请你回答一下智能指针有没有内存泄露的情况
解题思路
得分点 shared_ptr、循环引用、weak_ptr 标准回答 智能指针有内存泄露的情况:当两个类对象中各自有一个 shared_ptr 指向对方时,会造成循环引用,使引用计数失效,从而导致内存泄露。 为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
收起解题思路
92.请你说说左值、右值、左值引用、右值引用、右值引用的使用场景
解题思路
标准回答 1. 左值 在 C++ 中可以取地址的、有名字的就是左值 int a = 10; // 其中 a 就是左值 2. 右值 不能取地址的、没有名字的就是右值 int a = 10; // 其中 10 就是右值右值 3. 左值引用 左值引用就是对一个左值进行引用。传统的 C++ 引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址: int n; int * pt = new int; const int b = 101; int & rn = n; int & rt = *pt; const int & rb = b; const int & rb = 10; 4. 右值引用 右值引用就是对一个右值进行引用。C++ 11 新增了右值引用(rvalue reference),这种引用可指向右值(即可出现在赋值表达式右边的值),但不能对其应用地址运算符。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x + y 等表达式以及返回值的函数(条件是该函数返回的不是引用),右值引用使用 && 声明: int x = 10; int y = 23; int && r1 = 13; int && r2 = x + y; double && r3 = std::sqrt(2.0); 5. 右值引用的使用场景 右值引用可以实现移动语义、完美转发。
收起解题思路
93.请你说说动态库静态库的区别和优缺点
解题思路
得分点 命名方式、链接、内存、更新 标准回答 静态库和动态库的区别: 1. 命令方式不同 - 静态库命名 Linux : libxxx.a lib : 前缀(固定) xxx : 库的名字,自己起 .a : 后缀(固定) Windows : libxxx.lib - 动态库命名 Linux : libxxx.so lib : 前缀(固定) xxx : 库的名字,自己起 .so : 后缀(固定) Windows : libxxx.dll 2. 链接时间和方式不同 - 静态库的链接是将整个函数库的所有数据在编译时都整合进了目标代码 - 动态库的链接是程序执行到哪个函数链接哪个函数的库 静态库和动态库的优缺点: 1. 静态库优缺点 - 优点:发布程序时无需提供静态库,移植方便,运行速度相对快些 - 缺点:静态链接生成的可执行文件体积较大,消耗内存,如果所使用的静态库发生更新改变,程序必须重新编译,更新麻烦。 2. 动态库优缺点 - 优点:更加节省内存并减少页面交换,动态库改变并不影响使用的程序,动态函数库升级比较方便 - 缺点:发布程序时需要提供动态库
收起解题思路
94.说说 C 语言和 C++ 语言的区别
解题思路
得分点 面向对象和面向过程、重载、void、struct、函数参数默认值、内联函数、引用、动态内存分配、作用域 标准回答 C 语言和 C++ 语言的区别如下: 1. C 语言是面向过程的语言,而 C++ 支持面向对象,所以 C 语言自然没有面向对象的封装、继承、多态等特性,也不支持面向对象的一些语法; 2. C++ 支持函数重载,C 语言不支持; 3. C 程序中如果函数没有任何参数需要将参数定义为 void 以此来限定函数不可传递任何参数,如果不进行限定让参数表默认为空其意义是可以传递任何参数,在 C++ 中,不带参数的函数表示函数不能传递任何参数; 4. C 语言 struct 中不能有函数,而 C++ 语言 struct 中可以有函数; 5. C 语言函数参数不支持默认值,而 C++ 语言支持参数默认值; 6. C++ 语言支持内联函数,而 C 语言不支持; 7. C++ 语言支持引用,而 C 语言不支持; 8. C 语言采用 malloc 和 free 函数动态申请和释放内存,而 C++ 使用 new 和 delete 运算符; 9. C 语言中只有局部和全局两个作用域,而 C++ 中有局部、全局、类、名称空间作用域。
收起解题思路
95.简述一下 C++ 中的四种类型转换
解题思路
得分点 static_cast、dynamic_cast、const_cast、reinterpret_cast 标准回答 使用 C 风格的类型转换可以把想要的任何东西转换成我们需要的类型,但是这种类型转换太过松散,对于这种松散的情况,C++ 提供了更严格的类型转换,可以提供更好的控制转换过程,并添加 4 个类型转换运算符,使转换过程更规范:static_cast、dynamic_cast、const_cast、reinterpret_cast。 1. static_cast 静态转换 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换 - 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的 - 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的 用于基本数据类型之间的转换,如把 int 转换成 char,把 char 转换成 int。这种转换的安全性也要开发人员来保证 2. dynamic_cast 动态转换 dynamic_cast 主要用于类层次间的上行转换和下行转换 在类层次间进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的 在进行下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全 3. const_cast 常量转换 该运算符用来修改类型的const属性 常量指针被转化成非常量指针,并且仍然指向原来的对象 常量引用被转换成非常量引用,并且仍然指向原来的对象 注意:不能直接对非指针和非引用的变量使用 const_cast 操作符 4. reinterpret_cast 重新解释转换 这是最不安全的一种转换机制,最有可能出问题 主要用于将一种数据类型从一种类型转换为另一种类型,它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针
收起解题思路
96.weak_ptr 如何解决 shared_ptr 的循环引用问题?
解题思路
得分点 引用计数 标准回答 weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数,依此特性可以解决 shared_ptr 的循环引用问题。 weak_ptr 没有解引用 * 和获取指针 -> 运算符,它只能通过 lock 成员函数去获取对应的 shared_ptr 智能指针对象,从而获取对应的地址和内容。 不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。
收起解题思路
97.请你说说 const 的用法
解题思路
得分点 变量、指针、函数参数、成员、成员方法 标准回答 const 的用法有很多: 1. 用在变量身上,表示该变量只读,不能对它的值进行修改 const int a = 10; a = 20; // 编译会报错,因为 a 只读,不能对它进行修改 2. 结合指针一起使用 const int * p; // 常量指针 int * const p; // 指针常量 const int * const p; const int * p 是常量指针,表示指针变量 p 所指向的内容不能修改,指针变量 p 的内容可以修改; int * const p 是指针常量,表示指针变量 p 的内容不能修改,指针变量 p 所指向的内容可以修改; const int * const p 表示指针变量 p 的内容和所指向的内容都不可以修改。 3. const 用于函数参数 void foo(const int * p); void foo(const int & p); const 用于形参时说明形参在函数内部不能被改变,这是非常有用的,有时候函数参数传递指针或者引用,在函数内部不希望对指针和引用指向的数据进行修改,可以加上 const。 4. 在类中修饰成员方法,防止在方法中修改非 static 成员 class A { public: int a; void fun() const { a = 20; // 错误,const 修饰的成员方法中不能修改非静态成员变量 } } 5. const 修饰类的成员变量 class T { public: T() : a(10) { } private: const int a; static const int b; }; const int T::b = 20; 类的成员变量可以分为静态的和非静态的,如果 const 修饰的是静态的成员变量,可以在构造函数中对该变量进行初始化;如果 const 修饰的是静态的成员变量,则需要在类外对该变量进行初始化。
收起解题思路
98.请你说说C++引用的概念
解题思路
得分点 什么是引用、基本语法、注意事项、本质、应用 标准回答 1. 引用(Reference)是 C++ 相对于 C 语言的一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号(笔名),使用绰号(笔名)和本名都能表示一个人。 2. 基本语法 typename & ref = varname; 3. 使用引用的注意事项: - 引用必须引用合法的内存空间 - 引用在定义时必须初始化 - 引用一旦初始化后,就不能再引用其它数据 - 引用在定义时需要加上 &,在使用时不能加 &,使用时加 & 表示取地址 - 函数中不要返回局部变量的引用 4. 引用的本质是指针,低层的实现还是指针。
收起解题思路
99.说说内联函数和函数的区别,内联函数的作用
解题思路
得分点 inline、函数调用开销、寻址、展开代码、提高效率、宏定义 标准回答 1. 内联函数和函数的区别: - 内联函数比普通函数多了关键字 inline; - 内联函数避免了函数调用的开销;普通函数有调用的开销; - 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址; - 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句,如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行;普通函数没有这个要求。 2. 内联函数的作用: 因为函数调用时候需要创建时间、参数传入传递等操作,造成了时间和空间的额外开销。通过编译器预处理,在调用内联函数的地方将内联函数内的语句复制到调用函数的地方,也就是直接展开代码执行,从而提高了效率,减少了一些不必要的开销。同时内联函数还能解决宏定义的问题。
收起解题思路
100.请你说说虚函数可以是内联函数吗
解题思路
得分点 编译期、运行期 标准回答 1. 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。 2. 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联。 3. inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
收起解题思路
101.请你说说迭代器失效原因,有哪些情况
解题思路
标准回答 STL 中某些容器调用了某些成员方法后会导致迭代器失效。例如 vector 容器,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。 1. 序列式容器迭代器失效 对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。解决:erase 返回下一个有效的迭代器。 2. 关联式容器迭代器失效 对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。
收起解题思路
102.请你说说 auto 和 decltype 如何使用
解题思路
标准回答 C++11 提供了多种简化声明的功能,尤其在使用模板时。 1. auto 实现自动类型推断,要求进行显示初始化,让编译器能够将变量的类型设置为初始值的类型: auto a = 12; auto pt = &a; double fm(double a, int b) { return a + b; } auto pf = fm; 简化模板声明 for(std::initializer_list<double>::iterator p = il.begin(); p != il.end(); p++) for(auto p = il.begin(); p != il.end(); p++) 2. decltype decltype 将变量的类型声明为表达式指定的类型。 decltype(expression) var; decltype(x) y; // 让y的类型与x相同,x是一个表达式 double x; int n; decltype(x*n) q; decltype(&x) pd; template<typename t=""> void ef(T t, U u) { decltype(T*U) tu; }</typename></double>
收起解题思路
103.静态库和动态库如何制作及使用,区别是什么
解题思路
得分点 命名规则、制作指令、使用、区别 标准回答 1. 静态库的制作和使用 - 命名规则 Linux : libxxx.a lib : 前缀(固定) xxx : 库的名字,自己起 .a : 后缀(固定) Windows : libxxx.lib - 制作 a.gcc 获得 .o 文件 gcc xxx.c xxx.c -c b.将 .o 文件打包,使用 ar 工具(archive) ar rcs libxxx.a xxx.o xxx.o - 使用 静态库使用需要有库文件和头文件,编译程序时通过 “-l 静态库名” 参数进行编译。 2. 动态库的制作 - 命名规则 Linux : libxxx.so lib : 前缀(固定) xxx : 库的名字,自己起 .so : 后缀(固定) Windows : libxxx.dll - 制作 a.gcc 得到 .o 文件,得到和位置无关的代码 gcc -c –fpic/-fPIC a.c b.c b.gcc 得到动态库 gcc -shared a.o b.o -o libcalc.so - 使用 动态库使用需要有库文件和头文件,编译程序时通过 “-l 动态库名” 参数进行编译。在运行程序之前还需要配置动态库的加载路径,程序才能够正常运行。 3. 静态库和动态的区别 - 静态库 gcc 进行链接时,会把静态库中代码打包到可执行程序中,编译时加载;发布程序时无需提供静态库,移植方便;消耗内存,更新部署发布麻烦。 - 动态库 gcc 进行链接时,动态库的代码不会被打包到可执行程序中,运行时加载;发布程序时需要提供动态库;内存占用小,更新部署发布简单。
收起解题思路
104.请你说说索引怎么实现的 B+ 树,为什么选这个数据结构
解题思路
得分点 B+树、叶子节点建立连接 标准回答 索引本质上就是通过预排序+树型结构来加快检索的效率,而MySQL中使用InnoDB和MyISAM引擎时都使用了B+树实现索引。 它是一棵平衡多路查找树,是在二叉查找树基础上的改进数据结构。在二叉查找树上查找一个数据时,最坏情况的查找次数为树的深度,当数据量很大时,查询次数可能还是很大,造成大量的磁盘IO,从而影响查询效率; 为了减少磁盘IO的次数,必须降低树的深度,因此在二叉查找树基础上将树改成了多叉加上一些限制条件,就形成了B树; B+树中所有叶子节点值的总集就是全部关键字集合;B+树为所有叶子节点增加了链接,从而实现了快速的范围查找; 在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一键值的行记录时最多只需要2到4次 IO。这很不错,因为当前一般的机械磁盘每秒至少可以做100次IO,2~4次的IO意味着查询时间只需0.02~0.04秒。 在数据库中,B+树索引还可以分为聚集索引和辅助索引,但不管是聚集索引还是辅助索引,其内部都是B+树的,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息。
收起解题思路
105.虚析构函数有什么作用
解题思路
得分点 概念、防止内存泄露 标准回答 1. 概念 虚析构函数,是将基类的析构函数声明为 virtual class Base { public: Base() { } // 虚析构函数 virtual ~Base() { } } 2. 作用 虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄露。如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时就会造成内存泄露。
收起解题思路
106.请你说说什么情况会调用拷贝构造,什么时候会调用赋值操作
解题思路
得分点 初始化、函数参数值传递、返回局部对象、赋值 标准回答 1. 拷贝构造函数的调用时机 - 用一个对象初始化另外一个对象 - 对象以值传递的方式传递给函数参数 - 函数局部对象以值传递的方式从函数返回 2. 赋值操作的调用时机 - 将一个对象赋值给另外一个对象
收起解题思路
107.述一下 C++11 中的可变参数模板新特性
解题思路
得分点 概念、语法、模板参数包、展开参数包 标准回答 在 C++11 之前,类模板和函数模板只能含有固定数量的模板参数。C++11 增强了模板功能,它对参数进行了高度泛化,允许模板定义中包含 0 到任意个、任意类型的模板参数,这就是可变参数模板。可变参数模板的加入使得 C++11 的功能变得更加强大,能够很有效的提升灵活性。 1. 可变参数函数模板语法: template<typename... t=""> void fun(T...args) { // 函数体 } 模板参数中, typename(或者 class)后跟 ... 就表明 T 是一个可变模板参数,它可以接收多种数据类型,又称模板参数包。fun() 函数中,args 参数的类型用 T... 表示,表示 args 参数可以接收任意个参数,又称函数参数包。 2. 可变参数类模板语法: template <typename... types=""> class test; 3. 展开参数包的方式 - 可变参数函数模板可以采用递归方式、逗号表达式 + 初始化列表的方式展开参数包; - 可变参数类模板可以采用递归+继承的方式展开参数包。 加分回答 C++ 11 标准提供的 tuple 元组类就是一个典型的可变参数模板类,它的定义如下: template <typename... types=""> class tuple;</typename...></typename...></typename...>
收起解题思路
108.说说 C++ 中智能指针和指针的区别是什么?
解题思路
标准回答 1. 智能指针 如果在程序中使用 new 从堆(自由存储区)分配内存,等到不需要时,应使用 delete 将其释放。C++ 引入了智能指针 auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL)表明,需要有更精致的机制。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和 weak_ptr。所有新增的智能指针都能与 STL 容器和移动语义协同工作。 2. 指针 C 语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。 3. 智能指针和普通指针的区别 智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。指针是一种数据类型,用于保存内存地址,而智能指针是类模板。
收起解题思路