计算机C++面试宝典.docx
《计算机C++面试宝典.docx》由会员分享,可在线阅读,更多相关《计算机C++面试宝典.docx(29页珍藏版)》请在冰点文库上搜索。
计算机C++面试宝典
1、C中的malloc与C++中的new有什么区别?
(1)new、delete是操作符,可以重载,只能在C++中使用;
(2)malloc、free是函数,可以覆盖,C、C++中都可以使用;
(3)new可以调用对象的构造函数,对应的delete调用相应的析构函数;
(4)malloc仅仅分配内存,free仅仅回收内存,并不执行构造和析构函数;
(5)new、delete返回的是某种数据类型指针,malloc、free返回的是void指针。
2、delete与delete[]区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。
在MoreEffectiveC++中有更为详细的解释:
“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用delete来释放内存。
”delete与new配套,delete[]与new[]配套。
MemTest*mTest1=newMemTest[10];
MemTest*mTest2=newMemTest;
int*pInt1=newint[10];
int*pInt2=newint;
delete[]pInt1;//-1-
delete[]pInt2;//-2-
delete[]mTest1;//-3-
delete[]mTest2;//-4-
在-4-处报错。
这就说明:
对于内建简单数据类型,delete和delete[]功能是相同的。
对于自定义的复杂数据类型,delete和delete[]不能互用。
delete[]删除一个数组,delete删除一个指针。
简单来说,用new分配的内存用delete删除,用new[]分配的内存用delete[]删除。
delete[]会调用数组元素的析构函数。
内部数据类型没有析构函数,所以问题不大。
如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。
3、多态,虚函数,纯虚函数,抽象类
多态
简单而言,一个接口,多种实现。
也可以这么理解,同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
多态性分为两种,一种是编译时的多态性,另一种是运行时的多态性。
编译时的多态性是通过重载来实现的,编译器在编译阶段根据函数的参数个数、参数类型决定实现何种操作。
运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。
虚函数
虚函数是指一个在类中希望被重写的成员函数,当用一个基类指针或引用指向一个派生类对象的时候,调用一个虚函数,实际调用的是派生类的版本。
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数。
虚函数是成员函数,而且是非static的成员函数。
如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。
在派生类中,若要重写该虚函数,其原型必须满足如下条件:
(1)与基类的虚函数参数个数相同;
(2)其参数的类型与基类的虚函数的对应参数类型相同;
(3)如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的,但这种例外只适用于返回值,而不适用于参数)。
这种特性被称为返回类型协变(covarianceofreturntype),因为允许返回类型随类类型的变化而变化。
纯虚函数
纯虚函数是一种特殊的虚函数,是在基类中声明的但在基类中没有定义,要求任何派生类都要定义自己的实现方法。
在基类中实现纯虚函数的方法是在函数原型后加“=0”。
它的一般格式如下:
class<类名>
{
virtual<类型><函数名>(<参数表>)=0;
…
};
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。
这就是纯虚函数的作用。
抽象类
包含纯虚函数的类称为抽象类。
由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。
抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。
抽象类的主要作用是将有关的类组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。
抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。
一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。
如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。
如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类,而是一个可以建立对象的具体类。
4、指针找错题
分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。
试题1以下是引用片段:
voidtest1()//数组越界
{
charstring[10];
char*str1="0123456789";
strcpy(string,str1);
}
试题2以下是引用片段:
voidtest2()
{
charstring[10],str1[10];
inti;
for(i=0;i<10;i++)
{
str1='a';
}
strcpy(string,str1);
}
试题3以下是引用片段:
voidtest3(char*str1)
{
charstring[10];
if(strlen(str1)<=10)
{
strcpy(string,str1);
}
}
解答:
对试题1:
字符串str1需要11个字节才能存放下(包括末尾的'\0'),而string只有10个字节的空间,strcpy会导致数组越界。
对试题2:
如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string,str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分。
对试题3:
if(strlen(str1)<=10)应改为if(strlen(str1)<10),因为strlen的结果未统计'\0'所占用的1个字节。
剖析:
考查对基本功的掌握
(1)字符串以'\0'结尾;
(2)对数组越界把握的敏感度;
(3)库函数strcpy的工作方式;
(4)对strlen的掌握,它没有包括字符串末尾的'\0'。
试题4以下是引用片段:
voidGetMemory(char*p)
{
p=(char*)malloc(100);
}
voidTest(void)
{
char*str=NULL;
GetMemory(str);
strcpy(str,"helloworld");
printf(str);
}
试题5以下是引用片段:
char*GetMemory(void)
{
charp[]="helloworld";
returnp;
}
voidTest(void)
{
char*str=NULL;
str=GetMemory();
printf(str);
}
试题6以下是引用片段:
voidGetMemory(char**p,intnum)
{
*p=(char*)malloc(num);
}
voidTest(void)
{
char*str=NULL;
GetMemory(&str,100);
strcpy(str,"hello");
printf(str);
}
试题7以下是引用片段:
voidTest(void)
{
char*str=(char*)malloc(100);
strcpy(str,"hello");
free(str);
...//省略的其它语句
}
解答:
试题4传入中GetMemory(char*p)函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完后的str仍然为NULL。
试题5中
charp[]="helloworld";
returnp;
p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。
这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句*p=(char*)malloc(num);后未判断内存是否申请成功,应加上:
if(*p==NULL)
{
...//进行申请内存失败处理
}
试题7存在与试题6同样的问题,在执行char*str=(char*)malloc(100);后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上str=NULL;。
试题6的Test函数中也未对malloc的内存进行释放。
剖析:
试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。
但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
再看看下面的一段程序有什么错误,以下是引用片段:
swap(int*p1,int*p2)
{
int*p;
*p=*p1;
*p1=*p2;
*p2=*p;
}
在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。
在VC++中DEBUG运行时提示错误"AccessViolation"。
该程序应该改为:
以下是引用片段:
swap(int*p1,int*p2)
{
intp;
p=*p1;
*p1=*p2;
*p2=p;
}
5、如何编写一个标准strcpy函数?
总分值为10,下面给出几个不同得分的答案:
2分以下是引用片段:
voidstrcpy(char*strDest,char*strSrc)
{
while((*strDest++=*strSrc++)!
='\0');
}
4分以下是引用片段:
voidstrcpy(char*strDest,constchar*strSrc)
//将源字符串加const,表明其为输入参数,加2分
{
while((*strDest++=*strSrc++)!
='\0');
}
7分以下是引用片段:
voidstrcpy(char*strDest,constchar*strSrc)
{
//对源地址和目的地址加非0断言,加3分
assert((strDest!
=NULL)&&(strSrc!
=NULL));
while((*strDest++=*strSrc++)!
='\0');
}
10分以下是引用片段:
//为了实现链式操作,将目的地址返回,加3分!
char*strcpy(char*strDest,constchar*strSrc)
{
assert((strDest!
=NULL)&&(strSrc!
=NULL));
char*address=strDest;
while((*strDest++=*strSrc++)!
='\0');
returnaddress;
}
从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!
需要多么扎实的基本功才能写一个完美的strcpy啊!
读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为:
以下是引用片段:
intstrlen(constchar*str)//输入参数const
{
assert(strt!
=NULL);//断言字符串地址非0
intlen=0;//一定要初始化
while((*str++)!
='\0')
{
len++;
}
returnlen;
}
6、请你分别画出OSI的七层网络结构图和TCP/IP的五层结构图并简述OSI体系结构七层模型。
如下图所示:
应用层:
为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
表示层:
确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。
如有必要,表示层会通过使用一种通用格式来实现多种数据格式之间的转换。
会话层:
通过传输层(端口号:
传输端口与接收端口)建立数据传输的通路。
主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
传输层:
定义了一些传输数据的协议和端口号(WWW端口80等),如:
TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据);UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。
主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。
常常把这一层数据叫做段。
网络层:
在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。
Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
数据链路层:
定义了如何让格式化数据进行传输,以及如何控制对物理介质的访问。
这一层通常还提供错误检测和纠正机制,以确保数据的可靠传输。
物理层:
主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。
它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。
这一层的数据叫做比特。
7、8086是多少位的系统?
在数据总线上是怎么实现的?
8086微处理器共有4个16位的段寄存器,在寻址内存单元时,用它们直接或间接地存放段地址。
代码段寄存器CS:
存放当前执行的程序的段地址。
数据段寄存器DS:
存放当前执行的程序所用操作数的段地址。
堆栈段寄存器SS:
存放当前执行的程序所用堆栈的段地址。
附加段寄存器ES:
存放当前执行程序中一个辅助数据段的段地址。
由CS:
IP构成指令地址,SS:
SP构成堆栈的栈顶地址。
DS和ES用作数据段和附加段的段地址(段起始地址或段值)
8086/8088微处理器的存储器管理
(1)地址线(码)与寻址范围:
N条地址线,寻址范围=2N。
(2)8086有20条地址线,寻址范围为1MB由00000H~FFFFFH定义。
(3)8086微处理器是一个16位结构,用户可用的寄存器均为16位:
寻址64KB。
(4)8086/8088采用分段的方法对存储器进行管理。
具体做法是:
把1MB的存储器空间分成若干段,每段容量为64KB,每段存储器的起始地址必须是一个能被16整除的地址码,即在20位的二进制地址码中最低4位必须是“0”。
每个段首地址的高16位二进制代码就是该段的段号(称段基地址)或简称段地址,段号保存在段寄存器中。
我们可对段寄存器设置不同的值来使微处理器的存储器访问指向不同的段。
(5)段内的某个存储单元相对于该段段首地址的差值,称为段内偏移地址(也叫偏移量)用16位二进制代码表示。
(6)物理地址是由8086/8088芯片地址引线送出的20位地址码,它用来参加存储器的地址译码,最终读/写所访问的一个特定的存储单元。
(7)逻辑地址由某段的段地址和段内偏移地址(也叫偏移量)两部分所组成。
写成:
段地址:
偏移地址(例如,1234H:
0088H)。
(8)在硬件上起作用的是物理地址,物理地址=段基址×10H+偏移地址。
8、论述含参数的宏与函数的优缺点。
●函数调用时,先求出实参表达式的值,然后带入形参。
而使用带参的宏只是进行简单的字符替换。
●函数调用是在程序运行时处理的,分配临时的内存单元。
而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
●对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换。
而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。
宏定义时,字符串可以是任何类型的数据。
●调用函数只可得到一个返回值。
而用宏可以设法得到几个结果。
●使用宏次数多时,宏展开后源程序长,因为每展开一次都使程序增长。
而函数调用不使源程序变长。
●宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。
一般来说,用宏来代表简短的表达式比较合适。
内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。
而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。
你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
当然,内联函数也有一定的局限性。
就是函数中的执行代码不能太多,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。
这样,内联函数就和普通函数执行效率一样。
内联函数是不能为虚函数的,但样子上写成了内联的,即隐含的内联方式。
在某种情况下,虽然有些函数我们声明为所谓“内联”方式,但有时系统也会把它当作普通的函数来处理,这里的虚函数也一样,虽然同样被声明为所谓“内联”方式,但系统会把它当作非内联的方式来处理。
9、CPU在上电后,进入操作系统的main()之前必须做什么工作?
过程如下:
(1)BIOS自举,检查硬件等;
(2)读取MBR;
(3)转到MBR执行它的代码,它会检测活动分区;
(4)把活动分区的引导扇区的引导代码装入内存;
(5)运行引导代码;
(6)引导代码装入该分区的操作系统,也就是进入main()(当然不一定叫main,如linux下叫start_kernel)执行一系列的初始化,然后最终启动登录界面,实现启动过程。
附:
什么是MBR?
MBR是英文MasterBootRecord的缩写,中文意为主引导记录。
硬盘的0磁道的第一个扇区称为MBR,它的大小是512字节,而这个区域可以分为两个部分。
第一部分为pre-boot区(预启动区),占446字节;第二部分是Partitiontable区(分区表),占66个字节,该区相当于一个小程序,作用是判断哪个分区被标记为活动分区,然后去读取那个分区的启动区,并运行该区中的代码。
10、堆栈溢出一般是由什么原因导致的?
答:
(1)没有回收垃圾资源;
(2)层次太深的递归调用。
11、用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。
写出C程序。
答:
典型的约瑟夫环问题(可用循环链表处理)。
以下为利用循环链表的核心代码:
voidCountPrint(LNode*head,LNode*tail,intm)
{
LNode*cur=head;
LNode*pre=tail;
intcnt=m-1;
while(cur&&cur!
=cur->next)
{
if(cnt!
=0)
{
cnt--;
pre=cur;
cur=cur->next;
}
else
{
pre->next=cur->next;
printf("%d",cur->data);
free(cur);
cur=pre->next;
cnt=m-1;
}
}
if(cur!
=NULL)
{
printf("%d",cur->data);
free(cur);
cur=NULL;
head=tail=NULL;
}
printf("\n");
}
12、全局变量可不可以定义在可被多个C文件包含的头文件中?
为什么?
答:
可以。
在不同的C文件中以static形式来声明同名全局变量。
可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。
13、static全局变量与普通的全局变量有什么区别?
static局部变量和普通局部变量有什么区别?
static函数与普通函数有什么区别?
答:
全局变量(外部变量)的说明之前再冠以static就构成了静态的全局变量。
全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。
这两者在存储方式上并无不同。
这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。
由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。
把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。
static函数与普通函数作用域不同。
只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。
对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。
14、-1,2,7,28,,126请问28和126中间那个数是什么?
为什么?
答:
应该是4^3-1=63
规律是n^3-1(当n为偶数0,2,4)
n^3+1(当n为奇数1,3,5)
15、如何用两个栈实现一个队列的功能?
要求给出算法和思路!
答:
设2个栈为A,B,一开始均为空。
入队:
将新元素push入栈A。
出队:
(1)判断栈B是否为空;
(2)如果为空,则将栈A中所有元素依次pop出并push到栈B;
(3)将栈B的栈顶元素pop出。
这样实现的队列入队和出队的平均复杂度都还是O
(1)。
16、用预处理指令#define声明一个常数,用以表明1年中有多少秒。
(忽略闰年问题)
#defineSECONDS_PER_YEAR(60*60*24*365)UL
我在这想看到几件事情:
(1)#define语法的基本知识(例如:
不能以分号结束,括号的使用,等等);
(2)懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的;
(3)意识到这个表达式将使一个16位机的整型数溢出,因此要用到长整型符号L,