1.面向对象的特征?
封装、继承、多态。
2.一个C++源文件从文本到可执行文件经历的过程?
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码(编译原理相关,词法分析、语法分析、语义分析等),生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库打包连接成最终的可执行目标文件(或库文件以供其他程序使用)
3.new和malloc的区别?
new是一个操作符,而malloc()是一个库函数。
new会自动计算需分配的空间,malloc不行
new会调用构造函数,malloc不会
new是类型安全的,而malloc不是
malloc/free要库文件支持,new/delete则不要。
new返回指定类型指针,malloc返回void*指针,需要强制类型转换
new可以被重载,malloc不能
4.虚函数是什么以及其作用?
虚函数是允许被其子类重新定义的成员函数。可以实现用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员,从而实现多态。注意:构造函数不能为虚函数,但是析构函数可以为虚函数,并且虚析构函数可以防止父类指针销毁子类对象时不正常导致的内存泄漏。
5.虚函数表是什么?
虚函数是通过一张虚函数表来实现的。简称为V-Table。如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。[这篇博客](https://blog.csdn.net/u012630961/article/details/81226351)简单介绍了一下虚函数表的细节,个人觉得比较清晰,但是个别地方还需要仔细推敲消化。
6.什么函数不能声明为虚函数?构造函数为什么不能为虚函数?
普通函数(非成员函数)、构造函数、友元函数、静态成员函数、内联成员函数。
当派生类在创建对象的时候会调用基类的构造函数,但是如果基类的构造函数是虚函数的话,派生类的构造函数又会把基类的构造函数覆盖,所以无法进一步执行而出错。同时,虚函数通过虚函数表来实现,而指向虚函数表的指针也需要在对象实例化后创建,那么就违背了先实例化后调用的准则。
7.纯虚函数是什么?
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。带有纯虚函数的类为抽象类。析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
8.虚函数与纯虚函数的区别?
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
9.简单描述虚继承与虚基类?
为了避免多继承产生的二义性,在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
10.简单描述多态?
C++ 多态有两种:静态多态、动态多态。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。
11.C++内存分为哪几块?
栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
12.简单介绍内存池?
内存池是一种内存分配方式。通常我们习惯直接使用new、malloc申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
13.简单描述内存泄漏?
内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。
14.内存中的堆与栈有什么区别?
堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存。栈空间的内存是由系统自动分配,一般存放局部变量,比如对象的地址等值,不需要程序员对这块内存进行管理,栈是运行时的单位,而堆是存储的单位。堆中的共享常量和缓存 。栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
15.说几个C++11的新特性?
auto类型推导:让编译器通过初值推断变量的类型(auto定义的变量必须要有初始值),编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。
范围for循环:遍历给定序列的每个元素并对序列中的每个值执行某种操作。
lambda函数:用于定义并创建匿名的函数对象,以简化编程工作。
Override:override关键字保证了派生类中声明重写的函数与基类虚函数有相同的签名,可避免一些拼写错误
final 关键字:final限定某个类不能被继承或某个虚函数不能被重写。
空指针常量nullptr消除NULL的二义性问题。因为c++中NULL就是0,0 既可以表示整型,也可以表示一个空指针(void *)。nullptr有类型,且可以被隐式转换为指针类型。
线程支持、智能指针等
16.简单介绍智能指针?
智能指针:C++内存管理是一个令人很头疼的事情,尽管每次写完new都会写一个delete,但是如果程序还没有执行到delete的时候就跳转了或者函数返回了,那么就会导致内存泄漏,使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当类的实例超出了作用域的时候,就会自动调用其析构函数,析构函数会自动释放资源。
三种智能指针:unique_ptr,shared_ptr,weak_ptr。shared_ptr维护了一个指向control block的指针对象,来记录引用个数。weak_ptr用于避免shared_ptr相互指向产生的环形结构,造成的内存泄漏。weak_ptr count是弱引用个数;弱引用个数不影响shared count和对象本身,shared count为0时则直接销毁。
判断weak_ptr的对象是否失效有三种方法:1.expired():检查被引用的对象是否已删除。2.lock()会返回shared指针,判断该指针是否为空。3.use_count()也可以得到shared引用的个数,但速度较慢。
17.shared_ptr 和 unique_ptr区别?
unique具有唯一性,对指向的对象值存在唯一的unique_ptr。unique_ptr不可复制,赋值,但是move()可以转换对象的所有权,局部变量的返回值除外。与shared_ptr相比,若自定义删除器,需要在声明处指定删除器类型,而shared不需要,shared自定义删除器只需要指定删除器对象即可,在赋值时,可以随意赋值,删除器对象也会被赋值给新的对象。unique的实现中,删除器对象是作为unique_ptr的一部分,而shared_ptr,删除器对象保存在control_block中。
不能通过weak_ptr直接访问对象的方法,要先通过lock()转换为shared_ptr。
18.内联函数和宏定义的区别?
宏定义在预处理的时候进行简单的字符串替换,而内联函数在编译时在每个调用内联函数的地方将函数展开,这样不用使内联函数占用栈空间,提高效率。宏定义没有类型检查,但是内联函数还是具有函数的性质,有参数以及返回值。
19.简述几大基本排序?
冒泡排序,选择排序,插入排序,归并排序,快速排序,堆排序。(详细略)
20.快速排序最好和最坏的时间复杂度?
最好:O(nlogn) 每次选择的基准数是序列按大小排完序后最中间的数。最坏:O(n^2)每次选择的基准数是按大小排完序后两端的其中之一。
21.快速排序最坏情况如何改进?
可以选取区间中随机的一个数作为基准数取代选取第一个元素,也可以选取首元素、末元素以及中间元素的中间大的元素作为基准数。
22.struct与union的区别?
1).struct可以存储多个成员变量信息;而union每个成员会共用同一个存储空间。
2).系统分配给union的内存size就是size最大的成员所需空间大小,struct在分配空间时,根据成员类型不同,会存在字节对齐情况,具体对齐标准和机器有关,可能是4字节或8字节等对齐方式。
3).在任何同一时刻,union值存放了一个被先选中的成员,而结构体struct的所有成员都存在。
23.说说什么是内存对齐?字节对齐的规则是什么?
尽管内存是以字节为单位的,但是大部分处理器并不是以字节来存取数据,一般会以四字节、八字节或更长的单位来取内存。使用内存对齐可以保证每次取内存都是访问块内存地址首部以提高存取效率。
字节对齐规则:
1)结构体中每个变量首地址的偏移量必须能够被其有效对齐值 min(变量自身对齐值, 编译器指定对齐值) 整除。
2)结构体的自身对齐值为结构体中最宽变量的大小,结构体的大小必须被其有效对齐值 min(结构体的自身对齐值, 编译器指定对齐值) 整除。
24.空类的sizeof大小是多少?
空类的sizeof大小为1字节,因为空类同样可以被实例化,那么为了唯一标识实例地址,会隐含添加一个字节。
25.空类、含有虚函数的类、派生类、虚继承中的派生类、多重继承中的派生类等 的sizeof大小?
对于这个问题可详细参考图解C++对象模型https://www.cnblogs.com/QG-whz/p/4909359.html。
这里做简单回答:含有虚函数的类的实例由于需要有一个指针指向虚表,所以需要额外的4字节(32位机器)存放虚表指针。虚继承中派生类需要存放一个指向虚基类的指针同样需要4字节(32位机器)。注意计算时要考虑内存对齐。
26.free()一个指针两次,会出现什么问题 ?
free一次后,原来指针所指向的堆中的内容已经被清空了,但指针本身的值并没有被置为null,还是指向原来它所指向的内存空间,然后你再free一次时,由于堆中的内容已经是无效的东西,所以就会出错。不过,有的编译器在free时并没有清理堆中的内存,有时你对它free两次也不一定出错。不过这是一个很大的隐患,在实际写代码中千万要注意避开这点。
27.指针越界一定会出现错误吗 ?
不一定,越界后如果没有造成堆栈破坏就不会使程序运行出错,但是越界会带来程序结果出错,可能使指针指向其他本不应该指向的地方。
28.C++四种类型转换符各自的作用?
static_case: 1)在基本数据类型之间转换,如把 int 转换为 char,这种带来安全性问题由程序员来保证;2)在有类型指针与 void * 之间转换;(不能使用 static_cast 在有类型指针内转换。)3)用于类层次结构中基类和派生类之间指针或引用的转换。上行转换(派生类---->基类)是安全的;下行转换(基类---->派生类)由于没有动态类型检查,所以是不安全的。
dynamic_cast: 用于将一个父类的指针/引用转化为子类的指针/引用(下行转换)。基类必须要有虚函数,因为 dynamic_cast 是运行时类型检查,需要运行时类型信息,而这个信息是存储在类的虚函数表中。
const_cast: 常量指针(或引用)与非常量指针(或引用)之间的转换。
reinterpret: 用在任意指针(或引用)类型之间的转换。能够将整型转换为指针,也可以把指针转换为整型或数组。
29.浅拷贝是什么?深拷贝是什么?
浅拷贝:在拥有指针成员的类中,一个对象利用拷贝构造函数或者赋值函数拷贝或者赋值给另一个对象的时候,直接将这个对象的指针成员赋值给另一个对象的指针成员,将一个指针赋值给另一个指针,就会使两个指针指向同一个空间,这就产生了浅拷贝。浅拷贝会造成一些问题例如内存泄漏、同一片内存释放多次、一个指针修改了这块空间的值那么另一个指针也指向这块空间就会出错。
深拷贝:在拷贝构造函数或赋值函数中不是直接的将指针赋给另外一个对象的指针,而是新开辟一块内存空间,将被拷贝或赋值的对象的指针成员指向新开辟的内存空间,然后再将数据拷贝过去。
30.简单介绍布隆过滤器?
当存在大量数据内存放不下时,我们想知道一个数据是否在这些大量的数据里面的时候,就会使用布隆过滤器,一般用于字符串。当我们要映射一个值到布隆过滤器中时,我们需要使用多个不同的哈希函数生成多个不同的哈希值,并将每个生成的哈希值指向的 bit置为 1。但是可以想到,这样的方法会导致误判,误判是不能解决的,只能缓解。布隆过滤器判断的结果不存在是准确的,存在是不准确的。
31.hash表解决冲突的方法?
开放地址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
链地址法:基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
建立公共溢出区:这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
再哈希法: 再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
32.跳表插入删除过程?
插入时对该节点随机一个层数,当然层数有最大限制,在跳表中从高层往底层进行查找,找到插入位置之后修改插入节点的前后指针即可完成插入,删除同理。
33.新建一个空类,里面有什么函数?
无参构造函数,析构函数,拷贝构造函数,重载赋值运算符函数。
34.怎么判断两个结构体变量是否相等?
不能用函数memcmp来判断两个结构体是否相等:memcmp函数是逐个字节进行比较的,而struct存在字节对齐,同样的成员可能比较的结果不一样。可以通过重载==来判断相等。
35.可以使用static const成员函数吗?
不可以,const修饰成员函数其实修饰的是this指针,代表不可以通过函数来修改对象,但是static修饰的成员函数属于类,压根就不会有this指针。
36.volatile关键字的作用?
volatile 关键字告诉编译器该关键字修饰的变量是随时可能发生变化的,每次使用它的时候必须从内存中取出它的值,因而编译器生成的汇编代码会重新从它的地址处读取数据放在左值中。这样看来,如果该变量是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说volatile 可以保证对特殊地址的稳定访问。
37.构造函数可以抛出异常吗?析构函数呢?
构造函数可以抛出异常。C++标准指明析构函数不能、也不应该抛出异常。如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。
38.Float 和double在内存中的存储方法?
浮点数在内存中以二进制的科学计数法表示,表达式为N = 2^E * F;其中E为阶码(采用移位存储),F为尾数。float和double都由符号位、阶码、尾数三部分组成,float存储时使用4个字节,double存储时使用8个字节。
39.静态链接与动态链接?
静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时链接。
静态链接浪费空间 ,这是由于多进程情况下,每个进程都要保存静态链接函数的副本。更新困难 ,当链接的众多目标文件中有一个改变后,整个程序都要重新链接才能使用新的版本。但是静态链接运行效率高。
动态链接当系统多次使用同一个目标文件时,只需要加载一次即可,节省内存空间。程序升级变得容易,当升级某个共享模块时,只需要简单的将旧目标文件替换掉,程序下次运行时,新版目标文件会被自动装载到内存并链接起来,即完成升级。
40.为什么要有lambda表达式?
用于定义并创建匿名的函数对象,以简化编程工作。
41.什么是std::move()以及什么时候使用它?
std::move()是C ++标准库中用于转换为右值引用的函数。当需要在其他地方“传输”对象的内容时,可以使用move,而无需复制。 使用std :: move,对象也可以在不进行复制(并节省大量时间)的情况下获取临时对象的内容。避免不必要的深拷贝。
42.Static的用法?
1)静态局部变量:作用域只在函数内部,但是在全局静态存储区分配内存,也就是说生存周期随着程序运行结束而结束。会在第一次被执行的时候初始化,以后的函数调用不再初始化。
2)全局静态变量/static修饰的函数:隐藏。该变量/函数不能被其他文件引用。
3)静态数据成员:只会被初始化一次,与实例无关,并且只能在类外初始化。存储在全局静态区。
4)静态成员函数:用于修饰类的成员函数。只能访问静态数据成员或静态成员函数。
43.Const的用法?
1)const修饰变量:存储在常量区,不允许被修改。修饰指针变量分为指向常量的指针和指针本身是常量。如果修饰的是类的成员变量那么必须在类中定义的时候初始化或者在构造函数的初始化列表中初始化。
2)const修饰函数:如果修饰返回值表示返回值不可被修改。如果修饰成员函数表示不可在函数中修改任何成员变量。
3)const修饰对象:表示这个对象是一个常量,在初始化的时候要对所有成员变量都初始化。共有变量只能读,并且只能调用const成员函数。
44.如果const成员函数想要改变成员变量怎么办?
1)mutable修饰的成员变量可以突破const的限制,在被const修饰的函数里也能被修改。
2)可在函数内使用const_cast将this指针转化为class*const类型也就是表示指向的对象可以修改。
45.左值和右值的区别?
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
46.说一说移动语义(move)和完美转发(forward)?
Move作用是无论给move传递了一个左值引用还是一个右值引用,最终返回的,都是一个右值引用。forward指的是函数模板在向其他函数传递自身形参的时候,如果相应的实参是左值,那么它就应该被转发为左值,如果相应的实参是右值,那么它就应该被转发为右值。为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)的可能性。讲的比较仔细的一篇博客https://www.cnblogs.com/catch/p/3507883.html。就是看得头有点大。
47.模板是什么?使用模板编程实现快排?
模板是创建泛型函数或者类的一个工具,可以对类型进行参数化。使用模板的目的就是编写与类型无关的代码。利用模板简单实现快排https://paste.ubuntu.com/p/ZKYrFHwjCr/,和普通的快排几乎没什么区别,就是将类型作为参数传进来之后使用。
48.malloc底层原理?
参考博客https://www.cnblogs.com/zpcoding/p/10808969.html#_labelTop。
1)当开辟的空间小于128K的时候,执行系统调用brk(),其主要移动指针_edata(地址空间中堆段的末尾地址)。
2)当开辟的空间大于128K的时候,执行mmap()系统调用函数来在堆和栈中间称为“文件映射区域”的地方找一块空间来开辟。
49.C++如何调用C函数,C++和C混合编译?
在函数前面添加extern”C”。
50.c++如何判断内存是否泄露?
检测内存泄漏的关键原理就是,检查malloc/new和free/delete是否匹配,一些工具也就是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。
51.虚继承的实现原理?
讲得很仔细的博客https://blog.csdn.net/xiejingfa/article/details/48028491。虚继承之所以可以实现在多重派生子类中只保存一份共有虚基类拷贝,关键在于虚基类表指针,该指针指向了一个虚基类表,虚基类表中记录了虚基类表指针与本类的偏移地址和虚基类表指针到共有基类元素之间的偏移量,这样就维持了一份基类的拷贝。
52.placement new是什么?
placement new是operator new的一个重载版本,如果你想在已经分配的内存中创建一个对象,使用new是不行的。也就是说placement new允许你在一个已经分配好的内存中(栈或堆中)构造一个新的对象。它只是调用了构造函数,返回一个指向已经分配好的内存的一个指针,所以对象销毁的时候不需要调用delete释放空间,但必须调用析构函数销毁对象。
53.inline函数优点、缺点?
优点:1)inline定义的内联函数,函数代码被放入符号表中,在使用时进行替换(像宏一样展开),效率很高。2)类的内联函数也是函数。编绎器在调用一个内联函数,首先会检查参数问题,保证调用正确,像对待真正函数一样,消除了隐患及局限性。3)inline可以作为类的成员函数,可以使用所在类的保护成员及私有成员。
缺点:内联函数以复制为代价,活动产生开销1)如果函数的代码较长,使用内联将消耗过多内存 , 这种情况编译器可能会自动把它作为非内联函数处理2)如果函数体内有循环,那么执行函数代码时间比调用开销大。
54.explicit作用?
explicit用来防止由构造函数定义的隐式转换。隐式转换:可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换,也就是当一个函数的参数是一个拥有只具有一个参数的构造函数的类的对象时,可以通过单个实参来进行隐式转换成该类的对象。
55.new可以用free释放吗?
不可以,虽然语法上不会报错,但是逻辑上是有问题的,delete会调用析构函数,而free不会调用虚构函数,所以new/delete、malloc/free必须成对使用。
56.new低层实现?
底层调用malloc函数分配内存,然后调用构造函数。
57.unique_ptr可以拷贝吗?
不能,因为拷贝构造函数被禁用了。
58.怎么使一个类不能继承?
继承的时候子类在构造的时候要先执行父类的构造函数,所以只要让类的构造函数和析构函数都设置成private,编译器就会告诉我们这个类不能被继承了。
59.对void*你了解什么?
void* 是一个特殊的指针,它的意义为无类型指针,他可以被赋值为任意类型的指针,但是仅限当作一个指针使用。它不能直接被 * 访问地址的内容,因为还不知道具体要读多少个字节。
60.静态链接、动态链接具体做了什么?
静态链接是以目标文件为单位的,将各个目标文件连接起来形成可执行文件。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
61.编译期间的错误、链接期间的错误?
编译时期:变量使用前没有声明、书写代码时,括号不匹配、代码行末尾缺少分号、使用保留关键字作为变量名或程序名等。
链接时期:程序中使用某些函数只有声明,没有定义、找不到某些系统文件等。
62.什么叫全特化?偏特化呢?
全特化就是模板中的参数全被指定成了确定的类型。
偏特化就是模板中的模板参数没有被全部确定,需要编译器在编译的时候确定。
63.一个模板类在不同特化之后,得到的类还是不是同一个类?
不是同一个类,不论是特化还是泛型只要参数不同就不是同一个类。
64.new一个对象背后发生了什么?
1)调用operator new,给对象分配内存。
2)调用对象的构造函数。
3)返回指针。