1 选择题
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承
B: 封装
C: 多态
D: 抽象
答案:A
2. ( ) 是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承
B: 模板
C: 对象的自身引用
D: 动态绑定
答案:D
3. 面向对象设计中的继承和组合,下面说法错误的是?()
A :继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
用,也称为白盒复用 。
B :组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
态复用,也称为黑盒复用 。
C :优先使用继承,而不是组合,是面向对象设计的第二原则 。
D :继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
装性的表现 。
答案:C 优先使用继承,而不是组合,不是面向对象设计的第二原则 。
4 下面代码输出结果:( )
class A { public: void f(){ cout<<"A::f()"<<endl; } int a; }; class B : public A { public: void f(int a){cout<<"B::f()"<<endl;} int a; }; int main() { B b; b.f(); return 0; }
A.打印A::f()
B.打印B::f()
C.不能通过编译,因为基类和派生类中a的类型以及名称完全相同
D.以上说法都不对
答案:D 不能通过编译是正确的,但是原因却不是因为基类和派生类中a的类型以及名称完全相同,而是因为构成了隐藏,不能正确的调用到基类的f函数。
5 关于基类与派生类对象模型说法正确的是()
A.基类对象中包含了所有基类的成员变量
B.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量
C.子类对象中没有包含基类的私有成员
D.基类的静态成员可以不包含在子类对象中
E.以上说法都不对
答案:E 基类对象和子类对象都不包含静态变量。
6. 关于虚函数的描述正确的是 ( )
A :派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B :内联函数不能是虚函数
C :派生类必须重新定义基类的虚函数
D :虚函数可以是一个 static 型的函数
答案:B 派生类的虚函数与基类的虚函数如果要构成重写的话,必须要满足函数名,参数,返回类型相同;内联函数一定不会是虚函数,因为 虚函数要放到虚函数表中,而内联函数会直接展开成指令,并不会放到虚函数表,我们在编写代码时加上inline也不会报错的原因是内联只是个建议,编译器自己会检查,当发现是虚函数时会自动放弃内联属性;派生类要重写基类的虚函数,但不是绝对的;虚函数不能是静态成员函数,因为静态成员函数没有this指针,无法找到对象,就不能找到虚函数表。
7. 关于虚表说法正确的是()
A :一个类只能有一张虚表
B :基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C :虚表是在运行期间动态生成的
D :一个类的不同对象共享该类的虚表
答案:D 一个类可能有多张虚表,只是同类对象共享该类的虚表;基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类虚表中的地址一样,但是虚表的指针是不同的;虚表是在编译期间就已经确定好了。
8. 假设 A 类中有虚函数, B 继承自 A , B 重写 A 中的虚函数,也没有定义任何虚函数,则()
A : A 类对象的前 4 个字节存储虚表地址, B 类对象前 4 个字节不是虚表地址 。
B : A 类对象和 B 类对象前 4 个字节存储的都是虚基表的地址 。
C : A 类对象和 B 类对象前 4 个字节存储的虚表地址相同 。
D : A 类和 B 类虚表中虚函数个数相同,但 A 类和 B 类使用的不是同一张虚表 。
答案D 并没有说明平台原因,不一定是前4个字节。
9. 下面程序输出结果是什么 ? ()
#include<iostream> using namespace std; class A{ public: A(char *s) { cout<<s<<endl; } ~A(){} }; class B:virtual public A { public: B(char *s1,char*s2):A(s1) { cout<<s2<<endl; } }; class C:virtual public A { public: C(char *s1,char*s2):A(s1) { cout<<s2<<endl; } }; class D:public C,public B { public: D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1) { cout<<s4<<endl;} }; int main() { D *p=new D("class A","class B","class C","class D"); delete p; return 0; }
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
分析:这道题稍微不注意就有可能选错。我们来分析分析,我们通过代码可以知道这是一个简单的菱形继承模型,里面B和C加了虚拟继承消除了数据冗余的问题,所以只有一份A,在初始化列表阶段肯定会先初始化A,由于A只有一份所以只会在A的构造函数初始化,后面BC的初始化并不会重复调用A的初始化,只需要初始化一份就足够了,但是现在的问题是初始化列表的调用顺序,A构造坑定是第一,那么B和C究竟是谁先调用呢?我们之前将类和对象的时候提过,初始化列表的初始化顺序是声明顺序,并不是我们初始化的顺序,所以先声明了C,再声明了B,初始化列表就会先调用C的构造,再调用B的构造,故答案选D。
10. 多继承中指针偏移问题下面说法正确的是 ( )
class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main(){ Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
A : p1 == p2 == p3
B : p1 < p2 < p3
C : p1 == p3 != p2
D : p1 != p2 != p3
答案:C
11. 以下程序输出结果是什么()
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
分析:这道题又是一个大坑,稍微不注意就要选错。首先来观察子类重写了父类的func函数,当我们拿B类型的指针调用test函数时,调用到的是父类继承给子类的test函数,所以继承给父类的test函数与子类的一模一样,参数也是相同的,我们用test函数调用了func函数,但是我们是用什么调用的,是不是用的是A类型的this指针来调用func函数,所以这样是不是就构成了多态,因为我们用的是父类的指针来调用func函数,而恰好func函数已经完成了重写,所以调用到的函数应该是B的func函数,因为new的是B类型的对象。但是我们观察父类的子类func函数的参数居然有缺省值,那么应该用父类的,还是子类的呢?其实重写我们重写的只是函数的实现,函数的参数类型包括缺省值我们其实都是继承的父类的,所以这道题选择B
我们再简单的变换一下:
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} }; class B : public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } virtual void test(){ func();} }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }
这时选啥?此时已经不满足多态的条件了,而是直接调用父类的func函数,但是此时由于没有满足多态的条件,所以父类的函数缺省值并没有继承给子类,所以选择D
再变换一下:
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { A*p = new B; p->test(); return 0; }
这时又选择什么呢?其实这个变换以第一种情况类似,只是p的类型用了A*,但是new出来的类型依旧是B类型,所以仍然会构成多态,最终还是会调用到B的func,所以还是选择B。
12以下程序输出结果是( )
class A { public: A ():m_iVal(0){test();} virtual void func() { std::cout<<m_iVal<<‘ ’;} void test(){func();} public: int m_iVal; }; class B : public A { public: B(){test();} virtual void func() { ++m_iVal; std::cout<<m_iVal<<‘ ’; } }; int main(int argc ,char* argv[]) { A*p = new B; p->test(); return 0; }
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对
分析:new了一个B类型的对象,所以会调用B的构造函数,B的构造函数又会调用A的构造函数,所以先调用A的构造函数打印0;再调用B的构造函数,而B的构造函数调用的test是A继承下来的,与前面的题一样,这里也构成了多态,this指针是A类型的,func函数已经完成了重写,所以会调用B的func,打印1; 最后手动的调用test依旧是构成了多态,调用的是父类的func,所以打印2。综上,选择C.
2 问答题
1. 什么是多态?
回答:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。而多态又分为编译时多态和运行时多态。
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
回答:重载是函数在同一作用域内,函数名相同,函数参数的个数,顺序,类型不同,与返回值无关;重写是函数在子类和父类两个作用域,函数名称,函数参数,函数返回值都相同(协变除外);重定义是函数在父类和子类在两个作用域,函数名称相同,不构成重写,就是重定义。另外重定义不仅仅针对函数变量在父类和子类两个作用域名字相同也是构成重定义。
3. 多态的实现原理?
回答:多态的实现原理是因为对象中存在虚表指针,通过虚表指针来找到虚表,而虚表中存在着虚函数的地址,不同类的对象找到不同的虚表从而调用到不同的虚函数而实现了多态。
4. inline 函数可以是虚函数吗?
回答: 不能,因为虚函数要放到虚表中去,内联函数会直接展开成指令。但是我们在内联函数中前加virtual编译器不会报错,因为内联只是一种建议,当加了virtual后编译器会自动舍弃内联属性。
5. 静态成员可以是虚函数吗?
回答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
回答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7. 析构函数可以是虚函数吗?
回答:可以,并且最好把基类的析构函数定义成虚函数。比如我们new了一个子类对象用了父类指针来接受,当我们销毁时我们期望调用父类的析构函数来进行销毁,而把父类的析构函数定义成虚函数的好处是编译器会做出特殊处理将父子的析构函数都处理成destructor,从而构成多态方便正确调用。
8. 对象访问普通函数快还是虚函数更快?
回答:如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
回答:虚函数表是在编译阶段就生成的,一般情况存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
回答:C++菱形继承会出现数据冗余和二义性的问题。虚继承是为了消除数据冗余的问题;虚继承的原理是将冗余数据单独放在一边,让加了virtual的两个类共享该数据,但是为了方便这两个类都能够找得到该数据就在对象中多存了虚基表指针,通过该指针能够找到一张虚基表,虚基表中存在着偏移量,通过偏移量就能够方便的找到共享数据。
11. 什么是抽象类?抽象类的作用?
回答:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。