镜缘浮影 小人本住在 苏州的城外 家里有屋又有田 生活乐无边

C++基础(七).多态

2017-02-20
wilmosfang
c++
 
原文地址 http://soft.dog/2017/02/20/cpp-polymorphism-07/

前言

C++语言是C语言的拓展,C语言是面向过程的,C++在C的基础上增加了面向对象的方法

什么是面向对象呢,面向对象就是将数据和对数据的加工方法打包在一起,进行模块化的调用,通过方法来进行数据交换的一种设计方法

Tip: 本人关于程序的认知,可以参看前面写的 一个运维人员的编程思维

面向对象的程序设计有四个主要特点:

  • 抽象
  • 封装
  • 继承
  • 多态

下面就通过C++来对面向对象的核心特性进行分享

Tip: 此文中的基础概念参看了 《C++ 虚函数&纯虚函数&抽象类&接口&虚基类》 《C++抽象基类和纯虚函数》


概要


多态

在面向对象语言中,接口的多种不同实现方式即为多态

多态特性中,可以将子类类型的指针赋值给父类类型的指针;可以用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数

多态是通过虚函数实现的

多态可以让父类的指针有“多种形态”,这是一种泛型技术(所谓泛型技术,就是试图使用不变的代码来实现可变的算法)


虚函数

虚函数是一种特殊的成员函数,它的一般格式如下

class <类名>
    {
        virtual <类型><函数名>(<参数表>);
        …
    };

虚函数必须是类的非静态成员函数(且非构造函数),其访问权限是public

虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数进行重新定义。在派生类中定义的函数应与虚函数具有相同的形参个数和形参类型(覆盖),以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数

虚函数可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,基类虚成员函数调用基类指针,则会调用其真正指向的对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。若不是虚函数,则不管基类指针指向哪个派生类对象,调用时都会调用基类中定义的那个函数

Tip: 虚函数的引入就是为了实现多态的特性,让不同的子类可以有不同的实现方式


纯虚函数

纯虚函数是一种特殊的虚函数,它的一般格式如下

class <类名>
    {
        virtual <类型><函数名>(<参数表>)=0;
        …
    };

许多情况下,在基类中不能对虚函数给出有意义的实现,则把它声明为纯虚函数,它的实现留给该基类的派生类去做

纯虚函数的作用是为派生类提供一个一致的接口(纯虚函数相当于接口,不能直接实例化,需要派生类来实现函数定义)


虚函数与纯虚函数的区别

  • 1)类里声明为虚函数的话,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被重载,这样的话,编译器就可以使用后期绑定来达到多态了,纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现

  • 2)虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现

  • 3)虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然我们也可以完成自己的实现。纯虚函数的类用于“接口继承”,主要用于通信协议方面。关注的是接口的统一性,实现由子类完成。一般来说,接口类中只有纯虚函数的

  • 4)带纯虚函数的类叫抽象类,这种基类不能直接生成对象,而只有被继承,并且重写其虚函数后,才能使用


抽象类

带有纯虚函数的类称为抽象类

抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限

抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的

抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个 抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了

抽象类中,既可以有抽象方法,也可以有具体方法或者叫非抽象方法。抽象类中,既可以全是抽象方法,也可以全是非抽象方法。一个继承于抽象类的子类,只有实现了父类所有的抽象方法才能够是非抽象类


接口

接口只是一个概念,它在C++中用抽象类来实现

接口是专门被继承的,接口存在的意义也是被继承,和C++里的抽象类里的纯虚函数是相同的,不能被实例化

当继承一个接口时,接口里的所有函数必须全部被覆盖

Tip: 接口的意义在于提前协定标准,构建共识,可以更为高效和低成本地进行大规模协作,利于构建模块化和松耦合的系统


抽象类与接口的区别

  • 抽象类可以有构造方法,接口中不能有构造方法
  • 抽象类中可以有普通成员变量,接口中没有普通成员变量
  • 接口里边全部方法都必须是abstract的,抽象类的可以有实现了的方法
  • 抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型
  • 抽象类中可以包含静态方法,接口中不能包含静态方法
  • 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是 public static final 类型,并且默认即为 public static final 类型

虚基类

虚基类是一个相对概念,形式如下

class derive : virtual public base
{
};

虚基类是相对于它的派生类而言的,它本身可以是一个普通的类。只有它的派生类虚继承它的时候,它才称作虚基类,如果没有虚继承的话,就称为基类。比如类B虚继承于类A,那类A就称作类B的虚基类,如果没有虚继承,那类B就只是类A的基类

虚继承主要用于一个类继承多个类的情况,避免重复继承同一个类两次或多次

例如 由类A派生类B和类C,类D又同时继承类B和类C,这时候类D就要用虚继承的方式避免重复继承类A两次


代码示例

要求

  • 写一个程序,定义抽象基类Shape, 由它派生3个子类,Circle(圆),Rectangle(矩形),Trapezoid(梯形)
  • 用虚函数分别计算几种图形的面积,并求他们的和

要求:用基类指针数组,使它的每一个元素指向一个派生类对象

 Shape *p[3]

代码示例

shape.cpp

#include <iostream> //cout,endl 相关函数的声明

#define PI 3.1415926 //定义一个PI宏

using namespace std; //设定名称空间

class Shape //定义一个Shape的抽象基类
{
public:
  virtual float getArea()=0; //声明一个纯虚函数
  Shape(float x=0); //声明一个带默认参值的构造函数
protected:
  float h; //成员变量
};

Shape::Shape(float x) //构造函数的实现
{
  h=x;
}

class Circle:public Shape //定义一个继成自Shape抽象类的派生类Circle
{
public:
  Circle(float r=0); //构造方法
  float getArea(); //对getArea的重写
};

Circle::Circle(float r):Shape(r) //构造函数的实现,因为圆只有一个表示半径的成员变量,所以使用基类的构造方法进行初始化就够了
{}

float Circle::getArea() //对getArea的实现
{
  return PI*h*h;
}


class Rectangle:public Shape //定义一个继成自Shape抽象类的派生类Rectangle
{
public:
  Rectangle(float h=0,float x=0); //声明构造方法
  float getArea(); //对getArea的重写
protected:
  float w; //多了一个表示宽度的成员变量
};

Rectangle::Rectangle(float h,float x):Shape(h) //构造函数的实现,因为矩形多了一个表示宽度的成员变量,所以使用基类的构造方法进行初始化后,再只用对宽度单独赋值就可以了
{
  w=x;
}

float Rectangle::getArea() //对getArea的实现
{
  return h*w;
}

class Trapezoid:public Shape  //定义一个继成自Shape抽象类的派生类Trapezoid
{
public:
  Trapezoid(float h=0,float x=0,float y=0); //声明构造方法
  float getArea(); //对getArea的重写
protected:
  float a,b; //多了两个分别表示上底和下底的成员变量
};

Trapezoid::Trapezoid(float h , float x, float y):Shape(h)  //构造函数的实现,因为梯形多了两个分别表示上下底的成员变量,所以使用基类的构造方法进行初始化后,再只用对两底赋值就可以了
{
  a=x;
  b=y;
}

float Trapezoid::getArea() //对getArea的重写
{
  return h*(a+b)/2;
}

int main()
{
  Shape *p[3]={NULL,NULL,NULL}; //定义三个基类(抽象类)指针

  p[0] = new Circle(2); //申请一个Circle类对象,初始化半径为2,指针赋给p[0]
  p[1] = new Rectangle(2,5); //申请一个Rectangle类对象,初始化高为2,宽为5,指针赋给p[1]
  p[2] = new Trapezoid(2,5,8); //申请一个Trapezoid类对象,初始化高为2,上底为5,下底为8,指针赋给p[2]

  cout<<"Circle area:"<<p[0]->getArea()<<endl;
  cout<<"Rectangle area:"<<p[1]->getArea()<<endl;
  cout<<"Trapezoid area:"<<p[2]->getArea()<<endl;
  cout<<"the total area:"<<p[0]->getArea()+p[1]->getArea()+p[2]->getArea() <<endl;
  
  delete p[0];
  delete p[1];
  delete p[2]; //进行清场工作
  
  return 0;
}

编译执行

emacs@ubuntu:~/c++$ alias  gtx
alias gtx='g++ -Wall -g -o'
emacs@ubuntu:~/c++$ gtx shape.x shape.cpp
emacs@ubuntu:~/c++$ ./shape.x 
Circle area:12.5664
Rectangle area:10
Trapezoid area:13
the total area:35.5664
emacs@ubuntu:~/c++$

编译执行过程中没有报错,从结果来看,符合预期


总结

弄清下面概念对掌握c++很有帮助

  • 多态:指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作,C++支持两种多态性:编译时多态性,运行时多态性(编译时多态性:通过重载函数实现 ;运行时多态性:通过虚函数实现)
  • 虚函数 :在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
  • 纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
  • 抽象类:包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象
  • 接口:接口只是一个概念,它在C++中用抽象类来实现,只包含纯虚函数的抽闲类叫接口
  • 虚基类:虚基类是相对于它的派生类而言的(虚基类只是一种特殊关系),它本身可以是一个普通的类
  • 一个抽象类只能用作基类,只能用作派生,不能实例化(创建)对象。一个类要是包含至少一个纯虚函数,则这个类是抽象类。一个抽象类的子类可以添加更多的数据成员和成员函数
  • 抽象类的子类可以还是抽象类,可以添加更多的成员函数和成员方法,直到可以产生对象为止
  • 由于抽象类不能构造对象,因此它的构造函数不能被单独调用。它的构造函数只能在子类的成员初始化列表里面调用
  • 抽象类不一定有析构函数,如果有必须是虚析构函数
  • 一个函数不能有抽象类对象的值参数<参数不能传值>,这个函数不能有抽象类对象的值返回。然而可以有抽象类类型的指针和引用可以作为参数,同样抽象类的指针和引用可以作为函数的返回值类型。因为他们可以指向或者引用抽象类的子类对象
  • 纯虚函数是在子类里面被实现的。如果子类没有实现纯虚函数,纯虚函数将继承给子类。那么这时子类同样也是一个抽象类
原文地址 http://soft.dog/2017/02/20/cpp-polymorphism-07/

评论