虚基类、纯虚函数、抽象类

 

虚基类、纯虚函数和抽象类

1、虚基类   
          在《多继承》中讲过的例子中,由类A,类B1和类B2以及类C组成了类继承的层次结构。在该结构中,类C的对象将包含两个类A的子对象。由于类A是派生类C两条继承路径上的一个公共基类,那么这个公共基类将在派生类的对象中产生多个基类子对象。如果要想使这个公共基类在派生类中只产生一个基类子对象,则必须将这个基类设定为虚基类。  
          虚基类的引入和说明   
           前面简单地介绍了要引进虚基类的原因。实际上,引进虚基类的真正目的是为了解决二义性问题。   
  虚基类说明格式如下:
         
  virtual   <
继承方式><基类名>   

     其中,virtual是虚类的关键字。虚基类的说明是用在定义派生类时,写在派生类名的后面。例如:   
      class   A   
  {   
  public:   
          void   f();   
  protected:   
          int   a;   
  };   
  class   B   :   virtual   public   A   
  {   
  protected:   
          int   b;   
  };   
  class   C   :   virtual   public   A   
  {   
  protected:   
          int   c:   
  };   
  class   D   :   public   B,   public   C   
  {   
  public:   
          int   g();   
  private:   
          int   d;   
  };       
  由于使用了虚基类,使得类A,类B,类C和类D之间关系用DAG图示法表示如下:   

                                    A{   f(),   a   }   
                                      /           /   
                                    B{b}     C{c}   
                                      /           /   
                                    D{g(),d}      
          从该图中可见不同继承路径的虚基类子对象被合并成为一个对象。这便是虚基类的作用,这样将消除了合并之前可能出现的二义性。这时,在类D的对象中只存在一个类A的对象。因此,下面的引用都是正确的:       

  D   n;   
  n.f();   //对f()引用是正确的。   
  void   D::g()   
  {   
          f();   //对f()引用是正确的。   

  }       
  下面程序段是正确的。       
  D   n;   
  A   *pa;   
  pa   =   &n;   
              其中,pa是指向类A对象的指针,n是类D的一个对象,&nn对象的地址。pa=&n是让pa指针指向类D的对象,这是正确的,并且也无二义性。   

      虚基类的构造函数   
              前面讲过,为了初始化基类的子对象,派生类的构造函数要调用基类的构造函数。对于虚基类来讲,由于派生类的对象中只有一个虚基类子对象。为保证虚基类子对象只被初始化一次,这个虚基类构造函数必须只被调用一次。由于继承结构的层次可能很深,规定将在建立对象时所指定的类称为最派生类。C++规定,虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始列表中必须列出对虚基类构造函数的调用。如果未被列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象。   
              从虚基类直接或间接继承的派生类中的构造函数的成员初始化列表中都要列出这个虚基类构造函数   的调用。但是,只有用于建立对象的那个最派生类的构造函数调用虚基类的构造函数,而该派生类的基类中所列出的对这个虚基类的构造函数调用在执行中被忽略,这样便保证了对虚基类的对象只初始化一次。   
             C++又规定,在一个成员初始化列表中出现对虚基类和非虚基类构造函数的调用,则虚基类的构造函数先于非虚基类的构造函数的执行。   
      下面举一例子说明具有虚基类的派生类的构造函数的用法。   
      #include   <iostream.h>   
  class   A   
  {   
  public:   
          A(const   char   *s)   {   cout<<s<<endl;   }   
          ~A()   {}   
  };       
  class   B   :   virtual   public   A   
  {   
  public:   
          B(const   char   *s1,   const   char   *s2):A(s1)   
          {   
                  cout<<s2<<endl;   
          }   
  };       
  class   C   :   virtual   public   A   
  {   
  public:   
          C(const   char   *s1,   const   char   *s2):A(s1)   
          {   
          cout<<s2<<endl;   
          }   
  };      
  class   D   :   public   B,   public   C   
  {   
  public:   
          D(const   char   *s1,   const   char   *s2,   const   char   *s3,   const   char   *s4)   
                  :B(s1,   s2),   C(s1,   s3),   A(s1)   
          {   
                  cout<<s4<<endl;   
          }   
  };   
      void   main()   
  {   
          D   *ptr   =   new   D("class   A",   "class   B",   "class   C",   "class   D");   
          delete   ptr;   
  }     
  该程序的输出结果为:       

          class   A   
          class   B   
          class   C   
          class   D   
              在派生类BC中使用了虚基类,使得建立的D类对象只有一个虚基类子对象。   

              在派生类BCD的构造函数的成员初始化列表中都包含了对虚基类A的构造函数。   
              在建立类D对象时,只有类D的构造函数的成员初始化列表中列出的虚基类构造函数被调用,并且仅调用一次,而类D基类的构造函数的成员初始化列表中列出的虚基类构造函数不被执行。这一点将从该程序的输出结果可以看出。  
 2、纯虚函数和抽象类   
          纯虚函数是一种特殊的虚函数,它的一般格式如下:   
  class   <类名>   
  {   
          virtual   <类型><函数名>(<参数表>)=0;   
            
  };     
          在许多情况下,在基类中不能对虚函数给出有意义有实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。下面给出一个纯虚函数的例子。    
#include <iostream>

class   point  

{  

    public:  

       point(int   i=0,   int   j=0)   {   x0=i;   y0=j;   }  

       virtual   void   set()   =   0;  

       virtual   void   draw()   =   0;  

    protected:  

       int   x0,   y0;  

};  

 

class   line   :   public   point  

{  

    public:  

       line(int   i=0,   int   j=0,   int   m=0,   int   n=0):point(i,   j)  

       {  

           x1=m;   y1=n;  

       }  

       void   set()   {   std::cout<<"line::set()   called.\n";   }  

       void   draw()   {   std::cout<<"line::draw()   called.\n";   }  

    protected:  

       int   x1,   y1;  

    };  

 

class   ellipse   :   public   point  

{  

    public:  

       ellipse(int   i=0,   int   j=0,   int   p=0,   int   q=0):point(i,   j)  

       {  

           x2=p;   y2=q;  

       }  

       void   set()   {   std::cout<<"ellipse::set()   called.\n";   }  

       void   draw()   {   std::cout<<"ellipse::draw()   called.\n";   }  

    protected:  

       int   x2,   y2;  

};  

 

void   drawobj(point   *p)  

{  

    p->draw();  

}  

 

void   setobj(point   *p)  

{  

    p->set();  

}  
    #include <iostream>

#include "test.h"

 

using namespace std; 

int   main()  

{  

    line   *lineobj   =   new   line;  

    ellipse   *elliobj   =   new   ellipse;  

    drawobj(lineobj);  

    drawobj(elliobj);  

    cout<<endl;  

    setobj(lineobj);  

    setobj(elliobj);  

    cout<<"\nRedraw   the   object...\n";  

    drawobj(lineobj);  

    drawobj(elliobj); 

    return 0;

}  

  抽象类      
          带有纯虚函数的类称为抽象类。抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。   
          抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。   
          抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。  
          抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

                                                                                                                  ----摘自《C++面向对象程序设计基础教程》
<think>首先,用户的问题是关于C++中基类纯虚函数接受继承类函数重载的规则和限制。用户引用了几个站内引用,这些是关于虚函数和纯虚函数的讨论。关键点:-纯虚函数在基类中声明但没有定义,派生类必须实现它们。-纯虚函数使类成为抽象类,不能实例化。-派生类必须覆盖纯虚函数,否则派生类也变成抽象类。-用户问的是“基类纯虚函数是否可以接受继承类中的函数重载”,意思是派生类是否可以重载纯虚函数,以及规则是什么。从引用中提取相关信息:-引用[1]:提到如果派生类没有正确覆盖虚函数(比如参数不匹配),它不会覆盖基类的版本。例子中,基类有`virtualvoida()`,派生类有`voida(inta)`,这没有覆盖虚函数。-引用[2]:派生类必须实现基类的纯虚函数,否则派生类也是抽象类。-引用[3]:纯虚函数要求派生类必须重载以实现多态性。-引用[4]:纯虚函数必须在派生类中实现;派生类不重写纯虚函数会导致它也是抽象类。-引用[5]:派生类必须实现基类的纯虚函数。所以,核心规则是:派生类必须覆盖基类的纯虚函数,否则派生类无法实例化。覆盖意味着函数签名必须匹配。现在,关于“函数重载”:在C++中,函数重载指的是在同一个作用域中定义多个同名函数但参数不同。派生类可以重载基类中的函数,但覆盖(override)虚函数需要签名完全匹配。具体到用户的问题:-基类纯虚函数:例如`virtualvoidfunc()=0;`-派生类中,如果要覆盖它,函数签名必须相同:返回类型、函数名、参数列表。-如果派生类定义一个同名但参数不同的函数,它不是覆盖,而是重载或隐藏基类函数。-在纯虚函数的上下文中,派生类必须提供与纯虚函数签名完全匹配的实现。规则和限制:1.**覆盖要求签名匹配**:派生类的函数必须与基类的纯虚函数在签名上完全一致(返回类型、函数名、参数类型)。如果不同,它不会覆盖纯虚函数。2.**派生类成为抽象类**:如果派生类没有覆盖纯虚函数(即没有提供匹配的函数),派生类会继承纯虚函数并成为抽象类,不能实例化。3.**函数重载在派生类**:派生类可以定义与基类函数同名的函数但参数不同,但这不算是覆盖纯虚函数。基类的纯虚函数仍然需要被覆盖。4.**使用`override`关键字**:在C++11及以后,使用`override`关键字可以确保函数意图覆盖基类虚函数;如果签名不匹配,编译器会报错。实现机制:-纯虚函数在基类中声明为`virtualreturn_typefunction_name(parameters)=0;`-派生类中定义`return_typefunction_name(parameters)override{...}`(`override`是可选的,但推荐使用)-如果签名不匹配,派生类版本不覆盖纯虚函数纯虚函数保持未实现状态,使派生类抽象。例子:-基类:`classBase{public:virtualvoidfunc()=0;};`-有效派生:`classDerived:publicBase{public:voidfunc()override{/*implementation*/}};`→覆盖成功,派生类可实例化。-无效:`classDerived:publicBase{public:voidfunc(int){/*something*/}};`→没有覆盖纯虚函数`func()`,所以`Derived`是抽象类,不能创建对象。回答结构:1.**引言**:解释纯虚函数抽象类的概念。2.**规则**:详细说明覆盖纯虚函数的要求。3.**限制**:讨论签名不匹配时的后果。4.**实现机制**:简要描述编译器如何处理。5.**示例**:提供代码示例。6.**相关问题**:生成相关问题。确保使用中文回答。数学表达式:如果有数学内容,用`$...$`或`$$...$$`,但这里主要是C++代码,所以代码块用反引号。在回答中,自然地添加引用标识,如[^1],如果引用站内内容。用户提供了引用,我需要参考它们,但不要直接复制;整合信息。最后,生成相关问题。</think>### C++基类纯虚函数接受继承类函数重载的规则和限制 在C++中,纯虚函数(pure virtual function)是基类中声明但未定义的虚函数,形式为 `virtual 返回类型 函数名(参数) = 0;`。它使基类成为抽象类(abstract class),不能被实例化。派生类必须覆盖(override)基类的纯虚函数,否则派生类也会成为抽象类。用户问题聚焦于“基类纯虚函数是否可以接受继承类中的函数重载”,这涉及到函数覆盖与重载的区别、规则和限制。以下是详细解析: #### 1. **核心规则:覆盖要求签名完全匹配** - **覆盖(Override)的定义**:派生类必须提供一个函数,其签名(函数名、返回类型、参数列表)与基类的纯虚函数**完全一致**。只有这样,派生类才能覆盖纯虚函数,使其成为可实例化的非抽象类[^3][^4]。 - **函数重载(Overload)的区别**:如果派生类定义了与基类纯虚函数同名但参数不同的函数,这属于函数重载(在派生类作用域内新增函数),**不是覆盖**。基类的纯虚函数仍然未实现,派生类因此保持抽象类状态[^1]。 - **示例代码**: ```cpp // 基类定义纯虚函数 class Base { public: virtual void func() = 0; // 纯虚函数,签名: void func() }; // 有效覆盖:签名匹配 class DerivedGood : public Base { public: void func() override { // 正确覆盖,签名相同 std::cout << "DerivedGood 实现 func()\n"; } }; // 无效"重载":签名不匹配,未覆盖纯虚函数 class DerivedBad : public Base { public: void func(int param) { // 参数不同,属于重载而非覆盖 std::cout << "DerivedBad 重载 func(int)\n"; } // DerivedBad 是抽象类,因为未覆盖 Base::func() }; int main() { DerivedGood obj1; // 可实例化,覆盖成功 // DerivedBad obj2; // 编译错误!DerivedBad 是抽象类 } ``` - 在 `DerivedBad` 中,`func(int)` 是派生类的新函数,与基类纯虚函数 `func()` 签名不同,因此未覆盖纯虚函数,导致 `DerivedBad` 成为抽象类[^1][^4]。 #### 2. **关键限制** - **派生类必须实现覆盖**:如果派生类未提供签名匹配的函数,编译器会报错,派生类无法实例化。例如,在引用[2]中,派生类 `BB` 必须实现基类 `AA` 的纯虚函数 `func()` 才能创建对象[^2][^3]。 - **返回类型协变(Covariant Return Types)的例外**:覆盖时,返回类型可以不同,但必须是基类返回类型的派生类(仅适用于指针或引用类型)。例如: ```cpp class Base { public: virtual Base* clone() = 0; }; class Derived : public Base { public: Derived* clone() override { // 返回类型协变:Derived* 是 Base* 的派生 return new Derived(*this); } }; ``` 这是覆盖的有效形式,不违反签名匹配规则[^4]。 - **隐藏(Hiding)问题**:如果派生类定义了同名但参数不同的函数,它会隐藏(hide)基类中的同名函数(包括纯虚函数)。调用时需显式指定作用域,但这不解决纯虚函数的覆盖需求: ```cpp DerivedBad obj; obj.func(10); // 调用派生类重载函数 // obj.func(); // 错误!Base::func() 被隐藏且未实现 ``` - **`override` 关键字的作用**:在C++11及以上,使用 `override` 关键字可显式声明意图覆盖。如果签名不匹配,编译器会报错,避免错误: ```cpp class DerivedSafe : public Base { public: void func(int) override; // 编译错误!签名不匹配 }; ``` 这增强了代码安全性[^4]。 #### 3. **实现机制** - **虚函数表(vtable)**:编译器为每个包含虚函数的类生成虚函数表。纯虚函数在基类vtable中指向一个占位符(如 nullptr)。派生类覆盖时,其vtable条目替换为派生类函数的地址。 - **抽象类检查**:在派生类定义时,编译器检查是否所有纯虚函数都被覆盖。若有未覆盖的,派生类vtable中对应条目保持无效,导致类抽象[^3][^5]。 - **多态性保证**:只有签名匹配的覆盖才能实现运行时多态。通过基类指针调用时,会动态绑定到派生类实现: ```cpp Base* ptr = new DerivedGood(); ptr->func(); // 正确调用 DerivedGood::func() ``` #### 4. **总结规则和限制** - **规则**: - 派生类必须提供与基类纯虚函数签名完全匹配的函数以覆盖它。 - 使用 `override` 关键字可辅助编译器检查。 - **限制**: - 签名不匹配的函数被视为重载或新函数,不覆盖纯虚函数。 - 派生类未覆盖纯虚函数时,自动成为抽象类,无法实例化。 - 返回类型协变仅适用于指针/引用类型。 - **最佳实践**:始终确保派生类函数签名精确匹配,并使用 `override` 避免错误。 通过以上机制,C++强制派生类实现纯虚函数的接口,确保类型安全和多态行为[^3][^4][^5]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值