C++部分——C++继承和多态(3)

1.多重继承和虚拟继承

#include <iostream>
 using namespace std; 
class Parent{ 
publicParent():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<<"CHild1()"<<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都改为虚拟继承父,输出结果又是什么?
解析:首先讨论不存在虚拟的继承情况。
多重继承类对象的构造顺序与继承列表中基类的个结果排列顺序harmony和谐,而不是与构造函数的初始化列表顺序一致。在这里,派生继承的顺序是Child1,CHILD2(第24行),因此按照下面的步骤构造。1)构造Child1。由于Child1继承自父,因此先调用家长的构造函数,再调用Child1的构造函数。2)调用CHILD2。过程与(1)类似,先调用父的构造函数,再调用CHILD2的构造函数。3)调用派生类的构造函数。因此输出结果为:1.Parent(INT)2.Child1(INT)3.Parent(INT)4.Child2(INT)现在说明Child1和CHILD2都改为虚拟继承父情况。当Child1和CHILD2为虚拟继承时,当系统碰到多重继承的时候就会自动先加入一个虚拟基类(父)的拷贝。即首先调用了虚拟继承(家长)的构造函数了,在Child1和CHILD2指定调用父的构造函数就无效了。输出结果为:家长Child1(INT)CHILD2(INT)现在来总结一下,多继承中的构造函数顺序如下:1)任何虚拟基类的构造函数按照它们被继承的顺序构造2)任何非虚拟基类的构造函数按照它们被构造的顺序构造。3)任何成员对象的构造按照它们声明的顺序调用。4)类自身的构造函数。

2.为什么要引入抽象基类和纯虚函数

纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像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<<"Tiger eat"<<endl;}
};

void 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()
}

这里写图片描述
实际上,利用抽象类Animal把动物的共同行为抽出来了,那就是说:不管是什么动物,都需要睡觉和吃食物。在上面的代码中,Animal有两个纯虚函数分别对应这两个行为,因此Aninal为抽象基类,不能被实例化。Animal的两个纯虚函数sleep()和eat()在它的子类Tiger和Lion中都被定义了(如果子类中有一个基类的纯虚函数没有定义,那么子类也是抽象类)。虽然不能使用Animal animal的方式生成Animal对象,但可以使用Animal的指针指向Animal的派生类Tiger和Lion,使用指针调用Animal类中的接口(纯虚函数)完成多态。

3.虚函数与纯虚函数有什么区别?

1)类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译器就可以使用后期绑定来达到多态。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里面去实现。
2)虚函数在子类里面也可以不重载,但纯虚函数必须在子类去实现,这就像java的接口一样。通常把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为很难预料到父类里面的这个函数不在子类里面不去修改它的实现。
3)虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现。当然,大家也可以完成自己的实现。纯虚函数的类用于“介面继承”,即纯虚函数关注的是按接口的统一性,实现由子类完成。
4)带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类叫做抽象类 。

4.程序找错——抽象类不能实例化

#include<iostream>
using namespace std;
class Shape{public: 
    Shape(){}   
    ~Shape(){} 
    virtual void Draw()=0;
}
void main()
{   
Shape s1;
}

Shape类的Draw()函数是一个纯虚函数,因此Shape类就是一个抽象类,它是不能实例化一个对象的。因此代码第14行出现编译错误。解决方法是把Draw函数修改成一般的虚函数或者把s1定义成Shape的指针。

5.应用题——用面向对象的方法进行设计

编写一个与图形相关的应用程序,需要处理大量图形(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=&rect;    
    cout<<p->Area()<<endl;//调用Rectangle::Area() 
    p->Draw();//调用Rectangle::Draw() 
    p=&square;  
    cout<<p->Area()<<endl;//调用Square::Area()    
    p->Draw();//调用Square::Draw()    
    p=&circle;  
    cout<<p->Area()<<endl;//调用Circle::Area()    
    p->Draw();//调用Circle::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会有更多的虚表使用开销,导致程序执行效率上的下降。

6.什么是COM

COM即组件对象模型,它定义了一种二进制标准,使得任何编程语言存取它所编写的模块。

7.COM组件有什么特点

COM组件是遵循COM规范编写,以Win32动态链接库(DLL)或可执行文件(EXE)形式发布的可执行二进制代码,能够满足对组件架构的所有需求。遵循COM的规范标准,组件与应用,组件与组件之间可以互操作,极其方便地建立可伸缩的应用系统。COM是一种技术标准,其商业品牌为ActiveX。组件在应用开发方面具有以下特点:
1)组件是与开发工具语言无关的。开发人员可以根据特定情况选择特定语言工具实现组件的开发。编译之后的组件以二进制的形式发布,可跨Windows平台使用,而且源程序代码不会外泄,有效的保证了组件开发者的版权。
2)通过接口有效保证了组件的复用性。一个组件具有若干个接口,每个接口代表组件的某个属性或方法。其他组件或应用程序可以设置或调用这些属性和方法来进行特定的逻辑处理。组件和应用程序的连接是通过接口实现的。负责集成的开发人员无须了解组件功能是如何实现的,只需简单地创建组件对象并与其接口建立连接。在保证接口一致性地前提下,可以调换组件,更新版本,也可以把组件安插在不同的应用系统中。
3)组件运行效率高,便于使用和管理。因为组件是二进制代码,所以运行效率比ASP脚本高很多。核心的商务逻辑计算任务必须由组件来担当,ASP脚本只有组装的角色。而且组件在网络上的位置可被透明分配,组件和使用它的程序能在同一个进程中,不同进程中或不同机器上运行。组件之间是相互独立的。组件对象通过一个内部引用计数器来管理它自己的生存期,这个计数器存放任何时候连接到该对象的客户数。当引用计数变为0时,对象可以把自己从内存中释放掉。这使程序员不必考虑与提供可共享资源有关的问题。

8.如何理解COM对象和接口?

一个对象实现一个接口,意思就是该对象使用代码实现了接口的每个方法并且为这些函数通向COM库提供了COM的二进制指针。然后COM使这些函数运行在请求了一个指向该接口的任何客户端。
COM在接口的定义和实现上有根本的差别。接口实际上是由一组定义了用法的相互联系的函数原型组成,只是它不够被实现。这些函数原型就相当于C++中含有纯虚拟函数的基类。
一个接口定义制订了接口的成员函数,调用方法,返回类型,它们的参数的类型和数量以及这些函数要干什么。但是,这里并没有与接口实现相关的东西。
接口的实现就是程序员在一个接口定义上提供的执行相关动作的代码。客户调用完全取决于接口的定义。接口实现的一个实例,实际上就是一个指向一组方法的指针,即是指向一个接口的函数表,该函数表引用了该接口所有方法的实现。每个接口是一个固定的一组方法的集合,在运行时通过IID来定位。这里,IID是com支持的GUID的特殊的实例。这样做就不会产生单一系统上相同名字,接口的多个版本的COM之间的冲突了。
一个COM接口与C++类是不一样的。一个COM接口不是一个对象,它只是简单地关联一组函数,是客户和程序通信的二进制标准。只要它提供了指向接口方法的指针,这个对象就可以用任何语言来实现。COM接口是强类型的——每个接口都有它自己的接口标识符。另外,不能用老版本的接口标志符定义新的版本,接口的IID定义的接口合同是明确,唯一的。
继承在COM里并不意味这代码的重用。因为接口没有实现关联,接口继承并不意味着代码继承。意思仅仅是,一个接口同一个合同关联,就像C++的纯虚函数基类的创建和修改一样,可以添加方法或者更进一步的加强方法的使用。在COM里没有选择性继承。如果一个接口由另一个接口继承的话,它就包含了另一个接口定义的所有的方法。
管理实现一个COM对象的IUknow::QueryInterface方法有三个主要规则:
1)对象必须有一个标识符;
2)一个对象实例的接口集合必须是静态的(static)。
3)在对象中从任何一个其他的接口查询此接口都应该成功。

9.简述COM,ActiveX和DCOM

COM即组件对象模型,是组件之间相互接口的规范。其作用是使各种软件构件和应用软件能够用一种统一的标准方式进行交互。COM不是一种面向对象的语言,而是一种与源代码无关的二进制标准。
ActiveX是MIcrosoft提出的一套基于COM的构件技术标准,实际上是对象嵌入和连接(OLE)的新版本。
基于分布式环境下的COM被称作DCOM,它实现了COM对象与远程计算机上的另一个对象之间直接进行交互。DCOM规范定义了分散对象创建和对象间通信的机制,DCOM是ActiveX的基础。因为ActiveX主要是针对internet应用开发(相比OLE)的技术,当然也可以用于普通的桌面应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值