五、多态面试题
1.选择题
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
答案: A 当然是继承更富有啦
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
答案: D 动态绑定是函数调用时关联到具体对象
3. 面向对象设计中的继承和组合,下面说法错误的是?( )
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
答案: C 尽量少用继承,会破坏封装原则,多用组合,能降低耦合度
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的 D:纯虚函数必须是空函数
答案: A 包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数
答案: B inline函数没有地址,当inline成为虚函数后,虚表里面要放它的地址,构成多态时,根据虚函数表指针去call这个地址,就不能展开了,就忽略了内联属性,加了虚函数以后就不再是内联函数了。
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
答案: D
对于A如果是多继承,那么这个类的对象会有多张虚表;
对于B,监视:
1. #include<iostream> 2. using namespace std; 3. 4. class Animal 5. { 6. public: 7. virtual void Color()//颜色 8. { 9. cout << "virtual Animal::color" << endl; 10. } 11. 12. virtual void Name()//名称 13. { 14. cout << "virtual Animal::name" << endl; 15. } 16. }; 17. 18. class Coral :public Animal 19. {}; 20. 21. int main() 22. { 23. Animal a; 24. Coral c; 25. 26. return 0; 27. }
发现虚表指针不同,虽然虚表指针中存放的虚函数地址相同:
对于C:虚表在编译时就已经生成了
对于D:
1. #define _CRT_SECURE_NO_WARNINGS 1 2. #include<iostream> 3. using namespace std; 4. 5. class Animal 6. { 7. public: 8. virtual void Color()//颜色 9. { 10. cout << "virtual Animal::color" << endl; 11. } 12. 13. virtual void Name()//名称 14. { 15. cout << "virtual Animal::name" << endl; 16. } 17. }; 18. 19. int main() 20. { 21. Animal a; 22. Animal a1; 23. 24. return 0; 25. }
监视发现:a和a1的虚表指针地址相同,虚表指针中存放的虚函数地址也相同
7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表
答案:D 同第6题的B
8. 下面程序输出结果是什么? ()
1. #include<iostream> 2. using namespace std; 3. class A 4. { 5. public: 6. A(char *s) { cout<<s<<endl; } 7. ~A(){} 8. }; 9. class B:virtual public A 10. { 11. public: 12. B(char *s1,char*s2):A(s1) { cout<<s2<<endl; } 13. }; 14. class C:virtual public A 15. { 16. public: 17. C(char *s1,char*s2):A(s1) { cout<<s2<<endl; } 18. }; 19. class D:public B,public C 20. { 21. public: 22. D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1){ cout<<s4<<endl;} 23. }; 24. 25. int main() 26. { 27. D *p=new D("class A","class B","class C","class D"); 28. delete p; 29. return 0; 30. }
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
答案:A 子类构造函数必须调用父类的构造函数初始化父类的成员,因此执行D的构造函数前必须执行B和C的构造函数,执行B和C的构造函数前必须执行A的构造函数
9. 多继承中指针偏移问题?下面说法正确的是( )
1. class Base1 { public: int _b1; }; 2. class Base2 { public: int _b2; }; 3. class Derive : public Base1, public Base2 { public: int _d; }; 4. 5. int main() 6. { 7. Derive d; 8. Base1* p1 = &d; 9. Base2* p2 = &d; 10. Derive* p3 = &d; 11. return 0; 12. }
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
答案:C p3作为子类,普通多继承,示意图如下:Derive的成员包含Base1和Base2的成员
10. 以下程序输出结果是什么( )
1. class A 2. { 3. public: 4. virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} 5. virtual void test(){ func();} 6. }; 7. class B : public A 8. { 9. public: 10. void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } 11. }; 12. 13. int main(int argc ,char* argv[]) 14. { 15. B*p = new B; 16. p->test(); 17. return 0; 18. }
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F.以上都不正确
答案:B,p是子类指针,不是父类指针,test( )调用不构成多态。但有隐藏多态,p是子类指针,会把test传给this,抵用test之后会把p传给this,this在A里面,是父类指针,把p传给了this,父类指针指向了子类对象,父类的指针调func,是一个重写的虚函数,this是父类指针,子类继承了父类的虚函数,重写了函数实现,但是继承的是父类的接口定义,比如虽然子类的func函数没有加virtual,但是子类继承了父类的func函数,就是虚函数。所以就算子类重写父类虚函数时,给了缺省参数,但是根本不会用到。用的是父类接口定义+子类实现
2.问答题
1. 什么是多态?
答:不同继承关系的类对象,去调用同一函数,产生了不同的行为
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:
重载(也叫静态多态):两个函数在同一作用域,函数名和参数相同
重写(也叫覆盖、动态多态):两个函数分别在父类和子类的作用域;函数名、参数、返回值都必须相同;两个函数都必须是虚函数
隐藏(也叫重定义):两个函数分别在父类和子类的作用域,函数名相同,两个父类和子类的同名函数不构成重写就构成重定义
3. 多态的实现原理?
答:子类重写了父类虚函数,且通过父类指针调用虚函数,这就满足了多态的两个条件。子类完成父类虚函数重写以后,子类的虚表指针指向的是重写了的子类虚函数。指针或引用调用虚函数时,不是编译时确定,而是运行时才到指向的对象的虚表中找对应的虚函数调用,当指针或引用指向父类对象时,调用的就是父类的虚表中的虚函数,当指针或引用指向子类对象时,调用的就是子类虚表中的虚函数。
4. inline函数可以是虚函数吗?
答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
inline函数没有地址,当inline成为虚函数后,虚表里面要放它的地址,构成多态时,根据虚函数表指针去call这个地址,就不能展开了,就忽略了内联属性,加了虚函数以后就不再是内联函数了。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
假如构造函数是虚函数:
①调用构造函数虚函数必须要去虚表里面找,这就要求对象必须已经被初始化出来了。
②要初始化对象,就要调构造函数虚函数,但是对象还没有构造出来,虚表还没有初始化,还找不到构造函数虚函数地址。
这就陷入了死循环
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,最好把基类的析构函数定义成虚函数。这样子类继承父类后,子类的析构函数也会成为虚函数,在这样的场景下,期望达到多态行为,子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。多态场景下子类和父类的析构函数最好加上virtual关键字,让子类完成虚函数重写就不会导致内存泄漏了
8. 对象访问普通函数快还是虚函数更快?
答:
如果是普通对象,那么访问普通函数和虚函数是一样快的。
如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找,需要耗时。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:子类对象会有两份父类的成员,菱形继承会导致数据冗余和二义性。
虚继承通过虚基表指针的偏移量计算出父类成员的起始地址,这样就只需要在内存中存一份父类成员,解决了数据冗余和二义性的问题。
11. 什么是抽象类?抽象类的作用?
答:包含纯虚函数的类叫做抽象类。
抽象类不能实例化出对象。子类继承抽象类后也是抽象类,没有重写虚函数,不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物
② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)