C/C++笔试题

来源:互联网 发布:并口编程 windowsapi 编辑:程序博客网 时间:2024/04/29 20:21

C++笔试(1

1.求下面函数的返回值(微软)

  int func(x)

  {

  int countx = 0;

  while(x)

  {

  countx ++;

  x = x&(x-1);

  }

  return countx;

  }

  假定x = 9999 答案:8

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

 

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

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

 

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

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

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

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

 

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

  如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式: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

 

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

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

  好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生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 << "hello" << endl;

  因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。

  例3

  #i nclude <iostream.h>

  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<<vals[0];

  cout<<vals[9];

  }

  int &put(int n)

  {

  if (n>=0 && n<=9 ) return vals[n];

  else { cout<<"subscript error"; return error; }

  }

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

 

  6. “引用”与多态的关系?

  引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。

  例4

  Class A; Class B : Class A{...}; B b; A& ref = b;

 

  7. “引用”与指针的区别是什么?

  指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传refpointer的区别。

 

  8. 什么时候需要“引用”?

  流操作符<<>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。

 

  以上 2-8 参考:http://blog.csdn.net/wfwd/archive/2006/05/30/763551.aspx

 

  9. 结构与联合有和区别?

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

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

 

  10. 下面关于“联合”的题目的输出?

  a)

  #i nclude <stdio.h>

  union

  {

  int i;

  char x[2];

  }a;

  void main()

  {

  a.x[0] = 10;

  a.x[1] = 1;

  printf("%d",a.i);

  }

  答:266 (低位低地址,高位高地址,内存占用情况是Ox010A

  b)

  main()

  {

  union{ /*定义一个联合*/

         int i;

         struct{ /*在联合中定义一个结构*/

                   char first;

                char second;

         }half;

  }number;

  number.i=0x4241; /*联合成员赋值*/

  printf("%c%c/n", number.half.first, mumber.half.second);

  number.half.first='a'; /*联合中结构成员赋值*/

  number.half.second='b';

  printf("%x/n", number.i);

  getch();

  }

  答案: AB (0x41对应'A',是低位;Ox42对应'B',是高位)

  6261 (number.inumber.half共用一块地址空间)

 

  11. 已知strcpy的函数原型:char *strcpy(char *strDest, const char *strSrc)其中strDest 是目的字符串,strSrc 是源字符串。不调用C++/C 的字符串库函数,请编写函数 strcpy

  答案:

  char *strcpy(char *strDest, const char *strSrc)

  {

  if ( strDest == NULL || strSrc == NULL)

  return NULL ;

  if ( strDest == strSrc)

  return strDest ;

  char *tempptr = strDest ;

  while( (*strDest++ = *strSrc++) != /0)

  ;

  return tempptr ;

  }

 

  12. 已知String类定义如下:

  class String

  {

  public:

  String(const char *str = NULL); // 通用构造函数

  String(const String &another); // 拷贝构造函数

  ~ String(); // 析构函数

  String & operater =(const String &rhs); // 赋值函数

  private:

  char *m_data; // 用于保存字符串

  };

  尝试写出类的成员函数实现。

  答案:

  String::String(const char *str)

  {

  if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断

  {

  m_data = new char[1] ;

  m_data[0] = '/0' ;

  }

  else

  {

  m_data = new char[strlen(str) + 1];

  strcpy(m_data,str);

  }

  }

  String::String(const String &another)

  {

  m_data = new char[strlen(another.m_data) + 1];

  strcpy(m_data,other.m_data);

  }

  String& String::operator =(const String &rhs)

  {

  if ( this == &rhs)

  return *this ;

  delete []m_data; //删除原来的数据,新开一块内存

  m_data = new char[strlen(rhs.m_data) + 1];

  strcpy(m_data,rhs.m_data);

  return *this ;

  }

  String::~String()

  {

  delete []m_data ;

  }

 

  13. .h头文件中的ifndef/define/endif 的作用?

  答:防止该头文件被重复引用。

 

  14. i nclude<file.h> i nclude "file.h"的区别?

  答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h

 

  15.C++ 程序中调用被C 编译器编译后的函数,为什么要加extern C”?

  首先,作为externC/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用

  通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数

  extern "C"是连接申明(linkage declaration),extern "C"修饰的变量和函数是按照C语言方式编译和连接的,来看看C++中对类似C的函数是怎样编译的:

  作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:

  void foo( int x, int y );

  该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。

  _foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float

  同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

  未加extern "C"声明时的连接方式

  假设在C++中,模块A的头文件如下:

  // 模块A头文件

  moduleA.h

  #ifndef MODULE_A_H

  #define MODULE_A_H

  int foo( int x, int y );

  #endif

  在模块B中引用该函数:

  // 模块B实现文件

  moduleB.cpp

  #i nclude "moduleA.h"

  foo(2,3);

  实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

  加extern "C"声明后的编译和连接方式

  加extern "C"声明后,模块A的头文件变为:

  // 模块A头文件

  moduleA.h

  #ifndef MODULE_A_H

  #define MODULE_A_H

  extern "C" int foo( int x, int y );

  #endif

  在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

  (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

  (2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo

  如果在模块A中函数声明了fooextern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

  所以,可以用一句话概括extern C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++C及其它语言的混合编程。

  明白了C++extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧:

  extern "C"的惯用法

  (1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

  extern "C"

  {

  #i nclude "cExample.h"

  }

  而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

  C++引用C函数例子工程中包含的三个文件的源代码如下:

  /* c语言头文件:cExample.h */

  #ifndef C_EXAMPLE_H

  #define C_EXAMPLE_H

  extern int add(int x,int y);

  #endif

  /* c语言实现文件:cExample.c */

  #i nclude "cExample.h"

  int add( int x, int y )

  {

  return x + y;

  }

  // c++实现文件,调用addcppFile.cpp

  extern "C"

  {

  #i nclude "cExample.h"

  }

  int main(int argc, char* argv[])

  {

  add(2,3);

  return 0;

  }

  如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" {

  }

  (2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。

  C引用C++函数例子工程中包含的三个文件的源代码如下:

  //C++头文件 cppExample.h

  #ifndef CPP_EXAMPLE_H

  #define CPP_EXAMPLE_H

  extern "C" int add( int x, int y );

  #endif

  //C++实现文件 cppExample.cpp

  #i nclude "cppExample.h"

  int add( int x, int y )

  {

  return x + y;

  }

  /* C实现文件 cFile.c

  /* 这样会编译出错:#i nclude "cExample.h" */

  extern int add( int x, int y );

  int main( int argc, char* argv[] )

  {

  add( 2, 3 );

  return 0;

  }

 

  15题目的解答请参考《C++extern C”含义深层探索》注解:

 

  16. 关联、聚合(Aggregation)以及组合(Composition)的区别?

  涉及到UML中的一些概念:关联是表示两个类的一般性联系,比如“学生”和“老师”就是一种关联关系;聚合表示has-a的关系,是一种相对松散的关系,聚合类不需要对被聚合类负责,如下图所示,用空的菱形表示聚合关系:

  从实现的角度讲,聚合可以表示为:

  class A {...} class B { A* a; .....}

  而组合表示contains-a的关系,关联性强于聚合:组合类与被组合类有相同的生命周期,组合类要对被组合类负责,采用实心的菱形表示组合关系:

  实现的形式是:

  class A{...} class B{ A a; ...}

 

  参考文章:http://blog.csdn.net/wfwd/archive/2006/05/30/763753.aspx

  http://blog.csdn.net/wfwd/archive/2006/05/30/763760.aspx

 

  17.面向对象的三个基本特征,并简单叙述之?

  1. 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)

  2. 继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。

  3. 多态:是将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针

 

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

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

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

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

  从实现原理上来说:

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

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

 

  19. 多态的作用?

  主要是两个:1. 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;2. 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。

 

  20. AdoAdo.net的相同与不同?

  除了“能够让应用程序处理存储于DBMS 中的数据“这一基本相似点外,两者没有太多共同之处。但是Ado使用OLE DB 接口并基于微软的COM 技术,而ADO.NET 拥有自己的ADO.NET 接口并且基于微软的.NET 体系架构。众所周知.NET 体系不同于COM 体系,ADO.NET 接口也就完全不同于ADOOLE DB 接口,这也就是说ADO.NET ADO是两种数据访问方式。ADO.net 提供对XML 的支持。

 

  21. New delete malloc free 的联系与区别?

  答案:都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new 会自动调用对象的构造函数。delete 会调用对象的destructor,而free 不会调用对象的destructor.

 

  22. #define DOUBLE(x) x+x i = 5*DOUBLE(5) i 是多少?

  答案:i 30

 

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

  答案:当类中含有constreference 成员变量;基类的构造函数都需要初始化表。

 

  24. C++是不是类型安全的?

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

 

  25. main 函数执行以前,还会执行什么代码?

  答案:全局对象的构造函数会在main 函数之前执行。

 

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

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

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

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

 

  27.struct class 的区别

  答案:struct 的成员默认是公有的,而类的成员默认是私有的struct class 在其他方面是功能相当的。

  从感情上讲,大多数的开发者感到类和结构有很大的差别。感觉上结构仅仅象一堆缺乏封装和功能的开放的内存位,而类就象活的并且可靠的社会成员,它有智能服务,有牢固的封装屏障和一个良好定义的接口。既然大多数人都这么认为,那么只有在你的类有很少的方法并且有公有数据(这种事情在良好设计的系统中是存在的!)时,你也许应该使用 struct 关键字,否则,你应该使用 class 关键字。

 

  28.当一个类A 中没有生命任何成员变量与成员函数,这时sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么没有让它为零。(Autodesk

  答案:肯定不是零。举个反例,如果是零的话,声明一个class A[10]对象数组,而每一个对象占用的空间是零,这时就没办法区分A[0],A[1]…了。

 

  29. 8086 汇编下,逻辑地址和物理地址是怎样转换的?(Intel

  答案:通用寄存器给出的地址,是段内偏移地址,相应段寄存器地址*10H+通用寄存器内地址,就得到了真正要访问的地址。

 

  30. 比较C++中的4种类型转换方式?

  请参考:http://blog.csdn.net/wfwd/archive/2006/05/30/763785.aspx,重点是static_cast, dynamic_castreinterpret_cast的区别和应用。

 

  31.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。

  答案:

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

  int : if ( a == 0)

  float : const EXPRESSION EXP = 0.000001

  if ( a < EXP && a >-EXP)

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

 

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

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

  2 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。

 

  33.简述数组与指针的区别?

  数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。

  (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<< sizeof(a) << endl; // 12 字节

  cout<< sizeof(p) << endl; // 4 字节

  计算数组和指针的内存容量

  void Func(char a[100])

  {

  cout<< sizeof(a) << endl; // 4 字节而不是100 字节

  }

 

34.类成员函数的重载、覆盖和隐藏区别?

  答案:

  a.成员函数被重载的特征:

  (1)相同的范围(在同一个类中);

  (2)函数名字相同;

  (3)参数不同;

  (4virtual 关键字可有可无。

  b.覆盖是指派生类函数覆盖基类函数,特征是:

  (1)不同的范围(分别位于派生类与基类);

  (2)函数名字相同;

  (3)参数相同;

  (4)基类函数必须有virtual 关键字。

  c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

  (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

  (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)

 

  35. There are two int variables: a and b, dont use if, ? :, switchor other judgement statements, find out the biggest one of the two numbers.

  答案:( ( a + b ) + abs( a - b ) ) / 2

 

  36. 如何打印出当前源文件的文件名以及源文件的当前行号?

  答案:

  cout << __FILE__ ;

  cout<<__LINE__ ;

  __FILE____LINE__是系统预定义宏,这种宏并不是在某个文件中定义的,而是由编译器定义的。

 

  37. main 主函数执行完毕后,是否可能会再执行一段代码,给出说明?

  答案:可以,可以用_onexit 注册一个函数,它会在main 之后执行int fn1(void), fn2(void), fn3(void), fn4 (void);

  void main( void )

  {

  String str("zhanglin");

  _onexit( fn1 );

  _onexit( fn2 );

  _onexit( fn3 );

  _onexit( fn4 );

  printf( "This is executed first./n" );

  }

  int fn1()

  {

  printf( "next./n" );

  return 0;

  }

  int fn2()

  {

  printf( "executed " );

  return 0;

  }

  int fn3()

  {

  printf( "is " );

  return 0;

  }

  int fn4()

  {

  printf( "This " );

  return 0;

  }

  The _onexit function is passed the address of a function (func) to be called when the program terminates normally. Successive calls to _onexit create a register of functions that are executed in LIFO (last-in-first-out) order. The functions passed to _onexit cannot take parameters.

 

  38. 如何判断一段程序是由C 编译程序还是由C++编译程序编译的?

  答案:

  #ifdef __cplusplus

  cout<<"c++";

  #else

  cout<<"c";

  #endif

 

  39.文件中有一组整数,要求排序后输出到另一个文件中

  答案:

  #i nclude<iostream>

  #i nclude<fstream>

  using namespace std;

  void Order(vector<int>& data) //bubble sort

  {

  int count = data.size() ;

  int tag = false ; // 设置是否需要继续冒泡的标志位

  for ( int i = 0 ; i < count ; i++)

  {

  for ( int j = 0 ; j < count - i - 1 ; j++)

  {

  if ( data[j] > data[j+1])

  {

  tag = true ;

  int temp = data[j] ;

  data[j] = data[j+1] ;

  data[j+1] = temp ;

  }

  }

  if ( !tag )

  break ;

  }

  }

  void main( void )

  {

  vector<int>data;

  ifstream in("c://data.txt");

  if ( !in)

  {

  cout<<"file error!";

  exit(1);

  }

  int temp;

  while (!in.eof())

  {

  in>>temp;

  data.push_back(temp);

  }

  in.close(); //关闭输入文件流

  Order(data);

  ofstream out("c://result.txt");

  if ( !out)

  {

  cout<<"file error!";

  exit(1);

  }

  for ( i = 0 ; i < data.size() ; i++)

  out<<data[i]<<" ";

  out.close(); //关闭输出文件流

  }

 

  40. 链表题:一个链表的结点结构

  struct Node

  {

  int data ;

  Node *next ;

  };

  typedef struct Node Node ;

  (1)已知链表的头结点head,写一个函数把这个链表逆序 ( Intel)

  Node * ReverseList(Node *head) //链表逆序

  {

  if ( head == NULL || head->next == NULL )

  return head;

  Node *p1 = head ;

  Node *p2 = p1->next ;

  Node *p3 = p2->next ;

  p1->next = NULL ;

  while ( p3 != NULL )

  {

  p2->next = p1 ;

  p1 = p2 ;

  p2 = p3 ;

  p3 = p3->next ;

  }

  p2->next = p1 ;

  head = p2 ;

  return head ;

  }

  (2)已知两个链表head1 head2 各自有序,请把它们合并成一个链表依然有序。(保留所有结点,即便大小相同)

  Node * Merge(Node *head1 , Node *head2)

  {

  if ( head1 == NULL)

         return head2 ;

  if ( head2 == NULL)

         return head1 ;

  Node *head = NULL ;

  Node *p1 = NULL;

  Node *p2 = NULL;

  if ( head1->data < head2->data )

  {

  head = head1 ;

  p1 = head1->next;

  p2 = head2 ;

  }

  else

  {

  head = head2 ;

  p2 = head2->next ;

  p1 = head1 ;

  }

  Node *pcurrent = head ;

  while ( p1 != NULL && p2 != NULL)

  {

  if ( p1->data <= p2->data )

  {

  pcurrent->next = p1 ;

  pcurrent = p1 ;

  p1 = p1->next ;

  }

  else

  {

  pcurrent->next = p2 ;

  pcurrent = p2 ;

  p2 = p2->next ;

  }

  }

  if ( p1 != NULL )

      pcurrent->next = p1 ;

  if ( p2 != NULL )

         pcurrent->next = p2 ;

  return head ;

  }

  (3)已知两个链表head1 head2 各自有序,请把它们合并成一个链表依然有序,这次要求用递归方法进行。 (Autodesk)

  答案:

  Node * MergeRecursive(Node *head1 , Node *head2)

  {

  if ( head1 == NULL )

         return head2 ;

  if ( head2 == NULL)

         return head1 ;

  Node *head = NULL ;

  if ( head1->data < head2->data )

  {

         head = head1 ;

         head->next = MergeRecursive(head1->next,head2);

  }

  else

  {

         head = head2 ;

         head->next = MergeRecursive(head1,head2->next);

  }

         return head ;

  }

 

  41. 分析一下这段程序的输出 (Autodesk)

  class B

  {

  public:

  B()

  {

  cout<<"default constructor"<<endl;

  }

  ~B()

  {

  cout<<"destructed"<<endl;

  }

  B(int i):data(i) //B(int) works as a converter ( int -> instance of B)

  {

  cout<<"constructed by parameter " << data <<endl;

  }

  private:

  int data;

  };

  B Play( B b)

  {

  return b ;

  }

  (1) results:

  int main(int argc, char* argv[]) constructed by parameter 5

  { destructed B(5)形参析构

  B t1 = Play(5); B t2 = Play(t1);

  destructed t1形参析构

  return 0;

  destructed t2

  注意顺序!

  } destructed t1

  (2) results:

  int main(int argc, char* argv[]) constructed by parameter 5

  { destructed B(5)形参析构

  B t1 = Play(5); B t2 = Play(10);

  constructed by parameter 10

  return 0;

  destructed B(10)形参析构

  } destructed t2

  注意顺序!

  destructed t1

 

  42. 写一个函数找出一个整数数组中,第二大的数 microsoft

  答案:

  const int MINNUMBER = -32767 ;

  int find_sec_max( int data[] , int count)

  {

  int maxnumber = data[0] ;

  int sec_max = MINNUMBER ;

  for ( int i = 1 ; i < count ; i++)

  {

  if ( data[i] > maxnumber )

  {

  sec_max = maxnumber ;

  maxnumber = data[i] ;

  }

  else

  {

  if ( data[i] > sec_max )

  sec_max = data[i] ;

  }

  }

  return sec_max ;

  }

 

  43. 写一个在一个字符串(n)中寻找一个子串(m)第一个位置的函数。

  KMP算法效率最好,时间复杂度是O(n+m)

 

  44. 多重继承的内存分配问题:

  比如有class A : public class B, public class C {}

  那么A的内存结构大致是怎么样的?

  这个是compiler-dependent, 不同的实现其细节可能不同。

  如果不考虑有虚函数、虚继承的话就相当简单;否则的话,相当复杂。

  可以参考《深入探索C++对象模型》,或者:

  http://blog.csdn.net/wfwd/archive/2006/05/30/763797.aspx

 

  45. 如何判断一个单链表是有环的?(注意不能用标志位,最多只能用两个额外指针)

  struct node { char val; node* next;}

  bool check(const node* head) {} //return false : 无环;true: 有环

  一种On)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然):

  bool check(const node* head)

  {

  if(head==NULL) return false;

  node *low=head, *fast=head->next;

  while(fast!=NULL && fast->next!=NULL)

  {

  low=low->next;

  fast=fast->next->next;

  if(low==fast) return true;

  }

  return false;

 }

C++笔试(2

单向链表的反转是一个经常被问到的一个面试题,也是一个非常基础的问题。比如一个链表是这样的: 1->2->3->4->5 通过反转后成为5->4->3->2->1

最容易想到的方法遍历一遍链表,利用一个辅助指针,存储遍历过程中当前指针指向的下一个元素,然后将当前节点元素的指针反转后,利用已经存储的指针往后面继续遍历。源代码如下:

struct linka {

int data;

linka* next;

};

void reverse(linka*& head) {

if(head ==NULL)

return;

linka *pre, *cur, *ne;

pre=head;

cur=head->next;

while(cur)

{

   ne = cur->next;

   cur->next = pre;

   pre = cur;

   cur = ne;

}

head->next = NULL;

head = pre;

}

还有一种利用递归的方法。这种方法的基本思想是在反转当前节点之前先调用递归函数反转后续节点。源代码如下。不过这个方法有一个缺点,就是在反转后的最后一个结点会形成一个环,所以必须将函数的返回的节点的next域置为NULL。因为要改变head指针,所以我用了引用。算法的源代码如下:

linka* reverse(linka* p,linka*& head)

{

if(p == NULL || p->next == NULL)

{

   head=p;

   return p;

}

else

{

   linka* tmp = reverse(p->next,head);

   tmp->next = p;

   return p;

}

}

 

1. 以下三条输出语句分别输出什么?[C]

char str1[]       = "abc";

char str2[]       = "abc";

const char str3[] = "abc";

const char str4[] = "abc";

const char* str5 = "abc";

const char* str6 = "abc";

cout << boolalpha << ( str1==str2 ) << endl; // 输出什么?

cout << boolalpha << ( str3==str4 ) << endl; // 输出什么?

cout << boolalpha << ( str5==str6 ) << endl; // 输出什么?

 

答:分别输出false,false,truestr1str2都是字符数组,每个都有其自己的存储区,它们的值则是各存储区首地址,不等;str3str4同上,只是按const语义,它们所指向的数据区不能修改。str5str6并非数组而是字符指针,并不分配存储区,其后的“abc”以常量形式存于静态数据区,而它们自己仅是指向该区首地址的指针,相等。

 

12. 以下代码中的两个sizeof用法有问题吗?[C]

void UpperCase( char str[] ) // str 中的小写字母转换成大写字母

{

    for( size_t i=0; i<sizeof(str)/sizeof(str[0]); ++i )

        if( 'a'<=str[i] && str[i]<='z' )

            str[i] -= ('a'-'A' );

}

char str[] = "aBcDe";

cout << "str字符长度为: " << sizeof(str)/sizeof(str[0]) << endl;

UpperCase( str );

cout << str << endl;

答:函数内的sizeof有问题。根据语法,sizeof如用于数组,只能测出静态数组的大小,无法检测动态分配的或外部数组大小。函数外的str是一个静态定义的数组,因此其大小为6,函数内的str实际只是一个指向字符串的指针,没有任何额外的与数组相关的信息,因此sizeof作用于上只将其当指针看,一个指针为4个字节,因此返回4

 

13. C++内建型别 A B,在哪几种情况下B能隐式转化为A[C++中等]

答:

a. class B : public A { ……} // B公有继承自A,可以是间接继承的

b. class B { operator A( ); } // B实现了隐式转化为A的转化

c. class A { A( const B& ); } // A实现了non-explicit的参数为B(可以有其他带默认值的参数)构造函数

d. A& operator= ( const A& ); // 赋值操作,虽不是正宗的隐式类型转换,但也可以勉强算一个

 

4. 以下代码有什么问题?[C++]

struct Test

{

    Test( int ) {}

    Test() {}

    void fun() {}

};

void main( void )

{

    Test a(1);

    a.fun();

    Test b();

    b.fun();

}

答:变量b定义出错。按默认构造函数定义对象,不需要加括号

 

5. 以下代码有什么问题?[C++]

cout << (true?1:"1") << endl;

答:三元表达式“?:问号后面的两个操作数必须为同一类型

 

8. 以下代码能够编译通过吗,为什么?[C++]

unsigned int const size1 = 2;

char str1[ size1 ];

unsigned int temp = 0;

cin >> temp;

unsigned int const size2 = temp;

char str2[ size2 ];

答:str2定义出错,size2非编译器期间常量,而数组定义要求长度必须为编译期常量。

 

2. 以下反向遍历array数组的方法有什么错误?[STL]

vector array;

array.push_back( 1 );

array.push_back( 2 );

array.push_back( 3 );

for( vector::size_type i=array.size()-1; i>=0; --i ) // 反向遍历array数组

{

    cout << array[i] << endl;

}

答:首先数组定义有误,应加上类型参数:vector<int> array。其次vector::size_type被定义为unsigned int,即无符号数,这样做为循环变量的i0时再减1就会变成最大的整数,导致循环失去控制。

 

9. 以下代码中的输出语句输出0吗,为什么?[C++]

struct CLS

{

    int m_i;

    CLS( int i ) : m_i(i) {}

    CLS()

    {

        CLS(0);

    }

};

CLS obj;

cout << obj.m_i << endl;

答:不能。在默认构造函数内部再调用带参的构造函数属用户行为而非编译器行为,亦即仅执行函数调用,而不会执行其后的初始化表达式。只有在生成对象时,初始化表达式才会随相应的构造函数一起调用。

10. C++中的空类,默认产生哪些类成员函数?[C++]

答:

class Empty

{

public:

    Empty();                          // 缺省构造函数

    Empty( const Empty& );            // 拷贝构造函数

    ~Empty();                         // 析构函数

    Empty& operator=( const Empty& ); // 赋值运算符

    Empty* operator&();               // 取址运算符

    const Empty* operator&() const;   // 取址运算符 const

};

 

3. 以下两条输出语句分别输出什么?[C++]

float a = 1.0f;

cout << (int)a << endl;          //相当于将float a强制转换为int a,取float的整数部分,小数舍弃

cout << (int&)a << endl;              //相当于*((int*)&a)

cout << boolalpha << ( (int)a == (int&)a ) << endl; // 输出什么?

float b = 0.0f;

cout << (int)b << endl;

cout << (int&)b << endl;

cout << boolalpha << ( (int)b == (int&)b ) << endl; // 输出什么?

答:分别输出falsetrue。注意转换的应用。(int)a实际上是以浮点数a为参数构造了一个整型数,该整数的值是1(int&)a则是告诉编译器将a当作整数看(并没有做任何实质上的转换)。因为1以整数形式存放和以浮点形式存放其内存数据是不一样的,因此两者不等。对b的两种转换意义同上,但是0的整数形式和浮点形式其内存数据是一样的,因此在这种特殊情形下,两者相等(仅仅在数值意义上)。

注意,程序的输出会显示(int&)a=1065353216,这个值是怎么来的呢?前面已经说了,1以浮点数形式存放在内存中,按ieee754规定,其内容为0x0000803F(已考虑字节反序)。这也就是a这个变量所占据的内存单元的值。当(int&)a出现时,它相当于告诉它的上下文:“把这块地址当做整数看待!不要管它原来是什么。”这样,内容0x0000803F按整数解释,其值正好就是1065353216(十进制数)。

通过查看汇编代码可以证实“(int)a相当于重新构造了一个值等于a的整型数”之说,而(int&)的作用则仅仅是表达了一个类型信息,意义在于为cout<<==选择正确的重载版本。

 

6. 以下代码有什么问题?[STL]

typedef vector IntArray;

IntArray array;

array.push_back( 1 );

array.push_back( 2 );

array.push_back( 2 );

array.push_back( 3 );

// 删除array数组中所有的2

for( IntArray::iterator itor=array.begin(); itor!=array.end(); ++itor )

{

    if( 2 == *itor ) array.erase( itor );

}

答:同样有缺少类型参数的问题。另外,每次调用“array.erase( itor );”,被删除元素之后的内容会自动往前移,导致迭代漏项,应在删除一项后使itor--,使之从已经前移的下一个元素起继续遍历。

 

11. 写一个函数,完成内存之间的拷贝。[考虑问题是否全面]

答:

void* mymemcpy( void *dest, const void *src, size_t count )

{

    char* pdest = static_cast<char*>( dest );

    const char* psrc = static_cast<const char*>( src );

    if( pdest>psrc && pdest<psrc+cout ) 能考虑到这种情况就行了。此时srcdest出现内存重叠区域

    {

        for( size_t i=count-1; i!=-1; --i )

                pdest[i] = psrc[i];

    }

    else

    {

        for( size_t i=0; i<count; ++i )

            pdest[i] = psrc[i];

    }

    return dest;

}

 

C++多态技术

导言
多态(polymorphism)一词最初来源于希腊语polumorphos,含义是具有多种形式或形态的情形。在程序设计领域,一个广泛认可的定义是一种将不同的特殊行为和单个泛化记号相关联的能力。和纯粹的面向对象程序设计语言不同,C++中的多态有着更广泛的含义。除了常见的通过类继承和虚函数机制生效于运行期的动态多态(dynamic polymorphism)外,模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为静态多态(static polymorphism)。
事实上,带变量的宏和函数重载机制也允许将不同的特殊行为和单个泛化记号相关联。然而,习惯上我们并不将它们展现出来的行为称为多态(或静态多态)。今天,当我们谈及多态时,如果没有明确所指,默认就是动态多态,而静态多态则是指基于模板的多态。不过,在这篇以C++各种多态技术为主题的文章中,我们首先还是回顾一下C++社群争论已久的另一种多态:函数多态(function polymorphism),以及更不常提的宏多态(macro polymorphism


函数多态
也就是我们常说的函数重载(function overloading)。基于不同的参数列表,同一个函数名字可以指向不同的函数定义:
// overload_poly.cpp
#include <iostream>
#include <string>
//
定义两个重载函数

int my_add(int a, int b)
{
    return a + b;
}
int my_add(int a, std::string b)
{
    return a + atoi(b.c_str());
}
int main()
{
    int i = my_add(1, 2);                //
两个整数相加
    int s = my_add(1, "2");              //
一个整数和一个字符串相加
    std::cout << "i = " << i << "/n";
    std::cout << "s = " << s << "/n";
}
根据参数列表的不同(类型、个数或兼而有之),my_add(1, 2)my_add(1, "2")被分别编译为对my_add(int, int)my_add(int, std::string)的调用。实现原理在于编译器根据不同的参数列表对同名函数进行名字重整,而后这些同名函数就变成了彼此不同的函数。比方说,也许某个编译器会将my_add()函数名字分别重整为my_add_int_int()my_add_int_str() 严格来说,重载不能说是多态,因为函数符号名是编译器编译时就定好的了,对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!


宏多态
带变量的宏可以实现一种初级形式的静态多态:
// macro_poly.cpp
#include <iostream>
#include <string>
//
定义泛化记号:宏ADD

#define ADD(A, B) (A) + (B);
int main()
{
    int i1(1), i2(2);
    std::string s1("Hello, "), s2("world!");
    int i = ADD(i1, i2);                        //
两个整数相加
    std::string s = ADD(s1, s2);                //
两个字符串相加
    std::cout << "i = " << i << "/n";
    std::cout << "s = " << s << "/n";
}
当程序被编译时,表达式ADD(i1, i2)ADD(s1, s2)分别被替换为两个整数相加和两个字符串相加的具体表达式。整数相加体现为求和,而字符串相加则体现为连接。程序的输出结果符合直觉:
1 + 2 = 3
Hello, + world! = Hello, world!

动态多态

这就是众所周知的的多态。现代面向对象语言对这个概念的定义是一致的。其技术基础在于继承机制和虚函数。例如,我们可以定义一个抽象基类Vehicle和两个派生于Vehicle的具体类CarAirplane
// dynamic_poly.h
#include <iostream>
//
公共抽象基类Vehicle

class Vehicle
{
public:
    virtual void run() const = 0;
};
//
派生于Vehicle的具体类Car
class Car: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a car/n";
    }
};
//
派生于Vehicle的具体类Airplane
class Airplane: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a airplane/n";
    }
};
客户程序可以通过指向基类Vehicle的指针(或引用)来操纵具体对象。通过指向基类对象的指针(或引用)来调用一个虚函数,会导致对被指向的具体对象之相应成员的调用:
// dynamic_poly_1.cpp
#include <iostream>
#include <vector>
#include "dynamic_poly.h"
//
通过指针run任何vehicle

void run_vehicle(const Vehicle* vehicle)
{
    vehicle->run();            //
根据vehicle的具体类型调用对应的run()
}
int main()              //
经典的多态实例
{
    Car car;
    Airplane airplane;
    run_vehicle(&car);         //
调用Car::run()
    run_vehicle(&airplane);    //
调用Airplane::run()
}
此例中,关键的多态接口元素为虚函数run()。由于run_vehicle()的参数为指向基类Vehicle的指针,因而无法在编译期决定使用哪一个版本的run()。在运行期,为了分派函数调用,虚函数被调用的那个对象的完整动态类型将被访问。这样一来,对一个Car对象调用run_vehicle(),实际上将调用Car::run(),而对于Airplane对象而言将调用Airplane::run()
或许动态多态最吸引人之处在于处理异质对象集合的能力:

// dynamic_poly_2.cpp
#include <iostream>
#include <vector>
#include "dynamic_poly.h"
// run
异质vehicles集合

void run_vehicles(const std::vector<Vehicle*>& vehicles)
{
    for (unsigned int i = 0; i < vehicles.size(); ++i)
    {
        vehicles[i]->run();     //
根据具体vehicle的类型调用对应的run()
    }
}
int main()
{
    Car car;
    Airplane airplane;
    std::vector<Vehicle*> v;    //
异质vehicles集合
    v.push_back(&car);
    v.push_back(&airplane);
    run_vehicles(v);            // run
不同类型的vehicles
}
run_vehicles()中,vehicles[i]->run()依据正被迭代的元素的类型而调用不同的成员函数。这从一个侧面体现了面向对象编程风格的优雅。

静态多态

如果说动态多态是通过虚函数来表达共同接口的话,那么静态多态则是通过彼此单独定义但支持共同操作的具体类来表达共同性,换句话说,必须存在必需的同名成员函数。
我们可以采用静态多态机制重写上一节的例子。这一次,我们不再定义vehicles类层次结构,相反,我们编写彼此无关的具体类CarAirplane(它们都有一个run()成员函数):

// static_poly.h
#include <iostream>
//
具体类Car

class Car
{
public:
    void run() const
    {
        std::cout << "run a car/n";
    }
};
//
具体类Airplane
class Airplane
{
public:
    void run() const
    {
        std::cout << "run a airplane/n";
    }
};
run_vehicle()
应用程序被改写如下:
// static_poly_1.cpp
#include <iostream>
#include <vector>
#include "static_poly.h"
//
通过引用而run任何vehicle
template <typename Vehicle>        //
类型模板
void run_vehicle(const Vehicle& vehicle)
{
    vehicle.run();            //
根据vehicle的具体类型调用对应的run()
}
int main()
{
    Car car;
    Airplane airplane;
    run_vehicle(car);         //
调用Car::run()
    run_vehicle(airplane);    //
调用Airplane::run()
}
现在Vehicle用作模板参数而非公共基类对象(事实上,这里的Vehicle只是一个符合直觉的记号而已,此外别无它意)。经过编译器处理后,我们最终会得到run_vehicle<Car>() run_vehicle<Airplane>()两个不同的函数。这和动态多态不同,动态多态凭借虚函数分派机制在运行期只有一个run_vehicle()函数
我们无法再透明地处理异质对象集合了,因为所有类型都必须在编译期予以决定。不过,为不同的vehicles引入不同的集合只是举手之劳。由于无需再将集合元素局限于指针或引用,我们现在可以从执行性能和类型安全两方面获得好处:

// static_poly_2.cpp
#include <iostream>
#include <vector>
#include "static_poly.h"
// run
同质vehicles集合
template <typename Vehicle>
void run_vehicles(const std::vector<Vehicle>& vehicles)
{
    for (unsigned int i = 0; i < vehicles.size(); ++i)
    {
        vehicles[i].run();            //
根据vehicle的具体类型调用相应的run()
    }
}
int main()
{
    Car car1, car2;
    Airplane airplane1, airplane2;
    std::vector<Car> vc;              //
同质cars集合
    vc.push_back(car1);
    vc.push_back(car2);
    //vc.push_back(airplane1);        //
错误:类型不匹配
    run_vehicles(vc);                 // run cars
    std::vector<Airplane> vs;         //
同质airplanes集合
    vs.push_back(airplane1);
    vs.push_back(airplane2);
    //vs.push_back(car1);             //
错误:类型不匹配
    run_vehicles(vs);                 // run airplanes
}

两种多态机制的结合使用
在一些高级C++应用中,我们可能需要结合使用动态多态和静态多态两种机制,以期达到对象操作的优雅、安全和高效。例如,我们既希望一致而优雅地处理vehiclesrun问题,又希望安全而高效地完成给飞行器(飞机、飞艇等)进行空中加油这样的高难度动作。为此,我们首先将上面的vehicles类层次结构改写如下:

// dscombine_poly.h
#include <iostream>
#include <vector>
//
公共抽象基类Vehicle

class Vehicle
{
    public:
    virtual void run() const = 0;
};
//
派生于Vehicle的具体类Car
class Car: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a car/n";
    }
};
//
派生于Vehicle的具体类Airplane
class Airplane: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a airplane/n";
    }
    void add_oil() const
    {
        std::cout << "add oil to airplane/n";
    }
};
//
派生于Vehicle的具体类Airship
class Airship: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a airship/n";
    }
  
    void add_oil() const
    {
        std::cout << "add oil to airship/n";
    }
};
我们理想中的应用程序可以编写如下:
// dscombine_poly.cpp
#include <iostream>
#include <vector>
#include "dscombine_poly.h"
// run
异质vehicles集合
void run_vehicles(const std::vector<Vehicle*>& vehicles)
{
    for (unsigned int i = 0; i < vehicles.size(); ++i)
    {
        vehicles[i]->run();                 //
根据具体的vehicle类型调用对应的run()
    }
}
//
为某种特定的aircrafts同质对象集合进行空中加油
template <typename Aircraft>
void add_oil_to_aircrafts_in_the_sky(const std::vector<Aircraft>& aircrafts)
{
    for (unsigned int i = 0; i < aircrafts.size(); ++i)
    {
        aircrafts[i].add_oil();
    }
}
int main()
{
    Car car1, car2;
    Airplane airplane1, airplane2;
    Airship airship1, airship2;


    std::vector<Vehicle*> v;                //
异质vehicles集合
    v.push_back(&car1);
    v.push_back(&airplane1);
    v.push_back(&airship1);
    run_vehicles(v);                        // run
不同种类的vehicles

    std::vector<Airplane> vp;               //
同质airplanes集合
    vp.push_back(airplane1);
    vp.push_back(airplane2);
    add_oil_to_aircrafts_in_the_sky(vp);    //
airplanes进行空中加油

    std::vector<Airship> vs;                //
同质airships集合
    vs.push_back(airship1);
    vs.push_back(airship2);
    add_oil_to_aircrafts_in_the_sky(vs);    //
airships进行空中加油
}
我们保留了类层次结构,目的是为了能够利用run_vehicles()一致而优雅地处理异质对象集合vehiclesrun问题。同时,利用函数模板add_oil_to_aircrafts_in_the_sky<Aircraft>(),我们仍然可以处理特定种类的vehicles — aircrafts(包括airplanesairships)的空中加油问题。其中,我们避开使用指针,从而在执行性能和类型安全两方面达到了预期目标。

结语

长期以来,C++社群对于多态的内涵和外延一直争论不休。在comp.object这样的网络论坛上,此类话题争论至今仍随处可见。曾经有人将动态多态(dynamic polymorphism)称为inclusion polymorphism,而将静态多态(static polymorphism)称为parametric polymorphismparameterized polymorphism

我注意到2003年斯坦福大学公开的一份C++ and Object-Oriented Programming教案中明确提到了函数多态概念:Function overloading is also referred to as function polymorphism as it involves one function having many forms。文后的“参考文献”单元给出了这个网页链接。

可能你是第一次看到宏多态(macro polymorphism)这个术语。不必讶异 也许我就是造出这个术语的“第一人”。显然,带变量的宏(或类似于函数的宏或伪函数宏)的替换机制除了免除小型函数的调用开销之外,也表现出了类似的多态性。在我们上面的例子中,字符串相加所表现出来的符合直觉的连接操作,事实上是由底部运算符重载机制(operator overloading)支持的。值得指出的是,C++社群中有人将运算符重载所表现出来的多态称为ad hoc polymorphism

David VandevoordeNicolai M. Josuttis在他们的著作C++ Templates: The Complete Guide一书中系统地阐述了静态多态和动态多态技术。因为认为“和其他语言机制关系不大”,这本书没有提及“宏多态”(以及“函数多态”)。(需要说明的是,笔者本人是这本书的繁体中文版译者之一,本文正是基于这本书的第14The Polymorphic Power of Templates编写而成)

动态多态只需要一个多态函数,生成的可执行代码尺寸较小,静态多态必须针对不同的类型产生不同的模板实体,尺寸会大一些,但生成的代码会更快,因为无需通过指针进行间接操作。静态多态比动态多态更加类型安全,因为全部绑定都被检查于编译期。正如前面例子所示,你不可将一个错误的类型的对象插入到从一个模板实例化而来的容器之中。此外,正如你已经看到的那样,动态多态可以优雅地处理异质对象集合,而静态多态可以用来实现安全、高效的同质对象集合操作。

静态多态为C++带来了泛型编程(generic programming)的概念。泛型编程可以认为是“组件功能基于框架整体而设计”的模板编程。STL就是泛型编程的一个典范。STL是一个框架,它提供了大量的算法、容器和迭代器,全部以模板技术实现。从理论上讲,STL的功能当然可以使用动态多态来实现,不过这样一来其性能必将大打折扣。

静态多态还为C++社群带来了泛型模式(generic patterns)的概念。理论上,每一个需要通过虚函数和类继承而支持的设计模式都可以利用基于模板的静态多态技术(甚至可以结合使用动态多态和静态多态两种技术)而实现。正如你看到的那样,Andrei Alexandrescu的天才作品Modern C++ Design: Generic Programming and Design Patterns AppliedAddison-Wesley)和Loki程序库已经走在了我们的前面。

原创粉丝点击