多继承、虚函数、虚继承

父类子类指针函数调用注意事项---这部分是没有虚函数的说明
1,如果以一个基础类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态联翩)
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),这种做法很危险,也不符合生活习惯,在程序设计上也会给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定。

举个例子:

#include <iostream> 
using namespace std; 
class A 

public: 
    void foo() 
    { 
        cout << "A's foo()" << endl; 
        bar(); 
    } 
    void bar() 
    { 
        cout << "A's bar()" << endl; 
    } 
}; 
class B: public A 

public: 
    void foo() 
    { 
        cout << "B's foo()" << endl; 
        A::foo(); 
    } 
    void bar() 
    { 
        cout << "B's bar()" << endl; 
    } 
}; 
int main() 

   A aobj;
    B bobj; 
    A *aptr1 = &aobj; 

     A *aptr2 = &bobj; //虽然实际指向的对象是B派生类,但是为A基类的指针,所以与上面第三条相符
    aptr1->foo(); 
    aptr2->foo();

   aobj.foo();//正常调用A基类的成员函数

   bobj.foo();//调用B派生类的成员函数

aptr1->foo()输出结果是:A's foo()     A's bar()

aptr2->bar()输出结果是: A's foo() ; A's bar() 

aptr1.foo();输出结果是:A's foo();   A's bar();

aptr2.foo();输出结果是:    B's foo();  A's fool(); B's bar();


虚拟函数就是为了对“如果你以一个基础类指针指向一个衍生类对象,那么通过该指针,你只能访问基础类定义的成员函数”这条规则反其道而行之的设计。
如果你预期衍生类由可能重新定义一个成员函数,那么你就把它定义成虚拟函数( virtual )。
polymorphism就是让处理基础类别对象的程序代码能够通透的继续适当地处理衍生类对象。
纯虚拟函数:
virtual void myfunc ( ) =0;
纯虚拟函数不许定义其具体动作,它的存在只是为了在衍生类钟被重新定义。只要是拥有纯虚拟函数的类,就是抽象类,它们是不能够被实例化的(只能被继承)。如果一个继承类没有改写父类中的纯虚函数,那么他也是抽象类,也不能被实例化。
抽象类不能被实例化,不过我们可以拥有指向抽象类的指针,以便于操纵各个衍生类。
虚拟函数衍生下去仍然是虚拟函数,而且还可以省略掉关键字“virtual”。
看个例子:
#include <iostream> 
using namespace std; 
class A 

public: 
    virtual void foo() 
    { 
        cout << "A's foo()" << endl; 
        bar(); 
    } 
    virtual void bar() 
    { 
        cout << "A's bar()" << endl; 
    } 
}; 
class B: public A 

public: 
    void foo() 
    { 
        cout << "B's foo()" << endl; 
        A::foo(); 
    } 
    void bar() 
    { 
        cout << "B's bar()" << endl; 
    } 
}; 
int main() 

    B bobj; 
    A *aptr = &bobj; 
    aptr->foo(); 
    A aobj = *aptr; //转化为A类对象
    aobj.foo(); 

 

aptr->foo()输出结果是:
   B's foo()//这个明白,多态性
   A's foo()//这个也明白,执行A::foo();
   B's bar()//虽然调用的是这个函数:A::foo(); 但隐式传入的还是bobj 的地址,所以再次调用bar();调用时还是会调用B的函数, 与虚函数指针有关
aobj.foo()输出结果是:
  A's foo() //这个不是指针,aobj完全是一个A的对象,与多态没有关系

  A's bar() 

1.虚析构函数

class Base
{
public:
   Base(){}
   
virtual ~Base(){}
};

class Derived: public Base
{
public:
   Derived(){};
   
~Derived(){};
}

void foo()
{
   Base 
*pb;
   pb 
= new Derived;
   delete pb;

这是正确的用法,会发生动态绑定,它会先调用Derived的析构函数,然后是Base的析构函数
如果析构函数不加virtual,delete pb只会执行Base的析构函数,而不是真正的Derived析构函数。
因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base

2.纯虚析构函数

1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。 

2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 

3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
 
4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。 

5) 纯虚函数通常没有定义体,但也完全可以拥有

6)  析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 

7) 非纯的虚函数必须有定义体,不然是一个错误。 

8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的

虚函数必须是基类的静态成员函数,其访问权限可以是protected或public,在基类的类定义中定义虚函数的一般形式:  

     virtual 函数返回值类型 虚函数名(形参表)
         { 函数体 }
      虚函数的作用是实现动态联编(普通成员函数没有这个功能,只能简单的调用与重写),也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
     即实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略。 
  动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式:   指向基类的指针变量名->虚函数名(实参表) 或 基类对象的引用名. 虚函数名(实参表)  

条件

  所以,从以上程序分析,实现动态联编需要三个条件:
  1、 必须把动态联编的行为定义为类的虚函数。
  2、 类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来。
  3、 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。
 class A{
  public:
  void print(){ cout<<”This is A”<<endl;}
  };
 class B:public A{
  public:
  void print(){ cout<<”This is B”<<endl;}
  };
  int main(){ //为了在以后便于区分,我这段main()代码叫做main1
  A a;
  B b;
  a.print();
  b.print();
  }
  通过class A和class B的print()这个接口,可以看出这两个class因个体的差异而采用了不同的策略,输出的结果也是我们预料中的,分别是This is A和This is B。但这是否真正做到了多态性呢?No,多态还有个关键之处就是一切用指向基类的指针或引用来操作对象。那现在就把main()处的代码改一改。
  int main(){ //main2
  A a;
  B b;
  A* p1=&a;
  A* p2=&b;
  p1->print();
  p2->print();
  }
  运行一下看看结果,哟呵,蓦然回首,结果却是两个This is A。问题来了,p2明明指向的是class B的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数
  class A{
  public:
  virtual void print(){ cout<<”This is A”<<endl;} //现在成了虚函数了
  };
  class B:public A{
  public:
  void print(){ cout<<”This is B”<<endl;} //这里需要在前面加上关键字virtual吗?
  };
  毫无疑问,class A的成员函数print()已经成了虚函数,那么class B的print()成了虚函数了吗?回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了。
  现在重新运行main2的代码,这样输出的结果就是This is A和This is B了。
  现在来消化一下,我作个简单的总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
  使用虚函数,我们可以灵活的进行动态绑定,当然是以一定的开销为代价。 如果父类的函数(方法)根本没有必要或者无法实现,完全要依赖子类去实现的话,可以把此函数(方法)设为virtual 函数名=0 我们把这样的函数(方法)称为 纯虚函数
  如果一个类包含了纯虚函数,称此类为抽象类 abstract class是抽象类,至少包含一个纯虚函数的类就叫做抽象类。
       抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
      抽象类是不能定义对象的。

     但是如果一个类,所有的成员都是纯虚函数,那么它和一般的抽象类在用法上是有区别的。至少Microsoft给的COM接口定义全部都是仅由纯虚函数构成的类。因此把这样的类定义叫做纯虚类也不算错。

     纯虚函数和虚函数的区别在于前者不包含定义,而后者包含函数体。

      那么纯虚类就是不包含任何实现(包括成员函数定义和成员变量定义。前者代表算法,后者代表结构)。不包含任何算法和结构的类叫做纯虚类,应该没有问题。
      Java里没有纯虚函数这个概念。Java中 虚函数 叫做 abstract function, 抽象类 叫做 abstract class,Java根本没有virtual这个关键字,都用abstract代替,因此Java里面根本就没有Pure这个概念。有的就是interface。在interface里面定义的函数都不能有函数体,这个在Java里面叫做接口。
       那么C++里面与interface等同的概念就是纯虚类了,C++用 纯虚类 来模拟 interface 这个抽象概念,因此这里说的“纯虚类”与 Java的abstract class、C++的一般抽象类 不同。“纯虚类”与C++一般抽象类的区别就好比Java里面interface 和 abstract class的区别。
1)接口中不能有非抽象方法,但抽象类中可以有。
2)一个类能实现多个接口,但只能有一个父类。
3)接口并不属于继承结构,它实际与继承无关,因此无关的类也可以实现同一个接口。

 抽象类(abstract class)和接口(interface)的概念是面向对象设计中常用的概念, 也是比较容易混淆的概念. 在这里, 引用网上一种区分它们的思路(我感觉挺有道理):
1. 如果一个类B在语法上继承(extend)了类A, 那么在语义上类B是一个类A.
2. 如果一个类B在语法上实现了(implement)接口I, 那么类B遵从接口I制定的协议.
------------------------------------------------------------------------------------------------
使用abstract class的根本原因在于, 人们希望通过这样的方式, 表现不同层次的抽象. 

而interface的本质是一套协议. 在程序设计的发展中, 人们又发现接口可以用来表示对行为的抽象, 不过, 这只是interface的一种用法不是其本质.

------------------------------------------------------------------------------------------------

理论结合实际才是最好的学习方式, 不过在这里, 我只想举一些我见到过关于接口使用的反面教材:

1. 在接口中包含数据成员. 这几乎肯定是错的, 因为协议是规范是标准, 不应该跟具体实现有任何牵连, 也不应该给具体实现造成任何负担.

2. C++中 delete 掉一个接口. 例如:
class IInterface() 

Public: 
Virtual ~IInterface(){}; 
… 

Class ClassImpl : public IInterface 

… 

Int main() 

IInterface* pInterface = new ClassImpl(); 
… 
delete pInterface; 


从语法的角度和语言自身的角度来看, 这是可行的, 而且只要将接口的析构函数设置为virtual, 就能避免内存泄漏. 但我要说, 这不是语法和语言的问题, 而是从根本上就错了. 因为接口是一套协议, 一套规范, 并不是实现. Delete 一个接口的代码, 到底想要表达什么样的语义? 如果一段代码从语义上都说不通, 就不应该出现在程序中.

要在C++中表现接口的概念, 一种做法是这样:

class IInterface 

public: 
virtual void DoSomething() = 0; 


// 不应当有析构函数, 因为从语义上说, 接口是不能delete的. 


如果要delete, 只能delete一个类的实例:
Class A 

Public: 
Virtual ~A(); 
Public: 
Virtual void DoSomething() = 0; 

Class B : public A 

… 

Int main() 

A* pA = new B(); 
… 
Delete pA; 


我们可以这样做, 因为pA对应的是一个实例, 我们可以在A这一层将其销毁.

先举个例子,方便大家理解,然后从例子中抽象概括出结理论。

比如,一家生产门的公司,需要先定义好门的模板,以便能快速生产出各种规格的门。 
这里的模板通常会有两类模板:抽象类模板和接口模板。
抽象类模板:这个模板里面应该包含所有门都应该具有的共同属性(如,门的形状和颜色等)和共同行为(如,开门和关门)。
接口模板:有些门可能需要具有报警和指纹识别等功能,但这些功能又不是所有门必须具有的,所以像这样的行为应该放在单独的接口中。
有了上面的两类模板,以后生产门就很方便了:利用抽象类模板和包含了报警功能的接口模板就能生产具有报警功能的门了。同理,利用抽象类模板和包含了指纹识别功能的接口模板就能生产具有指纹识别功能的门了。

总之:抽象类用来抽象自然界一些具有相似性质和行为的对象。而接口用来抽象行为的标准和规范,用来告诉接口的实现者必要按照某种规范去完成某个功能。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值