C++基础知识点总结

来源:互联网 发布:js数组取前几个 编辑:程序博客网 时间:2024/05/17 23:08

1.引用与指针有什么区别?
1)引用必须被初始化,指针不必。(const变量也必须在一开始定义的时候就完成初始化)
2)引用初始化以后不能被改变,指针可以改变所指的对象。(也即引用初始化完成之后,就永远指向初始化时指定的对象;但指针可以动态修改指向,使之指向其他对象.)
3)不存在指向空值的引用,但是存在指向空值的指针。

2.描述一下extern的用法
参见博文:http://www.cnblogs.com/yyxt/p/3891712.html

3.对于一个频繁使用的短小函数,在C语言中应用什么实现,在C++中应用什么实现?
C用宏定义,C++用inline;
在C++中要尽量使用inline函数代替宏定义,具体分析参见博文:http://www.cnblogs.com/yyxt/p/4799898.html之 条款02;


4. 当一个类X 中没有生命任何非静态成员变量与成员函数,这时sizeof(X)的值是多少,请解释一下?

对于C++中的一个空类

[cpp] view plain copy
  1. classX { };   

事实上并不是空的,sizeof(X)并不等于0,一般的结果是1。每个X的对象都有一个隐晦的1bytes,是被编译器安插进去的一个char,这样可以使得这个class的两个objects在内存中配置独一无二的地址。

X作为另一个类的成员时,如:

[cpp] view plain copy
  1. classA  
  2.  {  
  3.  public:  
  4.      X x;  
  5.      int a;  
  6.  };  
由于X占一个字节,int4个字节,再加上编译器的alignment调整,sizeof(Y)=8

但是当一个类继承X时:

[cpp] view plain copy
  1. classY :public X  
  2.  {  
  3.  public:  
  4.       int a;  
  5.  };  
这时大部分编译器对于sizeof(Y)的结果是4,而不是8。这就是所谓的空白基类最优化在(empty baseoptimization-EBOemptybaseclassopimization-EBCO)。在空基类被继承后由于没有任何数据成员,所以子类优化掉基类所占的1byteEBO并不是c++标准所规定必须的,但是大部分编译器都会这么做.


5. 数组和指针有什么区别?

(1)数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

(2) 修改内容上的区别:

[cpp] view plain copy
  1. char a[] = “hello”;  
  2. a[0] = ‘X’;  
  3. char *p = “world”; // 注意p 指向常量字符串  
  4. p[0] = ‘X’; // 编译器不能发现该错误,运行时错误  
(3) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),vp 为指针得到的是一个指针变量的字节数4bytes,而不是p 所指的内存容量。

(4)当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针


6. strcpy()和memcpy()的区别?

strcpy和memcpy都是标准C库函数,它们有下面的特点。
strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。

已知strcpy函数的原型是:char* strcpy(char* dest, const char* src);
memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。
void *memcpy( void *dest, const void *src, size_t count );

strcpy和memcpy主要有以下3方面的区别:
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy


7. 总结static的应用和作用?
(1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
(4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

更多参见博文: http://www.cnblogs.com/yyxt/p/4011893.html


8. 总结const的应用和作用?

(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。


9. 结构(struct)和联合(union)的区别?
1) 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合转只存放了一个被选中的成员, 而结构的所有成员都存在。

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


10. 类(class)与结构(struct)的区别?

(1)默认的继承访问权限: struct是public的,class是private的;
(2)class是引用类型,struct是值类型;
(3)class可以继承类、接口和被继承,struct只能继承接口,不能被继承;
(4)class有默认的无参构造函数,有析构函数,struct没有默认的无参构造函数,且只能声明有参的构造函数,没有析构函数;
(5)class可以使用abstract和sealed,有protected修饰符,struct不可以用abstract和sealed,没有protected修饰符;
(6)class必须使用new初始化,结构可以不用new初始化;
(7) class实例由垃圾回收机制来保证内存的回收处理,而struct变量使用完后立即自动解除内存分配;

从职能观点来看,class表现为行为,而struct常用于存储数据;

作为参数传递时,class变量以按址方式传递,而struct变量是以按值方式传递的。

如何选择使用结构还是类:
1).堆栈的空间有限,对于大量的逻辑的对象,创建类要比创建结构好一些
2).结构表示如点、矩形和颜色这样的轻量对象,例如,如果声明一个含有 1000 个点对象的数组,则将为引用每个对象分配附加的内存。在此情况下,结构的成本较低
3).在表现抽象和多级别的对象层次时,类是最好的选择
4).大多数情况下该类型只是一些数据时,结构是最佳的选择


11. 简述assert()用法与注意事项:
(1) 在函数开始处检验传入参数的合法性;
(2) 每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败;
(3) 不能使用改变环境的语句,因为assert只在DEBUG个生效,如果这么做,会使用程序在真正运行时遇到问题;
(4) assert和后面的语句应空一行,以形成逻辑和视觉上的一致感;
(5) 有的地方,assert不能代替条件过滤。


12. windows消息系统由哪几部分构成?
由以下3部分组成:
1. 消息队列:操作系统负责为进程维护一个消息队列,程序运行时不断从该消息队列中获取消息、处理消息;
2. 消息循环:应用程序通过消息循环不断获取消息、处理消息。
3. 消息处理:消息循环负责将消息派发到相关的窗口上使用关联的窗口过程函数进行处理。


13. 在头文件中进行类的声明,在对应的实现文件中进行类的定义有什么意义?

一般都是代表一个基本功能的源文件引用相应的头文件。一个 相关功能的模块可能有若干对源文件和头文件组成。这是基于组件编程的核心。c语言中头文件中一般定义了函数的声明、结构体的定义、宏定义。(常量和全局变量最好放到源文件中).

优势:

1) 从业务扩展性上看:头文件中放函数的声明,函数由源文件实现,这就是将面向接口编程:接口和实现分开,这在面对业务变更频繁的需求中技术实现的好处是显而易见的: 只要定义出良好地、扩展性高的接口,实现是可以很方便的更换。

2) 从程序架构上看:代码在在大型程序中 需要分成不同的模块,单一模块中又可能分为不同的业务功能单元,他们间有很多相互的调用。头文件中的方法声明、结构体定义、宏就都可以充当这部分的模块与模块间、业务功能单位间的接口调用。模块与模块间,功能单元与功能单元间都是面向接口的调用,耦合性低,这正是基于组件编程的核心思想。(模块的高内聚低耦合)

3) 从某些技术角度实现上看:头文件可通过宏定义(头文件保护宏)来保证类定义、结构体定义、宏定义的唯一性。确实很方便,不容易出错.

(4) 在编译时,源文件里的实现会被编译成临时文件,运行时刻程序找到头文件里的接口,根据接口找到这些临时文件,来调用它们这些实现。这样可以提高编译效率,因为分开的话只需要编译一次生成对应的.obj文件后,再次应用该类的地方,这个类就不会被再次编译,从而大大的提高了编译效率


14. main 函数执行之前/之后还会执行什么代码?

main 函数之前执行:
    一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前;
main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
          全局对象的析构函数会在main函数之后执行;
          用atexit注册的函数也会在main之后执行。



15. 将一个字符串逆序?

参见博文: http://www.cnblogs.com/graphics/archive/2011/03/09/1977717.html


16. 关于数组的几道面试题

参见: 关于数组的几道面试题

17. 请简单描述Windows内存管理的方法
内存管理是操作系统中的重要部分三言两语讲不清楚的.下面只做最简单的描述:
当程序运行时需要从内存中读出这段程序的代码。代码的位置必须在物理内存中才能被运行,由于现在的操作系统中有非常多的程序运行着,内存中不能够完全放下,所以引出了虚拟内存的概念。把哪些不常用的程序片断就放入虚拟内存,当需要用到它的时候在load入主存(物理内存)中。这个就是内存管理所要做的事。内存管理还有另外一件事需要做:计算程序片段在主存中的物理位置,以便CPU调度。
内存管理有块式管理,页式管理,段式和段页式管理。现在常用段页式管理.
(1) 块式管理:把主存分为一大块、一大块的,当所需的程序片断不在主存时就分配一块主存空间,把程 序片断load入主存,就算所需的程序片度只有几个字节也只能把这一块分配给它。这样会造成很大的浪费,平均浪费了50%的内存空间,但时易于管理。
(2) 页式管理:把主存分为一页一页的,每一页的空间要比一块一块的空间小很多,显然这种方法的空间利用率要比块式管理高很多。
(3) 段式管理:把主存分为一段一段的,每一段的空间又要比一页一页的空间小很多,这种方法在空间利用率上又比页式管理高很多,但是也有另外一个缺点。一个程序片断可能会被分为几十段,这样很多时间就会被浪费在计算每一段的物理地址上(计算机最耗时间的大家都知道是I/O吧)。
(4) 段页式管理:结合了段式管理和页式管理的优点。把主存分为若干页,每一页又分为若干段。
各种内存管理都有它自己的方法来计算出程序片断在主存中的物理地址,其实都很相似. 这只是一个大概而已,不足以说明内存管理的皮毛。无论哪一本操作系统书上都有详细的讲解.


18.求下面函数的返回值

[cpp] view plain copy
  1. int func(x)   
  2. {   
  3.      int countx = 0;   
  4.      while(x)   
  5.      {   
  6.          countx ++;   
  7.          x = x&(x-1);   
  8.       }   
  9.       return countx;   
  10. }   

假定x = 100 答案:countx = 8

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


19.重载(overload)和重写(overried,也称“覆盖”)的区别?

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

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

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

从实现原理上来说:

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

重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。



20. C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。


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

[cpp] view plain copy
  1. *((void (*)( ))0x100000 ) ( );  
  2. //首先要将0x100000强制转换成函数指针,即:  
  3. (void (*)())0x100000  
  4. // 然后再调用它:  
  5. *((void (*)())0x100000)();  
  6. // 用typedef可以看得更直观些:  
  7. typedef void(*)() voidFuncPtr;  
  8. *((voidFuncPtr)0x100000)();  

22. 复杂声明

[cpp] view plain copy
  1. void * ( * (*fp1)(int))[10];  
  2. float (*(* fp2)(int,int,int))(int);  
  3. 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型。

C/C++经典面试题
面试题 1:变量的声明和定义有什么区别
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。

面试题 2:写出 bool 、int、 float、指针变量与“零值”比较的 if 语句

[cpp] view plain copy
  1. //bool 型数据:  
  2. if( flag )  
  3. {  
  4.     A;  
  5. }  
  6. else  
  7. {  
  8.     B;  
  9. }  
  10. // int 型数据:  
  11. if( 0 != flag )  
  12. {  
  13.     A;  
  14. }  
  15. else  
  16. {  
  17.     B;  
  18. }  
  19. // 指针型数:  
  20. if( NULL == flag )  
  21. {  
  22.     A;  
  23. }  
  24. else  
  25. {  
  26.     B;  
  27. }  
  28. //float 型数据:  
  29. if ( ( flag >= NORM ) && ( flag <= NORM ) )  
  30. {  
  31.     A;  
  32. }  
注意:应特别注意在 int、指针型变量和“零值”比较的时候,把“零值”放在左边,这样当把“==”误写成“=”时,编译器可以报错,否则这种逻辑错误不容易发现,并且可能导致很严重的后果。

面试题 3:sizeof 和 strlen 的区别
 sizeof 和 strlen 有以下区别:
sizeof 是一个操作符,strlen 是库函数。
 sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\0‘的字符串作参数。
 编译器在编译时就计算出了 sizeof 的结果。而 strlen 函数必须在运行时才能计算出来。并且 sizeof计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。
 数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是 sizeof。

面试题 4:C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
注意:编程时 static 的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而 C++的静态成员则可以在多个对象实例间进行通信,传递信息。

面试题 5:C中的 malloc 和C++中的 new 有什么区别
malloc 和 new 有以下不同:
(1)new、delete 是操作符,可以重载,只能在 C++中使用。
(2)malloc、free 是函数,可以覆盖,C、C++中都可以使用。
(3)new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数。
(4)malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
(5)new、delete 返回的是某种数据类型指针,malloc、free 返回的是 void 指针。
注意:malloc 申请的内存空间要用 free 释放,而 new 申请的内存空间要用 delete 释放,不要混用。因为两者实现的机理不同。

面试题 6:写一个“标准”宏 MIN

[cpp] view plain copy
  1. #define min(a,b)((a)<=(b)?(a):(b))  
注意:在调用时一定要注意这个宏定义的副作用,如下调用:
[cpp] view plain copy
  1. ((++*p)<=(x)?(++*p):(x)  
p 指针就自加了两次,违背了 MIN 的本意。

面试题 7:一个指针可以是 volatile 吗
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。
说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。

面试题 8:a 和&a 有什么区别
请写出以下代码的打印结果,主要目的是考察 a 和&a 的区别。

[cpp] view plain copy
  1. #include<stdio.h>  
  2. void main( void )  
  3. {  
  4.     int a[5]={1,2,3,4,5};  
  5.     int *ptr=(int *)(&a+1);  
  6.     printf("%d,%d",*(a+1),*(ptr-1));  
  7.     return;  
  8. }  
输出结果:2,5。
注意:数组名 a 可以作数组的首地址,而&a 是数组的指针。思考,将原式的 int *ptr=(int *)(&a+1);改为 int *ptr=(int *)(a+1);时输出结果将是什么呢?

面试题 9:简述 C、C++程序编译的内存分配情况
C、C++中内存分配方式可以分为三种:
(1)从静态存储区域分配:
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。
(2)在栈上分配:
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
一个 C、C++程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。

面试题 10:简述 strcpy、sprintf 与 memcpy 的区别
三者主要有以下不同之处:
(1)操作对象不同, strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串, memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
(2)执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

面试题 11:设置地址为 0x67a9 的整型变量的值为 0xaa66

[cpp] view plain copy
  1. int *ptr;  
  2. ptr = (int *)0x67a9;  
  3. *ptr = 0xaa66;  
说明:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。

面试题 12:面向对象的三大特征
面向对象的三大特征是封装性、继承性和多态性:
 封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection(private, protected,public)。
 继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。

多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
说明:面向对象的三个特征是实现面向对象技术的关键,每一个特征的相关技术都非常的复杂,程序员应该多看、多练。

面试题 13:C++的空类有哪些成员函数
缺省构造函数。
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符 const。
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。

面试题 14:谈谈你对拷贝构造函数和赋值运算符的认识
拷贝构造函数和赋值运算符重载有以下两个不同之处:
(1)拷贝构造函数生成新的类对象,而赋值运算符不能。
(2)由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉.
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

面试题 15:用 C++设计一个不能被继承的类

[cpp] view plain copy
  1. template <typename T> class A  
  2. {  
  3.     friend T;  
  4.     private:  
  5.     A() {}  
  6.     ~A() {}  
  7. };  
  8. class B : virtual public A<B>  
  9. {  
  10.     public:  
  11.     B() {}  
  12.     ~B() {}  
  13. };  
  14. class C : virtual public B  
  15. {  
  16.     public:  
  17.     C() {}  
  18.     ~C() {}  
  19. };  
  20. void main( void )  
  21. {  
  22.     B b;  
  23.     //C c;  
  24.     return;  
  25. }  
注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。

面试题 16:访问基类的私有虚函数
写出以下程序的输出结果:

[cpp] view plain copy
  1. #include <iostream.h>  
  2. class A  
  3. {  
  4.     virtual void g()  
  5.     {  
  6.         cout << "A::g" << endl;  
  7.     }  
  8.     private:  
  9.         virtual void f()  
  10.         {  
  11.             cout << "A::f" << endl;  
  12.         }  
  13. };  
  14. class B : public A  
  15. {  
  16.     void g()  
  17.     {  
  18.         cout << "B::g" << endl;  
  19.     }  
  20.     virtual void h()  
  21.     {  
  22.         cout << "B::h" << endl;  
  23.     }  
  24. };  
  25. typedef void( *Fun )( void );  
  26. void main()  
  27. {  
  28.     B b;  
  29.     Fun pFun;  
  30.     for(int i = 0 ; i < 3; i++)  
  31.     {  
  32.         pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i );  
  33.         pFun();  
  34.     }  
  35. }  
输出结果:
B::g
A::f
B::h
注意:本题主要考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。

面试题 17:简述类成员函数的重写、重载和隐藏的区别
(1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。

参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。

virtual 的区别:重写的基类中被重写的函数必须要有 virtual 修饰,而重载函数和被重载函数可以被virtual 修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。
与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。
当参数不相同时,无论基类中的参数是否被 virtual 修饰,基类的函数都是被隐藏,而不是被重写。
说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。

面试题 18:简述多态实现的原理
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr(对 vc 编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,
指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable。如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态C++面试的重要考点之一,而虚函数是实现多态的基础。

面试题 19:链表和数组有什么区别
数组和链表有以下几点不同:
(1)存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
(2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
(3)数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
(4)越界问题:链表不存在越界问题,数组有越界问题。
说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。

面试题 20:怎样把一个单链表反序

[cpp] view plain copy
  1. // (1)反转一个链表。循环算法。  
  2. AList reverse(List n)  
  3. {  
  4.     if(!n)  //判断链表是否为空,为空即退出。  
  5.     {  
  6.         return n;  
  7.     }  
  8.     list cur = n.next; //保存头结点的下个结点  
  9.     list pre = n;   //保存头结点  
  10.     list tmp;  
  11.     pre.next = null; //头结点的指针指空,转换后变尾结点  
  12.     while ( NULL != cur.next )  //循环直到 cur.next 为空  
  13.     {  
  14.         tmp = cur;  
  15.         tmp.next = pre  
  16.         pre = tmp;  
  17.         cur = cur.next;  
  18.     }  
  19.     return tmp;     //f 返回头指针  
  20. }  
  21. // (2)反转一个链表。递归算法。  
  22. List *reverse( List *oldList, List *newHead = NULL )  
  23. {  
  24.     List *next = oldList-> next;     //记录上次翻转后的链表  
  25.     oldList-> next = newHead;    //将当前结点插入到翻转后链表的开头  
  26.     newHead = oldList;  //递归处理剩余的链表  
  27.       
  28.     return ( next==NULL )? newHead: reverse( t, newHead );  
  29. }  
说明:循环算法就是图 10.2—图 10.5 的移动过程,比较好理解和想到。递归算法的设计虽有一点难度,但是理解了循环算法,再设计递归算法就简单多了。

面试题 21:简述队列和栈的异同
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”。
注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收。分配方式类似于链表。它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

面试题 22:能否用两个栈实现一个队列的功能

[cpp] view plain copy
  1. // 结点结构体:  
  2. typedef struct node  
  3. {  
  4.     int data;  
  5.     node *next;  
  6. }node,*LinkStack;  
  7. //创建空栈:  
  8. LinkStack CreateNULLStack( LinkStack &S)  
  9. {  
  10.     S = (LinkStack)malloc( sizeof( node ) );    //申请新结点  
  11.     if( NULL == S)  
  12.     {  
  13.         printf("Fail to malloc a new node.\n");  
  14.         return NULL;  
  15.     }  
  16.     S->data = 0;     //初始化新结点  
  17.     S->next = NULL;  
  18.     return S;  
  19. }  
  20. // 栈的插入函数:  
  21. LinkStack Push( LinkStack &S, int data)  
  22. {  
  23.     if( NULL == S)  //检验栈  
  24.     {  
  25.         printf("There no node in stack!");  
  26.         return NULL;  
  27.     }  
  28.     LinkStack p = NULL;  
  29.     p = (LinkStack)malloc( sizeof( node ) );    //申请新结点  
  30.     if( NULL == p)  
  31.     {  
  32.         printf("Fail to malloc a new node.\n");  
  33.         return S;  
  34.     }  
  35.     if( NULL == S->next)  
  36.     {  
  37.         p->next = NULL;  
  38.     }  
  39.     else  
  40.     {  
  41.         p->next = S->next;  
  42.     }  
  43.     p->data = data;      //初始化新结点  
  44.     S->next = p;     //插入新结点  
  45.     return S;  
  46. }  
  47. // 出栈函数:  
  48. node Pop( LinkStack &S)  
  49. {  
  50.     node temp;  
  51.     temp.data = 0;  
  52.     temp.next = NULL;  
  53.     if( NULL == S)  //检验栈  
  54.     {  
  55.         printf("There no node in stack!");  
  56.         return temp;  
  57.     }  
  58.     temp = *S;  
  59.     if( S->next == NULL )  
  60.     {  
  61.         printf("The stack is NULL,can't pop!\n");  
  62.         return temp;  
  63.     }  
  64.     LinkStack p = S ->next;  //节点出栈  
  65.     S->next = S->next->next;  
  66.     temp = *p;  
  67.     free( p );  
  68.     p = NULL;  
  69.     return temp;  
  70. }  
  71. // 双栈实现队列的入队函数:  
  72. LinkStack StackToQueuPush( LinkStack &S, int data)  
  73. {  
  74.     node n;  
  75.     LinkStack S1 = NULL;  
  76.     CreateNULLStack( S1 );  //创建空栈  
  77.     while( NULL != S->next )     //S 出栈入 S1  
  78.     {  
  79.         n = Pop( S );  
  80.         Push( S1, n.data );  
  81.     }  
  82.     Push( S1, data );   //新结点入栈  
  83.     while( NULL != S1->next )    //S1 出栈入 S  
  84.     {  
  85.         n = Pop( S1 );  
  86.         Push( S, n.data );  
  87.     }  
  88.     return S;  
  89. }  
说明:用两个栈能够实现一个队列的功能,那用两个队列能否实现一个队列的功能呢?结果是否定的,因为栈是先进后出,将两个栈连在一起,就是先进先出。而队列是现先进先出,无论多少个连在一起都是先进先出,而无法实现先进后出。

面试题 23:计算一颗二叉树的深度

[cpp] view plain copy
  1. // 深度的计算函数:  
  2. int depth(BiTree T)  
  3. {  
  4.     if(!T) return 0;  
  5.     //判断当前结点是否为叶子结点  
  6.     int d1= depth(T->lchild);//求当前结点的左孩子树的深度  
  7.     int d2= depth(T->rchild);//求当前结点的右孩子树的深度  
  8.     return (d1>d2?d1:d2)+1;  
  9. }  
注意:根据二叉树的结构特点,很多算法都可以用递归算法来实现。

面试题 24:编码实现直接插入排序

[cpp] view plain copy
  1. // 直接插入排序编程实现如下:  
  2. #include<iostream.h>  
  3. void main( void )  
  4. {  
  5.     int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 };  
  6.     int i,j;  
  7.     for( i = 0; i < 10; i++)  
  8.     {  
  9.         cout<<ARRAY[i]<<" ";  
  10.     }  
  11.     cout<<endl;  
  12.     for( i = 2; i <= 10; i++ )   //将 ARRAY[2],...,ARRAY[n]依次按序插入  
  13.     {  
  14.         if(ARRAY[i] < ARRAY[i-1])    //如果 ARRAY[i]大于一切有序的数值,  
  15.                     //ARRAY[i]将保持原位不动  
  16.         {  
  17.             ARRAY[0] = ARRAY[i];    //将 ARRAY[0]看做是哨兵,是 ARRAY[i]的副本  
  18.             j = i - 1;  
  19.             do{  
  20.                             //从右向左在有序区 ARRAY[1..i-1]中  
  21.                             //查找 ARRAY[i]的插入位置  
  22.                 ARRAY[j+1] = ARRAY[j]; //将数值大于 ARRAY[i]记录后移  
  23.                 j-- ;  
  24.             }while( ARRAY[0] < ARRAY[j] );  
  25.             ARRAY[j+1]=ARRAY[0];    //ARRAY[i]插入到正确的位置上  
  26.         }  
  27.     }  
  28.     for(i = 0; i < 10; i++)  
  29.     {  
  30.         cout<<ARRAY[i]<<" ";  
  31.     }  
  32.     cout<<endl;  
  33. }  
注意:所有为简化边界条件而引入的附加结点(元素)均可称为哨兵。引入哨兵后使得查找循环条件的时间大约减少了一半,对于记录数较大的文件节约的时间就相当可观。类似于排序这样使用频率非常高的算法,要尽可能地减少其运行时间。所以不能把上述算法中的哨兵视为雕虫小技。

面试题 25:编码实现冒泡排序

[cpp] view plain copy
  1. // 冒泡排序编程实现如下:  
  2. #include <stdio.h>  
  3. #define LEN 10 //数组长度  
  4. void main( void )  
  5. {  
  6.     int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; //待排序数组  
  7.     printf( "\n" );  
  8.     forint a = 0; a < LEN; a++ )   //打印数组内容  
  9.     {  
  10.         printf( "%d ", ARRAY[a] );  
  11.     }  
  12.     int i = 0;  
  13.     int j = 0;  
  14.     bool isChange; //设定交换标志  
  15.     for( i = 1; i < LEN; i++ )  
  16.     {   //最多做 LEN-1 趟排序  
  17.         isChange = 0;       //本趟排序开始前,交换标志应为假  
  18.         for( j = LEN-1; j >= i; j-- )        //对当前无序区 ARRAY[i..LEN]自下向上扫描  
  19.         {  
  20.             if( ARRAY[j+1] < ARRAY[j] )  
  21.             {   //交换记录  
  22.                 ARRAY[0] = ARRAY[j+1];  //ARRAY[0]不是哨兵,仅做暂存单元  
  23.                 ARRAY[j+1] = ARRAY[j];  
  24.                 ARRAY[j] = ARRAY[0];  
  25.                 isChange = 1;  //发生了交换,故将交换标志置为真  
  26.             }  
  27.         }  
  28.         printf( "\n" );  
  29.         for( a = 0; a < LEN; a++)  
  30.         //打印本次排序后数组内容  
  31.         {  
  32.             printf( "%d ", ARRAY[a] );  
  33.         }  
  34.         if( !isChange )  
  35.         {  
  36.             break;  //本趟排序未发生交换,提前终止  
  37.         }  
  38.     }  
  39.     printf( "\n" );  
  40.     return;  
  41. }  

26:编码实现直接选择排序

[cpp] view plain copy
  1. 26:编码实现直接选择排序  
  2. #include"stdio.h"  
  3. #define LEN 9  
  4. void main( void )  
  5. {  
  6.     int ARRAY[LEN]={ 5, 6, 8, 2, 4, 1, 9, 3, 7 };   //待序数组  
  7.     printf("Before sorted:\n");  
  8.     forint m = 0; m < LEN; m++ )   //打印排序前数组  
  9.     {  
  10.         printf( "%d", ARRAY[m] );  
  11.     }  
  12.     for (int i = 1; i <= LEN - 1; i++) //选择排序  
  13.     {  
  14.         int t = i - 1;  
  15.         int temp = 0;  
  16.         for (int j = i; j < LEN; j++)  
  17.         {  
  18.             if (ARRAY[j] < ARRAY[t])  
  19.             {  
  20.                 t = j;  
  21.             }  
  22.         }  
  23.         if (t != (i - 1))  
  24.         {  
  25.             temp = ARRAY[i - 1];  
  26.             ARRAY[i - 1] = ARRAY[t];  
  27.             ARRAY[t] = temp;  
  28.         }  
  29.     }  
  30.     printf( "\n" );  
  31.     printf("After sorted:\n");  
  32.     for( i = 0; i < LEN; i++ )   //打印排序后数组  
  33.     {  
  34.         printf( "%d ", ARRAY[i] );  
  35.     }  
  36.     printf( "\n" );  
  37. }  
注意:在直接选择排序中,具有相同关键码的对象可能会颠倒次序,因而直接选择排序算法是一种不稳定的排序方法。在本例中只是例举了简单的整形数组排序,肯定不会有什么问题。但是在复杂的数据元素序列组合中,只是根据单一的某一个关键值排序,直接选择排序则不保证其稳定性,这是直接选择排序的一个弱点。

面试题 27:编程实现堆排序

[cpp] view plain copy
  1. // 堆排序编程实现:  
  2. #include <stdio.h>  
  3. void createHeep(int ARRAY[],int sPoint, int Len)    //生成大根堆  
  4. {  
  5.     while( ( 2 * sPoint + 1 ) < Len )  
  6.     {  
  7.         int mPoint = 2 * sPoint + 1 ;  
  8.         if( ( 2 * sPoint + 2 ) < Len )  
  9.         {  
  10.             if(ARRAY[ 2 * sPoint + 1 ] < ARRAY[ 2 * sPoint + 2 ] )  
  11.             {  
  12.                 mPoint = 2*sPoint+2;  
  13.             }  
  14.         }  
  15.         if(ARRAY[ sPoint ] < ARRAY[ mPoint ])    //堆被破坏,需要重新调整  
  16.         {  
  17.             int tmpData= ARRAY[ sPoint ];   //交换 sPoint 与 mPoint 的数据  
  18.             ARRAY[ sPoint ] = ARRAY[ mPoint ];  
  19.             ARRAY[ mPoint ] = tmpData;  
  20.             sPoint = mPoint ;  
  21.         }  
  22.         else  
  23.         {  
  24.             break;  //堆未破坏,不再需要调整  
  25.         }  
  26.     }  
  27.     return;  
  28. }  
  29. void heepSort( int ARRAY[], int Len )   //堆排序  
  30. {  
  31.     int i=0;  
  32.     for ( i = ( Len / 2 - 1 ); i >= 0; i-- ) //将 Hr[0,Lenght-1]建成大根堆  
  33.     {  
  34.         createHeep(ARRAY, i, Len);  
  35.     }  
  36.     for ( i = Len - 1; i > 0; i-- )  
  37.     {  
  38.         int tmpData = ARRAY[0]; //与最后一个记录交换  
  39.         ARRAY[0] = ARRAY[i];  
  40.         ARRAY[i] = tmpData;  
  41.         createHeep( ARRAY, 0, i );  //将 H.r[0..i]重新调整为大根堆  
  42.     }  
  43.     return;  
  44. }  
  45. int main( void )  
  46. {  
  47.     int ARRAY[] ={ 5, 4, 7, 3, 9, 1, 6, 8, 2};  
  48.     printf("Before sorted:\n"); //打印排序前数组内容  
  49.     for ( int i = 0; i < 9; i++ )  
  50.     {  
  51.         printf("%d ", ARRAY[i]);  
  52.     }  
  53.     printf("\n");  
  54.     heepSort( ARRAY, 9 ); //堆排序  
  55.     printf("After sorted:\n"); //打印排序后数组内容  
  56.     for( i = 0; i < 9; i++ )  
  57.     {  
  58.         printf( "%d ", ARRAY[i] );  
  59.     }  
  60.     printf( "\n" );  
  61.     return 0;  
  62. }  
说明:堆排序,虽然实现复杂,但是非常的实用。另外读者可是自己设计实现小堆排序的算法。虽然和大堆排序的实现过程相似,但是却可以加深对堆排序的记忆和理解。

面试题 28:编程实现基数排序

[cpp] view plain copy
  1. #include <stdio.h>  
  2. #include <malloc.h>  
  3. #define LEN 8  
  4. typedef struct node     //队列结点  
  5. {  
  6.     int data;  
  7.     struct node * next;  
  8. }node,*QueueNode;  
  9. typedef struct Queue    //队列  
  10. {  
  11.     QueueNode front;  
  12.     QueueNode rear;  
  13. }Queue,*QueueLink;  
  14. QueueLink CreateNullQueue( QueueLink &Q)    //创建空队列  
  15. {  
  16.     Q = NULL;  
  17.     Q = ( QueueLink )malloc( sizeof( Queue ) );  
  18.     if( NULL == Q )  
  19.     {  
  20.         printf("Fail to malloc null queue!\n");  
  21.         return NULL;  
  22.     }  
  23.     15Q->front = ( QueueNode )malloc( sizeof( node ) );  
  24.     Q->rear = ( QueueNode )malloc( sizeof( node ) );  
  25.     if( NULL == Q->front || NULL == Q->rear )  
  26.     {  
  27.         printf("Fail to malloc a new queue's fornt or rear!\n");  
  28.         return NULL;  
  29.     }  
  30.     Q->rear = NULL;  
  31.     Q->front->next= Q->rear;  
  32.     return Q;  
  33. }  
  34. int lenData( node data[], int len)  //计算队列中各结点的数据的最大位数  
  35. {  
  36.     int m = 0;  
  37.     int temp = 0;  
  38.     int d;  
  39.     forint i = 0; i < len; i++)  
  40.     {  
  41.         d = data[i].data;  
  42.         while( d > 0)  
  43.         {  
  44.             d /= 10;  
  45.             temp ++;  
  46.         }  
  47.         if( temp > m )  
  48.         {  
  49.             m = temp;  
  50.         }  
  51.         temp = 0;  
  52.     }  
  53.     return m;  
  54. }  
  55.   
  56. QueueLink Push( QueueLink &Q , node node )  //将数据压入队列  
  57. {  
  58.     QueueNode p1,p;  
  59.     p =( QueueNode )malloc( sizeof( node ) );  
  60.     if( NULL == p )  
  61.     {  
  62.         printf("Fail to malloc a new node!\n");  
  63.         return NULL;  
  64.     }  
  65.     p1 = Q->front;  
  66.     while(p1->next != NULL)  
  67.     {  
  68.         p1 = p1->next;  
  69.     }  
  70.     p->data = node.data;  
  71.     p1->next = p;  
  72.     p->next = Q->rear;  
  73.     return NULL;  
  74. }  
  75. node Pop( QueueLink &Q)     //数据出队列  
  76. {  
  77.     node temp;  
  78.     temp.data = 0;  
  79.     temp.next = NULL;  
  80.     QueueNode p;  
  81.     p = Q->front->next;  
  82.     if( p != Q->rear )  
  83.     {  
  84.         temp = *p;  
  85.         Q->front->next = p->next;  
  86.         free( p );  
  87.         p = NULL;  
  88.     }  
  89.     return temp;  
  90. }  
  91.   
  92. int IsEmpty( QueueLink Q)  
  93. {  
  94.     if( Q->front->next == Q->rear )  
  95.     {  
  96.         return 0;  
  97.     }  
  98.     return 1;  
  99. }  
  100.   
  101. int main( void )  
  102.     {  
  103.     int i = 0;  
  104.     int Max = 0;    //记录结点中数据的最大位数  
  105.     int d = 10;  
  106.     int power = 1;  
  107.     int k = 0;  
  108.     node Array[LEN] ={{450, NULL}, {32,NULL}, { 781,NULL}, { 57 ,NULL},{ 145,NULL},{ 613,NULL},{ 401,NULL},{ 594,NULL}};    //队列结点数组  
  109.     QueueLink Queue[10];  
  110.     for( i = 0; i < 10; i++)  
  111.     {  
  112.         CreateNullQueue( Queue[i]); //初始化队列数组  
  113.     }  
  114.     for( i = 0; i < LEN; i++)  
  115.     {  
  116.         printf("%d ",Array[i].data);  
  117.     }  
  118.     printf("\n");  
  119.     Max = lenData( Array, LEN );    //计算数组中关键字的最大位数  
  120.     printf("%d\n",Max);  
  121.     for(int j = 0; j < Max; j++) //按位排序  
  122.     {  
  123.         if(j == 0) power = 1;  
  124.         else power = power *d;  
  125.         for(i = 0; i < LEN; i++)  
  126.         {  
  127.             k = Array[i].data /power - (Array[i].data/(power * d)) * d;  
  128.             Push( Queue[k], Array[i] );  
  129.         }  
  130.         for(int l = 0, k = 0; l < d; l++)    //排序后出队列重入数组  
  131.         {  
  132.             while( IsEmpty( Queue[l] ) )  
  133.             {  
  134.                 Array[k++] = Pop( Queue[l] );  
  135.             }  
  136.         }  
  137.         forint t = 0; t < LEN; t++)  
  138.         {  
  139.             printf("%d ",Array[t].data);  
  140.         }  
  141.         printf("\n");  
  142.     }  
  143.     return 0;  
  144. }  
说明:队列为基数排序的实现提供了很大的方便,适当的数据机构可以减少算法的复杂度,让更多的算法实现更容易。

面试题 29:谈谈你对编程规范的理解或认识
编程规范可总结为:程序的可行性,可读性、可移植性以及可测试性。
说明:这是编程规范的总纲目,面试者不一定要去背诵上面给出的那几个例子,应该去理解这几个例子说明的问题,想一想,自己如何解决可行性、可读性、可移植性以及可测试性这几个问题,结合以上几个例子和自己平时的编程习惯来回答这个问题。

面试题 30:short i = 0; i = i + 1L;这两句有错吗
代码一是错的,代码二是正确的。
说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。

面试题 31:&&和&、||和|有什么区别
(1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系。
(2)&&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可预想的后果(比如当两个操作数一个是 1 另一个是 2 时)。

面试题 32:C++的引用和 C 语言的指针有什么区别
指针和引用主要有以下区别:
(1)引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
(2)引用初始化以后不能被改变,指针可以改变所指的对象。
(3)不存在指向空值的引用,但是存在指向空值的指针。
注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误。所以使用时一定要小心谨慎。

面试题 33:在二元树中找出和为某一值的所有路径
输入一个整数和一棵二元树。从树的根结点开始往下访问,一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。例如,输入整数 9 和如下二元树:
           3
        /     \
      2        6
    / \
  5   4
则打印出两条路径:3,6 和 3,2,4。
【答案】

[cpp] view plain copy
  1. typedef struct path  
  2. {  
  3.     BiTNode* tree;//结点数据成员  
  4.     struct path* next;//结点指针成员  
  5. }PATH,*pPath;  
  6. // 初始化树的结点栈:  
  7. void init_path( pPath* L )  
  8. {  
  9.     *L = ( pPath )malloc( sizeof( PATH ) ); //创建空树  
  10.     ( *L )->next = NULL;  
  11. }  
  12. // 树结点入栈函数:  
  13. void push_path(pPath H, pBTree T)  
  14. {  
  15.     pPath p = H->next;  
  16.     pPath q = H;  
  17.     while( NULL != p )  
  18.     {  
  19.         q = p;  
  20.         p = p->next;  
  21.     }  
  22.     p = ( pPath )malloc( sizeof( PATH ) );  //申请新结点  
  23.     p->next = NULL;  //初始化新结点  
  24.     p->tree = T;  
  25.     q->next = p; //新结点入栈  
  26. }  
  27. // 树结点打印函数:  
  28. void print_path( pPath L )  
  29. {  
  30.     pPath p = L->next;  
  31.     while( NULL != p )  //打印当前栈中所有数据  
  32.     {  
  33.         printf("%d, ", p->tree->data);  
  34.         p = p->next;  
  35.     }  
  36. }  
  37. // 树结点出栈函数:  
  38. void pop_path( pPath H )  
  39. {  
  40.     pPath p = H->next;  
  41.     pPath q = H;  
  42.     if( NULL == p ) //检验当前栈是否为空  
  43.     {  
  44.         printf("Stack is null!\n");  
  45.         return;  
  46.     }  
  47.     p = p->next;  
  48.     while( NULL != p )  //出栈  
  49.     {  
  50.         q = q->next;  
  51.         p = p->next;  
  52.     }  
  53.     free( q->next ); //释放出栈结点空间  
  54.     q->next = NULL;  
  55. }  
  56. // 判断结点是否为叶子结点:  
  57. int IsLeaf(pBTree T)  
  58. {  
  59.     return ( T->lchild == NULL )&&( T->rchild==NULL );  
  60. }  
  61.   
  62. // 查找符合条件的路径:  
  63. int find_path(pBTree T, int sum, pPath L)  
  64. {  
  65.     push_path( L, T);  
  66.     record += T->data;  
  67.     if( ( record == sum ) && ( IsLeaf( T ) ) )  //打印符合条件的当前路径  
  68.     {  
  69.         print_path( L );  
  70.         printf( "\n" );  
  71.     }  
  72.     if( T->lchild != NULL )  //递归查找当前节点的左孩子  
  73.     {  
  74.         find_path( T->lchild, sum, L);  
  75.     }  
  76.     if( T->rchild != NULL )  //递归查找当前节点的右孩子  
  77.     {  
  78.         find_path( T->rchild, sum, L);  
  79.     }  
  80.     record -= T->data;  
  81.     pop_path(L);  
  82.     return 0;  
  83. }  
注意:数据结构一定要活学活用,例如本题,把所有的结点都压入栈,而不符合条件的结点弹出栈,很容易实现了有效路径的查找。虽然用链表也可以实现,但是用栈更利于理解这个问题,即适当的数据结构为更好的算法设计提供了有利的条件。

面试题 35:typedef 和 define 有什么区别
(1)用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
(2)执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
(3)作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
(4)对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。

面试题 36:关键字 const 是什么?
const 用来定义一个只读的变量或对象。主要优点:便于类型检查、同宏定义一样可以方便地进行参数的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。
说明:const 修饰函数参数,是一种编程规范的要求,便于阅读,一看即知这个参数不能被改变,实现时不易出错。

面试题 37:static 有什么作用
static 在 C 中主要用于定义全局静态变量、定义局部静态变量、定义静态函数。在 C++中新增了两种作用:定义静态数据成员、静态函数成员。
注意:因为 static 定义的变量分配在静态区,所以其定义的变量的默认值为 0,普通变量的默认值为随机数,在定义指针变量时要特别注意。

面试题 38:extern 有什么作用
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。

面试题 39:流操作符重载为什么返回引用
在程序中,流操作符>>和<<经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
注意:除了在赋值操作符和流操作符之外的其他的一些操作符中,如+、-、*、/等却千万不能返回引用。因为这四个操作符的对象都是右值,因此,它们必须构造一个对象作为返回值。

面试题 40:简述指针常量与常量指针区别
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

面试题 41:数组名和指针的区别
请写出以下代码的打印结果:

[cpp] view plain copy
  1. #include <iostream.h>  
  2. #include <string.h>  
  3. void main(void)  
  4. {  
  5.     char str[13]="Hello world!";  
  6.     char *pStr="Hello world!";  
  7.     cout<<sizeof(str)<<endl;  
  8.     cout<<sizeof(pStr)<<endl;  
  9.     cout<<strlen(str)<<endl;  
  10.     cout<<strlen(pStr)<<endl;  
  11.     return;  
  12. }  
【答案】
打印结果:
13
4
12
12
注意:一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富的多。但是当数组名当做参数传递给函数后,其失去原来的含义,变作普通的指针。另外要注意 sizeof 不是函数,只是操作符。

面试题 42:如何避免“野指针”
“野指针”产生原因及解决办法如下:
(1)指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
(2)指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向 NULL。
(3)指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。
注意:
“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,
在使用指针前一定要检验指针的合法性。

面试题 43:常引用有什么作用
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。

说明:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。

面试题 44:编码实现字符串转化为数字
编码实现函数 atoi(), 设计一个程序,把一个字符串转化为一个整型数值。例如数字:“5486321”,
转化成字符:5486321。
【答案】

[cpp] view plain copy
  1. int myAtoi(const char * str)  
  2. {  
  3.     int num = 0;//保存转换后的数值  
  4.     int isNegative = 0;     //记录字符串中是否有负号  
  5.     int n =0;  
  6.     char *p = str;  
  7.     if(p == NULL) //判断指针的合法性  
  8.     {  
  9.         return -1;  
  10.     }  
  11.     while(*p++ != '\0'//计算数字符串度  
  12.     {  
  13.         n++;  
  14.     }  
  15.     p = str;  
  16.     if(p[0] == '-'//判断数组是否有负号  
  17.     {  
  18.         isNegative = 1;  
  19.     }  
  20.     char temp = '0';  
  21.     for(int i = 0 ; i < n; i++)  
  22.     {  
  23.         char temp = *p++;  
  24.         if(temp > '9' ||temp < '0')   //滤除非数字字符  
  25.         {  
  26.             continue;  
  27.         }  
  28.         if(num !=0 || temp != '0')  //滤除字符串开始的 0 字符  
  29.         {  
  30.             temp -= 0x30;   //将数字字符转换为数值  
  31.             num += temp *int( pow(10 , n - 1 -i) );  
  32.         }  
  33.     }  
  34.     if(isNegative)  //如果字符串中有负号,将数值取反  
  35.     {  
  36.         return (0 - num);  
  37.     }  
  38.     else  
  39.     {  
  40.         return num; //返回转换后的数值  
  41.     }  
  42. }  
注意:此段代码只是实现了十进制字符串到数字的转化,读者可以自己去实现 2 进制,8 进制,10进制,16 进制的转化。

面试题 45:简述 strcpy、sprintf 与 memcpy 的区别
三者主要有以下不同之处:
(1)操作对象不同, strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串, memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
(2)执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来
选择合适的函数实现拷贝功能。

面试题 46:用 C 编写一个死循环程序

[cpp] view plain copy
  1. while(1)  
  2. { }  
说明:很多种途径都可实现同一种功能,但是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间较小,所以时间和空间优势是选择各种方法的首要考虑条件。

面试题 47:编码实现某一变量某位清 0 或置 1
给定一个整型变量 a,写两段代码,第一个设置 a 的 bit 3,第二个清 a 的 bit 3,在以上两个操作中,要保持其他位不变。
【答案】

[cpp] view plain copy
  1. #define BIT3 (0x1 << 3 )  
  2. Satic int a;  
  3. //设置 a 的 bit 3:  
  4. void set_bit3( void )  
  5. {  
  6.     a |= BIT3;//将 a 第 3 位置 1  
  7. }  
  8. //清 a 的 bit 3  
  9. void set_bit3( void )  
  10. {  
  11.     a &= ~BIT3;  //将 a 第 3 位清零  
  12. }  
说明:在置或清变量或寄存器的某一位时,一定要注意不要影响其他位。所以用加减法是很难实现的。

面试题 48:评论下面这个中断函数
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展——让标准 C 支持中断。具体代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt 关键字去定义一个中断服务子程序(ISR),请评论以下这段代码。

[cpp] view plain copy
  1. __interrupt double compute_area (double radius)  
  2. {  
  3.     double area = PI * radius * radius;  
  4.     printf(" Area = %f", area);  
  5.     return area;  
  6. }  
【答案】
这段中断服务程序主要有以下四个问题:
(1)ISR 不能返回一个值。
(2)ISR 不能传递参数。
(3)在 ISR 中做浮点运算是不明智的。
(4)printf()经常有重入和性能上的问题。
注意:本题的第三个和第四个问题虽不是考察的重点,但是如果能提到这两点可给面试官留下一个好印象。

面试题 49:构造函数能否为虚函数
构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。说明:虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。

面试题 50:谈谈你对面向对象的认识
面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。
说明:编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。
  
原创粉丝点击