C++之多态性、虚函数、虚继承

本来想自己写一篇,发现已经有人写的很棒了,就转载了
原文地址:http://www.cnblogs.com/CaiNiaoZJ/archive/2011/08/11/2134673.html
http://blog.csdn.net/crystal_avast/article/details/7678704

C++之多态性与虚函数

面向对象程序设计中的多态性是指向不同的对象发送同一个消息,不同对象对应同一消息产生不同行为。在程序中消息就是调用函数,不同的行为就是指不同的实现方法,即执行不同的函数体。也可以这样说就是实现了“一个接口,多种方法”。

  从实现的角度来讲,多态可以分为两类:编译时的多态性和运行时的多态性。前者是通过静态联编来实现的,比如C++中通过函数的重载和运算符的重载。后者则是通过动态联编来实现的,在C++中运行时的多态性主要是通过虚函数来实现的,也正是今天我们要讲的主要内容。

  1.不过在说虚函数之前,我想先介绍一个有关于基类与派生类对象之间的复制兼容关系的内容。它也是之后学习虚函数的基础。我们有时候会把整型数据赋值给双精度类型的变量。在赋值之前,先把整形数据转换为双精度的,在把它赋值给双精度类型的变量。这种不同类型数据之间的自动转换和赋值,称为赋值兼容。同样的,在基类和派生类之间也存在着赋值兼容关系,它是指需要基类对象的任何地方都可以使用公有派生类对象来代替。为什么只有公有继承的才可以呢,因为在公有继承中派生类保留了基类中除了构造和析构之外的所有成员,基类的公有或保护成员的访问权限都按原样保留下来,在派生类外可以调用基类的公有函数来访问基类的私有成员。因此基类能实现的功能,派生类也可以。

  那么它们具体是如何体现的呢?(1)派生类对象直接向基类赋值,赋值效果,基类数据成员和派生类中数据成员的值相同;(2)派生类对象可以初始化基类对象引用;(3)派生类对象的地址可以赋给基类对象的指针;(4)函数形参是基类对象或基类对象的引用,在调用函数时,可以用派生类的对象作为实参;

#include "stdafx.h"
#include<iostream>
#include<string>

class ABCBase
{
private:
        std::string ABC;    
public:
        ABCBase(std::string abc)
        {
            ABC=abc;
        }
void showABC();
};

void ABCBase::showABC()
{
    std::cout<<"字母ABC=>"<<ABC<<std::endl;
}

class X:public ABCBase
{
public:
        X(std::string x):ABCBase(x){}
};

void function(ABCBase &base)
{
base.showABC();
}


int main()
{
    ABCBase base("A");
base.showABC();

    X x("B");
base=x;
base.showABC();

    ABCBase &base1=x;
    base1.showABC();

    ABCBase *base2=&x;
    base2->showABC();

    function(x);

return0;
}

结果:
这里写图片描述

要注意的是:第一,在基类和派生类对象的赋值时,该派生类必须是公有继承的。第二,只允许派生类对象向基类对象赋值,反过来不允许;

  2.紧接着来讲一下虚函数,它允许函数调用与函数体之间的联系在运行时才建立,即在运行时才决定如何动作。虚函数声明的格式:

  virtual 返回类型 函数名(形参表)

  {

    函数体

  }

那么定义虚函数有什么用呢?让我们先来看看下面这个示例:

#include "stdafx.h"
#include <iostream>
#include <string>


class Graph
{
protected:
double x;
double y;
public:
        Graph(double x,double y);
void showArea();
};

Graph::Graph(double x,double y)
{
this->x=x;
this->y=y;
}

void Graph::showArea()
{
    std::cout<<"计算图形面积"<<std::endl;
}

class Rectangle:public Graph
{
public:
        Rectangle(double x,double y):Graph(x,y){};
void showArea();
};

void Rectangle::showArea()
{
    std::cout<<"矩形面积为:"<<x*y<<std::endl;
}

class Triangle:public Graph
{
public:
        Triangle(double d,double h):Graph(d,h){};
void showArea();
};

void Triangle::showArea()
{
    std::cout<<"三角形面积为:"<<x*y*0.5<<std::endl;
}

class Circle:public Graph
{
public:
        Circle(double r):Graph(r,r){};
void showArea();
};

void Circle::showArea()
{
    std::cout<<"圆形面积为:"<<3.14*x*y<<std::endl;
}

int main()
{
    Graph *graph;

    Rectangle rectangle(10,5);
    graph=&rectangle;
    graph->showArea();

    Triangle triangle(5,2.4);
    graph=&triangle;
    graph->showArea();

    Circle circle(2);
    graph=&circle;
    graph->showArea();

return0;
}

结果:
这里写图片描述

结果似乎和我们想象的不一样,既然Graph类(图形类)的对象graph指针分别指向了Rectangle类(矩形类)对象,Triangle类(三角类)对象,以及Circle类(圆类)对象,那么就应该执行它们自己所对应成员函数showArea(),怎么结果会是Graph类(图形类)的对象graph里的成员函数呢?这好像和我们在C++之继承与派生(2)一节里所讲到的派生类成员覆盖了基类中使用相同名称的成员(派生类对象调用同名成员函数是来自于自己类中成员函数,而非基类中上的)有所不同啊,其实当基类对象指针指向公有派生类的对象时,它只能访问从基类继承下来的成员,而不能访问派生类中定义的成员。但是使用动态指针就是为了表达一种动态调用的性质即当前指针指向哪个对象,就调用那个对象对应类的成员函数。那要怎么来解决的,这时虚函数就体现出了它的作用。其实我们只需要对上一个示例代码中所有的类里出现的showArea()函数声明之前加一个关键字virtual:

#include "stdafx.h"
#include <iostream>
#include <string>


class Graph
{
protected:
double x;
double y;
public:
        Graph(double x,double y);
voidvirtual showArea();//定义为虚函数或virtual void showArea()
};

Graph::Graph(double x,double y)
{
this->x=x;
this->y=y;
}

void Graph::showArea()
{
    std::cout<<"计算图形面积"<<std::endl;
}

class Rectangle:public Graph
{
public:
        Rectangle(double x,double y):Graph(x,y){};
virtualvoid showArea();//定义为虚函数
};

void Rectangle::showArea()
{
    std::cout<<"矩形面积为:"<<x*y<<std::endl;
}

class Triangle:public Graph
{
public:
        Triangle(double d,double h):Graph(d,h){};
virtualvoid showArea();//定义为虚函数
};

void Triangle::showArea()
{
    std::cout<<"三角形面积为:"<<x*y*0.5<<std::endl;
}

class Circle:public Graph
{
public:
        Circle(double r):Graph(r,r){};
virtualvoid showArea();//定义为虚函数
};

void Circle::showArea()
{
    std::cout<<"圆形面积为:"<<3.14*x*y<<std::endl;
}

int main()
{
    Graph *graph;

    Rectangle rectangle(10,5);
    graph=&rectangle;
    graph->showArea();

    Triangle triangle(5,2.4);
    graph=&triangle;
    graph->showArea();

    Circle circle(2);
    graph=&circle;
    graph->showArea();

return0;
}

其它代码原封不动,这样运行出来的结果就是我们所需要的:
这里写图片描述

在基类中的某成员函数被声明为虚函数后,在之后的派生类中科以重新来定义它。但定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须和基类中的原型完全相同。其实在上述修改后的示例代码里,只要在基类中显式声明了虚函数,那么在之后的派生类中就需要用virtual来显式声明了,可以略去,因为系统会根据其是否和基类中虚函数原型完全相同来判断是不是虚函数。因此,上述派生类中的虚函数如果不显式声明也还是虚函数。最后对虚函数做几点补充说明:(1)因为虚函数使用的基础是赋值兼容,而赋值兼容成立的条件是派生类之从基类公有派生而来。所以使用虚函数,派生类必须是基类公有派生的;(2)定义虚函数,不一定要在最高层的类中,而是看在需要动态多态性的几个层次中的最高层类中声明虚函数;(3)虽然在上述示例代码中main()主函数实现部分,我们也可以使用相应图形对象和点运算符的方式来访问虚函数,如:rectangcle.showArea(),但是这种调用在编译时进行静态联编,它没有充分利用虚函数的特性。只有通过基类对象来访问虚函数才能获得动态联编的特性;(4)一个虚函数无论配公有继承了多少次,它仍然是虚函数;(5)虚函数必须是所在类的成员函数,而不能是友元函数,也不能是静态成员函数。因为虚函数调用要靠特定的对象类决定该激活哪一个函数;(6)内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的即使虚函数在类内部定义,编译时将其看作非内联;(7)构造函数不能是虚函数,但析构函数可以是虚函数;

  如果在main()主函数中用new建立一个派生类无名对象和定义一个基类对象指针,并将无名对象的地址赋给基类对象指针时,当我们用delete运算符来撤销无名对象时,系统只执行基类析构函数,而不执行派生类析构函数。比如:

#include "stdafx.h"
#include <iostream>
#include <string>


class Graph
{
protected:
double x;
double y;
public:
        Graph(double x,double y);
voidvirtual showArea();//定义为虚函数或virtual void showArea()
~Graph();
};

Graph::Graph(double x,double y)
{
this->x=x;
this->y=y;
}

void Graph::showArea()
{
    std::cout<<"计算图形面积"<<std::endl;
}

Graph::~Graph()
{
    std::cout<<"调用图形类析构函数"<<std::endl;
}

class Rectangle:public Graph
{
public:
        Rectangle(double x,double y):Graph(x,y){};
virtualvoid showArea();//定义为虚函数
~Rectangle();
};

void Rectangle::showArea()
{
    std::cout<<"矩形面积为:"<<x*y<<std::endl;
}

Rectangle::~Rectangle()
{
    std::cout<<"调用矩形类析构函数"<<std::endl;
}

int main()
{
    Graph *graph;
    graph=new Rectangle(10,5);
    graph->showArea();

    delete graph;

return0;
}

结果:
这里写图片描述

因为在撤销指针graph所指的派生类对象,在调用析构函数时,采用静态联编,只调用了Graph类的析构函数。如果也想调用派生类Rectangle类的析构函数的话,可将Graph类的析构函数定义为虚析构函数。其定义的一般格式:

  virtual ~类名()

  {

    函数体

  };

虽然派生类的析构函数与基类的析构函数名字不同,但是如果将基类的析构函数定义为虚函数,由该基类派生而来的所有派生类的析构函数都自动成为虚函数。我们把上一示例中的Graph类的析构函数前加上关键字virtual,那么执行结果:

这里写图片描述

显然这个结果才是我们所需要的。

  3.上述示例中用了虚函数后,会发现其实Graph类(图形类)中的虚函数的函数体根本没有被用到过,就算被用到,该基类体现了图形的抽象的概念,并不与具体事物相联系。所以基类中的虚函数也没有实质性的功能。因此我们只需要在基类中留下一个函数名,而具体的实现留给派生类去定义。在C++中就是用纯虚函数来说明的。纯虚函数的一般形式:

  virtual 返回类型 函数名(形参表)=0;

这里的”=0”并不是函数的返回值等于零,它只是起到形式上的作用,告诉编译系统”这是纯虚函数”。纯虚函数不具备函数功能,不能被调用。

class Graph
{
protected:
double x;
double y;
public:
        Graph(double x,double y);
voidvirtual showArea()=0;//定义纯虚函数
};

Graph::Graph(double x,double y)
{
this->x=x;
this->y=y;
}

4.如果一个类中至少有一个纯虚函数,那么就称该类为抽象类。所以上述中Graph类就是抽象类。对于抽象类有以下几个注意点:(1)抽象类只能作为其他类的基类来使用,不能建立抽象类对象;(2)不允许从具体类中派生出抽象类(不包含纯虚函数的普通类);(3)抽象类不能用作函数的参数类型、返回类型和显示转化类型;(4)如果派生类中没有定义纯虚函数的实现,而只是继承成了基类的纯虚函数。那么该派生类仍然为抽象类。一旦给出了对基类中虚函数的实现,那么派生类就不是抽象类了,而是可以建立对象的具体类;

5.最后还是一样,我将用一个实例来总结一下今天所讲的内容(开发工具:vs2010):

#include "stdafx.h"
#include <iostream>
#include <string>


class Graph //抽象类
{
protected:
double x;
double y;
public:
        Graph(double x,double y);
//void virtual showArea();//定义为虚函数或virtual void showArea()
voidvirtual showArea()=0;//定义纯虚函数
virtual~Graph();//定义虚析构函数
};

Graph::Graph(double x,double y)
{
this->x=x;
this->y=y;
}

void Graph::showArea()
{
    std::cout<<"计算图形面积"<<std::endl;
}

Graph::~Graph()
{
    std::cout<<"调用图形类析构函数"<<std::endl;
}

class Rectangle:public Graph
{
public:
        Rectangle(double x,double y):Graph(x,y){};
void showArea();//虚函数
~Rectangle();//虚析构函数
};

void Rectangle::showArea()
{
    std::cout<<"矩形面积为:"<<x*y<<std::endl;
}

Rectangle::~Rectangle()
{
    std::cout<<"调用矩形类析构函数"<<std::endl;
}

class Triangle:public Graph
{
public:
        Triangle(double d,double h):Graph(d,h){};
virtualvoid showArea();//虚函数
~Triangle();//虚析构函数
};

void Triangle::showArea()
{
    std::cout<<"三角形面积为:"<<x*y*0.5<<std::endl;
}

Triangle::~Triangle()
{
    std::cout<<"调用三角形类析构函数"<<std::endl;
}


class Circle:public Graph
{
public:
        Circle(double r):Graph(r,r){};
virtualvoid showArea();//虚函数
~Circle();//虚析构函数
};

void Circle::showArea()
{
    std::cout<<"圆形面积为:"<<3.14*x*y<<std::endl;
}

Circle::~Circle()
{
    std::cout<<"调用圆形类析构函数"<<std::endl;
}

int main()
{
    {
//Graph g(10,10);//抽象类不能建立对象

        Graph *graph;

        Rectangle rectangle(10,5);
        graph=&rectangle;
        graph->showArea();

        Triangle triangle(5,2.4);
        graph=&triangle;
        graph->showArea();


        Graph *graph1;
        graph1=new Circle(2);//new运算符建立无名对象
        graph1->showArea();

        delete graph1;//delete运算符撤销派生类Circle无名对象
    }

return0;
}

结果:
这里写图片描述

C++中虚拟继承的概念

为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
class 派生类名:virtual 继承方式 基类名
virtual是关键字,声明该基类为派生类的虚基类。
在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。

C++虚拟继承
◇概念:
C++使用虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

◇解决问题:
解决了二义性问题,也节省了内存,避免了数据不一致的问题。

◇同义词:
虚基类(把一个动词当成一个名词而已)
当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。

◇语法:
class 派生类: virtual 基类1,virtual 基类2,…,virtual 基类n
{
…//派生类成员声明
};

◇执行顺序
首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;
执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;
执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;
执行派生类自己的构造函数;
析构以与构造相反的顺序执行;
mark
从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。
在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

◇因果:
多重继承->二义性->虚拟继承解决

◇二义性:

#include <iostream>
using namespace std;

class Base
{
public:
    Base()
    {
        cout << "Base called..." << endl;
    }
    void print()
    {
        cout << "Base print..." << endl;
    }

};

class Sub
{
public:
    Sub()
    {
        cout << "Sub called..." << endl;
    }
    void print()
    {
        cout << "Sub print..." << endl;
    }

};

class Child : public Base, public Sub
{
public:
    Child()
    {
        cout << "Child called..." << endl;
    }

};

int main()
{
    Child c;
    //这样不行,编译错误error: request for member 'print' is ambiguous
    // c.print();
    //只能这样
    c.Base::print();
    c.Sub::print();

    return 0;
}

◇多重继承:

#include <iostream>
using namespace std;

int flag = 0;


class Base
{
public:
    Base()
    {
        flag++;
        cout << "Base called..." << flag << endl;
    }
    void print()
    {
        cout << "Base print..." << endl;
    }

};

class Mid1 : public Base
{
public:
    Mid1()
    {
        cout << "Mid1 called" << endl;
    }

};

class Mid2 :  public Base
{
public:
    Mid2()
    {
        cout << "Mid2 called" << endl;
    }

};

class CHILD : public Mid1, public Mid2
{
public:
    CHILD()
    {
        cout << "CHILD called" << endl;
    }

};
int main()
{
    // Child c;
    CHILD d;
    //这样不行,编译错误error: request for member 'print' is ambiguous
    //d.print();
    //只能这样使用  
    d.Mid1::print();
    // c.print();
    // c.Base::print();
    // c.Sub::print();

    return 0;
}
 //output
 Base called : 0
 Mid1 called
 Base called : 1 
 Mid2 called
 Child called
 Base print

◇虚拟继承
在派生类继承基类时,加上一个virtual关键词则为虚拟继承

#include <iostream>
using namespace std;

int flag = 0;


class Base
{
public:
    Base()
    {
        flag++;
        cout << "Base called..." << flag << endl;
    }
    void print()
    {
        cout << "Base print..." << endl;
    }

};

class Sub
{
public:
    Sub()
    {
        cout << "Sub called..." << endl;
    }
    void print()
    {
        cout << "Sub print..." << endl;
    }

};

class Mid1 : virtual public Base
{
public:
    Mid1()
    {
        cout << "Mid1 called" << endl;
    }

};

class Mid2 : virtual public Base
{
public:
    Mid2()
    {
        cout << "Mid2 called" << endl;
    }

};

class Child : public Base, public Sub
{
public:
    Child()
    {
        cout << "Child called..." << endl;
    }

};

class CHILD : public Mid1, public Mid2
{
public:
    CHILD()
    {
        cout << "CHILD called" << endl;
    }

};
int main()
{
    // Child c;
    CHILD d;
    d.print();
    // c.print();
    // c.Base::print();
    // c.Sub::print();

    return 0;
}
//output
Base called : 0
Mid1 called
Mid2 called
Child called
Base print

◇通过输出的比较
1.在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
2.声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
3.观察类构造函数的构造顺序,拷贝也只有一份。

◇与虚函数关系
虚拟继承与虚函数有一定相似的地方,但他们之间是绝对没有任何联系的。
再想一次:虚拟继承,虚基类,虚函数。

展开阅读全文

没有更多推荐了,返回首页