用dest不能画窗和门,即使是莫砺锋给高宽的回信全是1mm的窗都不行,显示太宽,而且所有的墙都不能画

当前位置: >>
c笔试题汇总
网上流传的 c++笔试题汇总 1.求下面函数的返回值(微软) int func(x) { int countx = 0; while(x) { countx ++; x = x&(x-1); } } 假定 x = 9999。 答案:8 思路:将 x 转化为 2 进制,看含有的 1 的个数。 2. 什么是“引用”?申明和使用“引用”要注意哪些问题? 答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切 记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为 其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种 数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。 3. 将“引用”作为函数参数有哪些特点? (1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个 别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。 (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数, 当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。 因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。 (3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要 重复使用&*指针变量名&的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必 须用变量的地址作为实参。而引用更容易使用,更清晰。 4. 在什么时候需要使用“常引用”? 如果既要利用引用提高程序的效率 又要保护传递给函数的数据不在函数中被改变 就应使用常引用 常引用声明方式 const , , 。 : 类型标识符 &引用名=目标变量名; 例1 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& && 因此这两个操作符的返回值应该 是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个 流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个&&操作符实际上是针对不同对象的!这无法让 人接受。对于返回一个流指针则不能连续使用&&操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它 说明了引用的重要性以及无可替代性,也许这就是 C++语言中引入引用这个概念的原因吧。赋值操作符=。这个操作符象流 操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继 续赋值。因此引用成了这个操作符的惟一返回值选择。 例3 #i nclude 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&}& P& int &put(int n) { if (n&=0 && n&=9 ) return vals[n]; else { cout&&&subscript error&; } } (5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的 Item23 详细的讨论了这个问题。主要原因是这四个操作符没有 side effect,因此,它们必须构造一个对象作为返回值,可选的方 案包括:返回一个对象、返回一个局部变量的引用,返回一个 new 分配的对象的引用、返回一个静态对象引用。根据前面提 到的引用作为返回值的三个规则,第 2、3 两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为 true 而导致错误。所以可选的只剩下返回一个对象了。 6. “引用”与多态的关系? 引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。 例4 Class A; Class B : Class A{...}; B A& ref = 7. “引用”与指针的区别是什么? 指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是 目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传 ref 和 pointer 的区别。 8. 什么时候需要“引用”? 流操作符&&和&&、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。 2-8 参考以下内容 引用是 C++引入的新语言特性,是 C++常用的一个重要内容之一,正确、灵活地使用引用,可以使程序简洁、高效。我在工 作中发现,许多人使用它仅仅是想当然,在某些微妙的场合,很容易出错,究其原由,大多因为没有搞清本源。故在本篇中 我将对引用进行详细讨论,希望对大家更好地理解和使用引用起到抛砖引玉的作用。 一、引用简介 引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。 引用的声明方法:类型标识符 &引用名=目标变量名; 【例 1】: int &ra=a; //定义引用 ra,它是变量 a 的引用,即别名 说明: (1)&在此不是求地址运算,而是起标识作用。 (2)类型标识符是指目标变量的类型。 (3)声明引用时,必须同时对其进行初始化。 (4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量 名的别名。 ra=1; 等价于 a=1; (5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型, 因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra 与&a 相等。 (6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。 二、引用应用 1、引用作为参数 引用的一个重要作用就是作为函数的参数。 以前的 C 语言中函数参数传递是值传递, 如果有大块数据作为参数传递的时 候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了 一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。 【例 2】: void swap(int &p1, int &p2) //此处函数的形参 p1, p2 都是引用 { p=p1; p1=p2; p2=p; } 为在程序中调用该函数,则相应的主调函数的调用点处,直接以变量作为实参进行调用即可,而不需要实参变量有任何 的特殊要求。如:对应上面定义的 swap 函数,相应的主调函数可写为: main( ) { int a,b; cin&&a&&b; //输入 a,b 两变量的值 swap(a,b); //直接以变量 a 和 b 作为实参调用 swap 函数 cout&&a&& ' ' &&b; //输出结果 } 上述程序运行时,如果输入数据 10 20 并回车后,则输出结果为 20 10。 由【例 2】可看出: (1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的 一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。 (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的 参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造 函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。 (3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且 需要重复使用&*指针变量名&的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处, 必须用变量的地址作为实参。而引用更容易使用,更清晰。 如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。 2、常引用 常引用声明方式:const 类型标识符 &引用名=目标变量名; 用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为 const,达到了引用的安全性。 【例 3】: const int &ra=a; ra=1; //错误 a=1; //正确 这不光是让代码更健壮,也有些其它方面的需要。 【例 4】:假设有如下函数声明: string foo( ); void bar(string & s); 那么下面的表达式将是非法的: bar(foo( )); bar(&hello world&); 原因在于 foo( )和&hello world&串都会产生一个临时对象,而在 C++中,这些临时对象都是 const 类型的。因此上面 的表达式就是试图将一个 const 类型的对象转换为非 const 类型,这是非法的。 引用型参数应该在能被定义为 const 的情况下,尽量定义为 const 。 3、引用作为返回值 要以引用返回函数值,则函数定义时要按以下格式: 类型标识符 &函数名(形参列表及类型说明) {函数体} 说明: (1)以引用返回函数值,定义函数时需要在函数名前加& (2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。 【例 5】以下程序中定义了一个普通的函数 fn1(它用返回值的方法返回函数值),另外一个函数 fn2,它以引用的方 法返回函数值。 #include &iostream.h& //定义全局变量 temp float fn1(float r); //声明函数 fn1 float &fn2(float r); //声明函数 fn2 float fn1(float r) //定义函数 fn1,它以返回值的方法返回函数值 { temp=(float)(r*r*3.14); } float &fn2(float r) //定义函数 fn2,它以引用方式返回函数值 { temp=(float)(r*r*3.14); } void main() //主函数 { float a=fn1(10.0); //第 1 种情况,系统生成要返回值的副本(即临时变量) float &b=fn1(10.0); //第 2 种情况,可能会出错(不同 C++系统有不同规定) //不能从被调函数中返回一个临时变量或局部变量的引用 float c=fn2(10.0); //第 3 种情况,系统不生成返回值的副本 //可以从被调函数中返回一个全局变量的引用 float &d=fn2(10.0); //第 4 种情况,系统不生成返回值的副本 //可以从被调函数中返回一个全局变量的引用 cout&&a&&c&&d; } 引用作为返回值,必须遵守以下规则: (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& && 因此这两个操作符的返回值 应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回 一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个&&操作符实际上是针对不同对象的!这无 法让人接受。 对于返回一个流指针则不能连续使用&&操作符。 因此, 返回一个流对象引用是惟一选择。 这个唯一选择很关键, 它说明了引用的重要性以及无可替代性,也许这就是 C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象 流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被 继续赋值。因此引用成了这个操作符的惟一返回值选择。 【例 6】 测试用返回引用的函数值作为赋值表达式的左值。 #include &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&; } } (5) 在另外的一些操作符中, 却千万不能返回引用: +-*/ 四则运算符。 它们不能返回引用, Effective C++[1]的 Item23 详细的讨论了这个问题。主要原因是这四个操作符没有 side effect,因此,它们必须构造一个对象作为返回值,可选的方 案包括:返回一个对象、返回一个局部变量的引用,返回一个 new 分配的对象的引用、返回一个静态对象引用。根据前面提 到的引用作为返回值的三个规则,第 2、3 两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为 true 而导致错误。所以可选的只剩下返回一个对象了。 4、引用和多态 引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。 【例 7】: class class B A &Ref = // 用派生类对象初始化基类对象的引用 Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果 A 类中定义有虚函数,并且在 B 类中重写了这个虚函数,就可以通过 Ref 产生多态效果。 三、引用总结 (1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数 据或对象的传递效率和空间不如意的问题。 A; B:public A{……}; (2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过 const 的使用,保证了引用传 递的安全性。 (3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针, 程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。 (4)使用引用的时机。流操作符&&和&&、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情 况都推荐使用引用。 9. 结构与联合有和区别? 1. 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共 用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。 2. 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响 的。 10. 下面关于“联合”的题目的输出? a) #i nclude union { char x[2]; }a; void main() { a.x[0] = 10; a.x[1] = 1; printf(&%d&,a.i); } 答案:266 (低位低地址,高位高地址,内存占用情况是 Ox010A) b) main() { union{ /*定义一个联合*/ struct{ /*在联合中定义一个结构*/ } } number.i=0x4241; /*联合成员赋值*/ printf(&%c%c &, number.half.first, mumber.half.second); number.half.first='a'; /*联合中结构成员赋值*/ number.half.second='b'; printf(&%x &, number.i); getch(); } 答案: AB (0x41 对应'A',是低位;Ox42 对应'B',是高位) 6261 (number.i 和 number.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 strD char *tempptr = strD while( (*strDest++ = *strSrc++) != ‘ 的联系与区别? 21. New delete 与 malloc free 的联系与区别? 答案:都是在堆(heap)上进行动态的内存操作。用 malloc 函数需要指定内存分配的字节数并且不能初始化对象,new 会自 动调用对象的构造函数。delete 会调用对象的 destructor,而 free 不会调用对象的 destructor. 5*DOUBLE(5); 是多少? 22. #define DOUBLE(x) x+x ,i = 5*DOUBLE(5); i 是多少? 答案:i 为 30。 23. 有哪几种情况只能用 intialization list 而不能用 assignment? 答案:当类中含有 const、reference 成员变量;基类的构造函数都需要初始化表。 C++是不是类型安全的 是不是类型安全的? 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.当一个类 中没有生命任何成员变量与成员函数, sizeof(A)的值是多少 如果不是零, 的值是多少, 28.当一个类 A 中没有生命任何成员变量与成员函数,这时 sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么 没有让它为零。 Autodesk) (Autodesk 没有让它为零。 Autodesk) ( 答案:肯定不是零。举个反例,如果是零的话,声明一个 class A[10]对象数组,而每一个对象占用的空间是零,这时就没 办法区分 A[0],A[1]…了。 汇编下,逻辑地址和物理地址是怎样转换的?(Intel) ?(Intel 29. 在 8086 汇编下,逻辑地址和物理地址是怎样转换的?(Intel) 答案:通用寄存器给出的地址,是段内偏移地址,相应段寄存器地址*10H+通用寄存器内地址,就得到了真正要访问的地址。 C++中的 种类型转换方式? 30. 比较 C++中的 4 种类型转换方式? 请参考以下内容: 1、static_cast Operator MSDN: The expression static_cast & type-id & ( expression ) converts expression to the type of type-id based solely on the types present in the expression. No run-time type check is made to ensure the safety of the conversion. Syntax static_cast & type-id & ( expression ) The static_cast operator can be used for operations such as converting a pointer to a base class to a pointer to a derived class. Such conversions are not always safe. For example: class B { ... }; class D : public B { ... }; void f(B* pb, D* pd) { D* pd2 = static_cast&D*&(pb); B* pb2 = static_cast&B*&(pd); ... }// not safe, pb may // point to just B // safe conversionIn contrast to dynamic_cast, no run-time check is made on the static_cast conversion of pb. The object pointed to by pb may not be an object of type D, in which case the use of *pd2 could be disastrous. For instance, calling a function that is a member of the D class, but not the B class, could result in an access violation. The dynamic_cast and static_cast operators move a pointer throughout a class hierarchy. However, static_cast relies exclusively on the information provided in the cast statement and can therefore be unsafe. For example: class B { ... }; class D : public B { ... }; void f(B* pb) { D* pd1 = dynamic_cast&D*&(pb); D* pd2 = static_cast&D*&(pb); } If pb really points to an object of type D, then pd1 and pd2 will get the same value. They will also get the same value if pb == 0. If pb points to an object of type B and not to the complete D class, then dynamic_cast will know enough to return zero. However, static_cast relies on the programmer’s assertion that pb points to an object of type D and simply returns a pointer to that supposed D object. Consequently, static_cast can do the inverse of implicit conversions, in which case the results are undefined. It is left to the programmer to ensure that the results of a static_cast conversion are safe. This behavior also applies to types other than class types. For instance, static_cast can be used to convert from an int to a char. However, the resulting char may not have enough bits to hold the entire int value. Again, it is left to the programmer to ensure that the results of a static_cast conversion are safe. The static_cast operator can also be used to perform any implicit conversion, including standard conversions and user-defined conversions. For example: typedef unsigned char BYTE void f() { int i = 65; float f = 2.5; ch = static_cast&char&(i); dbl = static_cast&double&(f); ... i = static_cast&BYTE&(ch); ... } The static_cast operator can explicitly convert an integral value to an enumeration type. If the value of the integral type does not fall within the range of enumeration values, the resulting enumeration value is undefined. The static_cast operator converts a null pointer value to the null pointer value of the destination type. Any expression can be explicitly converted to type void by the static_cast operator. The destination void type can optionally include the const, volatile, or __unaligned attribute. The static_cast operator cannot cast away the const, volatile, or __unaligned attributes. static_cast 在功能上基本上与 C 风格的类型转换一样强大,含义也一样。它也有功能上限制。例如,你不能用 static_cast 象用 C 风格的类型转换一样把 struct 转换成 int 类型或者把 double 类型转换成指针类型,另外,static_cast 不能从表达 // int to char // float to double 式中去除 const 属性,因为另一个新的类型转换操作符 const_cast 有这样的功能。 2、const_cast Operator MSDN: The const_cast operator can be used to remove the const, volatile, and __unaligned attribute(s) from a class. Syntax const_cast & type-id & ( expression ) A pointer to any object type or a pointer to a data member can be explicitly converted to a type that is identical except for the const, volatile, and __unaligned qualifiers. For pointers and references, the result will refer to the original object. For pointers to data members, the result will refer to the same member as the original (uncast) pointer to data member. Depending on the type of the referenced object, a write operation through the resulting pointer, reference, or pointer to data member might produce undefined behavior. The const_cast operator converts a null pointer value to the null pointer value of the destination type. const_cast 用于类型转换掉表达式的 const 或 volatileness 属性。通过使用 const_cast,你向人们和编译器强调你通过类 型转换想做的只是改变一些东西的 constness 或者 volatileness 属性。这个含义被编译器所约束。如果你试图使用 const_cast 来完成修改 constness 或者 volatileness 属性之外的事情,你的类型转换将被拒绝。 3、dynamic_cast Operator MSDN: The expression dynamic_cast&type-id&( expression ) converts the operand expression to an object of type type-id. The type-id must be a pointer or a reference to a previously defined class type or a “pointer to void”. The type of expression must be a pointer if type-id is a pointer, or an l-value if type-id is a reference. Syntax dynamic_cast & type-id & ( expression ) If type-id is a pointer to an unambiguous accessible direct or indirect base class of expression, a pointer to the unique subobject of type type-id is the result. For example: class B { ... }; class C : public B { ... }; class D : public C { ... }; void f(D* pd) { C* pc = dynamic_cast&C*&(pd); B* pb = dynamic_cast&B*&(pd); ... } This type of conversion is called an “upcast” because it moves a pointer up a class hierarchy, from a derived class to a class it is derived from. An upcast is an implicit conversion. If type-id is void*, a run-time check is made to determine the actual type of expression. The result is a pointer to the complete object pointed to by expression. For example: class A { ... }; class B { ... }; void f() { A* pa = new A; B* pb = new B; void* pv = dynamic_cast&void*&(pa); // pv now points to an object of type A ... pv = dynamic_cast&void*&(pb); // pv now points to an object of type B // ok: C is a direct base class // ok: B is an indirect base class // pc points to C subobject of pd // pb points to B subobject of pd } If type-id is not void*, a run-time check is made to see if the object pointed to by expression can be converted to the type pointed to by type-id. If the type of expression is a base class of the type of type-id, a run-time check is made to see if expression actually points to a complete object of the type of type-id. If this is true, the result is a pointer to a complete object of the type of type-id. For example: class B { ... }; class D : public B { ... }; void f() { B* pb = new D; B* pb2 = new B; D* pd = dynamic_cast&D*&(pb); ... D* pd2 = dynamic_cast&D*&(pb2); ... } This type of conversion is called a “downcast” because it moves a pointer down a class hierarchy, from a given class to a class derived from it. dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用 dynamic_cast 把指向基类的指针 或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进 行类型转换时)或者抛出异常(当对引用进行类型转换时) 。 dynamic_casts 在帮助你浏览继承层次上是有限制的。它不能被用于缺乏虚函数的类型上,也不能用它来转换掉 constness。 4、reinterpret_cast Operator MSDN: The reinterpret_cast operator allows any pointer to be converted into any other pointer type. It also allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators. Syntax reinterpret_cast & type-id & ( expression ) The reinterpret_cast operator can be used for conversions such as char* to int*, or One_class* to Unrelated_class*, which are inherently unsafe. The result of a reinterpret_cast cannot safely be used for anything other than being cast back to its original type. Other uses are, at best, nonportable. The reinterpret_cast operator cannot cast away the const, volatile, or __unaligned attributes. 使用这个操作符的类型转换 其的转换结果几乎都是执行期定义 , (implementation-defined) 因此 使用 reinterpret_casts 。 , 的代码很难移植。 reinterpret_casts 的最普通的用途就是在函数指针类型之间进行转换。 比如转换函数指针的代码是不可移植的 (C++不保证所有的函数指针都被用一样的方法表示) ,在一些情况下这样的转换会产 生不正确的结果(参见条款 M31) ,所以你应该避免转换函数指针类型。 5、如果你使用的编译器缺乏对新的类型转换方式的支持,你可以用传统的类型转换方法代替 static_cast, const_cast, 以 及 reinterpret_cast。也可以用下面的宏替换来模拟新的类型转换语法: #define static_cast(TYPE,EXPR) #define const_cast(TYPE,EXPR) #define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR)) ((TYPE)(EXPR)) ((TYPE)(EXPR)) //error: pb2 points to a B, not a D // pd2 == NULL // ok: pb actually points to a D // unclear but ok这些模拟不会象真实的操作符一样安全 但是当你的编译器可以支持新的的类型转换时 它们可以简化你把代码升级的过程 , , 。 没有一个容易的方法来模拟 dynamic_cast 的操作,但是很多函数库提供了函数,安全地在派生类与基类之间进行类型转换。 如果你没有这些函数而你有必须进行这样的类型转换,你也可以回到 C 风格的类型转换方法上,但是这样的话你将不能获知 类型转换是否失败。当然,你也可以定义一个宏来模拟 dynamic_cast 的功能,就象模拟其它的类型转换一样: #define dynamic_cast(TYPE,EXPR) 和 reinterpret_cast 的区别和应用。(TYPE)(EXPR)请记住 这个模拟并不能完全实现 dynamic_cast 的功能 它没有办法知道转换是否失败 重点是 static_cast, dynamic_cast , , 。 31.分别写出 BOOL,int,float,指针类型的变量 的比较语句。 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.请说出 相比,有何优点? 32.请说出 const 与#define 相比,有何优点? 答案:1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替 换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。 2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。 33.简述数组与指针的区别? 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 = cout&& sizeof(a) && // 12 字节 cout&& sizeof(p) && // 4 字节 计算数组和指针的内存容量 void Func(char a[100]) { cout&& sizeof(a) && // 4 字节而不是 100 字节 } 34.类成员函数的重载、覆盖和隐藏区别? 34.类成员函数的重载、覆盖和隐藏区别? 类成员函数的重载 答案: a.成员函数被重载的特征: (1)相同的范围(在同一个类中) ; (2)函数名字相同; (3)参数不同; (4)virtual 关键字可有可无。 b.覆盖是指派生类函数覆盖基类函数,特征是: (1)不同的范围(分别位于派生类与基类) ; (2)函数名字相同; (3)参数相同; (4)基类函数必须有 virtual 关键字。 c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下: (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意 别与重载混淆) 。 (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字。此时,基类的函数被隐 藏(注意别与覆盖混淆) 35. There are two int variables: a and b, don’t use “if”, “? :”, “switch”or 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. C++编译程序编译的 编译程序编译的? 38. 如何判断一段程序是由 C 编译程序还是由 C++编译程序编译的? 答案: #ifdef __cplusplus cout&&&c++&; #else cout&&&c&; #endif 39.文件中有一组整数, 39.文件中有一组整数,要求排序后输出到另一个文件中 文件中有一组整数 答案: #i nclude #i nclude void Order(vector& data) //bubble sort { int count = data.size() ; int tag = // 设置是否需要继续冒泡的标志位 for ( int i = 0 ; i & i++) { for ( int j = 0 ; j & count - i - 1 ; j++) { if ( data[j] & data[j+1]) { tag = int temp = data[j] ; data[j] = data[j+1] ; data[j+1] = } } if ( !tag ) } } void main( void ) { ifstream in(&c:\\data.txt&); if ( !in) { cout&&&file error!&; exit(1); } while (!in.eof()) { in&& 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& 链表题: 40. 链表题:一个链表的结点结构 struct Node { Node * }; typedef struct Node N (1)已知链表的头结点 head,写一个函数把这个链表逆序 ( Intel) Node * ReverseList(Node *head) //链表逆序 { if ( head == NULL || head-&next == NULL ) Node *p1 = Node *p2 = p1-& Node *p3 = p2-& p1-&next = NULL ; while ( p3 != NULL ) { p2-&next = p1 ; p1 = p2 ; p2 = p3 ; p3 = p3-& } p2-&next = p1 ; head = p2 ; } (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-& p2 = head2 ; } else { head = head2 ; p2 = head2-& p1 = head1 ; } Node *pcurrent = while ( p1 != NULL && p2 != NULL) { if ( p1-&data &= p2-&data ) { pcurrent-&next = p1 ; pcurrent = p1 ; p1 = p1-& } else { pcurrent-&next = p2 ; pcurrent = p2 ; p2 = p2-& } } if ( p1 != NULL ) pcurrent-&next = p1 ; if ( p2 != NULL ) pcurrent-&next = p2 ; } (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); } } 41. 分析一下这段程序的输出 (Autodesk) class B { public: B() { cout&&&default constructor&&} ~B() { cout&&&destructed&&} B(int i):data(i) //B(int) works as a converter ( int -& instance of B) { cout&&&constructed by parameter & && data &} private: }; B Play( B b) { } (1) results: int main(int argc, char* argv[]) constructed by parameter 5 { destructed B(5)形参析构 B t1 = Play(5); B t2 = Play(t1); return 0; } 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); return 0; } destructed t2 destructed t1 写一个函数找出一个整数数组中, microsoft) 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 & i++) { if ( data[i] & maxnumber ) { sec_max = maxnumber = data[i] ; } else { if ( data[i] & sec_max ) sec_max = data[i] ; } } return sec_ } 写一个在一个字符串(n)中寻找一个子串(m)第一个位置的函数。 (n)中寻找一个子串(m)第一个位置的函数 43. 写一个在一个字符串(n)中寻找一个子串(m)第一个位置的函数。 KMP 算法效率最好,时间复杂度是O(n+m)。 多重继承的内存分配问题: 44. 多重继承的内存分配问题: 比如有 class A : public class B, public class C {} 注意顺序! constructed by parameter 10 destructed B(10)形参析构 destructed t1 形参析构 destructed t2 注意顺序! 那么 A 的内存结构大致是怎么样的? 这个是 compiler-dependent 的, 不同的实现其细节可能不同。 如果不考虑有虚函数、虚继承的话就相当简单;否则的话,相当复杂。 可以参考以下内容: 首先,我们顺次考察 C 兼容的结构(struct)的布局,单继承,多重继承,以及虚继承;接着,我们讲成员变量和成员函数 的访问,当然,这里面包含虚函 数的情况;再接下来,我们考察构造函数,析构函数,以及特殊的赋值操作符成员函数是 如何工作的,数组是如何动态构造和销毁的;最后,简单地介绍对异常处理 的支持。 对每个语言特性,我们将简要介绍该特性背后的动机,该特性自身的语意(当然,本文决不是“C++入门”,大家对此 要有充分认 识) ,以及该特性在微软的 VC++中是如何实现的。这里要注意区分抽象的 C++语言语意与其特定实现。微软之外 的其他 C++厂商可能提供一个完全不同的实 现,我们偶尔也会将 VC++的实现与其他实现进行比较。 类布局:本节讨论不同的继承方式造成的不同内存布局。 1、C 结构(struct) 由于 C++基于 C,所以 C++也“基本上”兼容 C。特别地,C++规范在“结构”上使用了和 C 相同的,简单的内存布局原 则:成员变量按其被声明的顺序排 列,按具体实现所规定的对齐原则在内存地址上对齐。所有的 C/C++厂商都保证他们的 C/C++编译器对于有效的 C 结构采用完全相同的布局。这里,A 是一 个简单的 C 结构,其成员布局和对齐方式都一目了然。 struct A { }; 译者注:从上图可见,A 在内存中占有 8 个字节,按照声明成员的顺序,前 4 个字节包含一个字符(实际占用 1 个字节, 3 个字节空着,补对齐) ,后 4 个字节包含一个整数。A 的指针就指向字符开始字节处。 2、有 C++特征的 C 结构 当然了,C++不是复杂的 C,C++本质上是面向对象的语言:包含继承、封装,以及多态。原始的 C 结构经过改造,成 了面向对象世界的基石――类。除了成 员变量外,C++类还可以封装成员函数和其他东西。然而,有趣的是,除非为了实现 虚函数和虚继承引入的隐藏成员变量外 C++类实例的大小完全取决于一个 类及其基类的成员变量!成员函数基本上不影响 , 类实例的大小。 这里提供的 B 是一个 C 结构,然而,该结构有一些 C++特征:控制成员可见 性的“public/protected/private”关键 字、成员函数、静态成员,以及嵌套的类型声明。虽然看着琳琅满目,实际上只有成员变量才占 用类实例的空间。要注意 的是,C++标准委员会不限制由“public/protected/private”关键字分开的各段在实现时的先后顺序,因此,不同的编译 器实现的内存布局可能并不相同。 (在 VC++中,成员变量总是按照声明时的顺序排列) 。 struct B { public: int bm1; protected: int bm2; private: int bm3; void bf(); static void bsf(); typedef void* struct N { }; }; 译者注:B 中,为何 static int bsm 不占用内存空间?因为它是静态成员,该数据存放在程序的数据段中,不在类实 例中。 3、单继承 C++提供继承的目的是在不同的类型之间提取共性。比如,科学家对物种进行分类,从而有种、属、纲等说法。有了这 种层次结构,我们才可能将某些具备特定 性质的东西归入到最合适的分类层次上,如“怀孩子的是哺乳动物”。由于这些 属性可以被子类继承,所以,我们只要知道“鲸鱼、人”是哺乳动物,就可以方便地 指出“鲸鱼、人都可以怀孩子”。那 些特例,如鸭嘴兽(生蛋的哺乳动物) ,则要求我们对缺省的属性或行为进行覆盖。 C++中的继承语法很简单,在子类后加上“:base”就可以了。下面的 D 继承自基类 C。 struct C { int c1; void cf(); }; struct D : C { int d1; void df(); }; 既然派生类要保留基类的所有属性和行为,自然地,每个派生类的实例都包含了一份完整的基类实例数据。在 D 中, 并不是说基类 C 的数据一定要放在 D 的数据之 前,只不过这样放的话,能够保证 D 中的 C 对象地址,恰好是 D 对象地址的 第一个字节。这种安排之下,有了派生类 D 的指针,要获得基类 C 的指针,就不必要计算 偏移量了。几乎所有知名的 C++ 厂商都采用这种内存安排。在单继承类层次下,每一个新的派生类都简单地把自己的成员变量添加到基类的成员变量之后。 看看上图,C 对象指针和 D 对象指针指向同一地址。 4、多重继承 大多数情况下,其实单继承就足够了。但是,C++为了我们的方便,还提供了多重继承。 比如,我们有 一个组织模型,其中有经理类(分任务) ,工人类(干活) 。那么,对于一线经理类,即既要从上级经理 那里领取任务干活,又要向下级工人分任务的角色来说,如 何在类层次中表达呢?单继承在此就有点力不胜任。我们可以 安排经理类先继承工人类,一线经理类再继承经理类,但这种层次结构错误地让经理类继承了工人类的 属性和行为。反之 亦然。当然,一线经理类也可以仅仅从一个类(经理类或工人类)继承,或者一个都不继承,重新声明一个或两个接口,但 这样的实现弊处太多: 多态不可能了;未能重用现有的接口;最严重的是,当接口变化时,必须多处维护。最合理的情况 似乎是一线经理从两个地方继承属性和行为――经理类、工人类。 C++就允许用多重继承来解决这样的问题: struct Manager ... { ... }; struct Worker ... { ... }; struct MiddleManager : Manager, Worker { ... }; 这样的继承将造成怎样的类布局呢?下面我们还是用“字母类”来举例: struct E { int e1; void ef(); }; struct F : C, E { int f1; void ff(); }; 结构 F 从 C 和 E 多重继承得来。与单继承相同的是,F 实例拷贝了每个基类的所有数据。与单继承不同的是,在多重继 承下,内嵌的两个基类的对象指针不可能全都与派生类对象指针相同: F // (void*)&f == (void*)(C*)&f; // (void*)&f & (void*)(E*)&f; 译者注:上面那行说明 C 对象指针与 F 对象指针相同,下面那行说明 E 对象指针与 F 对象指针不同。 观察类布局,可以看到 F 中内嵌的 E 对象,其指针与 F 指针并不相同。正如后文讨论强制转化和成员函数时指出的,这 个偏移量会造成少量的调用开销。 具体的编译器实现可以自由地选择内嵌基类和派生类的布局。VC++按照基类的声明顺序先排列基类实例数据,最后才 排列派生类数据。当然,派生类数据本身 也是按照声明顺序布局的(本规则并非一成不变,我们会看到,当一些基类有虚 函数而另一些基类没有时,内存布局并非如此) 。 5、虚继承 回到我们讨论的一线经理类例子。让我们考虑这种情况:如果经理类和工人类都继承自“雇员类”,将会发生什么? struct Employee { ... }; struct Manager : Employee { ... }; struct Worker : Employee { ... }; struct MiddleManager : Manager, Worker { ... }; 如果经理类和工人类都继承自雇员类,很自然地,它们每个类都会从雇员类获得一份数据拷贝。如果不作特殊处理, 一线经理类的实例将含有两个雇员类实例, 它们分别来自两个雇员基类。如果雇员类成员变量不多,问题不严重;如果成 员变量众多,则那份多余的拷贝将造成实例生成时的严重开销。更糟的是,这两份不同 的雇员实例可能分别被修改,造成 数据的不一致。因此,我们需要让经理类和工人类进行特殊的声明,说明它们愿意共享一份雇员基类实例数据。 很不幸,在 C++中,这种“共享继承”被称为“虚继承”,把问题搞得似乎很抽象。虚继承的语法很简单,在指定基类 时加上 virtual 关键字即可。 struct Employee { ... }; struct Manager : virtual Employee { ... }; struct Worker : virtual Employee { ... }; struct MiddleManager : Manager, Worker { ... }; 使用虚继承,比起单继承和多重继承有更大的实现开销、调用开销。回忆一下,在单继承和多重继承的情况下,内嵌 的基类实例地址比起派生类实例地址来,要么 地址相同(单继承,以及多重继承的最靠左基类) ,要么地址相差一个固定偏 移量(多重继承的非最靠左基类) 。然而,当虚继承时,一般说来,派生类地址和其虚 基类地址之间的偏移量是不固定的, 因为如果这个派生类又被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量 处。请 看下例: struct G : virtual C { int g1; void gf(); }; 译者注:GdGvbptrG(In G, the displacement of G’s virtual base pointer to G)意思是:在 G 中,G 对象的指 针与 G 的虚基类表指针之间的偏移量,在此可见为 0,因为 G 对象内存布局第一项就是虚基类表指针; GdGvbptrC (In G, the displacement of G’s virtual base pointer to C)意思是:在 G 中,C 对象的指针与 G 的虚基类表指针之间的偏移量, 在此可见为 4。 struct H : virtual C { int h1; void hf(); }; struct I : G, H { int i1; void _if(); }; 暂时不追究 vbptr 成员变量从何而来。从上面这些图可以直观地看到,在 G 对象中,内嵌的 C 基类对象的数据紧跟在 G 的数据之后,在 H 对象中,内嵌的 C 基类对象的数据也紧跟在 H 的数据之后。但是,在 I 对象中,内存布局就并非如此了。 VC++实现的内存布局中,G 对象实例中 G 对象和 C 对象之间的偏移,不同 于 I 对象实例中 G 对象和 C 对象之间的偏移。当 使用指针访问虚基类成员变量时,由于指针可以是指向派生类实例的基类指针,所以,编译器不能根据声明的指针类 型计 算偏移,而必须找到另一种间接的方法,从派生类指针计算虚基类的位置。 在 VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计 算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之 间的偏移量。 其它的实现方式中,有一种是在派生类中使用指针成员变量。这些指针成员变量指向派生类的虚基类,每个虚基类一 个指针。这种方式的优点是:获取虚基类地址 时,所用代码比较少。然而,编译器优化代码时通常都可以采取措施避免重 复计算虚基类地址。况且,这种实现方式还有一个大弊端:从多个虚基类派生时,类实例 将占用更多的内存空间;获取虚 基类的虚基类的地址时,需要多次使用指针,从而效率较低等等。 在 VC++中,G 拥有一个隐藏的“虚基类 表指针”成员,指向一个虚基类表,该表的第二项是 GdGvbptrC。 (在 G 中,虚 基类对象 C 的地址与 G 的“虚基类表指针”之间的偏移量 (当对于所有的派 生类来说偏移量不变时 省略“d”前的前缀) , ) 。 比如,在 32 位平台上,GdGvptrC 是 8 个字节。同样,在 I 实例中的 G 对象实例也有“虚基类表指 针”,不过该指针指向 一个适用于“G 处于 I 之中”的虚基类表,表中一项为 IdGvbptrC,值为 20。 观察前面的 G、H 和 I,我们可以得到如下关于 VC++虚继承下内存布局的结论: ?首先排列非虚继承的基类实例; ?有虚基类时,为每个基类增加一个隐藏的 vbptr,除非已经从非虚继承的类那里继承了一个 vbptr; ?排列派生类的新数据成员; ?在实例最后,排列每个虚基类的一个实例。 该布局安排使得虚基类的位置随着派生类的不同而“浮动不定”,但是,非虚基类因此也就凑在一起,彼此的偏移量固 定不变。 成员变量 介绍了类布局之后,我们接着考虑对不同的继承方式,访问成员变量的开销究竟如何。 没有继承。没有任何继承关系时,访问成员变量和 C 语言的情况完全一样:从指向对象的指针,考虑一定的偏移量即可。 C* pc-&c1; // *(pc + dCc1); 译者注:pc 是指向 C 的指针。 ? 访问 C 的成员变量 c1,只需要在 pc 上加上固定的偏移量 dCc1(在 C 中,C 指针地址与其 c1 成员变量之间的偏移量 值) ,再获取该指针的内容即可。 单继承。由于派生类实例与其基类实例之间的偏移量是常数 0,所以,可以直接利用基类指针和基类成员之间的偏移量 关系,如此计算得以简化。 D* pd-&c1; // *(pd + dDC + dCc1); // *(pd + dDc1); pd-&d1; // *(pd + dDd1); 译者注:D 从 C 单继承,pd 为指向 D 的指针。 ?当访问基类成员 c1 时,计算步骤本来应该为“pd+dDC+dCc1”,即为先计算 D 对象和 C 对象之间的偏移,再在此基础 上加上 C 对象指针与成员变量 c1 之间的偏移量。然而,由于 dDC 恒定为 0,所以直接计算 C 对象地址与 c1 之间的偏移就可 以了。 ?当访问派生类成员 d1 时,直接计算偏移量。 多重继承。虽然派生类与某个基类之间的偏移量可能不为 0,然而,该偏移量总是一个常数。只要是个常数,访问成员 变量,计算成员变量偏移时的计算就可以被简化。可见即使对于多重继承来说,访问成员变量开销仍然不大。 F* pf-&c1; // *(pf + dFC + dCc1); // *(pf + dFc1); pf-&e1; // *(pf + dFE + dEe1); // *(pf + dFe1); pf-&f1; // *(pf + dFf1); 译者注:F 继承自 C 和 E,pf 是指向 F 对象的指针。 ?访问 C 类成员 c1 时,F 对象与内嵌 C 对象的相对偏移为 0,可以直接计算 F 和 c1 的偏移; ?访问 E 类成员 e1 时,F 对象与内嵌 E 对象的相对偏移是一个常数,F 和 e1 之间的偏移计算也可以被简化; ?访问 F 自己的成员 f1 时,直接计算偏移量。 虚继承。当类有虚基类时,访问非虚基类的成员仍然是计算固定偏移量的问题。然而,访问虚基类的成员变量,开销 就增大了,因为必须经过如下步骤才能获得成 员变量的地址:获取“虚基类表指针”;获取虚基类表中某一表项的内容; 把内容中指出的偏移量加到“虚基类表指针”的地址上。然而,事情并非永远如此。正如 下面访问 I 对象的 c1 成员那样, 如果不是通过指针访问,而是直接通过对象实例,则派生类的布局可以在编译期间静态获得,偏移量也可以在编译时计算, 因此也 就不必要根据虚基类表的表项来间接计算了。 I* pi-&c1; // *(pi + dIGvbptr + (*(pi+dIGvbptr))[1] + dCc1); pi-&g1; // *(pi + dIG + dGg1); // *(pi + dIg1); pi-&h1; // *(pi + dIH + dHh1); // *(pi + dIh1); pi-&i1; // *(pi + dIi1); I i.c1; // *(&i + IdIC + dCc1); // *(&i + IdIc1); 译者注:I 继承自 G 和 H,G 和 H 的虚基类是 C,pi 是指向 I 对象的指针。 ?访问虚基类 C 的成员 c1 时 dIGvbptr 是“在 I 中 I 对象指针与 G 的“虚基类表指针”之间的偏移” *(pi + dIGvbptr) , , , 是虚基类表的开始地址,*(pi + dIGvbptr)[1]是虚基类表的第二项的内容(在 I 对象中,G 对象的“虚基类表指针”与虚 基类之间的偏移) ,dCc1 是 C 对象指针与成员变量 c1 之 间的偏移; ?访问非虚基类 G 的成员 g1 时,直接计算偏移量; ?访问非虚基类 H 的成员 h1 时,直接计算偏移量; ?访问自身成员 i1 时,直接使用偏移量; ?当声明了一个对象实例,用点“.”操作符访问虚基类成员 c1 时,由于编译时就完全知道对象的布局情况,所以可以 直接计算偏移量。 当访问类继承层次中,多层虚基类的成员变量时,情况又如何呢?比如,访问虚基类的虚基类的成员变量时?一些实 现方式为:保存一个指向直接虚基类的指针, 然后就可以从直接虚基类找到它的虚基类,逐级上推。VC++优化了这个过程。 VC++在虚基类表中增加了一些额外的项,这些项保存了从派生类到其各层虚基 类的偏移量。 强制转化 如果没有虚基类的问题,将一个指针强制转化为另一个类型的指针代价并不高昂。如果在要求转化的两个指针之间有 “基类-派生类”关系,编译器只需要简单地在两者之间加上或者减去一个偏移量即可(并且该量还往往为 0) 。 F* (C*) // (C*)(pf ? pf + dFC : 0); // (C*) (E*) // (E*)(pf ? pf + dFE : 0); C 和 E 是 F 的基类,将 F 的指针 pf 转化为 C*或 E*,只需要将 pf 加上一个相应的偏移量。转化为 C 类型指针 C*时,不 需要计算,因为 F 和 C 之间的偏移量 为 0。转化为 E 类型指针 E*时,必须在指针上加一个非 0 的偏移常量 dFE。C++规范要 求 NULL 指针在强制转化后依然为 NULL,因此在做强制转化需要 的运算之前,VC++会检查指针是否为 NULL。当然,这个检 查只有当指针被显示或者隐式转化为相关类型指针时才进行;当在派生类对象中调用基类的方法, 从而派生类指针被在后 台转化为一个基类的 Const “this” 指针时,这个检查就不需要进行了,因为在此时,该指针一定不为 NULL。 正如你猜想的,当继承关系中存在虚基类时,强制转化的开销会比较大。具体说来,和访问虚基类成员变量的开销相当。 I* (G*) // (G*) (H*) // (H*)(pi ? pi + dIH : 0); (C*) // (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr))[1]) : 0); 译者注:pi 是指向 I 对象的指针,G,H 是 I 的基类,C 是 G,H 的虚基类。 ?强制转化 pi 为 G*时,由于 G*和 I*的地址相同,不需要计算; ?强制转化 pi 为 H*时,只需要考虑一个常量偏移; ?强制转化 pi 为 C*时,所作的计算和访问虚基类成员变量的开销相同,首先得到 G 的虚基类表指针,再从虚基类表的 第二项中取出 G 到虚基类 C 的偏移量,最后根据 pi、虚基类表偏移和虚基类 C 与虚基类表指针之间的偏移计算出 C*。 一般说来,当从派生类中访问虚基类成员时,应该先强制转化派生类指针为虚基类指针,然后一直使用虚基类指针来访 问虚基类成员变量。这样做,可以避免每次都要计算虚基类地址的开销。见下例。 /* before: */ ... pi-&c1 ... pi-&c1 ... /* faster: */ C* pc = ... pc-&c1 ... pc-&c1 ... 译者注:前者一直使用派生类指针 pi,故每次访问 c1 都有计算虚基类地址的较大开销;后者先将 pi 转化为虚基类指 针 pc,故后续调用可以省去计算虚基类地址的开销。 成员函数 一个 C++成员函数只是类范围内的又一个成员。X 类每一个非静态的成员函数都会接受一个特殊的隐藏参数―― this 指针,类型为 X* const。该指针在后台初始化为指向成员函数工作于其上的对象。同样,在成员函数体内,成员变量的访 问是通过在后台计算与 this 指针的偏移来进行。 struct P { int p1; void pf(); // new virtual void pvf(); // new }; P 有一个非虚成员函数 pf(),以及一个虚成员函数 pvf()。很明显,虚成员函数造成对象实例占用更多内存空间,因 为虚成员函数需要虚函数表指针。 这一点以后还会谈到。这里要特别指出的是,声明非虚成员函数不会造成任何对象实例 的内存开销。现在,考虑 P::pf()的定义。 void P::pf() { // void P::pf([P *const this]) ++p1; }// ++(this-&p1);这里 P:pf()接受了一个隐藏的 this 指针参数,对于每个成员函数调用,编译器都会自动加上这个参数。同时,注意 成员变量访问也许比看起来要代价高 昂一些,因为成员变量访问通过 this 指针进行,在有的继承层次下,this 指针需要 调整,所以访问的开销可能会比较大。然而,从另一方面来说,编译器通 常会把 this 指针缓存到寄存器中,所以,成员变 量访问的代价不会比访问局部变量的效率更差。 译者注:访问局部变量,需要到 SP 寄存器中得到栈指针,再加上局部变量与栈顶的偏移。在没有虚基类的情况下,如 果编译器把 this 指针缓存到了寄存器中,访问成员变量的过程将与访问局部变量的开销相似。 1、覆盖成员函数 和成员变量一样,成员函数也会被继承。与成员变量不同的是,通过在派生类中重新定义基类函数,一个派生类可以 覆盖,或者说替换掉基类的函数定义。覆盖是 静态(根据成员函数的静态类型在编译时决定)还是动态(通过对象指针在 运行时动态决定) ,依赖于成员函数是否被声明为“虚函数”。 Q 从 P 继承了成员变量和成员函数。Q 声明了 pf(),覆盖了 P::pf()。Q 还声明了 pvf(),覆盖了 P::pvf()虚函数。Q 还声明了新的非虚成员函数 qf(),以及新的虚成员函数 qvf()。 struct Q : P { int q1; void pf(); // overrides P::pf void qf(); // new void pvf(); // overrides P::pvf virtual void qvf(); // new }; 对于非虚的成员函数来说,调用哪个成员函数是在编译时,根据“-&”操作符左边指针表达式的类型静态决定的。特 别地,即使 ppq 指向 Q 的实例, ppq-&pf()仍然调用的是 P::pf(),因为 ppq 被声明为“P*”。 (注意,“-&”操作符左边 的指针类型决定隐藏的 this 参数 的类型。) P P* pp = &p; Q P* ppq = &q; Q* pq = &q; pp-&pf(); // pp-&P::pf(); pq-&pf(); // pq-&Q::pf(); pq-&qf(); // pq-&Q::qf(); // P::pf(pp); // Q::pf((P*)pq); (错误!) // Q::qf(pq); ppq-&pf(); // ppq-&P::pf(); // P::pf(ppq);译者注:标记“错误”处,P*似应为 Q*。因为 pf 非虚函数,而 pq 的类型为 Q*,故应该调用到 Q 的 pf 函数上,从而该 函数应该要求一个 Q* const 类型的 this 指针。 对于虚函数调用来说,调用哪个成员函数在运行时决定。不管“-&”操作符左边的指针表达式的类型如何,调用的虚函 数都是由指针实际指向的实例类型所决定。比如,尽管 ppq 的类型是 P*,当 ppq 指向 Q 的实例时,调用的仍然是 Q::pvf()。 pp-&pvf(); pq-&pvf(); 应该是 P*。 为了实现这种机制,引入了隐藏的 vfptr 成员变量。一个 vfptr 被加入到类中(如果类中没有的话) ,该 vfptr 指向类 的虚函数表 (vftable) 。类中每个虚函数在该类的虚函数表中都占据一项。每项保存一个对于该类适用的虚函数的地址。 因此,调用虚函数的过程如下:取得实例的 vfptr;通过 vfptr 得到虚函数表的一项;通过虚函数表该项的函数地址间接调 用虚函数。也就是说,在普通函数调用的参数传递、调用、返回指令开销 外,虚函数调用还需要额外的开销。 回头再看看 P 和 Q 的内存布局,可以发现,VC++编译器把隐藏的 vfptr 成员变量放在 P 和 Q 实例 的开始处。这就使虚 函数的调用能够尽量快一些。实际上,VC++的实现方式是,保证任何有虚函数的类的第一项永远是 vfptr。这就可能要求在 实例布局 时,在基类前插入新的 vfptr,或者要求在多重继承时,虽然在右边,然而有 vfptr 的基类放到左边没有 vfptr 的基类的前面。 许多 C++的实现会共享或者重用从基类继承来的 vfptr。比如,Q 并不会有一个额外的 vfptr,指向一个专门存放新的 虚函数 qvf()的虚函数表。 Qvf 项只是简单地追加到 P 的虚函数表的末尾。如此一来,单继承的代价就不算高昂。一旦一个 实例有 vfptr 了,它就不需要更多的 vfptr。新的派生类 可以引入更多的虚函数,这些新的虚函数只是简单地在已存在的, “每类一个”的虚函数表的末尾追加新项。 // pp-&P::pvf(); // pq-&Q::pvf(); // P::pvf(pp); // Q::pvf((P*)pq); (错误!) ppq-&pvf(); // ppq-&Q::pvf(); // Q::pvf((Q*)ppq); 译者注:标记“错误”处,P*似应为 Q*。因为 pvf 是虚函数,pq 本来就是 Q*,又指向 Q 的实例,从哪个方面来看都不 2、多重继承下的虚函数 如果从多个有虚函数的基类继承,一个实例就有可能包含多个 vfptr。考虑如下的 R 和 S 类: struct R { int r1; virtual void pvf(); // new virtual void rvf(); // new }; struct S : P, R { int s1; void pvf(); // overrides P::pvf and R::pvf void rvf(); // overrides R::rvf void svf(); // new }; 这里 R 是另一个包含虚函数的类 因为 S 从 P 和 R 多重继承 S 的实例内嵌 P 和 R 的实例 以及 S 自身的数据成员 S::s1 。 , , 。 注意,在多重继承下,靠右的基 类 R,其实例的地址和 P 与 S 不同。S::pvf 覆盖了 P::pvf()和 R::pvf(),S::rvf()覆盖了 R::rvf()。 S S* ps = &s; ((P*)ps)-&pvf(); // (*(P*)ps)-&P::vfptr[0])((S*)(P*)ps) ((R*)ps)-&pvf(); // (*(R*)ps)-&R::vfptr[0])((S*)(R*)ps) ps-&pvf(); 译者注: ?调用((P*)ps)-&pvf()时,先到 P 的虚函数表中取出第一项,然后把 ps 转化为 S*作为 this 指针传递进去; ?调用((R*)ps)-&pvf()时,先到 R 的虚函数表中取出第一项,然后把 ps 转化为 S*作为 this 指针传递进去; 因为 S::pvf()覆盖了 P::pvf()和 R::pvf(),在 S 的虚函数表中,相应的项也应该被覆盖。然而,我们很快注意到, 不光可以用 P*,还 可以用 R*来调用 pvf()。问题出现了:R 的地址与 P 和 S 的地址不同。表达式(R*)ps 与表达式(P*)ps 指向类布局中不同的位置。因为函数 S:: pvf 希望获得一个 S*作为隐藏的 this 指针参数,虚函数必须把 R*转化为 S*。因 此,在 S 对 R 虚函数表的拷贝中,pvf 函数对应的项,指向的是一个 “调整块”的地址,该调整块使用必要的计算,把 R* 转换为需要的 S*。 译者注:这就是“thunk1: this-= sdPR; goto S::pvf”干的事。先根据 P 和 R 在 S 中的偏移,调整 this 为 P*,也就 是 S*,然后跳转到相应的虚函数处执行。 在微软 VC++实现中,对于有虚函数的多重继承,只有当派生类虚函数覆盖了多个基类的虚函数时,才使用调整块。 3、地址点与“逻辑 this 调整” 考虑下一个虚函数 S::rvf(),该函数覆盖了 R::rvf()。我们都知道 S::rvf()必须有一个隐藏的 S*类型的 this 参数。 但是,因为也可以用 R*来调用 rvf(),也就是说,R 的 rvf 虚函数槽可能以如下方式被用到: ((R*)ps)-&rvf(); // (*((R*)ps)-&R::vfptr[1])((R*)ps) 所以,大多数实现用另一个调整块将传递给 rvf 的 R*转换为 S*。还有一些实现在 S 的虚函数表末尾添加一个特别的 虚函数项,该虚函数项提供方法,从而 可以直接调用 ps-&rvf(),而不用先转换 R*。MSC++的实现不是这样,MSC++有意将 S::rvf 编译为接受一个指向 S 中嵌套的 R 实 例,而非指向 S 实例的指针(我们称这种行为是“给派生类的指针类型与该虚 函数第一次被引入时接受的指针类型相同”) 。所有这些在后台透明发生,对成员变量 的存取,成员函数的 this 指针,都 进行“逻辑 this 调整”。 当然,在 debugger 中,必须对这种 this 调整进行补偿。 ps-&rvf(); // ((R*)ps)-&rvf(); // S::rvf((R*)ps) 译者注:调用 rvf 虚函数时,直接给入 R*作为 this 指针。 所以,当覆盖非最左边的基类的虚函数时,MSC++一般不创建调整块,也不增加额外的虚函数项。 4、调整块 正如已经描述的,有时需要调整块来调整 this 指针的值(this 指针通常位于栈上返回地址之下,或者在寄存器中) , 在 this 指针上加或减去一个常量 偏移,再调用虚函数。某些实现(尤其是基于 cfront 的)并不使用调整块机制。它们在 每个虚函数表项中增加额外的偏移数据。每当虚函数被调用时,该偏移 数据(通常为 0),被加到对象的地址上,然后对象 的地址再作为 this 指针传入。 ps-&rvf(); // calls S::pvf() // struct { void (*pfn)(void*); size_ }; // (*ps-&vfptr[i].pfn)(ps + ps-&vfptr[i].disp); 译者注:当调用 rvf 虚函数时,前一句表示虚函数表每一项是一个结构,结构中包含偏移量;后一句表示调用第 i 个虚 函数时,this 指针使用保存在虚函数表中第 i 项的偏移量来进行调整。 这种方法的缺点是虚函数表增大了,虚函数的调用也更加复杂。 现代基于 PC 的实现一般采用“调整―跳转”技术: S::pvf-adjust: // MSC++ this -= SdPR; goto S::pvf() 当然,下面的代码序列更好(然而,当前没有任何实现采用该方法) : S::pvf-adjust: this -= SdPR; // fall into S::pvf() S::pvf() { ... } 译者注:IBM 的 C++编译器使用该方法。 5、虚继承下的虚函数 T 虚继承 P,覆盖 P 的虚成员函数,声明了新的虚函数。如果采用在基类虚函数表末尾添加新项的方式,则访问虚函数 总要求访问虚基类。在 VC++中,为了避 免获取虚函数表时,转换到虚基类 P 的高昂代价,T 中的新虚函数通过一个新的虚 函数表获取,从而带来了一个新的虚函数表指针。该指针放在 T 实例的顶端。 struct T : virtual P { int t1; void pvf(); }; void T::pvf() { ++p1; // ((P*)this)-&p1++; // vbtable lookup! ++t1; // this-&t1++; } 如上所示,即使是在虚函数中,访问虚基类的成员变量也要通过获取虚基类表的偏移,实行计算来进行。这样做之所 以必要,是因为虚函数可能被进一步继承的类所覆盖,而进一步继承的类的布局中,虚基类的位置变化了。下面就是这样的 一个类: struct U : T { int u1; }; 在此 U 增加了一个成员变量,从而改变了 P 的偏移。因为 VC++实现中,T::pvf()接受的是嵌套在 T 中的 P 的指针, 所以,需要提供一个调整块,把 this 指针调整到 T::t1 之后(该处即是 P 在 T 中的位置) 。 6、特殊成员函数 本节讨论编译器合成到特殊成员函数中的隐藏代码。 6.1 构造函数和析构函数 正如我们所见,在构造和析构过程中,有时需要初始化一些隐藏的成员变量。最坏的情况下,一个构造函数要执行如下 操作: * 如果是“最终派生类”,初始化 vbptr 成员变量,调用虚基类的构造函数; * 调用非虚基类的构造函数 * 调用成员变量的构造函数 * 初始化虚函数表成员变量 * 执行构造函数体中,程序所定义的其他初始化代码 (注意:一个“最终派生类”的实例,一定不是嵌套在其他派生类实例中的基类实例) 所以,如果你有一个包含虚函数的很深的继承层次,即使该继承层次由单继承构成,对象的构造可能也需要很多针对虚 函数表的初始化。 反之,析构函数必须按照与构造时严格相反的顺序来“肢解”一个对象。 * 合成并初始化虚函数表成员变量 // overrides P::pvf virtual void tvf(); // new * 执行析构函数体中,程序定义的其他析构代码 * 调用成员变量的析构函数(按照相反的顺序) * 调用直接非虚基类的析构函数(按照相反的顺序) * 如果是“最终派生类”,调用虚基类的析构函数(按照相反顺序) 在 VC++中,有虚基类的类的构造函数接受一个隐藏的“最终派生类标志”,标示虚基类是否需要初始化。对于析构函 数,VC++采用“分层析构模型”,代 码中加入一个隐藏的析构函数,该函数被用于析构包含虚基类的类(对于“最终派生 类”实例而言) ;代码中再加入另一个析构函数,析构不包含虚基类的类。前一 个析构函数调用后一个。 6.2 虚析构函数与 delete 操作符 考虑结构 V 和 W。 struct V { virtual ~V(); }; struct W : V { operator delete(); }; 析构函数可以为虚。一个类如果有虚析构函数的话,将会象有其他虚函数一样,拥有一个虚函数表指针,虚函数表中 包含一项,其内容为指向对该类适用的虚析 构函数的地址。这些机制和普通虚函数相同。虚析构函数的特别之处在于:当 类实例被销毁时,虚析构函数被隐含地调用。调用地(delete 发生的地方)虽然 不知道销毁的动态类型,然而,要保证调 用对该类型合适的 delete 操作符。例如,当 pv 指向 W 的实例时,当 W::~W 被调用之后,W 实例将由 W 类的 delete 操作符 来销毁。 V* pv = new V; pv = new W; pv = new W; :: // pv-&~W::W(); // use ::operator delete() 译者注: ?V 没有定义 delete 操作符,delete 时使用函数库的 delete 操作符; ?W 定义了 delete 操作符,delete 时使用自己的 delete 操作符; ?可以用全局范围标示符显示地调用函数库的 delete 操作符。 为了实现上述语意,VC++扩展了其“分层析构模型”,从而自动创建另一个隐藏的析构帮助函数――“deleting 析构 函数”,然后,用该函数的地址 来替换虚函数表中“实际”虚析构函数的地址。析构帮助函数调用对该类合适的析构函数, 然后为该类有选择性地调用合适的 delete 操作符。 数组 堆上分配空间的数组使虚析构函数进一步复杂化。问题变复杂的原因有两个: 1、堆上分配空间的数组,由于数组可大可小,所以,数组大小值应该和数组一起保存。因此,堆上分配空间的数组会 分配额外的空间来存储数组元素的个数; 2、当数组被删除时,数组中每个元素都要被正确地释放,即使当数组大小不确定时也必须成功完成该操作。然而,派 生类可能比基类占用更多的内存空间,从而使正确释放比较困难。 struct WW : W { int w1; }; pv = new W[m]; delete [] // delete m W's pv = new WW[n]; delete [] // delete n WW's (sizeof(WW) & sizeof(V)) 译者注:WW 从 W 继承,增加了一个成员变量,因此,WW 占用的内存空间比 W 大。然而,不管指针 pv 指向 W 的数组还是 WW 的数组,delete[]都必须正确地释放 WW 或 W 对象占用的内存空间。 虽然从严格意义上来说,数组 delete 的多态行为 C++标准并未定义,然而,微软有一些客户要求实现该行为。因此, 在 MSC++中,该行为是用另一个 编译器生成的虚析构帮助函数来完成。该函数被称为“向量 delete 析构函数”(因其针对 特定的类定制,比如 WW,所以,它能够遍历数组的每个元素,调用 对每个元素适用的析构函数) 。 异常处理 (sizeof(W) == sizeof(V)) // pv-&~W::W(); // use W::operator delete() // pv-&~V::V(); // use ::operator delete() 简单说来,异常处理是 C++标准委员会工作文件提供的一种机制,通过该机制,一个函数可以通知其调用者“异常”情 况的发生,调用者则能据此选择合适的代码来处理异常。该机制在传统的“函数调用返回,检查错误状态代码”方法之外, 给程序提供了另一种处理错误的手段。 因为 C++是面向对象的语言,很自然地,C++中用对象来表达异常状态。并且,使用何种异常处理也是基于“抛出的” 异常对象的静态或动态类型来决定的。 不光如此,既然 C++总是保证超出范围的对象能够被正确地销毁,异常实现也必须 保证当控制从异常抛出点转换到异常“捕获”点时(栈展开) ,超出范围的对象 能够被自动、正确地销毁。 考虑如下例子: struct X { X(); }; // exception object class struct Z { Z(); ~Z(); }; // class with a destructor extern void recover(const X&); void f(int), g(int); int main() { try { f(0); } catch (const X& rx) { recover(rx); } return 0; } void f(int i) { Z z1; g(i); Z z2; g(i-1); } void g(int j) { if (j & 0) throw X(); } 译者注:X 是异常类,Z 是带析构函数的工作类,recover 是错误处理函数,f 和 g 一起产生异常条件,g 实际抛出异常。 这段程序会抛出异常。在 main 中,加入了处理异常的 try & catch 框架,当调用 f(0)时,f 构造 z1,调用 g(0)后, 再构造 z2,再调用 g(-1),此时 g 发现参数为负,抛出 X 异常对象。我们希望在某个调 用层次上,该异常能够得到处理。 既然 g 和 f 都没有建立处理异常的框架,我们就只能希望 main 函数建立的异常处理框架能够处理 X 异常对象。实际上,确 实如 此。当控制被转移到 main 中异常捕获点时,从 g 中的异常抛出点到 main 中的异常捕获点之间,该范围内的对象都必 须被销毁。在本例中,z2 和 z1 应该被 销毁。 谈到异常处理的具体实现方式,一般情况下,在抛出点和捕获点都使用“表”来表述能够捕获异常对象的类型;并且, 实现要保证能够 在特定的捕获点真正捕获特定的异常对象;一般地,还要运用抛出的对象来初始化捕获语句的“实参”。 通过合理地选择编码方案,可以保证这些表格不会占用过多 的内存空间。 异常处理的开销到底如何?让我们再考虑一下函数 f。看起来 f 没有做异常处理。f 确实没有包含 try,catch,或者是 throw 关键字,因此,我们会猜异常处理应该对 f 没有什么影响。错!编译器必须保证一旦 z1 被构造,而后续调用的任何 函数向 f 抛回了异常,异常又出了 f 的范围时,z1 对象能被正确地销毁。同样,一旦 z2 被构造,编译器也必须保证后续抛 出异常时,能够正确地销毁 z2 和 z1。 要实现这些 “展开”语意,编译器必须在后台提供一种机制,该机制在调用者函数中,针对调用的函数抛出的异常动 态决定异常环境(处理点) 。这可能包括在每个函数的准备 工作和善后工作中增加额外的代码,在最糟糕的情况下,要针对 每一套对象初始化的情况更新状态变量。例如,上述例子中,z1 应被销毁的异常环境当然与 z2 和 z1 都应该被销毁的异常 环境不同,因此,不管是在构造 z1 后,还是继而在构造 z2 后,VC++都要分别在状态变量中更新(存储)新的值。 所有这些表,函数调用的准备和善后工作,状态变量的更新,都会使异常处理功能造成可观的内存空间和运行速度开销。 正如我们所见,即使在没有使用异常处理的函数中,该开销也会发生。 幸运的是,一些编译器可以提供编译选项,关闭异常处理机制。那些不需要异常处理机制的代码,就可以避免这些额外 的开销了。 如何判断一个单链表是有环的?(注意不能用标志位,最多只能用两个额外指针) ?(注意不能用标志位 45. 如何判断一个单链表是有环的?(注意不能用标志位,最多只能用两个额外指针) struct node { node*} bool check(const node* head) {} //return false : 无环;true: 有环 一种 O(n)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然) : bool check(const node* head) { if(head==NULL) node *low=head, *fast=head-& while(fast!=NULL && fast-&next!=NULL) { low=low-& fast=fast-&next-& if(low==fast) } } 1.是不是一个父类写了一个 virtual 函数,如果子类覆盖它的函数不加 virtual ,也能实现多态? 答:是的。 2.输入一个字符串,将其逆序后输出。(使用 C++,不建议用伪码) int main() { int i_Count = 0; char * cin&& char *p_Char = while(*p_Char != '\0') { p_Char++; i_Count++; } while(i_Count--) { cout&&*p_C } return 0; } 3.请简单描述 Windows 内存管理的方法。 不会 4. #include &stdafx.h& #define SQR(X) X*X int main(int argc, char* argv[]) { int a = 10; int k = 2; int m = 1; a /= SQR(k+m)/SQR(k+m); printf(&%d\n&,a); return 0; } 不会 5.const 符号常量; (1)const char *p (2)char const *p (3)char * const p 说明上面三种描述的区别; 答:(1) 是所指的内容不能改变 (2) 是指针不能改变 (3) 是两者都不能改变 6.下面是 C 语言中两种 if 语句判断方式。请问哪种写法更好?为什么? if (n == 10) // 第一种判断方式 if (10 == n) // 第二种判断方式 答:第二种好.可以防止像 if(10 = n)这样的错误出现 7.下面的代码有什么问题? void DoSomeThing(...) { char* ... p = malloc(1024); if (NULL == p) ... p = realloc(p, 2048); // 空间不够,重新分配到 2K if (NULL == p) ... } p = malloc(1024)应该写成 p = (char *)malloc(1024) 没有释放动态申请的空间,会造成内存泄漏. 8.下面的代码有什么问题?并请给出正确的写法。 void DoSomeThing(char* p) { char str[16]; assert(NULL != p); sscanf(p, &%s%d&, str, n); if (0 == strcmp(str, &something&)) { ... } } 1. 下面这段代码的输出是多少(在 32 位机上). char *p; // 4 // 80 // 1600 // 4 char *q[20]; int (*n)[10]; struct MyStruct { // 分配 1K 的空间char *m[20][20]; double dda1; }; MyS 2. (1) char a[2][2][3]={{{1,6,3},{5,4,15}},{{3,5,33},{23,12,7}} }; for(int i=0;i&12;i++) printf(&%d &,___*(**a+i)____); 在空格处填上合适的语句,顺序打印出 a 中的数字 (2) char **p, a[16][8]; 问:p=a 是否会导致程序在以后出现问题?为什么? 编译就通不过,p 是一个指针的指针,而 a 是一个 2 维数组的首地址。 但是*p = a 也是错误的。 3.用递归方式,非递归方式写函数将一个字符串反转. 函数原型如下:char *reverse(char *str); #include &stdio.h& /*======================================================= 函 数 名: reverse() 参 数: str 功能描述: 将一个字符串翻转 返 回 值: const char* 抛出异常: 无 作 者: 刘基伟
=======================================================*/ const char *reverse(char *str); int main() { const char * // 用于取得函数的返回值,来输出翻转后的结果 // 存储一个将要翻转的字符串 // 将字符串 chArray 翻转 // 打印字符串 chArray char chArray[] = & Hello World ! &; pch = reverse(chArray); printf(&%s\n&,pch); return 0; } const char *reverse(char *str) { if(str == NULL) return NULL; int nCount = 0; int nCount_ const char *pRemark_ char chT char *pString_ char *pString_ pString_begin = pRemark_begin = while(*str != '\0') { // 寻找字符串的结尾 // 用来统计字符串的大小 // 将字符串的大小折半 // 标记字符串的首地址 // 用于交换字符串的临时变量 // 存储交换的头指针 // 存储交换的尾指针 // 24 printf(&%d %d %d %d}

我要回帖

更多关于 建筑高宽比 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信