C++虚函数和多态性

一 多态性

多态的实现:

函数重载

运算符重载

虚函数

 

从面向对象技术来看,多态性可分为4类:

1.重载多态:函数重载 ,运算符重载

2.强制多态:将一个变量类型加以变化,以符合一个函数 或操作的要求。例如:强制类型转换

3.包含多态:同样的操作可用于一个类型及其子类型。包 含多态一般需要进行运行时类型检查,主要通过虚函数 来实现

4.参数多态:采用参数化模板,使得一个结构可以适用于 多种数据类型。函数模板类模板

 

从系统实现的角度来说

1.静态联编,编译时多态,静态多态性

  函数重载和运算符重载

  的优点是函数调用速度快、效率较高,缺点 是编程不够灵活 

2.动态联编,运行时多态,动态多态性

  继承和虚函数

  动态联编的优点是提供了更好的编程灵活性、问题 抽象性和程序易维护性

  动态联编的缺点是:与静态联编相比,函数调用速 度慢。

 

静态联编和多态联编的本质就是静态类型检查和动态类 型检查

 

对象的静态类型是指声明在程序代码中的类型

Shape *ps; // 静态类型 = Shape*

Shape *pc = new Circle; // 静态类型 = Shape*

Shape *pr = new Rectangle; // 静态类型 = Shape*

 

对象的动态类型是由它当前所指的对象的类型决定的。 即,对象的动态类型表示它将执行何种行为。

 

举一个例子引出虚函数

如下代码

// 示例静态联编。函数重载在多态性中的应用
#include <iostream>
using namespace std;
class Student
{ public:
void print()
{
cout<<"A student"<<endl;
}
};
class GStudent:public Student
{ public:
void print()
{
cout<<"A graduate student"<<endl;
}
};
int main()
{
Student s1,*ps;
GStudent s2;
s1.print();
s2.print();
s2.Student::print();
ps=&s1;
ps->print();
ps=&s2;
ps->print();
//希望调用对象s2的输出函数,但调用的却是对象s1的输出函数
cin.ignore();
return 0;
}
/*

A student
A graduate student
A student
A student
A student


*/

可以看的出,就是上一章的基类指针指向派生类对象的问题

 

基类指针可以指向派生类对象,但 仍然调用的是基类成员函数:但是 由于静态联编的原因,编译器不知 道指针指向的对象已经重写了该成 员函数

希望调用对象s2的输出函数,但调用的却是对象s1的输出函数

 

问题:如何实现通过一个基 类指针调用派生类重写后的 成员函数?

回答:必须通过动态联编技 术,把派生类中要重写的函 数声明为虚函数

 

那么就来到了虚函数

 

二 虚函数

 

  虚函数的作用:简单讲即实现多态。

  基类定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,
  动态地调用属于子类的该函数,且这样的函数调用是无法在编译器期间确认的,而是在运行期确认,也叫做迟绑定。

  虚函数是动态联编的基础。

  在类的声明中,在函数原型之前写virtual。虚函数必须 存在于类的继承环境中

  virtual 只用来说明类声明中的原型,不能用在函数实现 时。

  具有继承性,基类中声明了虚函数,派生类中无论是否 说明,同原型函数都自动为虚函数。

  调用方式:通过基类指针或引用,执行时会根据指针指 向的对象的类,决定调用哪个函数。

  如果采用对象调用虚函数,则采用的是静态联编方式。

 

使用对象指针对1中的进行修改的话,使用虚函数的效果如下:

// 范例1:示例动态联编(采用对象指针调用虚函数)。
#include <iostream>
using namespace std;
class Student
{
public:
    virtual void print() //定义虚函数
    {
        cout << "A student" << endl;
    }
};
class GStudent :public Student
{
public:
    virtual void print() //关键字virtual可以省略
    {
        cout << "A graduate student" << endl;
    }
};
int main()
{
    Student s1, *ps;
    GStudent s2;
    s1.print();
    s2.print();
    s2.Student::print();
    ps = &s1;
    ps->print();
    ps = &s2;
    ps->print(); //对象指针调用虚函数,采用动态联编方式
    cin.ignore();
    return 0;
}
/*
A student
A graduate student
A student
A student
A graduate student
*/

  可以看到达到了预期的效果

 

使用对象引用又如何呢

#include <iostream>
using namespace std;
class Student
{
public:
    virtual void print()
    {
        cout << "A student" << endl;
    }
};
class GStudent :public Student
{
public:
    virtual void print()
    {
        cout << "A graduate student" << endl;
    }
};
void fun(Student &s)
{
    s.print(); //采用对象引用调用虚函数
}
int main()
{
    Student s1;
    GStudent s2;
    fun(s1);
    fun(s2);
    cin.ignore();
    return 0;
}
/*
A student
A graduate student
*/

这里不用虚函数的话,结果就是

A student

A student

  虚函数的定义格式

  在派生类中重新定义虚函数时,必须保证函数的返 回值类型和参数与基类中的声明完全一致。

  如果在派生类中没有重新定义虚函数,则派生类的 对象将使用基类的虚函数代码。

  虚函数必须是类的成员函数,且静态函数、内联函 数不能声明为虚函数

  构造函数不能是虚函数

  析构函数可以是虚函数,且往往被定义为虚函数

  当一个基类中声明了虚函数,则其虚函数特性会在 其直接派生类和间接派生类中一直保持下去

 

  在派生类中重新定义基类的虚函数是函数重载另一种形式 ,但它不同于一般的函数重载。 当普通的函数重载时,其函数的参数或参数类型必须有所 不同,函数的返回类型也可以不同。 对于虚函数,如果仅仅是返回类型不同,其余均相同,系 统会给错误信息; 若仅仅函数名相同,而参数的个数、类型或顺序不同,系 统将它作为普通的函数重载,这时虚函数的特性将丢失。

  多继承和虚函数

  如果两个基类没有同名函数,那好办,分开看即可

// 例题6.3,多继承中虚函数的定义和应用范例
#include <iostream>
using namespace std;
class Base1 {
public:
    virtual void TestA()
    {
        cout << "Base1 TestA()" << endl;
    }
};
class Base2 {
public:
    virtual void TestB()
    {
        cout << "Base2 TestB()" << endl;
    }
};
class Derived :public Base1, public Base2
{
public:
    void TestA()
    {
        cout << "Derived TestA()" << endl;
    }
    void TestB()
    {
        cout << " Derived TestB()" << endl;
    }
};
int main()
{
    Derived D;
    Base1 *pB1 = &D;
    Base2 *pB2 = &D;
    pB1->TestA();
    pB2->TestB();
    cin.ignore();
    return 0;
}
/*
Derived TestA()
 Derived TestB()
*/

 这里看似简单,其实就是按照这个来就是了,下面的运行顺序按照这个理解。 

 

  多继承中基类有同名虚函数的情况

  就是两个基类有同名虚函数需要实现,那么怎么写呢?

// 例题6.4,多继承中基类有同名虚函数的情况
#include <iostream>
using namespace std;
class Base1 {
public:
    virtual void Test()
    {
        cout << "Base1 Test()" << endl;
    }
};
class Base2 {
public:
    virtual void Test()
    {
        cout << "Base2 Test()" << endl;
    }
};
class Derived :public Base1, public Base2
{
public:
    virtual void Test()
    {
        cout << "Derived Test ()" << endl;
    }
};
int main()
{
    Derived D;
    Base1 *pB1 = &D;
    Base2 *pB2 = &D;
    pB1->Test();
    pB2->Test();
    cin.ignore();
    return 0;
}
/*
Derived Test ()
Derived Test ()
*/

显然这两条语句都是调用了派 生类重载的Test函数。 为什么?派生类重载的Test函 数覆盖了两个基类的虚函数

我的理解是动态编译的话,实际类型由右边的决定。这里都是右边的Derive D

 

 

但是其实我不怎么理解这个问题,

还是按照这个老哥的想法来看:

有两个类A, B, 它们可能是别人实现的(或是别人提供的库中的类), 很复杂且已经在用, 你不能修改他们, 你想写一个类C同时具有这两个类的特性,

因为自己实现它代价实在是太大, 所以你想到用C继承A, B以达到效果, 但是有一个问题, A, B具有一个同名的虚函数, 你在C中怎么重新实现这个

虚函数呢? 先看下面的代码:

#include <string>
#include <iostream>
using namespace std;
 
class ChineseName
{
public:
    virtual string getname()
    {
        return string();
    }
};
 
class EnglishName
{
public:
    virtual string getname()
    {
        return string();
    }
};
 
class Name : public ChineseName, public EnglishName
{
public:
    virtual string getname()
    {
        return string("chinese or english name? I donot know");
    }
};
 
int main()
{
    Name n;
    ChineseName & c = n;
    EnglishName & e = n;
 
    cout << n.getname() << endl;
    cout << c.getname() << endl;
    cout << e.getname() << endl;
 
    return 0;
}

如我自己上面的例子一样,三个输出都一样,都是Name中的函数。

那么如何使得

c.getname()输出中文,
e.getname()输出英文呢,

最根本的问题是我只能在Name中实现一个getname(), 但是它却被要求有两个功能, 分别输出中文名和英文名, 这着实没办法

也就是说,总的来说,这个动态联编,都是调用派生类的东西的

但是一开始如果两个基类都没有同名的虚函数的话,如上上个例子,不同的基类指针可以有不同的结果,但是这两个

ChineseName & c = n;EnglishName & e = n; 

就因为派生类只能写一个,不能得到不同的结果

这个时候就引入一个中间类既可以解决这个问题了。

// 例题6.5,多继承中基类有同名虚函数的解决办法
#include <iostream>
using namespace std;
class Base1 {
public:
    virtual void Test()
    {
        cout << "Base1 Test()" << endl;
    }
};
class Base2 {
public:
    virtual void Test()
    {
        cout << "Base2 Test()" << endl;
    }
};
//定义针对两个基类的中间类
class MiddleBase1 :public Base1 {
protected:
    virtual void Base1_Test() { }
    virtual void Test()
    {
        Base1_Test();
    }
};
class MiddleBase2 :public Base2 {
protected:
    virtual void Base2_Test() { }
    virtual void Test()
    {
        Base2_Test();
    }
};
//定义最后的派生类
class Derived :public MiddleBase1, public MiddleBase2 {
public:
    void Base1_Test()
    {
        cout << "Derived TestA()" << endl;
    }
    void Base2_Test()
    {
        cout << "Derived TestB()" << endl;
    }
};
int main()
{
    Derived D;
    Base1 *pB1 = &D;
    Base2 *pB2 = &D;
    pB1->Test();
    pB2->Test();
    cin.ignore();
    return 0;
}

其实很简单,强行把两个一样的函数通过中间类变得不一样就是了。

调用顺序: pB1->Test MiddleBase1::Test() Derived::Base1_Test()

调用顺序: pB2->Test MiddleBase2::Test() Derived::Base2_Test()

 

  虚析构函数

z

 

 

  虚析构函数是为了解决基类指针指向派生类对象,并用基类的指针删除派生类对象。
  如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,
使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其
可移植性。
  所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:
当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
// 示例虚析构函数。
#include<iostream>
using namespace std;
class A
{
public:
    virtual ~A()
    {
        cout << "call A::~A()" << endl;
    }
};
class B :public A
{
    char* buf;
public:
    B(int i)
    {
        buf = new char[i];
    }
    virtual ~B()
    {
        delete[] buf;
        cout << "call B::~B()" << endl;
    }
};
void fun(A* a)
{
    delete a;
}
int main()
{
    A* a = new B(10);
    fun(a);
    cin.ignore(); return 0;
}
/*
call B::~B()
call A::~A()
如果类A中的析构函数不定义为虚函数,则程序的运
行结果为:
call A::~A( )
*/

了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。

这是基类中定义虚析构函数的意义

 

三 纯虚函数和抽象类

纯虚函数:基类中的虚函数仅给出函数原型,然后在派 生类中重新定义具体实现代码

virtual 类型 函数名(参数表)=0; //纯虚函数的声明形式

1.纯虚函数没有函数体

2.最后面的“=0”并不表示函数返回值为0,这只是形式上 的作用,用于通知编译系统这是纯虚函数。

3.这是函数原型说明,而不是函数定义

4.纯虚函数只有函数的名字而不具备函数的功能,不能被 调用,仅表示留待派生类中定义。

// 例题6.7,纯虚函数
#include <iostream>
using namespace std;
class Vehicle {
protected:
    int pos, speed;
public:
    Vehicle(int ps = 0, int spd = 0)
    {
        pos = ps; speed = spd;
    }
    void SetSpeed(int spd)
    {
        speed = spd;
    }
    void Show()
    {
        cout << "Position at " << pos << endl;
    }
    virtual void Run() = 0;
};
class Car :public Vehicle
{
public:
    void Run()
    {
        pos += speed;
    }
};
int main() {
    Vehicle *pvh;
    Car car;
    pvh = &car;
    pvh->SetSpeed(5); pvh->Show();
    pvh->Run(); pvh->Show();
    pvh->Run(); pvh->Show();
    cin.ignore(); return 0;
}
/*
Position at 0
Position at 5
Position at 10
*/

 

  抽象类

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

 

抽象类常用作基类,通常称为抽象基类。

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

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

抽象类只能用作其他类的基类,不能建立抽象类对象 (不能实例化)。

抽象类不能用作参数类型、函数返回值类型或显式转换 的类型,但可以说明指向抽象类的指针或引用,该指针 或引用可以指向抽象类的派生类,进而实现多态性。

如果派生类给出所有纯虚函数的函数实现,这个派生类 就可以定义自己的对象,而不再是抽象类,因而不再是 抽象类。反之,仍是抽象类。

 

// 示例纯虚函数及抽象类。计算图形面积。
#include <iostream>
using namespace std;
const double PI = 3.14159;
class Shapes //抽象类
{
protected:
    int x, y;
public:
    void setvalue(int d, int w = 0) { x = d; y = w; }
    virtual void disp() = 0; //纯虚函数
};
class Square :public Shapes
{
public:
    void disp() //计算矩形面积
    {
        cout << "area of rectangle:" << x * y << endl;
    }
};
class Circle :public Shapes
{
public:
    void disp() //计算圆面积
    {
        cout << "area of circle:" << PI * x*x << endl;
    }
};
int main()
{
    Shapes* ptr[2]; //定义抽象类指针
    Square s1;
    Circle c1;
    ptr[0] = &s1; //抽象类指针指向派生类对象
    ptr[0]->setvalue(10, 5);
    ptr[0]->disp(); //抽象类指针调用派生类成员函数
    ptr[1] = &c1; //抽象类指针指向派生类对象
    ptr[1]->setvalue(10);
    ptr[1]->disp(); //抽象类指针调用派生类成员函数
    cin.ignore();
    return 0;
}
/*
area of rectangle:50
area of circle:314.159
*/

 

四 多态性的实际应用

  一个特别的用法,使用基类成员函数指针产生多态性

看一段神奇的代码

 

具体详细了解成员函数指针

class A
{

  public:

   void strcpy(char *, const char *);

   void strcat(char *, const char *);

};
pmf = &A::strcpy; 

typedef void(A::*PMA)(char *, const char *); 

PMA pmf= &A::strcat; // pmf是PMF类型(类A成员指针)的变量 

#include <iostream>
using namespace std;
class Base 
{
public:
    virtual void Print()
    {
        cout << "In Base" << endl;
    }
};
class Derived :public Base {
public:
    void Print()
    {
        cout << "In Derived " << endl;
    }
};
void display(Base *pb, void (Base::*pf)())
{
    (pb->*pf)();
}
int main()
{
    Derived d;
    Base *pb = &d;
    void (Base::*pf)(); //定义指向类Base的成员函数的指针变量
    pf =& Base::Print;
    display(pb, pf); //输出Derived
    cin.ignore();
    return 0;
}
/*
In Derived
*/

 

  汽车信息处理程序

// 汽车信息处理程序
//例6.9
#include <iostream> #include <string> using namespace std; //定义汽车抽象基类 class Auto { protected: string stypename; int npassengers; string smanufacturer; public: Auto() { stypename = "Auto"; npassengers = 0; smanufacturer = "no manufaturer"; } virtual ~Auto() { } //静态函数,用于整理字符串,删除尾部换行符 static void TrimLine(char *sbuf) { while (sbuf != '\0') { if (*sbuf == '\r' || *sbuf == '\n') { *sbuf = '\0'; break; } } } virtual bool Input(FILE *fp) = 0; //纯虚函数,输入数据 virtual void Show() = 0; //纯虚函数,按照指定格式输出车的信息 }; class Car:public Auto //定义普通汽车Car类 { public: Car() { stypename = "Car"; } bool Input(FILE *fp) //重写虚函数Input() { char sbuf[100]; fgets(sbuf, 100, fp); TrimLine(sbuf); smanufacturer = sbuf; fgets(sbuf, 100, fp); npassengers = atoi(sbuf); return true; } void Show() { cout << "Style:" << stypename << endl; cout << "Manufaturer:" << smanufacturer << endl; cout << "Passenger:" << npassengers << endl; } }; class Truck : public Car //定义卡车Truck类 { protected: float fload; //载重量 public: Truck() { stypename = "Truck"; fload = 0; } bool Input(FILE *fp) //重写虚函数Input() { char sbuf[100]; Car::Input(fp); fgets(sbuf, 100, fp); fload = atof(sbuf); return true; } void Show() { Car::Show(); cout << "Load:" << fload << endl; } }; class Crane : public Truck //定义吊车Crane类 { protected: float fheight; //吊车的举物高度 public: Crane() { stypename = "Crane"; fheight = 0; } bool Input(FILE *fp) //重写虚函数Input() { char sbuf[100]; Truck::Input(fp); fgets(sbuf, 100, fp); fheight = atof(sbuf); return true; } void Show() { Truck::Show(); cout << "Height:" << fheight << endl; } }; int main() //定义主函数 { FILE *stream; stream = fopen("autos.txt", "r"); if (stream == NULL) { cout << "Can’t open the file." << endl; return 0; } Auto *autos[3]; char sbuf[100]; int index = 0; while (fgets(sbuf, 100, stream) != NULL && index < 3) { if (strncmp(sbuf, "Car", 3) == 0) //检查是否为Car类型 autos[index] = new Car(); else if (strncmp(sbuf, "Truck", 5) == 0) autos[index] = new Truck(); else if (strncmp(sbuf, "Crane", 5) == 0) autos[index] = new Crane(); else break; autos[index]->Input(stream); index++; } fclose(stream); for (int i = 0; i < index; i++) { autos[i]->Show(); cout << endl; delete autos[i]; } cin.ignore(); return 0; }

 

从这里就可以看的出使用一个基类指针指向派生类对象的方便之处了。

posted @ 2019-12-02 22:21  TheDa  阅读(346)  评论(0编辑  收藏  举报