汇总

目录

第一章 《C++中的C》

1.什么是强类型定义语言?什么是弱类型定义语言?二者有什么区别?

2.什么是动态类型语言?什么是静态类型语言?二者有什么区别?

3.什么是类型安全?

4.C++和C有什么不同?

5.总结const的应用和作用。

6.volatile关键字的作用是什么?

7.一个指针可以是volatile吗?

8.给出几个使用volatile关键字的示例。

9.说明C++中static_cast和dynamic_cast在使用上的不同

10.C++内存组织结构是什么样的?

11.在C++中支持malloc/free,为什么还需要new/delete?

12.说明new、delete、malloc、free之间的关系。

13.说明delete和delete[]的区别。

14.将引用作为函数参数有哪些特点?

15.C++的引用和C的指针有什么区别?

16.常引用有什么作用?

17.简要说明函数重载。

18.说明assert()的作用。

19.在C++程序中调用被C编译器编译后的函数为什么要加extern "C"?

第二章 《类和对象I》

1.谈谈你对面向对象的认识。

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

3.类定义?

4.类成员访问权限?

5.通过成员访问限定符将类的成员划分不同访问级别有什么好处?

6.什么是内联函数?使用内联函数有什么好处?

7.使用内联函数有哪些注意事项?

8.内联函数和宏定义有什么区别?

9.构造函数有什么性质?

10.什么是拷贝构造函数?

11.什么时候拷贝构造函数会自动被调用?

12.析构函数有什么性质?

13.类对象的存储空间分配方式?

14.什么是空类?系统会为空类添加哪些成员函数?

15.对于空类A,为什么sizeof(A)=1?

16.说明C和C++中的struct和class有什么区别?

17.静态成员包括什么?为什么提出静态成员?

18.静态数据成员?

19.类中静态数据成员有什么特点?

20.静态成员函数?

21.说明静态成员函数存在的意义?

22.静态对象?

23.对于用new动态创建的对象实例,如果不用delete销毁会怎么样?

24.成员函数通过什么来区分不同对象的数据成员?为什么它能够区分?

25.创建对象和销毁对象的顺序是什么样的?

26.什么是this指针?

27.说明赋值运算符=和拷贝构造函数的区别和联系?

28.什么是浅复制,什么是深复制?

29.什么时候必须重写拷贝构造函数?

30.简要说明类与对象有什么区别?

31.类的核心特性有哪些?

第三章 《类和对象II》

1.什么是常对象?使用常对象时要注意什么?

2.什么是常数据成员?常数据成员怎么赋初值?

3.带成员初始化列表的构造函数的执行顺序是怎样的?

4.在构造函数成员初始化列表中初始化数据成员和在构造函数体中初始化,结果是一样的?二者在性能方面有什么区别呢?

5.什么是常成员函数?使用常成员函数要注意什么?

6.mutable有什么作用?

7.C++中的explicit的作用是什么?

8.什么是子对象?

9.子对象的构造函数的执行次序是什么样的?

10.子对象的析构函数的执行次序是什么样的?

11.什么是静态子对象?

12.什么是前向引用声明?

13.说明C++中局部类和嵌套类的区别。

14.说明嵌套类的主要作用。

15.在软件开发中有一种单例模式,即在应用中仅允许创建类的一个实例。那么如何采用C++实现单例模式?

第四章 《友元和运算符重载》

1.什么是友元函数?友元函数有什么优缺点?

2.友元函数有什么特点?

3.定义友元函数时要注意什么?

4.什么是友元类?

5.友元关系有什么特点?

6.什么是运算符重载?运算符重载有哪些规定?

7.哪些运算符不能重载?为什么?

8.简述运算符重载的意义?

9.运算符重载有哪几种方式?

10.具体的运算符重载?

11.只能采用成员函数方式不能采用友元函数方式实现的运算符有哪些?为什么?

12.只能采用友元函数方式而不能采用成员函数方式实现的运算符有哪些?为什么?

13.说明类成员函数实现运算符重载与普通函数实现运算符重载的区别?

14.两个没有继承关系的类对象之间可以转换吗?有几种方式?

15.对于非C++内建类型A和B,有哪几种方法可以将B的对象转换成A的对象?

16.简要说明C++运算符重载的注意事项。

第五章 《模板和异常处理》

1.C++提供了几种模板机制?目的是什么?

2.什么是模板函数?

3.函数模板和模板函数有什么关系?

4.怎样定义函数模板?有什么注意事项?

5.怎样实例化函数模板?

6.实例化函数模板时要注意什么? 

7.函数模板可以重载吗?

8.函数模板和同名的非模板函数的重载方法的调用顺序是怎样的?

9.什么是类模板?怎样定义类模板?

10.怎样实例化类模板?有哪些注意事项?

11.类模板作为函数参数

12.类模板的友元函数

13.类模板与静态成员

14.类模板与非类型参数

15.有关异常处理的说明?

16.带有异常声明的函数原型

17.异常处理中对象的构造和析构顺序是怎样的?

18.可以在构造函数和析构函数中抛出异常吗?

19.简要说明使用catch(…)的优点和缺点。

第六章 《继承和派生》

1.继承方式有哪几种?

2.派生新类一般经历哪几个阶段?

3.派生类对象的内存结构是什么样的?

4.派生类的构造函数要完成哪些工作?

5.派生类构造函数的执行次序是怎样的?

6.在设计派生类构造函数(含拷贝构造函数)时要注意什么?

7.派生类的析构函数的执行次序?

8.类层次中的类模板

9.简述继承的优点和缺点。

10.哪几种情况需要采用成员初始化列表方式进行初始化?

11.请设计一个不能被继承的类。

12.基类对象和派生类对象的使用关系?

13.什么是虚继承?虚基类怎样声明?

14.虚继承中派生类对象的内存结构是什么样的?。。

15.虚继承中派生类构造函数的执行步骤是怎样的?

16.虚继承中派生类析构函数的执行步骤是怎样的?

17.一个空类的大小为1,从一个空类派生的空子类的大小也是1,那么从一个空类虚继承的空子类的大小是多少?

18.派生类构造函数和基类的构造函数有什么关系?派生类构造函数调用基类的构造函数有哪两种方式?

19.何谓虚基类?它有什么作用?如何使用虚基类?

第七章 《虚函数和多态性》

1.什么是函数的静态绑定和动态绑定?

2.怎样用虚函数实现动态多态性?

3.虚函数有什么特点?

4.怎样调用虚函数?

5.设计虚函数时要注意什么?

6.在含虚函数的多态继承环境中对象指针之间的转换有哪几种类型?

7.带虚函数的派生类对象的内存结构是怎样的?

8.静态成员函数能不能同时也是虚函数?

9.为什么构造函数不能设计成虚函数?

10.在C++中哪些函数不能声明为虚函数?

11.简述C++虚函数实现多态的原理。

12.如果基类的析构函数不是虚函数会带来什么问题?

13.在什么时候需要用虚析构函数?

14.简述类成员函数的重写(覆盖)、重载和隐藏的区别。

15.如果虚函数是非常有效的,是否可以把每个能够设置成虚函数的成员函数都声明为虚函数?

16.什么是纯虚函数?

17.抽象类有什么特点?

18.虚函数和纯虚函数的区别是什么?

19.简要说明你对C++中多态性、虚函数、纯虚函数和抽象类几个概念的理解。

20.什么是多态?如何理解编译时多态和运行时多态?


第一章 《C++中的C》


1.什么是强类型定义语言?什么是弱类型定义语言?二者有什么区别?

强类型语言

强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。

Java、.Net和C++等一些语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。

例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。

弱类型语言

弱类型语言也称为弱类型定义语言,与强类型定义相反。像VB,PHP等一些语言就属于弱类型语言。

简单理解就是一种变量类型可以被忽略的语言。比如VBScript是弱类型定义的,在VBScript中就可以将字符串'12'和整数3进行连接得到字符串'123',然后可以把它看成整数123,而不用显示转换。但其实他们的类型没有改变,VB只是在判断出一个表达式含有不同类型的变量之后,自动在这些变量前加了一个clong()或(int)()这样的转换函数而已。能做到这一点其实是归功于VB的编译器的智能化而已,这并非是VB语言本身的长处或短处。

强类型语言和弱类型语言比较

强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。

参考:强类型语言、弱类型语言


2.什么是动态类型语言?什么是静态类型语言?二者有什么区别?

动态类型语言

动态类型语言是指在运行期间才去做数据类型检查的语言。也就是说,在用动态类型的语言编程时,永远不用给任何变量指定数据类型,该语言会在你第一次赋值给变量的时候,在内部将数据类型记录下来。Python和Ruby就是一种典型的动态类型语言,其它的各种脚本语言如VBScript也多少属于动态类型语言。

静态类型语言

静态类型语言与动态类型语言刚好相反,它的数据类型是在编译期间检查的。也就是说,在编写程序的时候就要声明所有变量的数据类型。C/C++是静态类型语言的典型代表,其它的静态类型语言还有C#、Java等。

参考:动态类型语言、静态类型语言


3.什么是类型安全?

类型安全简单来说就是访问可以被授权访问的内存位置类型安全的代码不会试图访问自己未被授权的内存区域。一方面,类型安全被用来形容编程语言,主要根据这门编程语言是否提供类型安全的保障机制;另一方面,类型安全也可以用来形容程序,根据这个程序是否隐含类型错误。类型安全的语言和程序之前,其实没有必然的联系。类型安全的语言,使用不当,也可能写出来类型不安全的程序;类型不安全的语言,使用得当,也可以写出非常安全的程序。

C的类型安全

C语言不是类型安全的语言,原因如下:

1)很多情况下,会存在类型隐式转换,比如bool自动转成int类型;

2)malloc函数返回的是void *的空类型指针,通常需要这样的显示类型转换char* pStr=(char*)malloc(100*sizeof(char)),类型匹配没有问题。但如果出现int* pInt=(int*)malloc(100*sizeof(char))这样的转换,可能会带来一些问题,但C并不会提示。

当然,在有些情况下表现还是类型安全的,当从一个结构体指针转换成另一个结构体指针时,编译器会报错,除非显式转换。

C++的类型安全

C++也不是类型安全的语言,但远比C更具类型安全。相比于C,提供了一些安全保障机制

1)用操作符new来申请内存,严格与对象类型匹配,而malloc是void *;

2)函数参数为void *的可以改写成模板,模板支持运行时检查参数类型

3)使用const代替define来定义常量,具有类型、作用域,而不是简单的文本替换;

4)使用inline代替define来定义函数,结合函数的重载,在类型安全的前提下可以支持多种类型,如果改写成模板,会更安全;

5)提供dynamic_cast使得转换过程更安全。

尽管如此,但如果使用空类型指针或者在两个不同类型指针间做强制转换,很可能引发类型不安全的问题。

参考:类型安全


4.C++和C有什么不同?

就语言本身而言,C是C++的一个子集,C++在C的基础上增加了类和模板类型。一方面C++加强了C的过程化功能,引入了重载、异常处理等,另一方面更是扩展了面向对象设计的内容,如类、友元、继承、虚函数和模板等。

从编程角度,C是一种结构化编程语言,重点在于算法和数据结构,C程序设计首要考虑的是如何通过一个过程(包含函数和参数等)对输入进行运算处理得到输出;

而C++是面向对象的编程语言,C++程序设计首要考虑的是如何构造一个对象模型,包括数据封装、类、消息、对象接口和继承等,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程控制,所以两者的区别在于解决问题的思想方法不一样。之所以说C++比C更先进,是因为“设计”这个概念已经被融入C++之中。


5.总结const的应用和作用。

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


6.volatile关键字的作用是什么?

volatile关键字的含义是“易变的”,它告诉编译器volatile变量是随时可能发生变化的,与volatile变量有关的运算不要进行编译优化,以免出错,因为一般的编译器会进行编译优化。

用volatile关键字修饰的变量确保编译器不对其代码进行优化,且要求每次直接从内存读值。


7.一个指针可以是volatile吗?

可以,因为指针和普通变量一样,有时也可能会被意想不到地改变。例如,中断服务子程序修改一个指向buffer的指针时需要用volatile来修饰这个指针。


8.给出几个使用volatile关键字的示例。

一个定义为volatile的变量就是说它可能会意想不到的改变(改变它的情况有很多,例如操作系统,硬件,线程),在用到这个变量时必须每次都小心地从内存总读取这个变量的值,而不是使用保存在cache或者寄存器里的备份。

Volatile修饰符告诉编译程序不要对该变量所参与的操作进行优化。

例子:

1)  并行设备的硬件寄存器,如状态寄存器

2)  一个中断服务子程序中会访问到的非自动变量。

3)  多线程应用中被几个任务共享的变量。


9.说明C++中static_cast和dynamic_cast在使用上的不同

static_cast用于明确定义的转换,包括编译器允许不用强制转换的“安全”转换和不太安全但清楚定义的转换,如窄化转换(可能有信息丢失)、使用void *的强制转换和隐式类型转换等。dynamic_cast适用于类型安全的向下转换,常用在继承中的父类指针向子类指针的转换。若转换成功则返回该类型的指针,若失败则返回NULL。

参考:C++中static_cast和dynamic_cast


10.C++内存组织结构是什么样的?

代码段:用来存放程序的执行代码。通常代码段是可共享的和只读的,以防止程序被意外修改。

数据段:包含BSS段和静态数据区两个部分。BSS段用来存放程序中未初始化的外部变量和未初始化的静态局部变量(执行开始程序前BSS段会预先清空)。静态数据区用来存放程序中已初始化的外部变量、静态局部变量和常量。

堆空间:用于存放程序执行中动态分配的内存段,它的大小并不固定,可动态扩张或缩减。一般来讲在32位系统中堆内存可以达到4GB的空间。堆空间中存放的对象是全局的。

栈空间:存放程序中临时创建的局部变量(但不包括static定义的静态变量)、函数参数和函数返回地址等。一般情况下,栈空间有一定的大小,通常远小于堆空间。栈空间中存放的对象是局部的。


11.在C++中支持malloc/free,为什么还需要new/delete?

malloc/free是c/c++语言的标准库函数,new/delete是c++中的运算符。它们都用于申请动态内存和释放内存。

对于非内置数据类型的对象而言(eg:类对象),只用malloc/free无法满足动态对象分配空间的要求。这是因为对象在创建的同时需要自动执行构造函数,对象在消亡之前要自动执行析构函数,由于malloc/free是库函数而不是运算符,不在编译器的控制权限内,不能将执行构造函数和析构函数的任务强加给malloc/free。所以,在c++中需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理和释放内存工作的运算符delete。

对于内置数据类型而言,malloc/free和new/delete的作用是等价的。既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用纯C函数,而纯C程序只能用malloc/free管理动态内存。


12.说明new、delete、malloc、free之间的关系。

new/delete是运算符,malloc是库函数。

malloc和new都用于分配内存空间,但new会调用对象的构造函数,而malloc不会。free和delete都用于释放内存空间,但delete会调用对象的析构函数,而free不会。

如果用free释放new创建的动态对象,那么该对象因无法执行析构函数而可能导致程序出错;如果用delete释放malloc申请的动态内存,尽管不会导致程序出错,但是这样的程序可读性很差,所以,new/delete必须配对使用,malloc/free也一样。


13.说明delete和delete[]的区别。

delete运算符只会调用一次析构函数,而delete[]表达式会调用每一个成员的析构函数。例如,当delete[]运算符用于数组时,它为每个数据元素调用析构函数,然后调用delete释放内存。delete与new配套使用,delete[]与new[]配套使用。


14.将引用作为函数参数有哪些特点?

调用函数时实参和对应的引用形参共享相同的存储空间,所以在执行函数时引用型形参的改变和实参是同步的,也就是说对被调用函数中引用型形参的操作就是对实参的操作。尽管通过传递指针的方式可以达到改变实参的目的,但采用引用参数不仅简单,而且程序更清晰、可读性更好。

在调用函数时,在内存中并没有为实参对应的引用形参创建实参的副本,它是直接对实参操作,而使用一般变量传递函数的参数,当调用函数时需要为对应形参分配存储空间,形参变量是实参变量的副本,如果传递的是对象,还将调用拷贝构造函数,因此当参数传递的数据较大时用引用比用一般变量传递参数的效率要好,所占的空间要少。


15.C++的引用和C的指针有什么区别?

引用必须被初始化,但是不分配存储空间。指针在定义时不一定初始化,定义指针需要分配相应的存储空间。

引用初始化以后不能被改变,指针可以改变所指的对象。

不存在引用空值的引用,但存在指向空值的指针。


16.常引用有什么作用?

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


17.简要说明函数重载。

在同一个作用域内(如在同一个类中),可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。


18.说明assert()的作用。

assert()是一个调试程序时经常使用的宏,在程序执行时它计算括号内的表达式,如果表达式为假,程序将报告错误,并终止执行;如果表达式为真,则继续执行后面的语句。这个宏通常用来判断程序中是否出现了明显非法的数据,如果出现了则终止程序以免导致严重后果,同时也便于查找错误。


19.在C++程序中调用被C编译器编译后的函数为什么要加extern "C"?

C++支持函数重载,C不支持函数重载。函数被C++编译后在库中的名字与C语言不同。假设某个函数的原型为void foo(int x,int y),则该函数被C编译器编译后在库中的名称为_foo,而C++编译器会产生像_foo_int_int之类的名称。C++提供了C链接指示符extern "C"来解决名称匹配问题。


第二章 《类和对象I》


1.谈谈你对面向对象的认识。

面向对象是一种自下而上的程序设计方法,不像过程式设计那样一开始就要用main概括出整个程序(过程式设计的思路是功能分解、逐步求精),面向对象设计往往从问题的一部分着手,一点一点地构建出整个程序。面向对象设计以数据为中心,类作为表现数据的工具是划分程序的基本单位,类封装了数据,类的成员函数作为其对外的接口抽象地描述了类。用类将数据和操作这些数据的函数放在一起,可以说这就是面向对象设计方法的本质。面向对象的主要概念包括类与对象、继承和多态等。

传统的结构化程序设计多是基于功能的思想进行考虑和设计的,而面向对象的程序设计则是基于对象的角度考虑问题的,这样做能够使得程序更加简洁和清晰。


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

这样可以提高编译效率,因为如果分开只需要编译一次生成对应的.obj文件,然后在应用该类的地方这个类就不会被再次编译,从而大大提高了编译效率,另外使得程序结构更清晰。


3.类定义?

通常将类声明和类实现合起来称类定义。

类声明仅告诉编译器有一个指定名称的类,它具有哪些数据成员、哪些成员函数,并没有为数据成员和成员函数分配内存空间。

当遇到类定义时,C++编译器会为静态数据成员分配内存空间并初始化(如果有),但不会为其他数据成员分配内存空间,同时会为成员函数分配代码空间,这样每个成员函数都有一个存储其代码的地址。在进入链接阶段时将通过对象调用成员函数的地方用对应函数成员的地址替代,并传递当前对象的指针和相关参数。


4.类成员访问权限?

private:声明私有成员。私有数据成员只允许在类中访问,私有成员函数只允许被类中的其他成员函数调用。在类外不允许访问私有数据成员,也不允许调用私有成员函数;

public:声明共有成员。公有数据成员允许在类中或类外(通过类对象)访问,公有成员函数允许在类中或类外(通过类对象)调用;

protected:声明保护成员。保护数据成员只允许在类中或者子类中访问,保护成员函数允许在类中或者子类中调用。在类外不能访问该类的保护数据成员,也不能调用该类的保护成员函数。

默认情况下,一个类中的成员是私有的。


5.通过成员访问限定符将类的成员划分不同访问级别有什么好处?

首先是信息隐蔽,即实现的封装,将类的内部实现和外部接口分开,这样使用该类的程序不需要了解类的详细实现;

其次是数据保护,即将类的重要信息保护起来,以免其他程序不恰当地修改。


6.什么是内联函数?使用内联函数有什么好处?

类的成员函数用于实现某种操作,成员函数的定义体可以放在类声明体中,也可以放在类声明体外。在类声明体中实现的函数称为内联函数,在类声明体外实现的函数可以通过在函数声明和定义时加上inline来表示该函数是内联函数,否则不是内联函数。实际上,普通函数(非类的成员函数)也可以加上inline变成内联函数。

C++编译器在遇到调用内联函数的地方会用函数体中的代码来替换函数调用,好处是节省函数调用带来的参数传递、栈空间的进栈与出栈等开销,从而提高执行速度,但付出的代价是增加了代码长度。


7.使用内联函数有哪些注意事项?

使用inline关键字的函数不一定都会被编译器在调用处展开,inline对于编译器只是一个建议而已,因为把一个函数声明为inline函数并不一定真正适合在调用点上展开。

通常只将较短的函数(如1-5行的小函数)设计成内联函数,如果函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。

在内联函数内不允许有循环语句和开关语句。如果内联函数有这些语句,则编译器将该函数视为普通函数。

递归函数不能被用来做内联函数,因为无法在调用点上完全展开。

内联函数的定义必须出现在内联函数第一次被调用之前。

对于同一个程序的不同源程序文件,如果inline函数出现,其定义必须相同。如果两个定义不同,程序会产生不确定的行为。建议把inline函数的定义放在头文件中,在每个调用该inline函数的源程序文件中包含该头文件。

在执行程序时内联函数已经不存在了,那么在程序中能不能取一个内联函数的地址呢?是可以的,在这种情况下编译器为此生成一个函数体。


8.内联函数和宏定义有什么区别?

内联函数一般实现较小的功能,可以采用宏定义达到相同的目的,但二者是有区别的:

内联函数在执行时可调试,而宏定义不可以。

编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义不会。

内联函数可以访问类的成员变量,而宏定义不能。

在类中声明同时定义的成员函数自动转化为内联函数。


9.构造函数有什么性质?

构造函数的名称和类名相同;

构造函数尽管是一个成员函数,但没有任何函数类型,也不属于void函数;

一个类除了不带参数的默认构造函数外,还可以设计一个或多个带参数的重载构造函数;

如果一个类中没有定义任何构造函数,编译器会自动生成一个不带参数的默认构造函数(函数体为空)。

如果一个类中声明有任何带参数的构造函数,编译器不会自动生成不带参数的默认构造函数。


10.什么是拷贝构造函数?

拷贝构造函数(复制构造函数)是另外一种特殊的构造函数,具有一般构造函数的所有特性,主要用于通过一个已经存在的对象创建一个新的对象时实现对象之间数据成员的复制操作。

每个类都必须有一个拷贝构造函数,如果没有生命,编译器自动生成一个默认的拷贝构造函数,以实现数据成员的简单复制(浅复制)。

拷贝构造函数必须采用引用型参数,而不是传值参数,否则拷贝构造函数将产生无限递归,因为传值调用时必须建立传递给拷贝构造函数的对象副本,这样又要调用拷贝构造函数。而引用型参数不需要调用拷贝构造函数。


11.什么时候拷贝构造函数会自动被调用?

当用类的一个对象去初始化该类的另一个对象时;

如果函数的形参是非引用类对象,调用函数进行形参和实参结合时;

如果函数的返回值是非引用类对象,函数调用完成返回时。


12.析构函数有什么性质?

析构函数在类对象销毁时自动执行,用于对象内存的清理工作等。

一个类只能有一个析构函数,而且析构函数没有参数。

析构函数的名称是“~”加上类的名称。

析构函数也没有任何函数类型,不能像普通成员函数那样显式调用。

如果一个类没有声明析构函数,编译器会自动生成一个函数体为空的默认析构函数。


13.类对象的存储空间分配方式?

一个类对象的分配空间中仅包含所有非静态数据成员,并按这些数据成员定义的顺序存放。

一个类对象的大小为其所有非静态数据成员的类型大小之和。普通成员函数与sizeof无关。(当类中有一个或多个虚函数时,由于要维护虚函数表,所以有一个虚函数表指针。另外当存在虚继承时还需要一个指向虚基类的指针,每个指针占用一个地址大小的内存空间,即4个字节)。

类对象中的各个数据成员分配内存空间时遵守内存对其规则。


14.什么是空类?系统会为空类添加哪些成员函数?

空类指不包含任何数据成员和函数成员的类。

空类Empty默认产生的类成员函数如下:

 
  1. class Empty

  2. {

  3. public:

  4. Empty(); //默认的构造函数

  5. Empty(const Empty& ); //默认的拷贝构造函数

  6. ~Empty(); //默认的析构函数

  7. Empty& operator=(const Empty &); //默认的赋值运算符

  8. Empty* opetator&(); //默认的取址运算符

  9. const Empty* operator&() const; //默认的const取址运算符

  10. }


15.对于空类A,为什么sizeof(A)=1?

因为类是用来定义对象的,每个对象的存储空间中存储相应的数据成员值,通常第一个数据成员的地址为对象地址。如果长度为0,就无法实例化空类的对象。

为此编译器添加一个字节的数据成员,实际上对于不含任何数据成员的类,编译器都是如此。


16.说明C和C++中的struct和class有什么区别?

C和C++中的struct有区别,C中的struct不能有成员函数,而C++中的struct可以。

C++中的struct和class的主要区别为成员的默认访问权限不同,struct成员的默认权限为public,而class成员的默认权限为private。


17.静态成员包括什么?为什么提出静态成员?

静态成员包括静态数据成员和静态成员函数。

提出静态成员的目的是为了解决数据共享的问题。


18.静态数据成员?

静态数据成员是类中所有对象共享的成员,而不是某个对象的成员,也就是说,静态数据成员的存储空间不是放在每个对象中,而是和成员函数一样放在类的公共区中,所以有时将静态数据成员称为类变量。


19.类中静态数据成员有什么特点?

静态数据成员是类成员,无论定义多少个类对象,静态数据成员在程序中只保留一份。

静态数据成员存储在全局数据区,为本类的所有对象共享,不属于特定的类对象。

静态数据成员必须在类外进行初始化。


20.静态成员函数?

静态成员函数与静态数据成员类似,也是从属于类的。只要类存在,静态成员函数就可以使用,静态成员函数的声明需要使用static关键字。

静态成员函数只能访问静态数据成员,不能直接访问类中的非静态数据成员,因为非静态数据成员只有对象存在时才有意义,可以通过类对象来访问类的成员。


21.说明静态成员函数存在的意义?

静态成员函数只能操作静态数据成员,所以使用静态成员函数仅仅可以实现操作静态数据的功能,如静态私有成员在类外不能被访问,可通过类的静态成员函数来访问。


22.静态对象?

静态对象和普通静态变量一样,其生存期是整个程序,其作用域是所在的函数。


23.对于用new动态创建的对象实例,如果不用delete销毁会怎么样?

对于用new运算符动态创建的对象实例,可以使用delete运算符销毁,在销毁时会自动调用析构函数。

若不使用delete销毁,在程序结束时该对象实例仍然存在,并占用相应的存储空间,即系统不能自动销毁动态创建的对象实例。


24.成员函数通过什么来区分不同对象的数据成员?为什么它能够区分?

类的成员函数只存放一份,其入口参数有一个当前对象的地址,即this指针。

通过this指针指向当前对象,所以成员函数通过this指针来区分不同对象的数据成员,因为不同的类对象是单独存放的,其中包含数据成员值。


25.创建对象和销毁对象的顺序是什么样的?

当程序中创建各类对象时按对象的顺序调用构造函数。

程序中对象的销毁顺序如下:

用new创建的多个实例按delete的顺序依次销毁;

定义的多个局部子对象按创建子对象相反的顺序销毁;

定义的多个静态局部子对象按创建子对象的相反顺序销毁;

定义的多个全局对象(含静态全局对象)按创建对象的相反顺序销毁。

所以,当程序创建有多种类别的对象时析构函数的调用顺序并不一定正好和构造函数的调用顺序相反。


26.什么是this指针?

this是一个隐含于每一个类对象的特殊指针,该指针值是一个正在被某个成员函数操作的对象的地址。


27.说明赋值运算符=和拷贝构造函数的区别和联系?

赋值运算符和拷贝构造函数的相同点是两者都将一个对象的数据成员复制到另一个中。

两者的不同点是拷贝构造函数涉及新建一个对象,而赋值运算符被复制的对象已创建。


28.什么是浅复制,什么是深复制?

当两个对象之间进行复制时,若复制完成后它们还共享某些资源(内存空间),其中一个对象的销毁会影响另一个对象,这种对象之间的复制称为浅复制

当两个对象之间进行复制时,若复制完成后它们不会共享任何资源(内存空间),其中一个对象的销毁不会影响另一个对象,这种对象之间的复制称为深复制


29.什么时候必须重写拷贝构造函数?

当构造函数涉及动态存储分配空间时要自己写拷贝构造函数,并且要深复制。


30.简要说明类与对象有什么区别?

类是某一类事物的一般性的集合体,是相同或相似的各个事物共同特征的一种抽象。对象是类的实例(instance)。

对象与类的关系就像变量与数据类型的关系一样。


31.类的核心特性有哪些?

类具有封装性、继承性、多态性。

类的封装性为类的成员提供公有、保护和私有等多级访问权限,目的是隐藏类中私有成员的实现细节。

类的继承性提供从已存在的类创建新类的机制,继承使一个新类自动拥有父类的全部成员。

类的多态性提供类中方法执行的多样性,多态性有重载和重写两种表现形式。


第三章 《类和对象II》


1.什么是常对象?使用常对象时要注意什么?

常对象是指对象常量。定义格式:“类名 const 对象名;”或者“const 类名 对象名;”

使用常对象时要注意:

在定义常对象时必须进行初始化;

常对象的数据成员不能被更新;

如果一个对象被定义为常对象,则不能调用该对象的非const成员函数,否则会报错。这样做是为了防止非const成员函数修改常对象中的数据元素值,因为const成员函数是不可以修改对象中的数据成员值的。


2.什么是常数据成员?常数据成员怎么赋初值?

在类中使用const定义的数据成员是常数据成员。

常数据成员的赋初值只能通过构造函数,并且构造函数只能通过成员初始化列表来实现。


3.带成员初始化列表的构造函数的执行顺序是怎样的?

先执行初始化列表,再执行函数体,对于含有多个初始化项的列表不是按照从左到右或者右到左的顺序执行,而是按照数据成员在类中定义的顺序执行的。


4.在构造函数成员初始化列表中初始化数据成员和在构造函数体中初始化,结果是一样的?二者在性能方面有什么区别呢?

参考:构造函数初始化的方法及区别

对于类的非const数据成员,可以在成员初始化列表中初始化,也可以在构造函数体中初始化,二者的结果是一样的。

一般情况下,对于内置的数据类型(含指针和引用),在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的;

对于用户自定义的类类型(即子对象的情况),在性能上存在很大差别,因为前者只用调用一次构造函数,后者除了调用一次默认的构造函数,还要调用赋值运算符才能完成。而函数调用时浪费资源的,所以在这种情况下采用成员初始化列表来初始化数据成员性能更优。


5.什么是常成员函数?使用常成员函数要注意什么?

使用const关键字声明的函数为常成员函数,声明格式为“函数类型 函数名(参数表) const;

在使用常成员函数是要注意:

const是函数类型的一个组成部分,因此在实现部分也要带const关键字;

常成员函数不能更新对象的数据成员,也不能调用该类中的非常成员函数;

如果将一个对象定义为常对象,则通过该常对象只能调用它的常成员函数,不能调用非const成员函数;

const关键字可以参与区分重载函数。如“void display();”和“void display() const;”是对display的有效重载。


6.mutable有什么作用?

在定义一个常对象后只能调用const成员函数,而且const成员函数不能修改数据成员值。

如果希望通过常对象修改某些数据成员值,只需将其定义为mutable(易变的),mutable数据成员永远不会是const成员,即使它是一个const对象的数据成员。

const成员函数可以修改mutable数据成员的值,常对象通过调用该const成员函数即可修改该mutable数据成员的值。


7.C++中的explicit的作用是什么?

C++中的explicit关键字(含义是明确的,不隐瞒的)只能用于修饰只有一个参数的类构造函数,它的作用是表示该构造函数时显式的,而非隐式的,从而阻止不允许的经过构造函数进行的隐式转换。类构造函数在默认情况下都是隐式的。

在C++中一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数)承担了两个角色:一是构造器,二是默认且隐含的类型转换操作符。所以在写形如A a=n;这样的代码且恰好类A有单个参数的构造函数时编译器自动调用这个构造函数创建一个类A的对象a。

explicit不是重载区分符。一个函数加和不加explicit修饰,不构成重载函数。


8.什么是子对象?

当一个类的数据成员是另一个类的对象时,这个对象称为子对象。


9.子对象的构造函数的执行次序是什么样的?

当子对象B含有构造函数,则只能在含有对象B的类A的构造函数的成员初始化列表中进行初始化;如果子对象B不含构造函数,则对象B还可以在类A的构造函数体中初始化。

对象成员构造函数的调用顺序取决于这些子对象成员在类中的定义顺序,而与它们在成员初始化表中的位置无关。

在建立类对象时先调用各个对象成员的构造函数,初始化相应的子对象成员,然后才执行类的构造函数体,初始化本类中的其他数据成员。


10.子对象的析构函数的执行次序是什么样的?

析构函数的执行次序是先执行函数体,再以子对象在类A中定义的相反次序调用各类的析构函数。


11.什么是静态子对象?

在一个类中可以定义另外一个类的静态子对象,静态子对象需要在类外初始化。初始化格式为“子对象类名 所属类::静态子对象名[(参数表)]”

静态子对象属于类对象,和int等类型的静态数据成员类似,只是在销毁时要调用析构函数。


12.什么是前向引用声明?

C++中的类通常是先声明再使用。有时会遇到两个类相互引用的情况,这时必然有一个类在声明之前就被使用。解决这个问题的方法是使用前向引用声明。

前向引用声明是在引用未声明的类之前对该类进行预声明,它只是为程序引入一个代表该类的标识符,类的具体声明可以在程序的其他地方。

前向引用声明仅引入一个代表类的标识符,在没有遇到该类标识符的实际声明之前不能使用该类的任何成员。

例如:类A声明在前,类B声明在后。但类A的成员函数引用了B的对象和成员函数。这时,要对类B进行前向引用声明。并且类A中引用类B对象的成员函数的实现必须放在类B的声明之后,否则会出现不能识别类B的错误。


13.说明C++中局部类和嵌套类的区别。

在一个函数体内定义的类称为局部类。在局部类中只能使用它的外围作用域中的对象和函数进行联系,因为外围作用域中的变量与该局部类的对象无关。局部类的所有成员都必须定义在类体内,并且局部类不能包含静态成员。

在一个类中声明的类称为嵌套类,从作用域的角度看,嵌套类被隐藏在外围类之中,该类名只能在外围类中使用,如果在外围类的作用域外使用该类名,需要加名字限定。从访问权限的角度来看,嵌套类名与他的外围类的对象成员名具有相同的访问权限规则。不能访问嵌套类的对象中的私有成员函数,也不能对外围类的私有嵌套类建立对象。嵌套类的成员函数可以在它的类体外定义。


14.说明嵌套类的主要作用。

定义嵌套类的作用在于隐藏类名,减少全局的标识符,从而限制用户能否使用该类建立对象,这样可以提高类的抽象能力,并且强调了两个类(外围类和嵌套类)之间的主从关系。


15.在软件开发中有一种单例模式,即在应用中仅允许创建类的一个实例。那么如何采用C++实现单例模式?

略,以后补充,见P112。


第四章 《友元和运算符重载》


1.什么是友元函数?友元函数有什么优缺点?

友元函数指某些虽然不是类成员函数却能够通过类对象访问其所有成员的函数。类授予它的友元特别的访问权。

使用友元函数的优点是提高编程的灵活性和程序执行效率,缺点是破坏了类的封装机制。


2.友元函数有什么特点?

友元函数不是类的成员函数,但需要在类中声明。

若在类A中声明了友元函数fun(),fun()不能直接访问类A的成员,只能通过类A的对象a访问其所有的成员,包括私有和保护的成员。

由于友元函数不是类的成员函数,它没有隐含的this指针。

友元函数的调用方式和普通函数一样,不需要通过对象来调用。

实际上一个友元函数还可以作为另外类的成员函数,在这种情况下,它既有友元函数的特点,又有成员函数的特点。

由于友元函数可以通过类对象访问其所有成员,破坏了数据的安全性,所以使用友元函数必须谨慎,不要通过友元函数对数据成员进行危险的操作。


3.定义友元函数时要注意什么?

友元函数本质上是全局的,其声明位置可以放在类的任何地方,既可以在public区,也可以在protected或private区,意义完全一样。友元函数不是类的成员,所以友元函数是不能继承的,这也是友元函数不需要考虑访问权限的原因。

从原则上讲,友元函数在类里面声明,在类外实现。但是如果友元函数有该类类型的对象参数,可以在类里面实现。

当一个类的成员函数作为另一个类的友元函数时必须先定义成员函数所在的类,在声明友元函数时要加上成员函数所在类的类名和作用域运算符::。


4.什么是友元类?

和将一个函数设计为一个类的友元函数一样,可以将一个类声明为另一个类的友元类。若B类是A类的友元类,则B类的所有成员函数都是A类的友元函数。


5.友元关系有什么特点?

友元关系是非传递的,例如类B是类A的友元类,类C是类B的友元类,在类C和类A之间如果没有声明就没有任何友元关系。

友元关系是单向的,如果声明类B是类A的友元类,类B的成员函数就可以通过对象访问类A的私有和保护数据成员,但类A的成员函数不能访问类B的私有和保护数据成员。

友元关系是不能继承的,如函数f()是类A的友元函数,类A派生出类B,函数f()并不是类B的友元函数,除非在类B中作了特殊声明。


6.什么是运算符重载?运算符重载有哪些规定?

运算符重载就是用同一个运算符完成不同的运算功能。

和函数重载一样,运算符重载也是在编译阶段完成的,体现出静态的多态性。

C++运算符重载的相关规定如下:

不能改变原运算符的优先级和结合性。

默认参数不能和运算符重载一起使用,也就是说在设计运算符重载时不能使用默认参数。

不能改变原运算符的操作数个数。

不能创建新的运算符,只有已有运算符可以被重载。

当运算符作用于C++内部提供的数据类型时,原来的含义保持不变。

运算符可以被重载用于用户定义的类对象或者用户定义的类对象与内置数据类型变量的组合。

7.哪些运算符不能重载?为什么?

运算符说明不能重载的原因
.成员运算符为保证成员运算符对成员访问的安全性,故不允许重载
.*成员指针运算符同上
::作用域运算符左边的运算数是类型名,而不是表达式
?:三目运算符在C++中没有定义三目运算符重载的语法
sizeof求大小运算符其运算数是一个类型名,而不是一个表达式

8.简述运算符重载的意义?

为了使用户自定义数据类型的数据的操作与内置数据类型的数据的操作形式一致。


9.运算符重载有哪几种方式?

可以采用普通函数、友元函数或者类成员函数来实现。

一般来讲,单目运算符最好重载为成员函数,双目运算符最好重载为友元函数。


10.具体的运算符重载?

重载++、--单目运算符

 
  1. //采用成员函数重载格式:

  2. 函数类型 operator ++(); //重载前缀运算符

  3. 函数类型 operator ++(int); //重载后缀运算符

  4.  
  5. //采用友元函数重载格式:

  6. friend 函数类型 operator ++(A &); //重载前缀运算符

  7. friend 函数类型 operator ++(A &,int); //重载后缀运算符

重载比较运算符==、>、<等

 
  1. //采用成员函数重载格式:

  2. bool operator ==(const A &);

  3. //采用友元函数重载格式:

  4. friend bool operator==(const A &,const A &);

重载赋值运算符+=、-=、=

 
  1. //+=、-=这些可以采用两种方式,=只能重载为成员函数

  2. //成员函数方式:

  3. A &operator=(A &);

  4. //友元函数方式:

  5. friend A & operator+=(A &,const A &);

  6.  

重载下标运算符[]

 
  1. //成员函数方式:

  2. 函数类型 & operator[](int n);

重载new、delete、new[]、delete[]

new和delete只能被重载为类的成员函数或者普通函数,不能被重载为友元函数,而且不论是否使用关键字static进行修饰,重载的new、delete均为类的静态成员函数。

 
  1. //重载为成员函数:

  2. void *operator new(size_t size,其它形参);

  3. void operator delete(void *p);

重载类型转换运算符

C++中提供了标准类型的相互转换,如“int n=(int)1.87;”

同样可以进行这种类型转换运算符重载:

 
  1. operator 类型名()

  2. {

  3. 函数体;

  4. }

与前面的重载运算符函数不同的是,类型转换运算符重载函数没有返回类型,因为“类型名”就代表了它的返回类型,而且没有任何参数。在调用过程中要带一个对象实参。 

重载函数调用运算符()

重载函数调用运算符()之后,允许将类对象像函数一样使用。与其他重载运算符不同的是,函数调用运算符可以有任意个参数,因此可以定义多个不同的函数版本的函数调用运算符重载。

 
  1. //成员函数方式:

  2. 函数类型 operator()(形参);

重载输入输出运算符<<、>>

 
  1. //友元函数方式:

  2. friend ostream & operator <<(ostream & stream,类名 & 类引用名) //输出

  3. {

  4. 函数体;

  5. return stream;

  6. }

  7. //显式调用:cout<<对象

  8. //隐式调用:operator<<(cout,对象)

  9.  
  10. friend istream & operator >>(istream & stream,类名 & 类引用名) //输入

  11. {

  12. 函数体;

  13. return stream;

  14. }

 


11.只能采用成员函数方式不能采用友元函数方式实现的运算符有哪些?为什么?

有=、[]、()、->和new/delete(new[]/delete[])

=:在任何类中,如果没有显式定义重载=运算符,编译器会默认提供一个,如果采用友元方式重载=,编译器又定义一个默认的=运算符函数(在类里面),那么在对象赋值时使用哪一个呢?这样会造成调用的二义性。

[]:假设已定义了类A,若有“A a[10];”,编译器会提供默认的[]运算符。如果采用友元方式重载[],当遇到形如a[1]的表达式时编译器也会产生调用的二义性。

():如果采用友元方式重载(),当遇到形如a(1,2)的表达式时是调用构造函数还是重载()运算符呢?编译器会产生调用的二义性。

->:假设已定义了类A,若有A *pa,编译器会提供默认的->运算符。如果采用友元方式重载->运算符,当遇到形如pa->f()的表达式时编译器会产生调用的二义性。

new/delete:这对运算符比较特殊,有固定的使用格式,如new运算符的返回类型必须是void *,且第一个参数必须是unsigned int类型,而使用友元方式实现重载时第一个参数应该是类对象,所以无法采用友元方式重载。

实际上,运算符=、()、[]、->是程序中最常用的符号,尽管可以通过上下文的判断消除二义性,但增加了编译器的复杂性,所以C++规定不允许这些运算符采用友元函数方式重载,简化编译器设计。


12.只能采用友元函数方式而不能采用成员函数方式实现的运算符有哪些?为什么?

只有<</>>。由于<<和>>运算符都不是通过对象调用的,所以它们不能采用类成员函数实现重载,因为调用类成员函数时第一个参数必须是类的对象,而<<和>>的第一个参数是流对象引用,所以只能采用友元方式重载。


13.说明类成员函数实现运算符重载与普通函数实现运算符重载的区别?

两者的区别主要是函数参数的个数不同,采用类成员函数实现运算符重载时隐含当前对象,所以比普通函数实现运算符重载的参数个数少一个。


14.两个没有继承关系的类对象之间可以转换吗?有几种方式?

可以转换,有三种方式。以将A类对象a转换成B类对象b为例:

  1. 通过在类B中设计相应的转换构造函数进行转换:B(const A & ){//进行转换}
  2. 通过在类A中设计重载类型转换运算符()成员函数进行转换:operator B(){//进行转换}
  3. 通过类指针实现强制转换:A a;  B* pb=(B*)(&a);                 //这种转换方式实际上是两个不同类对象的各数据成员的按字节赋值转换。

15.对于非C++内建类型A和B,有哪几种方法可以将B的对象转换成A的对象?

  1. class B:public A{…};    //B公有继承自A,可以实现间接转换;
  2. class B{ operator A(){…};}; //在类B中设计类型转换重载成员函数,实现隐式转换;
  3. class A{A(const B&){…};};    //在A中设计转换构造函数,可以实现非隐式转换;
  4. A & operator=(const B &){…};  //在A中设计重载赋值运算符函数,通过赋值语句实现转换。

16.简要说明C++运算符重载的注意事项。

  1. 一般情况下,单目运算符最好重载为类的成员函数,双目运算符最好重载为类的友元函数。
  2. 有些双目运算符不能重载为类的友元函数,例如=、()、[]、->
  3. 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
  4. 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选友元函数。
  5. 当重载运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一个类对象(或者是该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部类型的对象,该运算符函数必须作为一个友元函数来实现。

第五章 《模板和异常处理》


1.C++提供了几种模板机制?目的是什么?

C++提供了两种模板机制,即函数模板和类模板。模板中的类型参数也称为类属参数。模板的声明或定义只能在全局或类范围内进行,不能在局部范围(如函数)内进行。

使用模板的目的就是为了能够让程序员编写与类型无关的代码。


2.什么是模板函数?

在定义了一个函数模板之后,当编译器发现有一个对应的函数调用时将根据实参中的类型来确认是否匹配函数模板中对应的类型形参,然后生成一个重载函数。

该重载函数的定义体与函数模板的函数定义体相同,它称为模板函数。


3.函数模板和模板函数有什么关系?

函数模板和模板函数的区别是函数模板是一个模板,其中用到通用类型参数,不能直接执行;模板函数是一个具体的函数,它由编译器在遇到具体函数调用时生成,具有程序代码,可以执行。


4.怎样定义函数模板?有什么注意事项?

定义函数模板的一般格式为:

 
  1. template <类型形参表> //类型参数声明

  2. 返回类型 函数名(形参表) //函数模板

  3. {

  4. 函数体;

  5. }

  6.  
  7. //举个例子:

  8. template <class T> //注意末尾不要加分号

  9. T abs(T x)

  10. {

  11. if(x<0)

  12. return -x;

  13. return x;

  14. }

“类型形参表”可以包含基本数据类型,也可以包含类类型,但并非可以是任意的数据类型,由于抽象类不能定义对象,所以抽象类不行。类型形参需要加前缀class或typename表示“跟随类型形参”。如果类型形参多于一个,则每个类型形参都要使用class或typename。这里class和typename没有任何区别,C++早期对模板的支持并不严谨,没有引入新的关键字,后来又引入了新关键字typename,专门用来定义类型参数。

“类型形参表”中的参数必须是唯一的,而且应该在函数体中至少出现一次。例如template<class T,class T>是错误的,其中类型参数名称重复。

在template语句和函数模板声明之间不允许有其他语句。


5.怎样实例化函数模板?

函数模板是不能直接执行的,需要在实例化为模板函数后才能执行。

当编译系统发现有一个函数调用“函数名(实参表)”或者“函数名<类型实参表>(实参表)”时,C++将根据实参表中的类型生成一个重载函数,即模板函数。

以4中的函数模板为例:

可以自动类型推导调用,如:abs(-10);

可以具体类型显式调用,如:abs<int>(-10);


6.实例化函数模板时要注意什么? 

  1. 和普通函数一样,如果函数模板定义放在实例化之后,需要对其声明。例如4中abs函数模板声明方式为:
     
    1. template <class T>

    2. T abs(T x);

     

  2. 虽然模板参数T可以实例化成各种类型,但是采用模板参数T的个参数之间必须保持完全一致的类型。模板类型并不具备隐式类型转换的识别能力。

  3. 在函数模板实例化时,template中定义的每个类型参数都应该得到明确的类型值。

  4. 在函数模板实例化中显式给出类型参数的数据类型时,可以部分给出类型参数的数据类型,在实例化过程中按template中指定的顺序进行类型参数的匹配。

  5. 当类型形参表中包含用户定义的类类型时,有时需要在类中设计相关的重载运算符以便模板函数能对类对象进行正确的运算,否则会出现错误。


7.函数模板可以重载吗?

函数模板可以重载,这需要提供其他函数模板,指定不同参数的相同函数名。

 
  1. //函数模板的重载

  2. template <class T>

  3. void dispArr(T *arr,int n);

  4.  
  5. template <class T>

  6. void dispArr(T *arr,int i,int j);

另外,函数模板还可以重载其他非模板函数(同名但参数不同)。


8.函数模板和同名的非模板函数的重载方法的调用顺序是怎样的?

一个实际的函数调用,它既可以和一个重载函数相匹配,或是参数转换后与某一重载函数相匹配,又可以与某一函数模板相匹配,这并不会产生二义性,只是调用哪一个需按照一定的规则安排先后顺序:

  1. 寻找一个参数完全匹配的非模板函数,如果找到了就调用它。
  2. 在1失败后寻找一个函数模板,使其实例化,产生一个匹配的模板函数,若找到了就调用它。
  3. 在1和2均失败后尝试低一级的非模板重载函数,例如通过类型转换可产生参数匹配等,若找到了就调用它。
  4. 若以上均失败,则表示是一个错误的调用。

9.什么是类模板?怎样定义类模板?

类模板允许用户为类定义一种模式,使得类中的某些数据成员、成员函数的参数和成员函数的返回值能取任意类型。

类模板的成员函数被认为是函数模板,也称为类属函数,因此当给出一个类模板的成员函数的定义时必须遵循函数模板的定义。

定义类模板的一般格式为:

 
  1. template <类型形参表>

  2. class 类名

  3. {

  4. 类模板的代码;

  5. };

  6.  
  7. template <类型形参表>

  8. 返回类型 类名<类型名表>::成员函数1(形参表)

  9. {

  10. 成员函数定义体;

  11. }

类模板的成员函数可以在类模板的定义中定义(默认为inline函数),也可以在类模板定义之外定义(此时在成员函数定义的前面必须加上template及模板参数)。


10.怎样实例化类模板?有哪些注意事项?

类模板不能直接使用,必须先实例化为相应的模板类,在定义该模板类的对象后才能使用。在定义类模板之后,创建模板类的一般格式如下:

类模板名 <类型实参表> 对象表;

注意:

  1. 在实例化类模板时不同于实例化函数模板,这里必须指定完整的类型实参表,其过程是类模板-》模板类-》对象。
  2. 类模板在什么时候会被实例化呢?一是当使用了类模板实例的名字,并且上下文环境要求存在类的定义时;二是对象类型是一个类模板实例,当对象被定义时,此点被称为类的实例化点;三是一个指针或引用指向一个类模板实例,当检查这个指针或引用所指的对象时。
  3. 类模板成员函数本身也是一个函数模板,类模板被实例化时并不自动实例化,只有当它被调用或取地址时才被实例化。

11.类模板作为函数参数

函数的形参类型可以是类模板或类模板的引用,对应的实参应该是该类模板实例化的模板类对象。同时,对于带有类模板参数的函数,这个函数必须是函数模板。


12.类模板的友元函数

在一个类模板中可以设计友元函数。友元函数的形参类型可以是类模板或类模板的引用,对应的实参是该类模板实例化的模板类对象。

同时,对于带有类模板参数的友元函数,这个友元函数必须是函数模板。

如果在类模板中设计与参数类型无关的友元函数,那么在类外面实现时也不能省略template类型参数声明,否则将其看成是普通全局函数,而且它是从该类模板实例化的每个模板类的友元函数。与参数类型有关的友元函数,它对应特定的类型T,成为该类型模板类的友元函数。


13.类模板与静态成员

在非模板类中,类的所有对象共享一个静态数据成员,静态数据成员应在文件范围内初始化。

从类模板(含静态数据成员)实例化的每个模板类都有自己的模板类静态数据成员,该模板类的所有对象共享一个静态数据成员。

和非模板类的静态数据成员一样,模板类的静态数据成员也应在文件范围内初始化。每个模板类有自己的静态数据成员副本。


14.类模板与非类型参数


15.有关异常处理的说明?

  1. try和catch块必须要用花括号括起来,即使花括号内只有一个语句也不能省略花括号。
  2. try和catch块单独使用没有意义,必须组合使用。在一个try…catch中只能有一个try块,但可以有多个catch块,以便与不同的异常信息匹配。如果在try块中发现了异常,则通过throw语句后面的表达式创建一个异常对象(临时对象),且抛出了该异常,这个异常就可以被try语句块后面的某个catch语句所捕捉并处理,捕捉与处理的条件是被抛出的异常对象的类型与catch语句的异常类型相匹配。由于C++使用数据类型来区分不同的异常,因此在判断异常时throw语句中的表达式的值没有实际意义,而表达式的类型特别重要。
  3. 异常捕捉的类型匹配的苛刻程度可以和模板的类型匹配媲美,它不允许相容类型的隐式转换。例如抛出char类型用int就捕捉不到。
  4. 在C++中一旦抛出一个异常,如果程序没有任何捕捉,那么系统将会自动调用一个系统函数terminate,由它调用abort终止程序。
  5. 如果在catch块中没有指定异常信息的类型,而用删节号“…”,则表示它可以捕捉任何类型的异常信息。
  6. 当有多层try-catch时,系统采用栈结构处理,throw语句抛出的异常是一级一级地处理的。
  7. 如果throw不包括任何表达式,即为“throw;”,表示抛出一个无法捕捉的异常,即使是catch(…)也不能捕捉到。当有多层try-catch时,则表示把当前正在处理的异常信息再次抛出,传给其上一层的catch处理。

16.带有异常声明的函数原型

在设计一个函数时,除了正确设计函数的参数与返回值外,有时还需要设计函数的异常引发方式,以便异常处理程序能够处理函数调用过程中引发的异常。

 
  1. void f(int i) throw(T1,T2,T3); //函数仅能引发三种异常

  2. void f(int i) throw(); //函数不引发任何类型的异常

  3. void f(int i); //函数可引发任意类型的异常

当函数声明中没有异常声明throw部分,则该函数可引发任意类型的异常;

当函数声明中有异常声明部分,但throw部分只有一个空表,则表明该函数不引发任何类型的异常;

当函数声明中有异常声明部分,且throw部分不为空,则函数只能引发声明类型的异常。 


17.异常处理中对象的构造和析构顺序是怎样的?

C++的异常处理机制不仅能够处理各种不同类型的异常,还具有为异常抛掷前构造的所有局部对象自动调用析构函数的能力。

在程序中,在找到一个匹配的catch异常处理后,如果catch子句的异常类型声明是一个值参数,则其初始化方式是复制被抛掷的异常对象;如果catch子句的异常类型声明是一个引用,则其初始化方式是使该引用指向异常对象

当catch子句的异常类型声明参数被初始化后便开始展开栈的过程,这包括将从对应的try块开始到异常被抛掷之间构造(且尚未析构)的所有对象进行析构,析构的次序与构造的次序相反,然后程序从最后一个catch处理之后开始恢复执行。


18.可以在构造函数和析构函数中抛出异常吗?

构造函数中可以抛出异常,在这种情况下析构函数将不会被执行,需要手动去释放内存。

析构函数不能抛出异常,因为在析构函数中抛出异常有可能导致资源释放不彻底。

参考:C++构造函数和析构函数抛出异常的注意事项


19.简要说明使用catch(…)的优点和缺点。

catch(…)的优点是能够捕获try块中抛出的所有错误,缺点是catch没有参数,无法获取抛掷异常的相关信息,无法知道错误原因。


第六章 《继承和派生》


1.继承方式有哪几种?

public:表示公有继承,此时基类public和protected成员的访问属性在派生类中保持原来的访问属性不变,而基类private成员在派生类中不可访问。即基类的public和protected成员分别作为派生类的public和protected成员,派生类的其他成员可以直接访问它们。

private:表示私有继承(默认的继承方式),此时基类public和protected成员都以private权限出现在派生类中,而基类private成员在派生类中不可访问。即基类的public和protected成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生类的对象无法访问它们。无论是派生类的成员还是派生类的对象都无法访问从基类继承的private成员。

protected:表示保护继承,此时基类public和protected成员都以protected权限出现在派生类中,而基类private成员不可访问。即基类的public和protected成员被继承以后作为派生类的保护成员,这样,派生类的其他成员就可以直接访问它们,但在类外通过派生类的对象无法访问。无论是派生类的成员还是派生类的对象都无法访问从基类继承的private成员。


2.派生新类一般经历哪几个阶段?

  1. 吸收基类成员:在C++的类继承首先将基类的成员全部接收,这样派生类实际上就包含了它的基类中除构造函数和析构函数之外的所有成员(注意是全部继承,但有访问权限的限制,例如派生类中不能访问基类的私有成员)。
  2. 改造基类成员:对基类成员的隐藏,即在派生类中定义一个和基类成员同名的成员,由于作用域不同,于是发生同名隐藏,基类中的相应成员被替换成派生类中的同名成员。对于成员函数,无论参数列表是否相同,只要函数名相同,都会发生隐藏。
  3. 添加新的成员:根据实际需要给派生类添加适当的数据成员和成员函数,从而实现必要的新增功能。

对于private或者protected继承方式,基类的公有成员在派生类中变成私有或者保护的,可以在派生类中采用访问声明方式将其恢复成公有成员。

访问声明的一般格式为:

基类名::成员名;   //成员函数名后不要加上()

注意访问声明仅能将继承的成员恢复到原来的访问权限,例如,如果原来在基类中成员是公有的,被继承方式protected或private屏蔽后只能用访问声明恢复为公有的,而不能改为保护或私有的。


3.派生类对象的内存结构是什么样的?

从前面的介绍可知C++类中有四种成员,即静态数据成员、非静态数据成员、静态函数和非静态函数。

非静态数据成员放在每个对象内部,作为对象专有的数据成员,而静态数据成员被抽取出来放在程序的静态数据区内,为该类的所有对象共享,只保留一份,非静态成员函数和静态成员函数最终都被提取出来放在程序的代码段中并为该类的所有对象共享,因此每个成员函数只能存在一份代码实体。

那么,类继承中派生类对象的内存结构是什么样的呢?

  1. 派生类的对象中存放所有基类的非静态数据成员,包括基类的私有数据成员,只是在派生类中不能访问基类的私有成员。
  2. 在派生类对象中各数据成员是按继承方式列表中指定的类顺序排列的。

4.派生类的构造函数要完成哪些工作?

当从基类派生子类时基类的构造函数不能够继承到派生类中,因此派生类的构造函数必须通过调用基类构造函数来完成初始化工作,所以在定义派生类的构造函数时除了对自己的数据成员进行初始化外,还必须负责调用基类构造函数使基类的数据成员得以初始化。如果派生类中还有子对象,还应包含对子对象初始化,这些都包含在成员初始化列表中。

实际上,如果派生类构造函数的成员初始化列表中没有显式列出对基类构造函数的调用,也自动包含对基类默认构造函数的调用。


5.派生类构造函数的执行次序是怎样的?

  1. 调用基类的构造函数:当有多个基类时按派生类定义的次序调用,而不是按初始化列表中的次序。
  2. 调用派生类中子对象(如果有子对象)的构造函数,按子对象创建的顺序调用,而不是按照初始化表中的次序。
  3. 执行函数体。

6.在设计派生类构造函数(含拷贝构造函数)时要注意什么?

  1. 在调用派生类构造函数时总是会调用基类的构造函数,通常在成员初始化列表中给出其调用方式。
  2. 有时派生类构造函数的成员初始化列表中省略对基类构造函数的调用,其条件是在基类中必须有默认的构造函数或者根本没有构造函数(系统自动创建)。
  3. 当基类的重载构造函数使用一个或多个参数时(即基类重载了带参构造函数,且没有无参构造函数时)派生类必须定义构造函数,并通过成员初始化列表提供将参数传递给基类构造函数的途径。
  4. 若派生类中包含类A的子对象a,其定义方式只能是A a;即使该对象需要为其数据成员x赋值10,也不能写成A a(10);,只能在类C的构造函数成员初始化列表中添加a(10)来实现数据成员的初始化。

7.派生类的析构函数的执行次序?

和构造函数一样,析构函数也不能被继承,因此在执行派生类的析构函数时基类的析构函数也将被调用,其顺序与执行构造函数的顺序正好相反。

派生类析构函数的一般执行次序为:

  1. 执行派生类的析构函数;
  2. 调用子对象的析构函数:按类声明中对象成员出现的逆序调用,而不是按初始化列表中的次序。
  3. 调用基类的析构函数:多个基类则按派生类继承声明时列出的逆序从右到左调用,而不是按成员初始化列表中的次序。

8.类层次中的类模板

和类一样,类模板之间也可以存在继承关系,即可以从类模板派生其他类模板。


9.简述继承的优点和缺点。

继承的优点如下:

  1. 类继承是在编译时静态定义的,且可直接使用。
  2. 类继承可以提高代码的重用性。
  3. 类继承可以较方便地改变父类的实现。

继承的缺点:

  1. 继承在编译时就定义了,所以无法在运行时改变从父类继承的实现。
  2. 父类通常至少定义了子类的部分行为,父类的任何改变都可能影响子类的行为。
  3. 如果继承下来的实现不适合解决新的问题,则子类必须重写或被其他更合适的类替换,这种依赖关系限制了灵活性并最终限制了复用性。

10.哪几种情况需要采用成员初始化列表方式进行初始化?

  1. 带有const修饰的常数据成员,例如const int x
  2. 带引用的数据成员,例如int &x
  3. 调用基类构造函数使基类的数据成员得以初始化
  4. 如果派生类中有子对象,还应包含对子对象初始化

11.请设计一个不能被继承的类。

要想让一个类不能被继承,只需要将其构造函数设计为私有的。但此时不能创建该类的任何对象,因为这样的类是没有意义的。为此设计两个静态公有成员函数分别创建该类的实例指针和释放该实例。

对应的类如下:

 
  1. class A

  2. {

  3. public:

  4. static A *GetInstance(){return new A;}

  5. static void DeleteInstance(A *pInstance)

  6. {

  7. delete pInstance;

  8. pInstance=NULL;

  9. }

  10. private:

  11. A(){} //私有构造函数

  12. ~A(){} //私有析构函数

  13. };

可以采用以下代码创建类A的实例指针和释放该实例:

 
  1. A *pa=A::GetInstance();

  2. A::DeleteInstance(pa);


12.基类对象和派生类对象的使用关系?

派生类对象能作为基类对象处理

由于派生类具有基类的所有成员,所以把派生类的对象赋给基类对象是合理的,不过要求这种继承方式必须是public方式。反过来赋值是不允许的,因为派生类的成员通常比基类的成员多。

基类指针能指向派生类对象

因为派生类继承自基类,所以派生类对象的指针可以转换为指向基类的指针,这种向上的引用方式是安全的,称为向上映射。

但是这种方式只能引用基类的成员,如果试图通过基类指针访问那些只有在派生类中才有的成员,编译系统会报告错误。

派生类指针强制指向基类对象

用户可以用基类指针指向派生类对象,那么反过来可不可以呢?

结论是一般情况下不可以,但有两种情况例外。一是采用静态转换运算符static_cast进行强制转换,二是除非基类中包含有虚函数。


13.什么是虚继承?虚基类怎样声明?

在复杂的类层次中,可能存在重复继承的问题,即一个派生类多次继承同一个基类,这种情况下基类的多个相同成员如何区分呢?

解决的办法有两种:

一是使用作用域分辨符来唯一标识并分别访问它们;二是将多次重复继承的基类设置成虚基类,这就是虚继承。

虚基类是当基类被继承时在基类的继承方式前加上关键字virtual,即虚基类的声明格式为:virtual 继承方式 基类名

当某类的部分或全部直接基类是从另一个共同基类派生而来时,这些直接基类中从上一级基类继承来的成员就拥有相同的名字,这时可以将直接基类的共同基类设置为虚基类,从不同路径继承过来的该类成员在内存中只拥有一个副本,从而解决了同名成员的唯一标识问题。


14.虚继承中派生类对象的内存结构是什么样的?。。

这个问题有点不太好说清楚

和普通继承不同的是,派生类对象还需要表示虚继承关系。

当存在虚继承时(不考虑虚函数的情况),派生类对象内存结构的说明如下:

  1. 采用一个虚基类表(vbtable)来存放派生类的所有虚基类,虚基类表里面每一项的单位是4个字节,第一项保存的是虚基类表指针在派生类对象中的相对地址,第二项保存的是第一个虚基类数据成员在派生类对象中的相对地址,第三项保存的是第2个虚基类数据成员在派生类对象中的相对地址,以此类推。简单来讲就是虚基类表中保存了派生类所有虚基类的地址,所以,无论是单虚继承还是多虚继承,都只需要一个虚基类表。在派生类对象中增加指向该虚基类表的指针vbptr,其大小为4个字节,称为虚基类表指针。
  2. 在派生类对象中最开头存放虚基类表指针(并非在任何情况下都是放在最开头),然后存放自己的数据成员,最后存放虚基类的数据成员。在多虚继承时,按继承方式列表中的顺序存放各虚基类的数据成员。

15.虚继承中派生类构造函数的执行步骤是怎样的?

如果派生类有一个虚基类,那么在派生类构造函数的成员初始化列表中需要列出对虚基类构造函数的调用,如果未列出则表明调用的是虚基类的默认无参数的构造函数。

派生类构造函数的执行步骤如下:

  1. 调用基类的构造函数,多个基类则按派生类继承列表中列出的次序(即从左到右)调用,而不是按成员初始化列表中的次序。
  2. 调用子对象成员的构造函数(如果有子对象成员),按类创建对象成员的次序调用,而不是按成员初始化列表中的次序。
  3. 执行派生类的构造函数。

在以上构造函数的调用过程中,同一层中对虚基类构造函数的调用总是先于普通基类的构造函数。当同一层中有多个虚基类时,按继承列表中给出的先后次序调用虚基类的构造函数。


16.虚继承中派生类析构函数的执行步骤是怎样的?

同样,如果存在虚基类,派生类析构函数的执行会涉及其基类或虚基类的析构函数的执行。析构函数的调用次序与构造函数的调用正好相反。

派生类析构函数的执行步骤如下:

  1. 执行派生类的析构函数。
  2. 调用子对象的析构函数(若存在子对象),按类声明中创建子对象的逆序调用,而不是按成员初始化列表中的逆序。
  3. 调用普通基类的析构函数,多个基类则按派生类继承列表中列出次序的逆序(即从右到左)调用,而不是按成员初始化列表中的次序。

在以上析构函数调用过程中,同一层中对普通基类析构函数的调用总是先于虚基类的析构函数。


17.一个空类的大小为1,从一个空类派生的空子类的大小也是1,那么从一个空类虚继承的空子类的大小是多少?

从一个空类虚继承的空子类的大小是4,因为其中需要增加一个虚基类表指针表示虚继承关系。


18.派生类构造函数和基类的构造函数有什么关系?派生类构造函数调用基类的构造函数有哪两种方式?

在调用派生类的构造函数创建派生类对象时,系统首先调用基类的构造函数创建其基类对象。

派生类构造函数调用基类的构造函数有以下两种方式:

  1. 隐式调用:指在派生类的构造函数中不指定对应的基类的构造函数,这时调用的是基类的默认构造函数(即含有默认参数值或不带参数的构造函数)。
  2. 显式调用:指在派生类的构造函数中指定要调用的基类的构造函数,并将派生类构造函数的部分参数传递给基类的构造函数。

19.何谓虚基类?它有什么作用?如何使用虚基类?

虚基类是一种继承方式,如果采用虚基类方式定义派生类,则在创建派生类的对象时类层次结构中的某个虚基类的成员只保留一个,即虚基类的一个副本被所有派生类对象共享。

采用虚基类方式定义派生类的方法是在指定的基类名的前面加上关键字virtual,而定义基类时不需要使用关键字virtual。


第七章 《虚函数和多态性》


1.什么是函数的静态绑定和动态绑定?

函数绑定就是将函数入口地址和函数调用相联系的过程,分为静态绑定动态绑定两种形式。

两者的区别在于静态绑定在程序执行前完成,由编译系统或操作系统装入程序计算函数的入口地址;

动态绑定(也称迟后绑定或者迟后编译)在程序执行过程中完成,由程序自身计算函数的入口地址。

C语言仅支持静态绑定,C++作为一种面向对象的程序设计语言,它既支持静态绑定,又支持动态绑定

虚函数是动态绑定的基础,用于类继承关系中,它是在基类中定义的成员函数,而且是非静态成员函数。

声明虚函数的一般格式为:virtual 函数类型 函数名(参数表),其中,用关键字virtual声明的函数称为虚函数。

如果在某类中的成员函数被声明为虚函数,这就意味着它在派生类中可能有不同的实现。


2.怎样用虚函数实现动态多态性?

  1. 在基类中定义虚函数。
  2. 在派生类中定义与基类虚函数同名、同参数、同返回类型的成员函数。

虽然基类中的虚函数与各派生类中的虚函数同名、同参数,但由于各虚函数的函数体是不同的,因此,可用同名虚函数在执行时完成不同对象的操作,从而实现动态绑定。在派生类中重新实现基类中的虚函数称为重写或覆盖


3.虚函数有什么特点?

  1. 虚函数只能用于类层次结构中,如果一个类没有派生出其他任何类,定义虚函数是没有意义的。
  2. 在派生类中重写的虚函数必须与基类中被重写的虚函数有相同的函数名、函数类型、参数个数及参数类型。
  3. 虚函数具有继承性,在某个类中用virtual关键字声明的一个虚函数,其派生类及派生类的派生类中重写的函数均为虚函数,即使没有用virtual声明。

4.怎样调用虚函数?

  1. 用基类定义指针变量,例如“基类 *p
  2. 将基类对象地址或派生类对象地址赋给该指针变量,例如“p=&基类对象”或者“p=&派生类对象
  3. 用“p->虚函数(实参)”方式去调用基类或派生类的虚函数

在C++动态绑定的处理方式中仍包含静态类型检查,如虚函数参数类型的错误都能够在编译阶段被检查出来。


5.设计虚函数时要注意什么?

在一个基类中将所有的成员函数都尽可能地设计成虚函数总是有益的(如果该类没有派生出子类,将其成员函数声明为虚函数是没有意义的),除了会增加一些内存开销,没有其他坏处,尽管如此,也不是任何成员函数都可以设置成虚函数。

在设计虚函数时要注意以下几点:

  1. 只有类的成员函数才能声明为虚函数,这是因为虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。
  2. 静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
  3. 内联函数不能是虚函数,因为内联函数是不能在执行中动态确定其位置的,所以当在类内定义的成员函数(默认为内联函数)设置为虚函数时编译系统仍将其作为非内联函数处理,不进行内联函数那样的展开。
  4. 构造函数不能是虚函数,因为在调用构造函数对象还是一片未定型的空间,只有在构造完成后对象才能成为一个类的名副其实的实例。
  5. 在类继承结构中通常将析构函数设置为虚函数,设置虚析构函数的目的在于使用delete运算符销毁一个实例时能确保析构函数被正确地执行,这是因为设置虚析构函数后可以利用动态绑定方式选择相应的析构函数。
  6. 在基类中声明了虚函数后,派生类声明的虚函数应该与基类中虚函数的参数个数相等、对应参数的类型相同。

6.在含虚函数的多态继承环境中对象指针之间的转换有哪几种类型?

有三种类型:子类向基类的向上转换(隐式转换)、基类向子类的向下转换和横向转换。

向上转换:

由于向上转换是一种隐式转换,可以直接转换,也可以使用dynamic_cast运算符,但使用基类指针调用派生类中的未重写的父类成员函数的行为都会被编译器视为非法。

向下转换:

向下转换是一种强制转换,可以使用dynamic_cast运算符实现。这种转换是指对象指针的转换,而且基类A的对象指针pa是指向派生类对象的,不是对象的直接转换

例如,当A是B的基类时:

 
  1. //正确:

  2. A *pa=new B;

  3. B *pb=dynamic_cast<B *>(pa);

  4.  
  5. //错误:

  6. A a;

  7. B b=dynamic_cast<B>(a);

在使用dynamic_cast运算符实现向下转换时,若转换失败返回NULL,若转换成功返回正常转换后的对象指针。 

当A是B的基类时:

 
  1. A a;

  2. B b;

  3. B *pb=dynamic_cast<B*>(&a); //转换失败,pb为NULL

  4.  
  5. A *pa1=&b; //隐式向上转换

  6.  
  7. B *pb1=(B *)pa1; //强制转换

  8.  
  9. B *pb2=dynamic_cast<B*>(pa1); //用dynamic_cast向下转换

  1.  通常派生类对象要大于基类对象,所以直接将基类对象指针转换为派生类对象指针时可能会导致不可预见异常。如上面第三行所示。尽管没有报错,但pb为NULL,说明这是不安全的转换。
  2. 如果一个实际指向派生类对象的指针被转换成基类指针(第5行所示),然后再用dynamic_cast转换回来,一定能成功,这是dynamic_cast用于向下转换的常见方式。如第9行所示。此时pb2不为NULL,可以通过该指针调用类B的成员函数。
  3. 第7行能够实现强制转换,但与dynamic_cast转换相比它是不安全的转换,dynamic_cast一次做两个操作,它检验请求的转换是否真有效,只有在有效时才会执行转换,检验过程是发生在执行时刻的,而前者不会做检验工作。

横向转换:

在多继承层次结构中一个派生类有两个基类,在这两个基类对象指针之间的相互转换称为横向转换。

例:类A和类B是类C的基类,正确的横向转换为:

 
  1. A *pa=new C; //A的对象指针pa指向派生类C的实例

  2. B *pb=dynamic_cast<B *>(pa); //将pa转换为基类B的对象指针pb


7.带虚函数的派生类对象的内存结构是怎样的?

对于单个类,所有成员函数都是在代码区中唯一存放一份,而非静态数据成员则是每个对象存储一份,并按照定义的顺序依次存放。

当一个类中有虚函数时,其对象的存储就会在非静态数据成员的最前面添加一个vfptr指针,这个指针用来指向一个虚函数表(一个虚函数指针数组即虚函数表),称为虚函数表指针。

虚函数表中存储着当前类对象的所有虚函数的地址,这样vfptr就像数据成员一样存放,占4个字节。在访问虚函数时通过vfptr间址找到vtable表,再进而间址找到要调用的函数,这样就在一定程度上摆脱了类型制约。

1.非虚继承的情况:

在没有虚继承的类层次结构中,基类对象和派生类对象的存储组织如下:

  1. 在派生类对象中按照继承方式列表声明的顺序依次存放基类对象的非静态数据成员,最后是派生类非静态数据成员。
  2. 若基类声明了虚函数,则基类对象开头有一个指向基类虚函数表的指针,然后是基类非静态数据成员。
  3. 在基类B虚函数表中依次是B基类的虚函数。若B基类又是另外一个基类A派生的,则类A的虚函数放在派生类B的虚函数的前面,如果类B中重写了基类A的某个虚函数,则在B的虚函数表中用自己的该函数替换原虚函数。
  4. 派生类中独有的虚函数被加在第一个基类的虚函数表的后面。

2.虚继承的情况:

在有虚继承的类层次结构中只是将虚继承和虚函数机制结合起来,基类对象和派生类对象的存储组织如下:

  1. 在派生类对象中按照继承方式列表声明顺序依次存放非虚基类对象的数据成员,然后是派生类数据成员,最后是虚基类对象的数据成员。
  2. 若基类声明了虚函数,则基类对象头部有一个虚函数表指针,然后是基类数据成员。在基类虚函数表中依次是基类的虚函数。若某个虚函数是重写的,则要进行替换。
  3. 若直接从虚基类派生的类没有非虚父类,且声明了新的虚函数,则该派生类有自己的虚函数表,在该派生类的头部;否则派生类独有的虚函数被加在第一个非虚基类的虚函数表的后面。
  4. 直接从虚基类派生的类的内部还有一个虚基类表指针,在数据成员之前,非虚基类对象之后(若有)。

8.静态成员函数能不能同时也是虚函数?

不能。

静态成员函数是类函数,在调用时不需要类对象,但调用虚函数时需要从一个对象中指向虚函数表的指针得到该函数的地址,因此需要通过类对象调用虚函数。两者相互矛盾,所以静态成员函数不能设计成虚函数。


9.为什么构造函数不能设计成虚函数?

构造函数的任务是为了建立合适的对象,在调用构造函数后对象才创建好(包括虚表和虚函数表的创建工作),而调用虚函数需要类对象。如果构造函数被设计成虚函数,在调用虚构造函数时对象还没有创建好,这两者发生互为前提的情况,造成死锁,所以构造函数不能设计成虚函数。


10.在C++中哪些函数不能声明为虚函数?

普通函数(非成员函数)、构造函数、内联成员函数、静态成员函数和友元函数不能声明为虚函数,原因如下。

  1. 虚函数只能用于基类和派生类,所以普通函数(非成员函数)不能声明为虚函数。
  2. 构造函数在调用时对象尚未创建好,必须在对象创建完毕的情况下才能调用虚函数,所以构造函数不能声明为虚函数。
  3. 内联成员函数的实质是在调用的地方直接将代码扩展开,没有函数地址,所以内联函数不能声明为虚函数(实际上类中实现的虚函数不会作为内联函数将调用处的代码展开)。
  4. 静态成员函数是不能被继承的,它只属于一个类,不能实现动态绑定,所以静态成员函数不能声明为虚函数。
  5. 友元函数不是类的成员函数,也不能被继承,所以友元函数不能声明为虚函数。

11.简述C++虚函数实现多态的原理。

若编译器发现一个类中有虚函数,会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数代码的指针,如果是派生类并重写了虚函数,会用新的重写虚函数的地址替换原来的地址。在调用该类对象的构造函数时,编译器还会在该类对象隐含插入一个虚函数表指针vfptr指向该虚函数表,通过这个vfptr可以找到该类的相应函数体,这种动态连编是实现多态的核心。


12.如果基类的析构函数不是虚函数会带来什么问题?

可能使得派生类的析构函数用不上,这样派生类对象的资源得不到释放,会导致内存泄漏。


13.在什么时候需要用虚析构函数?

当基类指针pa指向用new运算符生成的派生类对象时,如果使用delete pa语句销毁pa指向的实例,出现其派生类部分没有释放掉而造成释放不彻底的现象,在这种情况下需要虚析构函数。


14.简述类成员函数的重写(覆盖)、重载和隐藏的区别。

重写是指派生类函数重新实现基类函数。特征如下:

  1. 不同的范围(分别位于派生类和基类)
  2. 函数名字相同
  3. 参数相同
  4. 基类函数必须有virtual关键字。

重载成员函数的特征如下:

  1. 相同的范围(在同一个类中)
  2. 函数名字相同
  3. 参数不同
  4. virtual关键字可有可无

隐藏是指派生类的函数屏蔽了与其同名的基类函数,特征如下:

  1. 如果派生类的函数与基类的函数同名,但参数不同,此时不论有无virtual关键字,基类的函数都将被隐藏
  2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时基类的函数被隐藏

归纳起来,重写、重载和隐藏的区别如下:

重写和重载主要有以下几点不同:

  1. 范围区别:被重写和重写的函数在两个类中,而重载和被重载的函数都在同一个类中。
  2. 参数的区别:被重写的函数和重写的函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
  3. virtual的区别:在派生类中重写基类的函数必须在基类中用virtual声明,即为虚函数,而重载函数和被重载函数可以用virtual声明,也可以没有,即虚函数和非虚函数都可以重载。

隐藏和重写/重载有以下几点不同:

  1. 范围区别:隐藏和重载的范围不同,和重写一样,隐藏函数和被隐藏函数不在同一个类中
  2. 参数的区别:隐藏函数和被隐藏函数的参数列表可以相同,也可以不同,但是函数名肯定要相同。当参数不同时,无论函数在基类中是否用virtual声明,基类的函数都是被隐藏,而不是被重写。

15.如果虚函数是非常有效的,是否可以把每个能够设置成虚函数的成员函数都声明为虚函数?

这样做有时是不合适的。由于每个含虚函数的类都要维护一个虚函数表,因此在使用虚函数时会产生一定的系统开销。

另外,虚函数用于继承结构,如果一个类不派生其他类,那么根本没必要使用虚函数。


16.什么是纯虚函数?

纯虚函数是一种特殊的虚函数,它是被标明为不具体实现的虚函数,从语法上讲,纯虚函数就是在虚函数的后面加上“=0”,表示该虚函数无函数体,这里的“=”并非赋值运算。

声明纯虚函数的一般格式为:virtual 函数类型 函数名(参数表)=0;

在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。包含有纯虚函数的类称为抽象类。


17.抽象类有什么特点?

抽象类是一种特殊的类,至少有一个纯虚函数。抽象类的作用主要是将有关的类组织在一个集成层次结构中,由它为它们提供一个公共的根,相关的子类是从这个根派生出来的。

抽象类的特点如下:

  1. 抽象类只能用作其他类的基类,不能建立抽象类对象
  2. 抽象类不能用作参数类型、函数返回类型或显式转换的类型
  3. 可以定义指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。

与抽象类相对应,将能够建立对象的类称为具体类。


18.虚函数和纯虚函数的区别是什么?

虚函数在基类中是有定义的,即便定义为空,在子类中可以重写。

纯虚函数在基类中没有定义,必须在子类中加以实现。


19.简要说明你对C++中多态性、虚函数、纯虚函数和抽象类几个概念的理解。

多态性是指不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时多态性通过继承和虚函数来体现;在程序编译时多态性体现在函数和运算符的重载上。

虚函数是在基类中用关键字virtual声明的成员函数。它提供了一种接口界面,允许在派生类中对基类的虚函数重新定义。

纯虚函数是用=0代替函数体的虚函数,其作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义,作为接口存在,纯虚函数不具备函数的功能,一般不能直接被调用。

从基类继承来的纯虚函数在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类。在抽象类中不仅包括纯虚函数,也可包括虚函数,抽象类必须用作派生其他类的基类,不能用于直接创建对象实例,但仍可使用指向抽象类的指针支持运行时的多态性。


20.什么是多态?如何理解编译时多态和运行时多态?

所谓多态是指一个名字可以具有多种语义。多态性分为静态和动态两种。

静态多态性是指定义在一个类中的同名函数,它们根据参数表(类型和个数)区别语义(重载函数),并通过静态联编实现,例如在一个类中定义的不同参数的构造函数以及运算符重载函数等。

动态多态性是指定义在一个类层次的不同类中的重写函数,它们具有相同的参数表,因此要根据对象指针指向的对象类型来区别语义,例如虚函数,它通过动态联编实现。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值