面试题11 虚函数相关的选择题
现有下面类和变量的定义
#include <iostream>
#include <complex>
using namespace std;
class Base {
public:
Base() { cout << "Base-ctor " << endl; }
~Base() { cout << "Base-dtor " << endl; }
virtual void f(int) { cout << "Base::f(int) " << endl; }
virtual void f(double) { cout << "Base::f(double) " << endl; }
virtual void g(int i = 10) { cout << "Base::g() " << endl; }
};
class Derived :public Base {
public:
Derived() {cout << "Derived-ctor " << endl;}
~Derived() {cout << "Derived-dctor " << endl;}
void f(complex<double> c) {cout << "Derived::f(complex)" << endl;}
virtual void g(int i = 20) {cout << "Base::g() " << i << endl; }
};
int main() {
Base b;
Derived d;
Base *pb = new Derived();
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
pb->f(1.0);
pb->g();
return 0;
}
从4个选项中选择正确一个。
(1)cout << sizeof(Base) << endl;
A. 4 B. 32 C. 20 D. 与平台相关
(2)cout << sizeof(Derived) << endl;
A. 4 B. 8 C. 36 D. 与平台相关
(3)pb->f(1.0);
A. Derived::f(complex) B. Base::f(double)
C. Base::f(int) D. Derived::f(double)
(4)pb->g();
A. Base::g() 10 B. Base::g() 20 C. Derived::g() 10 D. Derived::g() 20
【解析】
题(1),Base类没有任何数据成员,并且含有虚函数,所以系统会为它分配一个指针指向虚函数表。指针的大小是4个字节。
题(2),Derived类没有任何数据成员,它是Base的派生类,因此它继承了Base的虚函数表。系统也会为它分配一个指针指向这张虚函数表。
题(3),Base类中定义了两个f()的重载函数,Derived只有一个f(),其参数类型为complex,因此Derived并没有Base的f()进行覆盖。由于参数1.0默认是double类型的,
因此调用的是Base::f(double).
题(4),Base和Derived都定义了含有相同参数列表的g(),因此这里发生多态了。pb指针指向的是Derived类的对象,因此调用的是Derived类的g()。
这里要注意,由于参数的值在编译期就已经问心有愧定的,而不是在运行期,因此参数i应该取Base类的默认值,即10.
【答案】
程序执行结果
(1)A
(2)A
(3)B
(4)C
面试题12 为什么需要多重继承?它的优缺点是什么
【解析】
实际生活中,一些事物往往会拥有两个或两个以上事物的属性。为了解决这个问题,C++引入了多重继承的概念。
C++允许为一个派生类指定多个基类,这样的继随结构被称作多重继承。举个例子:
人(Person)可以派生出作者(Author)和程序员(Programmer),然而程序员作者同时拥有作家和程序员的两个属性,即既能编程又能写作。如图7.2所示。
使用多重继承的例子程序如下。
#include <iostream>
using namespace std;
class Person
{
public:
void sleep() {cout << "sleep" << endl;}
void eat() {cout << "eat" << endl;}
};
class Author: public Person //Author继承自Person
{
public:
void writeBook() {cout << "write Book" << endl;}
};
class Programmer: public Person //Programmer继承自Person
{
public:
void writeCode() {cout << "write Code" << endl;}
};
class Programmer_Author: public Programmer, public Author//多重继承
{
};
int main()
{
Programmer_Author pa;
pa.writeBook();//调用基类Author的方法
pa.writeCode();//调用基类Programmer的方法
pa.eat(); //编译错误,eat()定义不明确
pa.sleep(); //编译错误,sleep()定义不明确
return 0;
}
多重继承的优点很明显,就是对象可以调用多个基类中的接口,如代码第31行与代码第32行对象pa分别调用Author类的writeBook()函数和Programmer类的writeCode()函数。多重继承的缺点是什么呢?如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性。代码第33,34行就是因为这个原因而出现编译错误的。因为通过多重继承的programmer_Author类拥有Author类和Programmer类的一份拷贝,而Author类和Programmer类都分别拥有Person类的一份拷贝,所以Programmer_Author类拥有Person类的两份拷贝,在调用Person类的接口时,编译器会不清楚需要调用哪一份拷贝,从而产生错误。
对于这个问题,通常有两个解决方案:
(1)加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。
(2)使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。比如在第11行和17行的继承语句中加入virtaul就可以了。
class Author: virtual public Person //Author虚继承自Person
class Programmer: virtual public Person //Programmer虚继承自Person
【答案】
实际生活中,一些事物往往会拥有两个或两个以上事物的属性,为了解决这个问题,C++引入了多重继承的概念。
多重继承的优点是对象可以调用多个基类中的接口。
多重继承的缺点是容易出出继承向上的二义性。
面试题13 多重继承中的二义性
下面程序中的多重继承有什么问题?
#include <iostream>
using namespace std;
class cat
{
public:
void show()
{
cout << "cat" << endl;
}
};
class fish
{
public:
void show()
{
cout << "fish" << endl;
}
};
class catfish: public cat, public fish
{
};
int main()
{
catfish obj;
obj.show();
cout << "hello world" << endl;
return 0;
}
【解析】
程序中catfish类多重继承cat类和fish类,因此继承了cat的show()方法和fish的show()方法。由于这两个方法同名,代码第27行直接用obj.show()时,无法区分应该执行如个基类的show()方法,因此会出现编译错误。
【答案】
代码第27行出现编译错误,执行到obj.show()时无法区分应该执行哪个基类的show()方法,可以心成obj.cat::show()访问cat的show()成员。
面试题14 多重继承二义性的消除
类A派生B和C,类D从B,C派生,如何将一个类A的指针指向一个类D的实例?
【解析】
这道题实际上考查的是如何消除多重继承引起的向上继承二义性问题。程序代码如下所示。
class A{};
class B: public A{};//B继承自A
class C: public A{};//C继承自A
class D: public B, public C{};
int main()
{
D d;
A *pb = &d;//编译错误
return 0;
}
由于B,C继承自A,B、C都拥有A的一份拷贝,类D多重继承自B、C,因此拥有A的两份拷贝。如果此时一个类A的指针指向一个类D的实例,会出现“模糊的转换”之类的编译错误。解决办法如下。
class A{};
class B: virtual public A{};//B虚拟继承自A
class C: virtual public A{};//C虚拟继承自A
class D: public B, public C{};
int main()
{
D d;
A *pb = &d;//成功转换
return 0;
}
将B、C都改为虚拟继承自A,则类D多重继承自B、C时,就不会重复拥有A的拷贝了,因此也就不会出现转换错误了。
【答案】
把B、C都改为虚拟继承自A,消除继承的二议性。
面试题15 多重继承和虚拟继承
下面的程序输出结果是什么?
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(): num(0) {cout << "Parent" << endl;}
Parent(int n): num(n){cout << "Parent(int)" << endl;}
private:
int num;
};
class Child1: public Parent
{
public:
Child1() {cout << "Child1()" << endl;}
Child1(int num): Parent(num){cout << "Child1(int)" << endl;}
};
class Child2: public Parent
{
public:
Child2() {cout << "Child2()" << endl;}
Child2(int num): Parent(num){cout << "Child2(int)" << endl;}
};
class Derived:public Child1, public Child2
{
public:
Derived(): Child1(0), Child2(1){}
Derived(int num): Child2(num), Child1(num+1) {}
};
int main()
{
Derived d(4);
return 0;
}
如果类Child1和Child2都改为virtual继承Parent,输出结果又是什么?
【解析】
首先讨论不存在virtual继承的情况。
多重继承类对象的构造顺序与其继承列表中基类的排列顺序一致,而不是与构造函数的初始化列表顺序一致。
在这里,Derived继承的顺序是Child1、Child2(第24行),因此按照下面的步骤构造。
(1)构造Child1.由于Child1继承自Parent,因此先调用Parent的构造函数,再调用Child1的构造函数。
(2)调用Child2.过程与(1)类似,先调用Parent的构造函数,再调用Child2的构造函数。
(3)调用Derived类的构造函数。
因此输出结果为:
现在说明Child1和Child2为虚拟继承时,当系统碰到多重继承的时候就会自动先加入一个虚拟基类(Parent)的拷贝,即首先调用了虚拟基类(Parent)默认的构造函数,
然后再调用派生类(Child1和Child2)的构造函数和自已(Derived)的构造函数。由于只生成一份拷贝,因此以后再也不会调用虚拟基类(Parent)的构造函数了,
在Child1和Child2指定调用Parent的构造函数就无效了。
输出结果为:
现在来总结一下,多继承中的构造函数顺序如下:
(1)任何虚拟基类的构造函数按照它们被继承的顺序构造。
(2)任何非虚拟基类的构造函数按照它们被构造的顺序构造。
(3)任何成员对象的构造按照它们声明的顺序调用。
(4)类自身的构造函数
【答案】
不存在vitual继承时的输出结果为:
存在vitual继承时的输出结果为:
面试题16 为什么要引入抽象基类和纯虚函数
【解析】
纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像Java中的接口函数。
如果基类含有一个或多个纯虚函数,那么它就属于抽象基类,不能被实例化。
为什么要引和抽象基类和纯虚函数呢?原因有以下两点:
(1)为了方便使用多态特性。
(2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、狮子等子类,但动特本身生成对象明显不合常理。抽象基类不能够被实例化,
它定义的纯虚函数相当于接口,能把派生类的共同行为提取出来。
以上面的动特、老虎、狮子类为例:
#include <iostream>
#include <memory.h>
#include <assert.h>
using namespace std;
class Animal
{
public:
virtual void sleep() = 0;//纯虚函数,必须在派生类被定义
virtual void eat() = 0;//纯虚函数,必须在派生类被定义
};
class Tiger: public Animal
{
public:
void sleep() {cout << "Tiger sleep" << endl;}
void eat() {cout << "Tiger eat" << endl;}
};
class Lion: public Animal
{
public:
void sleep() {cout << "Lion sleep" << endl;}
void eat() {cout << "Lion eat" << endl;}
};
int main()
{
Animal *p; //Animal指针,不能使用Animal animal定义对象
Tiger tiger;
Lion lion;
p = &tiger; //指向Tiger对象
p->sleep(); //调用Tiger::sleep()
p->eat(); //调用Tiger::eat()
p = &lion; //指向Lion对象
p->sleep(); //调用Lion::sleep()
p->eat(); //调用Lion::eat()
return 0;
}
执行结果:
实示上,利用抽象类Animal把动物的共同行为抽出来了,那就是:不管是什么动物,都需要睡觉和吃食物。在上面的代码中,Animal有两个纯虚函数分别对应这两个行为,因此Animal为抽象基类,不能被实例化。Animal的两个纯虚函数sleep()和eat()在它的子类Tiger和Lion中都被定义了(如果子类中有一个基类的纯虚函数没有定义,那么子类也是抽象类)。虽然不能使用Animal animal的方式生成Animal对象,但可以使用Animal的指针指向Animal的派生类Tiger和Lion,使用指针调用Animal类中的接口(纯虚函数)完成多态。
面试题17 虚函数与纯虚函数有什么区别
【解析】
虚函数和纯虚函娄有以下方面的区别。
(1)类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
(2)虚函数在子类里面也可以不重载;但纯虚函数必须在子类去实现,这就像Java的接口一样。通常把很多函数加上virtual,
是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为很难预料到父类里面的这个函数不在子类里面不去修改它的实现。
(3)虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现。当然,大家也可以完成自已的实出。纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类完成。
(4)带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。
面试题18 程序找错——抽象类不能实例化
#include <iostream>
using namespace std;
class Shape
{
public:
Shape() {}
~Shape() {}
virtual void Draw() = 0;
};
int main()
{
Shape s1;
return 0;
}
【答案】
Shape类的Draw()函数是一个纯虚函数,因此Shape类就是一个抽象类,它是不能实例化一个对象的。因此代码第14行出现编译错误。解决办法是把Draw函数修改成一般的虚函数或者把s1定义成Shape的指针。
面试题19 应用题——用面向对象的方法进行设计
编写一个与图形有关的应用程序,需要处理大量图形(shape)信息。图形有矩形(Rectangle)、正方形(Square)、圆形(Circle)等种类,应用需要计算出这些图形的面积,并且可能需要在某个设备上进行显示(使用在标准输出上打印信息的方式作为示意)。
A,请用面向对象的方法对以上应用进行设计,编写可能需要的类。
B,请给出实现以上应用功能的示例性代码,从某处获取图形信息,并且进行计算和绘。;
C,Square是否继承自Rectangle?为什么?
【解析】
显然,不能说一个形状能有什么对象,而是说长方形或圆形等具体的图形类有对象。因此Shape为抽象类,其派生类有Rectangle和Circle等具体图形类。那定义形状(Shape)类有什么用处呢?显示,任何图形都有面积(Area),并且都能被显示(Draw),因此把这些共同的行为抽象出来作为Shape类的方法。
由于Shape类为抽象类,因此这些方法在Shape类中就是纯虚函数,代码如下。
#include <iostream>
using namespace std;
#define PI 3.14159 //圆周率
//形状类
class Shape {
public:
Shape(){}
~Shape() {}
virtual void Draw() = 0; //纯虚函数
virtual double Area() = 0; //纯虚函数
};
//长方形类
class Rectangle :public Shape {
public:
Rectangle() :a(0), b(0) {}
Rectangle(int x, int y) :a(x), b(y) {}
virtual void Draw()
{
cout << "Rectangle,area:" << Area() << endl;
}
//
virtual double Area() { return a*b; }
private:
int a;
int b;
};
//圆形类
class Circle :public Shape {
public:
Circle(double x) :r(x) {}
virtual void Draw()
{
cout << "Circle,area:" << Area() << endl;
}
virtual double Area() { return PI*r*r; }
private:
double r;
};
//正方形类
class Square :public Rectangle {
public:
Square(int length):a(length){}
virtual void Draw()
{
cout << "Square,area:" << Area() << endl;
}
virtual double Area() { return a*a; }
private:
int a;
};
//
int main() {
Rectangle rect(10, 20);
Square square(10);
Circle circle(8);
Shape *p; //抽象类指针
p = ▭
cout << p->Area() << endl; //调用Rectangle::Area()
p->Draw(); //调用Rectangle::Draw()
p = □
cout << p->Area() << endl; //调用Square::Area()
p->Draw(); //调用Square::Draw()
p = &circle;
cout << p->Area() << endl; //调用Circle::Area()
p->Draw(); //调用Circlee::Draw()
return 0;
}
在主函数中,使用了Shape类的指针去访问不同图形类的Draw()和Area()方法。这样,Shape类中的Draw()和Area()纯虚函数就被认为是接口,只要使用Shape类指针操作这些接口就可以了,而不用关心是子类中的具体实现。
实际上,正方形也可以直接继承自Shape类,但由于正方形可以看成是长和宽相等的长方形,可以认为是一种特殊的长方形,所以这里它继承自Rectangle类。这样做的好处是操作方便,比如说Rectangle中如果存在一个如下的虚函数:
virtual void foo() { cout << "Rectangle" << endl;}
注意,这个虚函数表示的是长方形的行为,而不是属于形状(Shape)的行为,并且如果这个foo()同时也属于正方形的行为,那么可以在Square类中对其进行覆盖。
virtual void foo() { cout << "Square" << endl;}
于是可以用Rectangle类指针操作Square类对象以达到多态。
当然,也会带来一些性能上的问题。大家知道,Square类继承Rectangle类,于是Square继承了Rectangle的虚表。如果Rectangle存在不同于Shape类的虚函数,则这张虚表所包括的项目就会增加。因此Square会有更多的虚表使用开销,导致程序执行效率上的下降。