C++相关知识汇总
(20210822更新)
Q1:C++的三个特性是什么?怎么理解?
Q2:多态的底层是怎么实现的?
Q3:构造函数里可以调用虚函数吗?
Q4:构造函数可以是虚函数吗?
Q5:静态函数可以是虚函数吗?
Q6:虚函数的安全性有什么问题吗?
Q7:析构函数可以是虚函数吗?
Q8:如果析构函数不是虚函数,一定会发生内存泄漏么?
Q9:重载、隐藏、重写(覆盖)三者的区别是什么?
Q10:纯虚函数和虚函数的区别是什么?
Q11:友元函数可以是虚函数吗?
Q12:一个C++编译的程序,内存分为哪几个部分?
Q13:堆和栈的区别有哪些?
Q14:new-delete与malloc-free的区别是什么?
Q15:new operator和operator new的区别是什么?placement new知道么?
Q16:什么是深复制什么是浅复制?
Q17:内存泄漏是如何引发的?怎么处理?
Q18:智能指针都有哪几种?
Q19:进程、线程、纤程有什么区别?
Q20:线程间同步有哪些方式?
Q21:进程间通信有哪些方式?
Q22:简述对TCP/UDP协议的理解
Q23:TCP协议为什么要三次握手/四次分手?
Q24:简述一次http请求的完整过程
Q25:指针和引用的区别是什么?
Q26:C++容器都有哪些?底层是怎么实现的?
Q27:什么是红黑树?
Q28:为什么map底层要用红黑树实现而不用AVL实现?
Q29:C++有哪些新特性?
Q30:static变量的意义和内存分配是怎样的?static静态成员函数呢?
Q31:顶层const和底层const是什么意思?
Q32:volatile关键字的意义是什么?
Q33:四种类型转换符旨在解决什么问题?
Q34:什么是C++的RTTI机制?
Q35:class类的内存是怎样分布的?
Q36:设计模式有哪些?是如何实现的?
Q37:常见的IO模型有哪些?
——————————————————————我是分割线—————————————————————————
Q1:C++的三个特性是什么?怎么理解?
A1:封装、继承、多态。
封装:在面向对象的思想中,将数据和对数据的操作包装在一个类里,只对外开放接口将内部细节隐藏,达到数据的抽象性、隐藏性、封装性。
继承:保持原有的特性基础上进行扩展,是设计层面的复用。
多态:同一个名字的函数可以有不同的功能。编译时的多态:函数的重载,函数名相同但参数列表不同。子类对象引用自身类实例的方法也是编译时多态。运行时的多态:存在继承关系的类,通过父类的指针或引用,调用了一个在父类中是virtual类型的函数,实现动态绑定机制。要求:①父类函数必须为虚函数;②子类重写函数名称、列表、返回值和父类均相同。
Q2:多态的底层是怎么实现的?
https://coolshell.cn/articles/12165.html#%E5%AE%89%E5%85%A8%E6%80%A7
A2:编译时多态:C++的编译器编译后的函数名会含有参数列表,保证根据传参不同调用不同的重载函数。(如果不想让C++编译器含参数列表编译,就加上extern C,会按照C来编译,就不能实现多态了,很多时候用C++调用C的头文件,是因为编译方式不同导致找不到函数)
运行时多态:基类会在编译阶段形成虚函数表,放在内存的.rdata只读数据段。类结构的前四个字节是指向虚函数表的指针,派生类会继承父类的虚函数表,如果是继承多个父类就是继承多个虚函数表。如果是重写,那么派生类的重写后函数会覆盖派生类虚函数表中的基类虚函数,如果不是重写,则会在派生类虚函数表后面追加派生类的函数地址。
Q3:构造函数里可以调用虚函数吗?
A3:可以但没有意义。派生类要等父类构造完才能构造,C++规定为了避免内存异常在父类构造的时候,虚构造函数会被当做普通函数不管派生类里是否重写。
Q4:构造函数可以是虚函数吗?
A4:不可以。使用虚函数的目的是多态,构造函数是用来构造所在类的对象,不会用于构造派生类对象,不会被继承。另外,构造函数中会产生虚函数表,如果构造函数本身就是虚函数,虚函数表还没有产生,虚函数地址无法存放。
Q5:静态函数可以是虚函数吗?
A5:不可以。static成员不属于任何类对象或类实例,static成员也无法访问this指针,访问不了虚函数表。
Q6:虚函数的安全性有什么问题吗?
A6:虚函数本质是通过指针访问虚函数表里的虚函数,通过改变指针的地址父类指针也可以访问子类的自有虚函数,另外,即便是父类的虚函数是私有函数or保护函数,通过改变指针的地址也可以被访问,二者都会产生安全问题。
Q7:析构函数可以是虚函数吗?
A7:析构函数必须是虚函数!编译器是通过指针的类型来析构的,子类在析构的时候需要用子类的析构函数,如果父类的析构函数不是虚函数,那通过父类指针析构的子类对象将以父类的析构函数来析构子类。
Q8:如果析构函数不是虚函数,一定会发生内存泄漏么?(本题答案待考证)
A8:不一定。如果子类没有动态分配内存,那么栈上的内存会自动释放,不会产生内存泄漏。Q9:重载、隐藏、重写(覆盖)三者的区别是什么?
A9:
重载:是指同一可访问区内被声明的几个具有不同参数列表(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型(统一为void,否则报错)。
隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
Q10:纯虚函数和虚函数的区别是什么?
A10:纯虚函数是没有被定义的函数,在基类里只进行了声明,必须要在派生类里实现才可以使用。普通的虚函数可以在子类中被重写使用,如果被重写,那么基类指针会按照产生的实例来调用相应的子类重写函数,如果没有被重写,那么基类指针调用的是自身的虚函数。
Q11:友元函数可以是虚函数吗?
A11:友元不是成员函数,只有成员函数才可以是虚拟的,因此友元函数不能是虚函数。但可以通过让友元函数调用虚成员函数来解决友元的虚拟问题。
Q12:一个C++编译的程序,内存分为哪几个部分?
A12:可以分为栈区、堆区、全局区、常量区和代码区。
栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
堆区(heap):一般由程序员分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
常量区(文字常量区):存放常量字符串,程序结束后由系统释放。
代码区(自由存储区):存放函数体(类成员函数和全局区)的二进制代码。
Q13:堆和栈的区别有哪些?
A13:
1.堆和栈的生长方向不同
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
2.堆会随使用产生碎片,栈不会
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列
3.管理方式不同
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
4.堆未及时释放容易造成内存泄漏
5.堆都是动态分配的,栈可以动态分配可以静态分配
静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
6.分配效率
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,因此栈的效率比较高。
堆是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
7.应用场景
堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,程序开发时应尽可能使用栈。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
Q14:new-delete与malloc-free的区别是什么?
A14:new-delete是运算符,而malloc-free是库函数,malloc-free不能进行构造和析构。
new-delete的功能可以完全覆盖malloc-free,但由于C++会调用C函数,而C只能使用malloc-free,因此保留了malloc-free。很多编译器的new/delete都是以malloc/free为基础来实现的,借以malloc实现的new。
|
malloc-free |
new-delete |
|
|
内存分配位置 |
堆 |
自由存储区 |
|
分配成功返回值 |
void* |
对应类型指针 |
|
分配失败返回值 |
NULL |
抛出异常 |
|
分配内存大小 |
用户计算 |
自动 |
|
处理数组 |
× |
√ |
|
已分配内存的扩充 |
√ |
× |
|
互相调用 |
× |
√ |
|
分配内存时内存不足 |
用户不可处理 |
用户可重新申请 |
|
重载 |
× |
√ |
|
调用构造/析构 |
× |
√ |
Q15:new operator和operator new的区别是什么?placement new知道么?
A15:new就是new operator,使用new创建新对象的时候进行了三件事:①使用operator new分配内存,底层调用malloc实现;②调用构造函数;③返回相应类型的指针。
placement new保持一块内存,反复构造析构,这样可以省略中间的多次分配内存,如果想自己管理内存可以使用。
Q16:什么是深复制什么是浅复制?
A16:复制构造函数这种,只复制了指针没有开辟新的内存的复制是浅复制,而开辟内存将原对象的相关值复制到新的内存中的复制是深复制。当进行浅复制,其中一个对象析构,浅复制过来的指针所指的内存空间也会被释放,此时如果有其他使用该内存的对象将会引发错误。解决方法是定义开辟空间的构造函数进行深拷贝。
Q17:内存泄漏是如何引发的?怎么处理?
A17:内存泄漏源于new出的内存没有全部被delete掉导致的内存消耗,使用智能指针能将内存泄漏风险降至最低。智能指针利用C++ RAII(Resource Acquisition Is Initialization,资源获取就是初始化)原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。(std::mutex互斥锁就是用的这个原则,退出局部变量使用域的时候自动释放锁)
Q18:智能指针都有哪几种?
A18:
1.auto_ptr(旧)--> unique_ptr(C++11)
在构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使用起来就像普通的指针。独享资源所有权。由于其构造函数声明为explicit的,因此不能通过隐式转换来构造,只能显示调用构造函数。
2.share_ptr(C++11)
允许限定的资源被多个指针共享。
3.weak_ptr
weak_ptr是一种用于解决shared_ptr相互引用时产生死锁问题的智能指针。如果有两个shared_ptr相互引用,那么这两个shared_ptr指针的引用计数永远不会下降为0,资源永远不会释放。weak_ptr是对对象的一种弱引用,它不会增加对象的use_count,weak_ptr和shared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptr,weak_ptr也可以通过调用lock函数来获得shared_ptr。
Q19:进程、线程、纤程有什么区别?
A19:进程是CPU的基本工作单元,按照就绪、运行、阻塞三种状态进行程序处理。线程是CPU的基本调度单元,进程是线程的容器,在CPU切换到基本的进程单元后,进程想提高CPU的利用率,可以采用多线程,分食CPU的时间分片。纤程又名协程,是由程序控制的使用更小时间分片的工作单位,在go和lua上能够比较好的实现,python也有关键字支持,Java的Kilim框架模拟出了纤程的功能,C++貌似还不太方便。
Q20:线程间同步有哪些方式?
A20:互斥锁、条件变量、读写锁、信号量。
Q21:进程间通信有哪些方式?
A21:管道(父子进程)、有名管道、信号、信号量、消息队列、共享内存、套接字。
Q22:简述对TCP/UDP协议的理解(TCP-reno/cubic)
A22:TCP,Transmission Control Protocol,传输控制协议。面向连接、可靠、基于字节流。牺牲效率保证传输的正确,应用层:FTP、HTTP、SMTP、POP3等协议为了保证传输正确都基于TCP协议。
UDP,User Datagram Protocol,用户数据报协议。无连接、不可靠、基于报文。没有可靠性保证但效率非常高,通常用于实时数据传输,应用层:NFS、TFTP、DHCP、DNS等协议为了能更快传输都基于UDP协议。基于UDP的文件传输系统仅适用于传输小文件,因为大文件有丢失的风险。
Q23:TCP协议为什么要三次握手/四次分手?
A23:三次握手是为了上下行信道确认,四次分手同理的同时还要避免有一方仍要发送数据。
Q24:简述一次http请求的完整过程
A24:(1)域名解析。浏览器先查找是否有网址映射关系,查找流程如下:本地host文件àDNS解析器缓存à本地DNS服务器à上一层DNS服务器à一直向上转发知道查找到相应的IPà结果返回本地DNS服务器à返回客户机。(2)寻址。ARP协议广播寻找目的IP的下一跳,由存在匹配IP(IP+掩码)的设备返回MAC地址,作为路径的下一跳缓存在设备的路由表中。(3)TCP协议三次握手。(4)发送http消息。(5)http服务器返回响应(页面文件)。(6)TCP四次挥手。
Q25:指针和引用的区别是什么?
A25:指针是指向对象的地址,引用是对象本身。对指针进行处理(++/--等)是改变指针指向,而对引用进行处理是改变对象的值。
Q26:C++容器都有哪些?底层是怎么实现的?
A26:基本容器有vector,deque,list,map,set等。
vector:动态数组,空间连续,如果扩容一般按照50%的空间扩容,之后全部拷贝过去,所以如果不停地需要扩容效率就很低。查询因为是可以索引,效率很高。vector类中是按照first、last、end三个指针实现的,myfirst是容器/元素顶部,mylast是元素尾部,myend是容器尾部,如果元素数量超过end就需要扩容。扩容的事情,如果你提前知道大概要多大的空间,可以直接先用reserve预留出来,省的每次扩容都要拷贝。
deque:deque中的每一段连续空间分布在内存的不连续空间上,然后用一个所谓的map作为主控,记录每一段内存空间的入口,从而做到整体连续的假象。
list:双向链表,插入简单,没有空间预留,即使即分配,查找效率低。
set:底层是红黑树,可以自动排序,插入删除时需要点时间整理树。
map:底层红黑树,key值自动排序,插入需要整理红黑树。
Q27:什么是红黑树?
A27:红黑树是为了弥补二分搜索树多次单边插入导致不平衡而引入的解决办法。
红黑树首先是一个平衡二叉树,满足根节点大于左子树小于右子树。
红黑树特点:
1.根节点和叶子节点(都是空节点)都是黑色。
2.每个红色节点的两个子节点都是黑色。
3.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
4.每次插入和删除都通过变色和旋转来保证红黑树符合上面的规则。
Q28:为什么map底层要用红黑树实现而不用AVL实现?
A28:AVL树是高度平衡,而红黑树通过增加节点颜色从而实现部分平衡,这就导致,插入节点两者都可以最多两次实现复衡,而删除节点,红黑树最多三次旋转即可实现复衡,旋转的量级是O(1),而avl树需要维护从被删除节点到根节点这几个节点的平衡,旋转的量级是O(logn),所以红黑树效率更高,开销更小,但是因为红黑树是非严格平衡,所以它的查找效率比avl树低。RB-Tree是功能、性能、空间开销的折中结果。
Q29:C++有哪些新特性?
A29:右值引用,移动语义,完美转发,智能指针,lambda 匿名函数
Q30:static变量的意义和内存分配是怎样的?static静态成员函数呢?
A30:static变量是为了让一个类的所有对象能够共享数据,static静态变量在类体外进行初始化,当执行到相关语句时才进行内存空间的分配,内存空间在全局区,作用域为本文件。static静态函数只能用于访问static变量和其他static函数,即便没有创建对象,static静态成员函数都可以被调用。
Q31:顶层const和底层const是什么意思?
A31:const在*的左边是底层const,例如:int iTmp=10;const int *a=&iTmp;(或者int const *a=&iTmp,这两种表述一样,只看const和*的相对位置)代表指针a所指的值10是不可以被改变的,可以使用iTmp=9,但是不可以*a=9.但是指针指向可以改变,例如:int iTmp2=9;a=&iTmp2;
const在*的右边是顶层const,例如:int iTmp=10;int * const a=&iTmp;代表指针的指向不能改变,但是可以通过指针修改指针所指的值。
Q32:volatile关键字的意义是什么?
A32:多线程程序中当多个线程都会更改某一个变量,应该用volatile声明,保证每次数据的更改都是从内存同步的,而不是从寄存器中取出来的。
Q33:四种类型转换符旨在解决什么问题?
A33:为了解决C风格的转换随意和提供关键字区分,C++设立了四种类型转换符:
static_cast<newType>(data);①近似类型转换,intàdouble,shortàint,constà非const,向上转型(派生类à基类)②void*转型(malloc),void *àint *,char *àvoid*等。③【这里没看懂】有转换构造函数或者类型转换函数的类与其它类型之间的转换,例如 double 转 Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。
const_cast<newType>(data);用于const/volatileà非const/volatile
reinterpret_cast<newType>(data);不相关的类型转换,从底层对数据进行重新解释,高危操作。
dynamic_cast<newType>(data);既可以向上转型(派生类à基类)又可以向下转型(基类à派生类),向下转型需要借助RTTI机制检测,必须是安全的才能成功。
Q34:什么是C++的RTTI机制?
A34:RTTI:Run Time Type Identification,即运行时类型识别。比如运行时,要根据用户的输入选择父类指针的指向,输入小于100指针指向父类函数,大于等于100指向子类函数。在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。
Q35:class类的内存是怎样分布的?
A35:1.如果实例化一个空类,内存中只占用一个字节,作为标识。
2.包含成员变量不包含成员函数时,根据内存对齐原则,将各个成员变量分布进内存。
3.就算包括成员函数,成员函数是不占用类的内存空间的,所以此时类的内存空间仍然和只包含成员变量的时候一样。而成员函数因为是公共的,一个类只有一份,一般根据编译器的不同保存在代码区或者只读区。
4.如果有虚函数的话,前四个字节会有一个虚函数表指针指向虚函数。(上一节面经有讲到),注意子类的虚函数表会先拷贝父类的表,然后替换和父类中一样函数的,最后补上子类自身的函数。
Q36:设计模式有哪些?是如何实现的?
A36:
1.单例模式
主要思想à构造函数私有化+指针禁止+复制构造禁止+线程安全+自动释放

线程安全的原因↓
https://stackoverflow.com/questions/34457432/c11-singleton-static-variable-is-thread-safe-why
2.简单工厂模式&工厂方法模式&抽象工厂模式
(1)简单工厂模式
思想:告诉工厂类生产什么,工厂类根据传参判断生产什么,生产的产品有共同的特点。例:球厂,篮球足球排球……
缺点:工厂类集中了所有产品类的创建逻辑,如果产品量较大,会使得工厂类变的非常臃肿。
(2)工厂方法模式
思想:每个工厂生产一种产品,生产那种产品就先造哪个工厂
缺点:产品类数据较多时,需要实现大量的工厂类,这无疑增加了代码量。
(3)抽象工厂模式
思想:一个工厂生产多种产品,生产那种产品就先造哪个工厂
缺点:当增加一个新系列的产品时,不仅需要现实具体的产品类,还需要增加一个新的创建接口,扩展相对困难。
(4)
未完待续
Q37:常见的IO模型有哪些?
A37:
同步阻塞IO-BIO:等待请求成功后返回
同步非阻塞IO-NIO:请求后立刻返回,不管成功与否,需要多次询问才能达到成功的结果
异步阻塞IO-IO多路复用:由select/epoll等多路复用器进行阻塞,任意文件描述符就绪则返回,关联Reactor设计模式
异步非阻塞IO-AIO:是不是真正的异步IO是按照是否由用户线程自行读取数据、处理数据区分的。当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。关联Proactor设计模式。
可惜的是,在 Linux 下的异步 I/O 是不完善的, aio 系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的 socket 是不支持的,这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。而 Windows 里实现了一套完整的支持 socket 的异步编程接口,这套接口就是 IOCP,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案。

浙公网安备 33010602011771号