您的位置:首页 > 编程语言 > C语言/C++

面试总结5--C++基础问题Part1

2015-12-01 15:02 357 查看
 从2015年4月开始参加实习招聘笔试面试,再到8月底开始内推,9月、10月正式校招,大大小小面试参加了不下20场,也拿到了自己心仪的Offer。总结了一些C++面试中常问的基础题目,希望能够给有需要的朋友一些帮助。由于大多数题目是自己总结的也有些是网上搜集的资料,如有不恰当之处请多包涵!1、STL中的vector:增减元素对迭代器的影响?解答:这个问题主要是针对连续内存容器和非连续内存容器。a、对于连续内存容器,如vector、deque等,增减元素均会使得当前之后的所有迭代器失效。因此,以删除元素为例:由于erase()总是指向被删除元素的下一个元素的有效迭代器,因此,可以利用该连续内存容器的成员erase()函数的返回值。常见的编程写法为:
	for(auto iter = myvec.begin(); iter != myvec.end())  //另外注意这里用 "!=" 而非 "<"{if(delete iter)iter = myvec.erase(iter);else ++iter;}
还有两种极端的情况是:(1)、vector插入元素时位置过于靠前,导致需要后移的元素太多,因此vector增加元素建议使用push_back而非insert;(2)、当增加元素后整个vector的大小超过了预设,这时会导致vector重新分分配内存,效率极低。因此习惯的编程方法为:在声明了一个vector后,立即调用reserve函数,令vector可以动态扩容。通常vector是按照之前大小的2倍来增长的。b、对于非连续内存容器,如set、map等。增减元素只会使得当前迭代器无效。仍以删除元素为例,由于删除元素后,erase()返回的迭代器将是无效的迭代器。因此,需要在调用erase()之前,就使得迭代器指向删除元素的下一个元素。常见的编程写法为:
         for(autoiter = myset.begin(); iter != myset.end()) //另外注意这里用 "!=" 而非 "<"{if(deleteiter)myset.erase(iter++);  //使用一个后置自增就OK了else++iter;}
2、New和malloc的区别?解答:new可分为operatornew(new 操作)、new operator(new 操作符)和placement new(定位 new)。其中operator new执行和malloc相同的任务,即分配内存,但对构造函数一无所知;而 new operator则调用operator new,分配内存后再调用对象构造函数进行对象的构造。其中operatornew是可以重载的。placement new,就是operator new的一个重载版本,允许你在一个已经分配好的内存中构造一个新的对象。而网上对new说法,大多针对operator new而言,因此说new是带有类型的(以为调用了类的构造函数),不过如果直接就说new是带有类型的话,明显是不合适的,比如原生的operator new。3、C++如何避免内存泄漏?解答:这其实可以看做是一个编程风格的问题。a、使用RAII(ResourceAcquisition Is Initialization,资源获取即初始化)技法,以构造函数获取资源(内存),析构函数释放。b、相比于使用原生指针,更建议使用智能指针,尤其是C++11标准化后的智能指针。c、注意delete和delete[]的使用方法。d、这是很复杂的一种情况,是关于类的copy constructor的。首先先介绍一些概念。同defaultconstructor一样,标准保证,如果类作者没有为class声明一个copy constructor,那么编译器会在需要的时候产生出来(这也是一个常考点:问道"如果类作者未定义出default/copy constructor,编译器会自动产生一个吗?"答案是否定的)不过请注意!!这里编译器即使产生出来,也是为满足它的需求,而非类作者的需求!!而什么时候是编译器"需要"的时候呢?是在当这个class【不表现出】bitwise copy semantics(位逐次拷贝,即浅拷贝)的时候。在4中情况下class【不表现出】bitwisecopy semantics(1)、当class内含一个memberobject且该member object声明了一个copy constructor(无论该copy ctor是类作者自己生明的还是编译器合成的);(2)、当class继承自一个baseclass且该base class有一个copy constructor(无论该copy ctor是类作者自己生明的还是编译器合成的);(3)、当class声明了virtualfunction;(4)、当class派生自一个继承链,且该链中存在virtual base class时。言归正传,如果class中仅仅是一些普通资源,那么bitwisecopy semantics是完全够用的;然而,挡在该class中存在了一块动态分配的内存,并且在之后执行了bitwise copy semantics后,将会有一个按位拷贝的对象和原来class中的某个成员指向同一块heap空间,当执行它们的析构函数后,该内存将被释放两次,这是未定义的行为。因此,在必要的时候需要使用Memberwise copy semantics(即深拷贝),来避免内存泄露。位拷贝拷贝的是地址,而值拷贝则拷贝的是内容。4、STL中排序算法的实现是什么?解答:STL中的sort(),在数据量大时,采用quicksort,分段递归排序;一旦分段后的数量小于某个门限值,改用Insertion sort,避免quicksort深度递归带来的过大的额外负担,如果递归层次过深,还会改用heapsort。
sort采用的是成熟的"快速排序算法"(目前大部分STL版本已经不是采用简单的快速排序,而是结合内插排序算法) 可以保证很好的平均性能、复杂度,stable_sort采用的是"归并排序",分派足够内存是,其算法复杂度为n*log(n), 否则其复杂度为n*log(n)*log(n),其优点是会保持相等
5、类是怎么通过虚函数实现多态的?多态性是“一个接口,多种方法”,多态性分为两类: 静态多态性和动态多态性。以前学过的函数重载和运算符重载实现的多态性属于静态多态性,动态多态性是通过虚函数(virtual function)实现的。静态多态性是指:在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。类中有虚函数存在,所以编译器就会为他做添加一个vptr指针,并为他们分别创建一个表vtbl,vptr指向那个表,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址。,只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。子类重写的虚函数的地址直接替换了父类虚函数在虚表中的位置,因此当访问虚函数时,该虚表中的函数是谁的就访问谁。注意:存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理单继承与多继承:单继承所有的虚函数都包含在虚函数表中,多重继承有多个虚函数表,当子类对父类的虚函数有重写时,子类的函数覆盖父类的函数在对应的虚函数位置,当子类有新的虚函数时,这些虚函数被加在第一个虚函数表的后面虚继承:使公共的基类在子类中只有一份,我们看到虚继承在多重继承的基础上多了vbtable来存储到公共基类的偏移6、操作系统的作业调度机制作业(job)是操作系统中一个常见的概念,所谓作业是指用户在一次计算过程或者事务处理过程中,要求计算机系统所作工作的集合。所谓作业调度是指按照某种原则,从后备作业队列中选取作业进入内存,并为作业做好运行前的准备工作以及作业完成后的善后处理工作。常用的作业调度算法有五种:先来先服务调度算法,短作业优先调度算法,响应比高者优先调度算法,最高优先数调度算法,均衡调度算法。7、深拷贝,浅拷贝?什么时候用到拷贝函数?a.一个对象以值传递的方式传入函数体; b.一个对象以值传递的方式从函数返回; c.一个对象需要通过另外一个对象进行初始化。如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。浅拷贝和深拷贝主要区别就是复制指针时是否重新创建内存空间。如果没有没有创建内存只赋值地址为浅拷贝,创建新内存把值全部拷贝一份就是深拷贝。浅拷贝在类里面有指针成员的情况下只会复制指针的地址,会导致两个成员指针指向同一块内存,这样在要是分别delete释放时就会出现问题,因此需要用深拷贝。8、子类、父类中的名称遮掩,如何避免?子类public继承父类的函数,子类的方法名会遮掩父类的相同名的方法,即使父类和子类内的函数有不同的参数类型也适用而且不论函数是virtual或non-virtual。子类要想访问父类的方法,使用using 父类名::函数名。 或转交函数。9、为什么auto_ptr不能用在容器中?STL容器在分配内存的时候,必须要能够拷贝构造容器的元素。而且拷贝构造的时候,不能修改原来元素的值。而auto_ptr在拷贝构造的时候,一定会修改元素的值。所以STL元素不能使用auto_ptr。因为两个auto_ptr不能管理同一块内存。10、new/delete和malloc/free的区别和联系1、malloc和free是C语言标准函数库中的两个函数,new/delete是C++语言中两个运算符。2、malloc/free和new/delete都是用来申请动态内存的。3、new 不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会进行初始化类成员的工作,同样free 也不会调用析构函数。4、malloc得到的指针无类型,new出来的指针是带有类型信息的。5、对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时 要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。6、如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如 果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete 必须配对使用,malloc/free也一样。11、C++中的Static用法?静态全局变量有以下特点:• 该变量在全局数据区分配内存;• 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);• 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的; 静态局部变量有以下特点:• 该变量在全局数据区分配内存;• 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;• 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;• 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;定义静态函数的好处:• 静态函数不能被其它文件所用;• 其它文件中可以定义相同名字的函数,不会发生冲突;类中Static关键字:静态数据成员有以下特点:• 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;• 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。在Example 5中,语句int Myclass::Sum=0;是定义静态数据成员;• 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;• 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;• 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:<数据类型><类名>::<静态数据成员名>=<值>• 类的静态数据成员有两种访问形式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;• 同全局变量相比,使用静态数据成员有两个优势:1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;静态成员函数:• 出现在类体外的函数定义不能指定关键字static;• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;• 非静态成员函数可以任意地访问静态成员函数和静态数据成员;• 静态成员函数不能访问非静态成员函数和非静态数据成员;• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;• 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:<类名>::<静态成员函数名>(<参数表>)调用类的静态成员函数。12、进程空间布局?一个由 C/C++编译的程序占用的内存(memory)分为以下几个部分:1.程序代码区(.text) - 存放函数体的二进制代码 。2.文字常量区(.rodata) - 常量字符串就是放在这里的,程序结束后由系统释放(rodata—readonly data)。3.全局区/静态区(static) - 全局变量 和 静态变量的存储是放在一块的。初始化的全局变量和静态变量在一块区域(.rwdataor .data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss), 程序结束后由系统释放。*在 C++中,已经不再严格区分bss和 data了,它们共享一块内存区域4.堆区(heap) - 一般由程序员分配释放(new/malloc/calloc delete/free),若程序员不释放,程序结束时可能由OS 回收。注意:它与数据结构中的堆是两回事,但分配方式倒类似于链表。5.栈区(stack) - 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。13、堆和栈的区别?管理方式:对于栈来讲,是由编译器自动管理;对于堆来说,释放工作由程序员控制,容易产生memory leak。空间大小:一般来讲在 32 位系统下,堆内存可以达到接近 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6 下面,默认的栈空间大小大约是1M。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量碎片,使程序效率降低;对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出。生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。分配方式:堆都是动态分配的,没有静态分配的堆;栈有2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配由alloca 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现。分配效率:栈是机器系统提供的数据结构,计算机会在底层分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高;堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,然后进行返回。显然,堆的效率比栈要低得多。无论是堆还是栈,都要防止越界现象的发生。14、MFC中点击一个铵钮的整个消息响应流程一个MFC消息响应函数在程序中有三处相关信息:函数原型、函数实现和以及用来关联消息和消息响应函数的宏。(1)在消息响应函数的原型代码中,函数声明的前部有一个afx_msg限定符,也是一个宏,该宏表明这个函数是一个消息响应函数的声明。(2)消息映射宏:在视图类的源文件中,BEGIN_MESSAGE_MAP()和END_MASSAGE_MAP()这两个宏之间定义了消息映射表,例如对于画线,其中有一个ON_WM_LBUTTONDOWN()消息映射宏,这个宏的作用就是把鼠标左键按下消息(WM_LBUTTONDOWN)与一个消息响应函数关联起来,通过这种机制,一旦有消息产生,程序就会调用相应的消息响应函数来进行处理。(3)消息响应函数的定义:在视图类的源文件中,可以看到OnLButtonDown函数的定义。头文件中在两个AFX_MSG注释宏之间是消息响应函数原型的声明。源文件中有两处:一处是在两个AFX_MSG_MAP注释宏之间的消息映射宏,通过这个宏把消息与消息响应函数关联起来;另一处是源文件中的消息响应函数的实现代码。MFC消息映射机制的具体实现方法是:在每个能接收和处理消息的类中,定义一个消息和消息函数静态对照表,即消息映射表.在消息映射表中,消息与对应的消息处理函数指针是成对出现的.某个类能处理的所有消息及其对应的消息处理函数的地址都列在这个类所对应的静态表中.当有消息需要处理时,程序只要搜索该消息静态表,查看表中是否含有该消息,就可知道该类能否处理此消息.如果能处理此消息,则同样依照静态表能很容易找到并调用对应的消息处理函数.15、override重写和overload重载的区别成员函数被重载的特征:(1)相同的范围(在同一个类中);(2)函数名字相同;(3)参数不同;(4)virtual关键字可有可无。函数重载不能靠返回值来进行区分重写是指派生类函数重写基类函数,是C++的多态的表现,特征是:(1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;(4)返回值(即函数原型)都要与基类的函数相同(5)基类函数必须有virtual关键字。重写函数的访问修饰符可以不同,尽管virtual函数是private的,在派生类中重写的函数可以是public或protect的“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。16linux下如何修改进程优先级?linux下的进程调度优先级是从-20到19,一共40个级别,数字越大,表示进程的优先级越低。查看进程优先级有两个办法:ps和top。top命令显示的NI列的值。nice命令的使用在启动进程时指定请求进程执行优先级。新建一个进程并设置优先级
Nice
-19
tar
zcf pack.tar.gz documents

修改已经存在的进程的优先级

Renice
19 1799
17
linux下性能监控命令uptime介绍,平均负载的具体含义是什么?linux uptime命令主要用于获取主机运行时间和查询linux系统负载等信息。系统平均负载是指在特定时间间隔内运行队列中的平均进程数。如果每个CPU内核的当前活动进程数不大于3的话,那么系统的性能是良好的。如果每个CPU内核的任务数大于5,那么这台机器的性能有严重问题。18、const和define区别1) 编译器处理方式不同define宏是在预处理阶段展开。const常量是编译运行阶段使用。(2) 类型和安全检查不同define宏没有类型,不做任何类型检查,仅仅是展开。const常量有具体的类型,在编译阶段会执行类型检查。(3) 存储方式不同define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。const常量会在内存中分配(可以是堆中也可以是栈中)。(4)const 可以节省空间,避免不必要的内存分配。const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。(5) 提高了效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。不能在类声明中初始化const数据成员。const数据成员的初始化只能在类构造函数的初始化表中进行,既然C++中有更好的const为什么还要使用宏?const无法代替宏作为卫哨来防止文件的重复包含。类中的常量:有时我们希望某些常量只在类中有效。由于#define定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const修饰数据成员来实现。const数据成员的确是存在的,但其含义却不是我们所期望的。const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。在整个类中都恒定的常量:应该用类中的枚举常量来实现。例如classA{…enum{ SIZE1 = 100, SIZE2 = 200}; // 枚举常量intarray1[SIZE1];intarray2[SIZE2];};枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159)。sizeof(A) = 1200;其中枚举部长空间。19、模板代码膨胀如何消除?把C++模板中与参数无关的代码分离出来。也就是让与参数无关的代码只有一份拷贝。(1) 模板生成多个类和多个函数,所以任何模板代码都不该与某个造成膨胀的模板参数产生相依关系。(2) 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数或类成员变量替换template参数(3) 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。20、代码重构常用方法?1、提取类/抽离方法2、提取方法3、分离条件4、引入参数对象/保留全局对象5、用符号常量替换无意义数字6、重命名方法
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: