分享

C 多态你必须了解这几点

 zeroxer2008 2022-01-13

大家好,我是行码棋。

今天继续C++的知识总结,C++的面向对象的多态性是非常重要的的,所以很有必要进行学习的。下面是C++多态性的相关描述。

1.赋值兼容性问题

赋值兼容规则为:可以将公有派生类对象赋值给基类对象,反之是不允许的。

赋值兼容与限制可归结为以下五点:

1.派生类对象可以赋值给基类对象,系统将派生类对象中从基类继承来的成员赋给基类对象。

2.不能将基类对象赋值给派生类对象。

3.私有或保护继承的派生类对象,不可以赋值给基类对象。

4.可将派生类对象的地址赋给基类的指针变量。例如 Point *ptr = &line;

5.派生类对象可初始化基类的引用。例如 Point &refp = line;

注意:在后两种情况下,使用基类指针或引用时,只能访问从相应基类中继承来的成员,而不允许访问其他基类成员或派生类中增加的成员。

2.多态性

多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。

图片

2.1.虚函数

「虚函数的作用:」

是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

它通过基类指针或引用,执行时会根据指针实际指向的对象的类,决定调用哪个对象的成员函数。

「声明形式:」

virtual<函数返回值><函数名>(形参表);

virtual 函数类型 函数名(形参表){ 函数体;}

  • 在类的声明中,在函数原型之前写virtual。
  • virtual 只用来说明类声明中的原型,不能用在「函数实现」时(就是使用函数的时候)。
  • 虚函数的定义是在基类中进行的,它是在基类中在那些需要定义为虚函数的成员函数的声明中冠以关键字 virtual。
  • 在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其函数原型,包括函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
#include<iostream>
using namespace std;
class B0
{

 public:
  virtual void print(const char *p)
        
//定义虚函数 print 
   cout<<p<<'print()'<<endl;
  }
};
class B1:public B0
{
 public:
  virtual void print(const char *p)
        
//重新定义虚函数 print 
   cout<<p<<'print()'<<endl;
  }
};
class B2:public B1
{
 public:
  virtual void print(const char *p)
        
//重新定义虚函数 print 
   cout<<p<<'print()'<<endl;
  }
};
int main()
{
 B0 ob0,*p; //定义基类对象 ob0 和对象指针 p
 p = &ob0; 
 p->print('B0::');   //调用基类 B0 的 print 
 B1 ob1;  //定义派生类 B1 的对象 
 p = &ob1;
 p->print('B1::');  //调用派生类 B1 的 print 
 B2 ob2;
 p = &ob2;
 p->print('B2::');
 return 0;
}
点击并拖拽以移动

运行结果:

B0::print()
B1::print()
B2::print()
点击并拖拽以移动

「注意:」

  • 若在基类中,只声明虚函数原型(需加上 virtual),而在类外定义虚函数时,则不必再加 virtual。代码示例:
class B
{

  public:
    virtual void print(const char *p);
};
void B::print(const char *p)
{
    cout<<p<<'print()'<<endl;
}
点击并拖拽以移动
  • 在派生类中,虚函数被重新定义时,其函数的原型与基类中的函数原型(即包括函数类型、函数名、参数个数、参数类型的顺序)都必须「完全相同」

  • 当一个成员函数被定义为虚函数后,其派生类中符合重新定义虚函数要求的同名函数都自动称为虚函数。在「派生类中」重新定义该虚函数时,「关键字 virtual 可以不写」。但是,为了使程序更加「清晰」「最好」在每一层派生类中定义该函数时都「加上关键字 virtual。」

  • 虚函数「必须是其所在类的成员函数」,而「不能是友元函数」,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。

  • 一个函数一经说明为虚函数,则无论说明它的类被继承了多少层,在每一层派生类中该函数将「永远保持其 virtual 特性」

  • 使用对象名和点运算符的方式调用虚函数是在编译时进行的,是「静态联编」「没有」利用虚函数的特性。只有通过「基类指针访问虚函数时」才能获得运行时的「多态性」

2.2.虚函数和重载函数

定义虚函数的目的是为了让派生类覆盖(Overriding)它。

覆盖「不同于」重载,它要求重新定义的函数在参数和返回值方面与原函数「完全相同」。否则将属于重载(参数不同)或导致一个编译错误(返回值类型不同)。与函数重载相同,虚函数也体现了 OOP 技术的多态性。

「虚函数和重载函数的对比:」

函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。

与重载不同的是: 同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。

函数名返回值参数束定时间适用范围语义相关性
虚函数运行时派生类一组类似函数
重载函数可不同不同编译时任意可语义无关

如果一组虚函数只有两个函数的返回值类型不一样,即使是这样,也会编译出错。

2.3.虚析构函数

在 C++ 中,不能声明虚构造函数,但是可以声明虚析构函数。

使用场景:

  • 可能通过基类指针删除派生类对象的时候
  • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。

语法格式:

virtual ~类名
点击并拖拽以移动

如果在主函数中用 new 运算符建立一个派生类的无名对象和定义了一个基类的对象指针,并将无名对象的地址赋给这个对象指针。

当用 delete 运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。

当撤销指针 P 所指的派生类的无名对象,而调用析构函数时,采用了静态联编方式,只调用了基类 A 的析构函数。

正如下面的代码:

#include<bits/stdc++.h>
using namespace std;
class A
{

public:
    ~A(){cout<<'调用基类 A 的析构函数\n';}
};
class B:public A
{
public:
    ~B(){cout<<'调用派生类 B 的析构函数\n';}
};
int main()
{
 A *p; //定义指向基类 A 的指针变量 p
 p = new B;//用运算符 new为派生类的无名对象动态地分配了一个存储空间,并将地址赋给对象指针p
 delete p;//用 delete 撤销无名对象,释放动态存储空间
 return 0
}
点击并拖拽以移动

结果:

调用基类 A 的析构函数
点击并拖拽以移动

「使用虚析构函数后:」

#include<bits/stdc++.h>
using namespace std;
class A
{

public:
    virtual ~A(){cout<<'调用基类 A 的析构函数\n';}
};
class B:public A
{
public:
    virtual~B(){cout<<'调用派生类 B 的析构函数\n';}
};
int main()
{
 A *p; //定义指向基类 A 的指针变量 p
 p = new B;//用运算符 new为派生类的无名对象动态地分配了一个存储空间,并将地址赋给对象指针p
 delete p;//用 delete 撤销无名对象,释放动态存储空间
 return 0
}
点击并拖拽以移动

运行结果:

调用派生类 B 的析构函数
调用基类 A 的析构函数
点击并拖拽以移动

由于使用了虚析构函数,程序执行了「动态联编」,实现了运行的动态性。虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。

2.5.抽象类和纯虚函数

虚函数为一个类体系中所有类提供了一个统一的接口。然而在有些情况下,定义基类时虽然知道其子孙类应当具有某一接口,但其自身由于某种原因却无法实现该接口,换句话,它在该基类中没有定义具体的操作内容。这里就应将该接口说明成一个纯虚函数,其具体操作由各子孙类来定义,带有纯虚函数的类称为抽象类。

抽象类的一般形式:

class  类名 
{
    
     virtual 类型 函数名(参数表)=0;    //纯虚函数     
     ...
};
点击并拖拽以移动

「纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上“= 0”」,表明在基类中不用定义该函数,它的实现部分——函数体留给派生类去做。

「我们把包含纯虚函数的类称之为抽象类。」

对于抽象类来说,「C++是不允许它去实例化对象的。也就是说,抽象类无法实例化对象。」

「抽象类为抽象和 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。」

「注意:」

  • 抽象类只能作为基类来使用。
  • 不能声明抽象类的对象,即抽象类不能用作参数类型、函数返回值或显式转换的类型。
  • 可以声明一个抽象类的指针和引用。通过指针或引用,我们就可以指向并访问派生类对象,以访问派生类的成员。
  • 抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以声明自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。
#include<iostream>
using namespace std;
 
class Base
{

public:
 virtual void show()=0;//纯虚函数
};
class Child0:public Base
{
 void show(){cout<<'child0 show()'<<endl;}
};
class Child1:public Child0
{
 void show(){cout<<'child1 show()'<<endl;}
};
void Callshow(Base *pbase)
{
 pbase->show();
}
 
int main()
{
 //Base base;错误,因为Base是抽象类不能被实例化
 Base *pBase;
 Child0 ch0;
 Child1 ch1;
 pBase = &ch0;
 Callshow(pBase);
 pBase = &ch1;
 Callshow(pBase);
 return 0;
}
点击并拖拽以移动

运行结果:

child0 show()
child1 show()
点击并拖拽以移动

「附:」

有一个类似的操作符「dynamic_cast」可以实现指向对象指针类型转换的功能:

关键字dynamic_cast(动态强制转换):

操作符dynamic_cast将一个指向基类的指针转换为一个指向派生类的指针(如果不能正确转换,则返回0——空指针)。

图片

今天的介绍就到这里,欢迎大家的关注哦,我会持续分享干货内容。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多