APP下载

C++经典面试题(最全 面中率最高)

消息来源:baojiabao.com 作者: 发布时间:2024-05-18

报价宝综合消息C++经典面试题(最全 面中率最高)

1.new、delete、malloc、free关系

delete会呼叫物件的解构函式,和new对应free只会释放内存,new呼叫建构函式。malloc与free是C++/C语言的标准库函式,new/delete是C++的运算子。它们都可用于申请动态内存和释放内存。对于非内部资料型别的物件而言,光用maloc/free无法满足动态物件的要求。物件在建立的同时要自动执行建构函式,物件在消亡之前要自动执行解构函式。由于malloc/free是库函式而不是运算子,不在编译器控制权限之内,不能够把执行建构函式和解构函式的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算子new,以及一个能完成清理与释放内存工作的运算子delete。注意new/delete不是库函式。

2.delete与 delete 区别

delete只会呼叫一次解构函式,而delete会呼叫每一个成员的解构函式。在More Effective C++中有更为详细的解释:“当delete操作符用于阵列时,它为每个阵列元素呼叫解构函式,然后呼叫operator delete来释放内存。”delete与new配套,delete 与new 配套

MemTest *mTest1=new MemTest[10];

MemTest *mTest2=new MemTest;

Int *pInt1=new int [10];

Int *pInt2=new int;

deletepInt1; //-1-

deletepInt2; //-2-

deletemTest1;//-3-

deletemTest2;//-4-

在-4-处报错。

这就说明:对于内建简单资料型别,delete和delete功能是相同的。对于自定义的复杂资料型别,delete和delete不能互用。delete删除一个数组,delete删除一个指标。简单来说,用new分配的内存用delete删除;用new分配的内存用delete删除。delete会呼叫阵列元素的解构函式。内部资料型别没有解构函式,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个物件,否则,它就会认为指向的是一个数组。

3.C++有哪些性质(面向物件特点)

封装,继承和多型。

4.子类析构时要呼叫父类的解构函式吗?

解构函式呼叫的次序是先派生类的析构后基类的析构,也就是说在基类的的析构呼叫的时候,派生类的资讯已经全部销毁了。定义一个物件时先呼叫基类的建构函式、然后呼叫派生类的建构函式;析构的时候恰好相反:先呼叫派生类的解构函式、然后呼叫基类的解构函式。

5.多型,虚拟函式,纯虚拟函式

多型:是对于不同物件接收相同讯息时产生不同的动作。C++的多型性具体体现在执行和编译两个方面:在程式执行时的多型性通过继承和虚拟函式来体现;

在程式编译时多型性体现在函式和运算子的过载上;

虚拟函式:在基类中冠以关键字 virtual 的成员函式。 它提供了一种界面界面。允许在派生类中对基类的虚拟函式重新定义。

纯虚拟函式的作用:在基类中为其派生类保留一个函式的名字,以便派生类根据需要对它进行定义。作为界面而存在 纯虚拟函式不具备函式的功能,一般不能直接被呼叫。

从基类继承来的纯虚拟函式,在派生类中仍是虚拟函式。如果一个类中至少有一个纯虚拟函式,那么这个类被称为抽象类(abstract class)。

抽象类中不仅包括纯虚拟函式,也可包括虚拟函式。抽象类必须用作派生其他类的基类,而不能用于直接建立物件例项。但仍可使用指向抽象类的指标支援执行时多型性。

6.求下面函式的返回值(微软)

int func(x)

{

int countx = 0;

while(x)

{

countx ++;

x = x&(x-1);

}

return countx;

}

假定x = 9999。 答案:8

思路:将x转化为2进位制,看含有的1的个数。

7.什么是“引用”?申明和使用“引用”要注意哪些问题?

答:引用就是某个目标变数的“别名”(alias),对应用的操作与对变数直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用宣告完毕后,相当于目标变数名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变数名的别名。宣告一个引用,不是新定义了一个变数,它只表示该引用名是目标变数名的一个别名,它本身不是一种资料型别,因此引用本身不占储存单元,系统也不给引用分配储存单元。不能建立阵列的引用。

8.将“引用”作为函式引数有哪些特点?

(1)传递引用给函式与传递指标的效果是一样的。这时,被调函式的形参就成为原来主调函式中的实参变数或物件的一个别名来使用,所以在被调函式中对形参变数的操作就是对其相应的目标物件(在主调函式中)的操作。

(2)使用引用传递函式的引数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变数传递函式的引数,当发生函式呼叫时,需要给形参分配储存单元,形参变数是实参变数的副本;如果传递的是物件,还将呼叫拷贝建构函式。因此,当引数传递的资料较大时,用引用比用一般变数传递引数的效率和所占空间都好。

(3)使用指标作为函式的引数虽然也能达到与使用引用的效果,但是,在被调函式中同样要给形参分配储存单元,且需要重复使用"*指标变数名"的形式进行运算,这很容易产生错误且程式的阅读性较差;另一方面,在主调函式的呼叫点处,必须用变数的地址作为实参。而引用更容易使用,更清晰。

9.在什么时候需要使用“常引用”?

如果既要利用引用提高程式的效率,又要保护传递给函式的资料不在函式中被改变,就应使用常引用。常引用宣告方式:const 型别识别符号 &引用名=目标变数名;

例1

int a ;

const int &ra=a;

ra=1; //错误

a=1; //正确

例2

string foo;

void bar(string & s);

那么下面的表示式将是非法的:

bar(foo);

bar("hello world");

原因在于foo和"hello world"串都会产生一个临时物件,而在C++中,这些临时物件都是const型别的。因此上面的表示式就是试图将一个const型别的物件转换为非const型别,这是非法的。引用型引数应该在能被定义为const的情况下,尽量定义为const 。

10.将“引用”作为函式返回值型别的格式、好处和需要遵守的规则?

格式:型别识别符号 &函式名(形参列表及型别说明){ //函式体 }

好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个区域性变数的引用是不可取的。因为随着该区域性变数生存期的结束,相应的引用也会失效,产生runtime error!

注意事项:

(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)相关联的时候,其赋值常常与某些其它属性或者物件的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它物件可以获得该属性的非常量引用(或指标),那么对该属性的单纯赋值就会破坏业务规则的完整性。

(4)流操作符过载返回值申明为“引用”的作用:

流操作符>,这两个操作符常常希望被连续使用,例如:cout

赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。

#include

int &put(int n);

int vals[10];

int error=-1;

void main

{

put(0)=10; //以put(0)函式值作为左值,等价于vals[0]=10;

put(9)=20; //以put(9)函式值作为左值,等价于vals[9]=20;

cout

cout

}

int &put(int n)

{

if (n>=0 && n

else { cout

}

(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算子。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个物件作为返回值,可选的方案包括:返回一个物件、返回一个区域性变数的引用,返回一个new分配的物件的引用、返回一个静态物件引用。根据前面提到的引用作为返回值的三个规则,2、3两个方案都被否决了。静态物件的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个物件了。

11、结构与联合有和区别?

(1). 结构和联合都是由多个不同的资料型别成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。

(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

12、试写出程式结果:

int a=4;

int &f(int x)

{ a=a+x;

return a;

}

int main(void)

{ int t=5;

cout

f(t)=20; a = 20

cout

t=f(t); a = 30 t = 30

cout

}

13.过载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?

常考的题目。从定义上来说:

过载:是指允许存在多个同名函式,而这些函式的引数表不同(或许引数个数不同,或许引数型别不同,或许两者都不同)。

重写:是指子类重新定义父类虚拟函式的方法。

从实现原理上来说:

过载:编译器根据函式不同的引数表,对同名函式的名称做修饰,然后这些同名函式就成了不同的函式(至少对于编译器来说是这样的)。如,有两个同名函式:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函式名称可能是这样的:int_func、str_func。对于这两个函式的呼叫,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早系结),因此,过载和多型无关!

重写:和多型真正相关。当子类重新定义了父类的虚拟函式后,父类指标根据赋给它的不同的子类指标,动态的呼叫属于子类的该函式,这样的函式呼叫在编译期间是无法确定的(呼叫的子类的虚拟函式的地址无法给出)。因此,这样的函式地址是在执行期系结的(晚系结)。

14.有哪几种情况只能用intialization list 而不能用assignment?

答案:当类中含有const、reference 成员变数;基类的建构函式都需要初始化表。

15. C++是不是型别安全的?

答案:不是。两个不同型别的指标之间可以强制转换(用reinterpret cast)。C#是型别安全的。

16. main 函式执行以前,还会执行什么程式码?

答案:全域性物件的建构函式会在main 函式之前执行。

17. 描述内存分配方式以及它们的区别?

1) 从静态储存区域分配。内存在程式编译的时候就已经分配好,这块内存在程式的整个执行期间都存在。例如全域性变数,static 变数。

2) 在栈上建立。在执行函式时,函式内区域性变数的储存单元都可以在栈上建立,函式执行结束时这些储存单元自动被释放。栈内存分配运算内置于处理器的指令集。

3) 从堆上分配,亦称动态内存分配。程式在执行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

18.分别写出BOOL,int,float,指标型别的变数a 与“零”的比较语句。

答案:

BOOL : if ( !a ) or if(a)

int : if ( a == 0)

float : const EXPRESSION EXP = 0.000001

if ( a -EXP)

pointer : if ( a != NULL) or if(a == NULL)

19.请说出const与#define 相比,有何优点?

答案:

const作用:定义常量、修饰函式引数、修饰函式返回值三个作用。被Const修饰的东西都受到强制保护,可以预防意外的变动,能提高程式的健壮性。

1) const 常量有资料型别,而宏常量没有资料型别。编译器可以对前者进行型别安全检查。而对后者只进行字元替换,没有型别安全检查,并且在字元替换可能会产生意料不到的错误。

2) 有些整合化的除错工具可以对const 常量进行除错,但是不能对宏常量进行除错。

20.简述阵列与指标的区别?

阵列要么在静态储存区被建立(如全域性阵列),要么在栈上被建立。指标可以随时指向任意型别的内存块。

(1)修改内容上的差别

char a = “hello”;

a[0] = ‘X’;

char *p = “world”; // 注意p 指向常量字串

p[0] = ‘X’; // 编译器不能发现该错误,执行时错误

(2) 用运算子sizeof 可以计算出阵列的容量(字节数)。sizeof(p),p 为指标得到的是一个指标变数的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指标所指的内存容量,除非在申请内存时记住它。注意当阵列作为函式的引数进行传递时,该阵列自动退化为同类型的指标。

char a = "hello world";

char *p = a;

cout

cout

计算阵列和指标的内存容量

void Func(char a[100])

{

cout

}

21题: int (*s[10])(int) 表示的是什么?

int (*s[10])(int) 函式指标阵列,每个指标指向一个int func(int param)的函式。

22题:栈内存与文字常量区

char str1 = "abc";

char str2 = "abc";

const char str3 = "abc";

const char str4 = "abc";

const char *str5 = "abc";

const char *str6 = "abc";

char *str7 = "abc";

char *str8 = "abc";

cout cout cout

cout

结果是:0 0 1 1

解答:str1,str2,str3,str4是阵列变数,它们有各自的内存空间;而str5,str6,str7,str8是指标,它们指向相同的常量区域。

23题:将程式跳转到指定内存地址

要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程式跳转到绝对地址是0x100000去执行,应该怎么做?

*((void (*))0x100000 ) ;

首先要将0x100000强制转换成函式指标,即:

(void (*))0x100000

然后再呼叫它:

*((void (*))0x100000);

用typedef可以看得更直观些:

typedef void(*) voidFuncPtr;

*((voidFuncPtr)0x100000);

24题:int id[sizeof(unsigned long)];这个对吗?为什么?

25题:引用与指标有什么区别?

【参考答案】

1) 引用必须被初始化,指标不必。

2) 引用初始化以后不能被改变,指标可以改变所指的物件。

3) 不存在指向空值的引用,但是存在指向空值的指标。

26题:const 与 #define 的比较 ,const有什么优点?

(1) const 常量有资料型别,而宏常量没有资料型别。编译器可以对前者进行型别安全检查。而对后者只进行字元替换,没有型别安全检查,并且在字元替换可能会产生意料不到的错误(边际效应) 。

(2) 有些整合化的除错工具可以对 const 常量进行除错,但是不能对宏常量进行除错。

27题:复杂宣告

void * ( * (*fp1)(int))[10];

float (*(* fp2)(int,int,int))(int);

int (* ( * fp3))[10];

分别表示什么意思?

【标准答案】

1.void * ( * (*fp1)(int))[10]; fp1是一个指标,指向一个函式,这个函式的引数为int型,函式的返回值是一个指标,这个指标指向一个数组,这个阵列有10个元素,每个元素是一个void*型指标。

2.float (*(* fp2)(int,int,int))(int); fp2是一个指标,指向一个函式,这个函式的引数为3个int型,函式的返回值是一个指标,这个指标指向一个函式,这个函式的引数为int型,函式的返回值是float型。

3.int (* ( * fp3))[10]; fp3是一个指标,指向一个函式,这个函式的引数为空,函式的返回值是一个指标,这个指标指向一个数组,这个阵列有10个元素,每个元素是一个指标,指向一个函式,这个函式的引数为空,函式的返回值是int型。

28题:内存的分配方式有几种?

【参考答案】

一、从静态储存区域分配。内存在程式编译的时候就已经分配好,这块内存在程式的整个执行期间都存在。例如全域性变数。

二、在栈上建立。在执行函式时,函式内区域性变数的储存单元都可以在栈上建立,函式执行结束时这些储存单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

三、从堆上分配,亦称动态内存分配。程式在执行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

29题:基类的解构函式不是虚拟函式,会带来什么问题?

【参考答案】派生类的解构函式用不上,会造成资源的泄漏。

30题:全域性变数和区域性变数有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?

【参考答案】

生命周期不同:

全域性变数随主程式建立和建立,随主程式销毁而销毁;区域性变数在区域性函式内部,甚至区域性循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全域性变数程式的各个部分都可以用到;区域性变数只能在区域性使用;分配在栈区。

操作系统和编译器通过内存分配的位置来知道的,全域性变数分配在全域性资料段并且在程式开始执行的时候被载入。区域性变数则分配在堆叠里面 。

2019-11-26 07:51:00

相关文章