【C++基础】 第七节课 多态与虚函数

本文深入探讨C++中的多态性概念,包括静态和动态类型、静态与动态绑定、虚函数、纯虚函数、抽象类等核心内容,以及派生类到基类的转换和接口继承与实现继承的区别。

目录

1 C++的多态性

2 派生类到基类的转换

3 虚函数

4 纯虚函数

5 抽象类

6 接口继承和实现继承


 

 

 

1 C++的多态性

1.1 静态类型和动态类型

1)静态类型是指针或引用在定义时指定的类型,在编译期就已经确定。

2)动态类型是由指针或引用当前所指向的类型相关,在运行期才可以确定。

3)静态类型不可以更改,动态类型可以更改。

class B
{
}
class C: public B
{
}
class D:public C
{
}

D *pd = new D;   //静态类型
C *pc = new C;

B *pb = pd;      //动态类型
pb = pc;

1.2 静态绑定和动态绑定

1)静态绑定:绑定的是对象的静态类型,根据静态类型决定要执行何种操作。发生在编译器

2)动态绑定:绑定的是对象的动态类型,根据动态类型决定要执行何种操作。发生在运行期。

1.3 编译时的多态性-静态绑定

多态性是指向不同的对象发出同样的消息,不同类型的对象接收时会产生完全不同的行为。

int max(int val1, int val2);
char max(char val1, char val2);

max(2,3);
max('a','A');   //发生在编译的时候
Complex& operator+ (Complex& c1, Complex& c2)

Complex c1;
Complex c2;
c1 + c2;

函数重载,操作符重载表现为编译时的多态性,即静态绑定。

1.4 运行时的多态性

在程序运行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,动态的确定操作所针对的对象。即动态绑定。

Base b;
Derived d;
Base *pb = &b;
Base *pb = &d;
pb->display();
pb->display();

C++的动态绑定依赖于运行时指针(引用)所指向对象的实际类型,根据对象类型,程序自动计算并连接函数的入口地址。

2 派生类到基类的转换

每个派生类的对象都包含基类子对象,可以将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类对象的指针指向派生类对象的基类部分。

void func(const Base& rb);
Base b;
func(b);           
Base *p =  &b;
Derived d;
func(d);  -基类引用绑定到派生类对象

p = &d;  -指针动态类型发生改变

1)引用和指针的动态类型与静态类型不同,这是C++支持多态性的关键。

2)如果是普通成员函数(非虚函数),无论指向的对象是什么类型,都实现静态绑定,执行基类类型定义的成员函数。

 

3 虚函数

class Base:
{
  public:
  void display();
};
class Derived: Base
{
  public:
  void display();
};

Derived d;
Base *p = &d;
p->display();

1) 基类引用或者指针可以引用基类对象,也可以引用派生类对象,但通过它们调用的函数却是基类的函数。

2)C++默认的成员函数是非虚函数,不会触发动态绑定。

3.1 虚函数的引入

触发动态绑定的两个条件:

1)只有指定为虚函数的成员函数才能进行动态绑定。

2)必须通过基类类型的引用或者指针进行函数调用。

3)当基类引用或指针调用虚函数时,到运行时根据所指的对象动态类型才能确定调用基类还是派生类的函数,即实现动态绑定。

3.2 虚函数的定义

virtual 函数类型 函数名(参数表)

virtual void display();

虚函数的本质:不是重载(overload)而是覆盖(override)

虚函数的调用方式:在程序的执行过程中,依据指针或者引用指向的对象的动态类型,确定调用基类还是派生类的同名函数,实现动态绑定。

3.3 虚函数实现动态绑定的原理

class Base
{
    public:
    virtual void f();
    virtual void g();
    virtual void h();
};

Base b;

包含虚函数的类都有一个虚函数表(V-Table)。它是一个类的虚函数的地址表。类中的每个虚函数在虚函数表中都占据一项位置,每项保存一个虚函数的入口地址。

3.4 虚构函数的特点

1)基类通常将派生类需要重定义的函数定义为虚函数。

2)当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、参数的个数必须一一对应,函数的返回的类型也相同。

3)一旦在基类中声明为虚函数,它就一直未虚函数,派生类重定义虚函数时,可以使用virtual关键字,也可以不使用。

4)如果在类外定义虚函数,不需要加virtual关键字,类内声明时要加virtual关键字。

3.5 虚析构函数

何时需要虚析构函数?

删除指向动态分配对象的指针时,会自动运行对象所属类的析构函数。

Base *p = new Derived;
delete p;

处理继承层次中的对象时,指针的静态类型与被删除对象的动态类型不同,动态申请的是派生类的对象,但删除的基类指针所指向的对象,调用的是基类的析构函数,派生类的析构函数未被调用。

1)析构函数为虚函数时,通过指针调用,释放指针所指向的内存时,将根据指针指向的对象的类型来决定调用哪个析构函数。

2)派生类中有指针类型的数据成员时,必须要定义基类的析构函数为虚析构函数。

3)即使析构函数没有要释放内存的工作要做,继承层次的根类也应该定义一个虚析构函数,这是一个良好的习惯。

4)基类为了将析构函数设为虚函数而定义了空的虚析构函数,基类可以不满足三法则,即定义了析构函数就必须定义复制构造,赋值操作符重载函数。

3.6 虚函数(override)和重载函数(overload)的比较

1)重载函数要求函数有相同的函数名称,并有不同的参数列表;而虚函数则完全相同。

2)重载函数可以是成员函数或友元函数,而虚函数只能是成员函数。

3)重载函数的调用是以所传递参数的差别作为调用不同函数的依据,虚函数时根据对象动态类型的不同去调用不同的虚函数。

4)重载函数在编译时表现出多态性,而虚函数在运行时表现出多态功能。

3.7 设置虚函数的要点

1)只有类的成员函数才能声明为虚函数,这是因为虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。

2)静态成员函数不能是虚函数,因为静态成员函数不属于某个对象。

3)内联函数不能是虚函数,因内联函数是不能在运行中动态确定其位置的,即使虚函数在类的内部定义,编译时仍将其看作是非内联的。

4)构造函数不能是虚函数,因为构造时对象还是一片未确定的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。

5)析构函数可以是虚函数,而且通常应该声明为虚函数。

6)赋值操作符重载函数不能是虚函数,会引起混淆,而且没有实际意义。

内容回顾:

1)什么是动态绑定?

2)定义虚析构函数的作用是?

为了确保派生类析构函数也被析构掉。

3)实现动态绑定的两个条件是什么?

  a) 函数要是虚函数

  b) 基类的指针或引用指向派生类

4)静态成员函数可以定义为虚函数吗?

不能

4 纯虚函数

4.1 纯虚函数的定义

纯虚函数是一种特殊的虚函数,它是被表明为不具体实现的虚函数。

virtual 函数类型 函数名(参数表)=0; //

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

1)纯虚函数没有函数体;

2)=0只是起一个标识的作用,标识这个函数是纯虚函数。

3)这是一个声明语句,最后应该有个分号;

4.2 纯虚函数的作用

1)在基类中为其派生类保留一个函数名字,以便派生类根据需要对它进行定义。否则无法实现多态性。

2)纯虚函数为后代类型提供了可以覆盖的接口,但是定义纯虚函数的这个类中不会调用。

3)从基类继承来的纯虚函数,在派生类中仍是虚函数。

5 抽象类

1)如果一个类中至少有一个纯虚函数,那么这个类就成为纯虚函数。

2)抽象基类是本类族的公共接口。从同一基类派生出的多个类有同一接口。

3)抽象类必须用作派生其他类的基类,而不能用于直接创建对象,但可以声明抽象类的指针和引用,支持运行时多态性。

4)抽象类必须包括纯虚函数,同时也可包括虚函数。

5)派生类中应重新定义与基类中的纯虚函数同名,同类型,同形参表列的函数,否则它仍被看作一个抽象类。

抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的上层。

对于抽象类的使用有几点规定:

1)抽象类只能用作其他类的基类,不能建立抽象类对象。

2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。

3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

与抽象类相对应的,将能够建立对象的类称为具体类。

4)在基类的构造和析构函数中调用纯虚函数将会带来未知结果。

6 接口继承和实现继承

接口继承:派生类只继承基类声明的函数的接口,也就是声明;

实现继承:派生类同时继承基类声明的函数的接口和声明。

class Base
{
    public:
        virtual void f1() = 0; //继承函数的接口
        virtual void f2();     //继承函数的接口和缺省实现
        void f3();             //继承函数的接口和实现
};

练习:

#include <iostream>

using namespace std;

class Shape
{
private:
    int m_id;
public:
    Shape(int id = 1):m_id(id){}
    virtual ~Shape(){}
    virtual double get_area() const = 0;
    virtual void show_error() const
    {   
        cout << "input error" << endl;
    }   
    int get_id() const
    {   
        return m_id;
    }   
};

class Rect : public Shape
{
private:
    int m_len;
    int m_wid;
public:
    Rect(int id, int len, int wid):Shape(id),m_len(len),m_wid(wid){}
    double get_area() const
    {   
        if(m_len <= 0 || m_wid <= 0)
        {
            show_error();
            return -1; 
        }
        return m_len*m_wid;
    }   
    void show_error() const{
        if(m_len <=0)
        {
            cout << "m_len input error: "<< m_len << endl;
        }
        if(m_wid <= 0)
        {
            cout << "m_wid input error: "<< m_wid << endl;
        }
    }   
};

class Circle : public Shape
{
private:
    int m_r;
public:
    Circle(int id, int r):Shape(id),m_r(r){}
    double get_area() const{
        if(m_r <= 0)
        {
            show_error();
            return -1;
        }
        return 3.14*m_r*m_r;
    }
};

int main()
{
    Shape *s1 = new Rect(1000,100,-200);
    cout <<s1->get_id()<< ":" <<s1->get_area()<<endl;
    delete s1;
    s1 = new Circle(1001,-10);
    cout <<s1->get_id()<<":" <<s1->get_area()<<endl;
    delete s1;

    return 0;

}

 

6  成员函数属性总结

内容回顾

1)抽象类可以定义类对象吗?可以定义指向其对象的指针和引用吗?

2)如果基类中定义了一个纯虚函数,派生类中未重新定义此函数,则此时能定义派生类对象吗?

3)基类定义的非虚函数能够实现接口继承还是实现继承?

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值