此文是本人在牛客网复习C++摘抄的C++面试宝典电子版。
原文链接 https://www.nowcoder.com/tutorial/93/3e889999205d447daad9ec50a6f85d85
C和C++的区别
请你来说一说static关键字
参考回答:
1.加了static关键字的全局变量只能在本文件中使用。例如在a.c中定义了static int a=10;那么在b.c中用extern int a是拿不到a的值得,a的作用域只在a.c中。 2.static定义的静态局部变量分配在数据段上,普通的局部变量分配在栈上,会因为函数栈帧的释放而被释放掉。
3. 对一个类中成员变量和成员函数来说,加了static关键字,则此变量/函数就没有了this指针了,必须通过类名才能访问
● 说一说c++中四种cast转换
参考回答:
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
说说你了解的类型转换
参考回答:
reinterpret_cast:可以用于任意类型的指针之间的转换,对转换的结果不做任何保证
dynamic_cast:这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常
const_cast:对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。
请说一下C/C++ 中指针和引用的区别?
参考回答:
空间:1.指针有自己的一块空间,而引用只是一个别名;
大小:2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
初始化:3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
??4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
Const: 5.可以有const指针,但是没有const引用;
改变:6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
级数:7.指针可以有多级指针(**p),而引用至于一级;
??8.指针和引用使用++运算符的意义不一样;
??9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
请你说一下你理解的c++中的smart pointer四个智能指针: shared_ptr,unique_ptr,weak_ptr,auto_ptr
参考回答:
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针:
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
请你回答一下野指针是什么?
参考回答:
野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针
请你回答一下为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数 考点:虚函数 析构函数
参考回答:
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
请你来说一下fork函数
参考回答:
Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:
#include
#include
pid_t fork(void);
成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。
最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。
请你来说一下C++中析构函数的作用
参考回答:
析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
请你来说一下静态函数和虚函数的区别
参考回答:
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
请你来说一说重载和覆盖
参考回答:
重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
请你说一说你理解的虚函数和多态
参考回答:
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
请你来说一下C++里是怎么定义常量的?常量存放在内存的哪个位置?
参考回答:
常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
请你来回答一下const修饰成员函数的目的是什么?
参考回答:
const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。
请你来说一说隐式类型转换
参考回答:
首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。
请你来说一说C++函数栈空间的最大值
参考回答:
默认是1M,不过可以调整
请你回答一下new/delete与malloc/free的区别是什么
参考回答:
首先,new/delete是C++的关键字,而malloc/free是C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数
请你回答一下malloc与new区别
参考回答:
malloc需要给定申请内存的大小,返回的指针需要强转。
new会调用构造函数,不用指定内存大小,返回的指针不用强转。
请你说说虚函数表具体是怎样实现运行时多态的?
参考回答:
子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。
请你说说C语言是怎么进行函数调用的?
参考回答:
每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
请你说说C语言参数压栈顺序?
参考回答:
从右到左
● 请你说说C++如何处理返回值?
参考回答:
生成一个临时变量,把它的引用作为函数参数传入函数内。
请你回答一下C++中拷贝赋值函数的形参能否进行值传递?
参考回答:
不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。
请你说一说select
参考回答:
select在使用前,先将需要监控的描述符对应的bit位置1,然后将其传给select,当有任何一个事件发生时,select将会返回所有的描述符,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大
请你说说fork,wait,exec函数
参考回答:
父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1
请你来说一下map和set有什么区别,分别又是怎么实现的?
参考回答:
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。
map和set区别在于:
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和
请你说一说stl里面set和map怎么实现的
参考回答:
集合,所有元素都会根据元素的值自动被排序,且不允许重复。
底层实现:红黑树
set 底层是通过红黑树(RB-tree)来实现的,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的 STL 的 set 即以 RB-Tree 为底层机制。又由于 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 set 操作行为,都只有转调用 RB-tree 的操作行为而已。
适用场景:有序不重复集合
2、map
映射。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层:红黑树
适用场景:有序键值对不重复映射
mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
请你来介绍一下STL的allocaotr
参考回答:
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
delete运算分两个阶段:(1)调用对象希构函数;(2)掉员工::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
请你来说一说STL迭代器删除元素
参考回答:
这个主要考察的是迭代器失效的问题。1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
请你说一说STL中MAP数据存放形式
参考回答:
红黑树。unordered map底层结构是哈希表
请你讲讲STL有什么基本组成
参考回答:
STL主要由:以下几部分组成:
容器迭代器仿函数算法分配器配接器
他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
请你说说STL中map与unordered_map
参考回答:
1、Map映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层实现:红黑树
适用场景:有序键值对不重复映射
2、Multimap
多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
底层实现:红黑树
适用场景:有序键值对可重复映射
请你说一说vector和list的区别,应用,越详细越好
参考回答:
1、概念:
1)Vector
连续存储的容器,动态数组,在堆上分配空间
底层实现:数组
两倍容量增长:
vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
性能:
访问:O(1)
插入:在最后插入(空间够):很快
在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
在中间插入(空间够):内存拷贝
在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
删除:在最后删除:很快
在中间删除:内存拷贝
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2、List
动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
性能:
访问:随机访问性能很差,只能快速访问头尾节点。
插入:很快,一般是常数开销
删除:很快,一般是常数开销
适用场景:经常插入删除大量数据
2、区别:
1)vector底层实现是数组;list是双向 链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
3、应用
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
请你来说一下STL中迭代器的作用,有指针为何还要迭代器
参考回答:
1、迭代器
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
2、迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、、++、–等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
3、迭代器产生原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
请你说一说epoll原理
参考回答:
调用顺序:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。
请你回答一下STL里resize和reserve的区别
参考回答:
resize():改变当前容器内含有元素的数量(size()),eg: vectorv; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;
你来说一下C++中类成员的访问权限
参考回答:
参考回答:C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员
请你来说一下C++中struct和class的区别
参考回答:
在C++中,可以用struct和class定义类,都可以继承。区别在于:structural的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
另外,class还可以定义模板类形参,比如template
请你回答一下C++类内可以定义引用数据成员吗?
参考回答:
可以,必须通过成员函数初始化列表初始化。
请你回答一下什么是右值引用,跟左值又有什么区别?
参考回答:
右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:
左值和右值的概念:
左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
右值引用和左值引用的区别:
请你来说一下一个C++源文件从文本到可执行文件经历的过程?
参考回答:
对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
请你来回答一下include头文件的顺序以及双引号””和尖括号<>的区别?
参考回答:
Include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
当前头文件目录
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
请你回答一下malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?
参考回答:
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
请你说一说C++的内存管理是怎样的?
参考回答:
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。从低地址向高地址增长
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值 从高地址向低地址
请你回答一下如何判断内存泄漏?
参考回答:
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
请你来说一下什么时候会发生段错误
参考回答:
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
使用野指针
试图修改字符串常量的内容
你来回答一下什么是memory leak,也就是内存泄漏
参考回答:
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的分类:
请你来回答一下new和malloc的区别
参考回答:
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、申请数组时:new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
请自己设计一下如何采用单线程的方式处理高并发
参考回答:
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来
请你说一说C++ STL 的内存优化
参考回答:
1)二级配置器结构
STL内存管理使用二级内存配置器。
1、第一级配置器
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
一级空间配置器分配的是大于128字节的空间
如果分配不成功,调用句柄释放一部分内存
如果还不能分配成功,抛出异常
2、第二级配置器
在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。
3、分配原则
如果要分配的区块大于128bytes,则移交给第一级配置器处理。
如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表
如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块
如果内存池空间足够,则取出内存
如果不够分配20块,则分配最多的块数给自由链表,并且更新每次申请的块数
如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器
2)二级内存池
二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24…120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。
1、空间配置函数allocate
首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。
2、空间释放函数deallocate
首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。
3、重新填充空闲链表refill
在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。
从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。
假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。
3、总结:
1)二级配置器结构
STL内存管理使用二级内存配置器。
1、第一级配置器
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
一级空间配置器分配的是大于128字节的空间
如果分配不成功,调用句柄释放一部分内存
如果还不能分配成功,抛出异常
2、第二级配置器
在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。
3、分配原则
如果要分配的区块大于128bytes,则移交给第一级配置器处理。
如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表
如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块
如果内存池空间足够,则取出内存
如果不够分配20块,则分配最多的块数给自由链表,并且更新每次申请的块数
如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器
2)二级内存池
二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24…120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。
1、空间配置函数allocate
首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。
2、空间释放函数deallocate
首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。
3、重新填充空闲链表refill
在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。
从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。
假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。
3、总结:
请问C++11有哪些新特性?
参考回答:
C++11 最常用的新特性如下:
auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
初始化列表:使用初始化列表来对类进行初始化
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
atomic原子操作用于多线程资源互斥操作
新增STL容器array以及tuple