c++面试题
一 用简洁的语言描述 c++
在 c 语言的基础上开发的一种面向对象编程的语言; 应用广泛; 支持多种编程范式,面向对象编程,泛型编程,和过程化编程;广泛应用于系统开发,引擎开发;支持类,封装,重载等特性。
二 c 和 c++ 的区别
- C++ 在 c 的基础上添加类;
- C主要是面向过程,C + + 主要面向对象;
- C主要考虑通过一个过程将输入量经过各种运算后得到一个输出, C++ 主要考虑是如何构造一个对象模型,让这个模型契合与之对应的问题域, 这样就可以通过获取对象的状态信息得到输出。
三 什么是面向对象
面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。
四 封装,继承,多态,虚函数
封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
继承:继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
多态:是指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态。
A. 多态
定义: “一个接口,多种方法”,程序在运行时才决定调用的函数。
实现: C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
目的: 封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。
B.什么是虚函数,什么函数不能声明为虚函数?
-
那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。
-
构造函数。因为要构造一个对象,必须清楚地知道要构造什么,否则无法构造一个对象。析构函数可以为纯虚函数。
C.为什么要用纯虚函数?
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
D. 在什么情况下使用纯虚函数(pure vitrual function)?
- 当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
- 这个方法必须在派生类(derived class)中被实现;
E. 虚函数与纯虚函数的区别
虚函数为了重载和多态。 在基类中是有定义的,即便定义为空。 在子类中可以重写。
纯虚函数在基类中没有定义, 必须在子类中加以实现。
多态的基础是继承,需要虚函数的支持,简单的多态是很简单的。
子类继承父类大部分的资源,不能继承的有构造函数,析构函数,拷贝构造函数, operator=函数,友元函数等等
五 设计模式
六 常见的STL容器有哪些,算法哪些
STL包括两部分,容器和算法。(重要的还有融合这二者的迭代器)
1. 容器
即为存放数据的地方。分为两类。
- 序列式容器:vector,list,deque等
- 关联式容器: 内部结构基本上是一颗平衡二叉树。所谓关联,指每个元素都有兼职和一个实值。
举例:vector是动态分配存储空间的容器。
2. 算法
排序,复制等等,与各个容器相关联。
3. 迭代器
STL的精髓。可以这样描述,它提供了一种方法,使之能够按照顺序访问某个容器所含的各个元素,但是无需暴露该容器的内部结构。它将容器和算法分开,好让这二者独立设计。
七 开发中常用到的数据结构有哪些。
数组,链表,树。也会用到栈(先进后出)和队列(先进先出)。
1.数组和链表的区别。(很简单,但是很常考,记得要回答全面)
C++语言中可以用数组处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小。而在实际应用中,用户使用数组之 前有时无法准确确定数组的大小,只能将数组定义成足够大小,这样数组中有些空间可能不被使用,从而造成内存空间的浪费。链表是一种常见的数据组织形式,它 采用动态分配内存的形式实现。需要时可以用new分配内存空间,不需要时用delete将已分配的空间释放,不会造成内存空间的浪费。
从逻辑结构来看:
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况,即数组的大小一旦定义就不能改变。当数据增加时,可能超出原先 定义的元素个数;当数据减少时,造成内存浪费;链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删 除数据项时,需要移动其它数据项)。
从内存存储来看:
(静态)数组从栈中分配空间(用NEW创建的在堆中), 对于程序员方便快速,但是自由度小;链表从堆中分配空间, 自由度大但是申请管理比较麻烦.
从访问方式来看:
数组在内存中是连续存储的,因此,可以利用下标索引进行随机访问;链表是链式存储结构,在访问元素的时候只能通过线性的方式由前到后顺序访问,所以访问效率比数组要低。
2. 二叉树的遍历
二叉树的遍历,就是按照某条搜索路径访问树中的每一个结点,使得每个结点均被访问一次,而且仅被访问一次。
- 常见的遍历次序有:
先序遍历:先访问根结点,再访问左子树,最后访问右子树
中序遍历:先访问左子树,再访问根结点,最后访问右子树
后序遍历:先访问左子树,再访问右子树,最后访问根结点 - 时间复杂度 O(n)
http://www.linuxidc.com/Linux/2015-08/122480.htm
八. const与static的用法
1. const:
- const修饰类的成员变量,表示该成员变量不能被修改。
- const修饰函数,表示本函数不会修改类内的数据成员。不会调用其他非const成员函数。
- const函数只能调用const函数,非const函数可以调用const函数
类外定义的const成员函数,在定义和声明出都需要const修饰符。
2. static:
2.1 对变量:
a. 局部变量:
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。位于内存中静态存储区; 未初始化的局部动初始化为0. 作用域仍是局部作用域.注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。
b. 全局变量.
在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。静态存储区,未经初始化的全局静态变量会被程序自动初始化为0,全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。注: static修饰全局变量并未改变其存储位置及生命周期, 而是改变了其作用域,使得当前文件外的源文件无法访问该变量.不能被其他文件访问和修改,其他文件中可以使用相同名字的变量,不会产生冲突.
2.2 对类:
a. 成员变量.
用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。
注意:
- 不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
- 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
- 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。
b. 成员函数
注意:
a. 用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
b. 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不-需要加static修饰符。
c. 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
d. 不可以同时用const和static修饰成员函数。
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
九. 类的static变量在什么时候初始化,函数的static变量在什么时候初始化。
类的静态成员在类实例化之前就存在了,并分配了内存。函数的static变量在执行此函数时进行实例化。
十 指针和引用
- 指针是一个变量,存放地址的变量,指向内存的一个存储单元,引用仅是个别名。
- 引用必须被初始化, 指针不必
- 引用使用时无需加*。
- 引用没有const修饰,指针有const修饰
- sizeof引用对象得到的是所指对象,变量的大小;sizeof指针得到的是指针本身的大小
- 指针可有多级,引用只可一级
- 内存分配上,程序为指针变量分配内存,不为引用分配内存。
1. 引用作为参数的优点:
(1)传递引用给函数与传递指针的效果是一样的。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!;
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
2. 注意:
(1)不能返回局部变量的引用。这条可以参照Effective C + +[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用(这个要注意啦,很多人没意识到,哈哈。。。)。 这条可以参照Effective C+ +[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。 这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
3. 引用与多态的关系?
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例
十一 内存
1.内存类别
栈 --由编译器自动分配释放, 局部遍历存放位置
堆 --由程序员分配和释放.
全局区(静态区) --全局变量和静态变量的存储是放在一起的, 初始化的全局变量和static静态变量在一块区域.
程序代码区 --存放二进制代码.
在函数体中定义的变量通常是在栈上, 用malloc, 等分配内存的函数分配得到的就是在堆上. 在所有函数体外定义的是全局量, 加了static修饰符后不管在哪里都存放在全局区, 在所有函数体外定义的static变量表示在该文件有效, 不能extern 到别的文件用. 在函数体内定义的static表示只在该函数体内有效.
2. 堆栈溢出的原因:
数组越界, 没有回收内存, 深层次递归调用
3. 内存分配方式
内存分配的三种方式: a 静态存储区,程序编译时便分好, 整个运行期间都存在,比如全局变量,常量; b, 栈上分配; 堆上分配。
4. 避免内存泄漏
原因:动态分配的内存没有手动释放完全.
避免:使用的时候应记得指针的长度; 分配多少内存应记得释放多少, 保证一一对应的关系; 动态分配内存的指针最好不要再次赋值.
十二 常用排序算法.
1. 冒泡:
两两比较相邻关键字,如果反序则交换.
for ( int i = 0; i < A. length-1; i++){
for ( int j = i + 1; j < A.length-1; j++){
if ( A[i] > A[j])
swap(A, i, j);
}
}
}
2. 选择:
先不急于调换位置, 先从A[i]开始逐个检查, 记下最小数的坐标.扫描完一遍之后根据需要进行交换.
for ( int i = 0; i < A.length-1; i ++){
int min = i;
for ( int j = i + 1; j < A.length; j ++)
if ( A[min] > A[j])
min = j;
}
if ( i != min)
swap(A, i, min);
3. 插入:
将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的 有序表.
for (int i = 1; i < A.length; i ++){
if( A[i] < A[i - 1]){
int tmp = A[i];
int j;
for ( j = i - 1; j >= 0 && A[j] > tmp; j--){
A[j+1] = A[j];
}
A[j+1] = tmp;
}
}
4. 快速排序:
通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可以分别对这两部分记录进行排序,已达到整个序列有序的目的.
快速排序是一种不稳定的排序方法.
5. 各种排序算法的使用范围总结:
(1)当数据规模较小的时候,可以用简单的排序算法如直接插入排序或直接选择排序。
(2)当文件的初态已经基本有序时,可以 用直接插入排序或冒泡排序。
(3)当数据规模比较大时,应用速度快的排序算法。可以考虑用快速排序。当记录随机分布的时候,快排的平均时间最短,但可能出 现最坏的情况,这时候的时间复杂度是O(n^2),且递归深度为n,所需的栈空间问O(n)。
(4)堆排序不会出现快排那样的最坏情况,且堆排序所需的辅 助空间比快排要少。但这两种算法都不是稳定的,若要求排序时稳定的,可以考虑用归并排序。
(5)归并排序可以用于内排序,也可以用于外排序。在外排序时, 通常采用多路归并,并且通过解决长顺串的合并,产生长的初始串,提高主机与外设并行能力等措施,以减少访问外存额次数,提高外排序的效率。
排序分类 平均时间 时间复杂度 辅助存储空间
简单排序 O(n2) O(n2) O(1)
快速排序 O(nlog2n) O(nlog2n) O(nlog2n)
堆排序 O(nlog2n) O(nlog2n) O(1)
归并排序 O(nlog2n) O(nlog2n) O(n)
十三 请说出const与#define 相比,有何优点?
const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
十四 c++中静态函数和静态变量.
类静态数据成员在编译时创建并初始化,在该类的任何对象简历之前就存在,不属于任何对象, 而非静态类成员则是属于对象所有.类静态数据成员只是一个拷贝,为所有此类的对象所共享.
类静态成员函数属于某个类,不属于某个对象,由该类所有对象共享.
- static成员变量实现了同类对象间的信息共享
- static成员类外储存,求类大小,并不包含在内.
- static成员是命名空间, 属于类的全局变量.
- static成员只能类外初始化.
- 可以通过类名访问, 也可通过对象访问.
静态成员函数的意义不在于信息共享和数据沟通, 而是在于管理静态数据成员, 完成对静态数据成员的封装.
静态成员函数只能访问静态数据成员, 原因在于非静态成员函数, 在调用时, this指针被当做参数传进, 而静态成员函数属于类, 不属于对象, 没有this指针.
十五 new和malloc的算法.
malloc和free是c语言的标准库函数, new/delete是c++的运算符.都可用来申请动态内存释放.
由于malloc/free是库函数,不是运算符,因此不能将执行构造函数和析构函数的任务强加于malloc/free. c++需要一个能够完成动态内存分配和初始化工作的运算符new. 主要, new/delete不是库函数.
c++可以调用c函数, 而c程序只能用malloc/free管理动态内存.
new可以认为是malloc加构造函数的执行. new出来的指针都是直接带类型信息的, 而malloc返回的都是void指针.
十六 switch 的参数类型不能是
实形
十七 如何引用一个已经定义过的全局变量
引用头文件和extern关键字。 如果采用引用头文件, 若变量写错了,则在编译期间便会出错。 如果用extern则在链接阶段报错。
十八 频繁出现的短小函数, 在c和c++中分别如何实现。
c中使用宏定义, c++中使用inline内联函数
十九 数组与指针的区别。
数组被定义在静态存储区(全局变量)或栈上, 指针可指向任意类型数据块。
二十 c++函数值传递的方式。
值传递, 指针传递, 引用传递
二十一 extern“C”作用
实现C++与C混合编程
因为c+ +编译后库中函数名会变得很长, 与c生成的不一致, 造成c+ +不能直接调用c函数。加上extern后, 就能直接调用c函数。
首先,作为extern是C/C+ +语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数
extern "C"是连接申明(linkage declaration),被extern "C"修饰的变量和函数是按照C语言方式编译和连接的,来看看C+ +中对类似。
二十二 定义一个宏时应注意什么。
定义部分的每个形参和整个表达式都必须用括号括起来,以避免不可预料的错误。
二十三 系统会自动打开和关闭的三个标准文件是
stdin, stdout, stderr
二十四 结构与联合有和区别?
- 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
- 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
二十五 .h头文件中的ifndef/define/endif 的作用?
防止该头文件被重复引用。
二十六 重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?
常考的题目。
1. 从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
2. 从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译 器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的 调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
二十八 struct 和 class 的区别
答案:struct 的成员默认是公有的,而类的成员默认是私有的。struct 和 class 在其他方面是功能相当的。
二十九 如何判断浮点数是否为0,或判读两个浮点数是否相等。
答案:对于浮点数x,若判断其是否等于0.5,不可直接用 “==” 号, 正确的做法应该是:
if (fabs(x - 0.5) < DBL_EPSILON)
{
//满足这个条件,我们就认为x和0.5相等,否则不等
puts("ok");//打印了ok
10 }
具体原因比较复杂,一言以蔽之,计算机无法精确的表示浮点数。
三十 new 和 delete 的实现原理。
有一篇博客分析的很透彻,可点击这里查看。