CPP多态性与虚函数

多态性与虚函数

面向对象程序设计中的多态性

多态性(polymorphism)
调用同一个函数名,根据需要实现不同的功能。

  • 编译时的多态性:静态多态(函数重载、运算符重载)
  • 运行时的多态性:动态多态(虚函数)

函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时就能决定调用的是哪个函数。静态多态性又称编译时的多态性。静态多态是通过函数重载、运算符重载实现的。

运行时的多态性是指在程序执行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况来动态地确定。动态多态是通过虚函数(virtual function)实现的。

派生类与基类对象之间的赋值

  • 1
    可以将派生类对象的值赋给基类对象,但只对从基类继承来的成员赋值。
class Base{
};

class Derived:public Base{
};

Base b;
Derived d;
b = d;
  • 派生类对象可以初始化基类的引用
Derived d;
Base &b = d;
//但b只能引用从基类继承来的成员
  • 派生类对象的地址赋给基类的指针变量

bp只能引用从基类继承来的成员。

Base *bp;
Derived d;
bp = &d;

公有派生类对象可以被当作基类的对象使用。反之则不可。
通过基类对象名、指针只能使用从基类继承的成员。

class A{
public:
    void show(){cout<<"A"<<endl;}
};
class AA:public A
{
public:
    void show(){cout<<"AA"<<endl;}
};
void f(A *pa){
    pa->show();
}

int main()
{
    A a;
    AA aa;
    f(&a);
    f(&aa);
    return 0;
}

函数f()的形参是类A的指针,所以调用的方法只能是基类A中的方法。

A
A

这和java的多态大不一样。

public class Polymorphism {
    public static void main(String[] args) {
        A a = new A();
        AA aa = new AA();
        f(a);
        f(aa);
    }
    static void f(A a){
        a.show();
    }
}

class A {
    void show() {
        System.out.println("A");
    }
}
class AA extends A{
    void show()
    {
        System.out.println("AA");
    }
}
A
AA
  • 这种赋值与访问性的关系。发现只有共有派生的子类才能向上转型。
    在这里插入图片描述

虚函数

在程序运行时通过基类指针调用相同的函数名,而实现不同功能的函数称为虚函数。

一旦把基类的成员函数定义为虚函数,由基类所派生出来的所有派生类中,该函数均保持虚函数的特性
在派生类中重新定义基类中的虚函数时,可以不用关键字virtual来修饰这个成员函数 。

虚函数的声明
virtual关键字

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

若要通过基类指针访问派生类中相同名字的函数,必须将基类中同名函数定义为虚函数,这样,将各种派生类对象的地址赋给基类的指针变量后,就可以动态地根据这种赋值调用不同类中的函数

若要达成java的效果,只需
基类A的同名函数show()前加上virtual关键字。

class A{
public:
    virtual void show(){cout<<"A"<<endl;}
};

若要通过基类引用访问派生类中相同名字的函数,也是可以的。

class A{
public:
    virtual void show(){cout<<"A"<<endl;}
};
class AA:public A
{
public:
    void show(){cout<<"AA"<<endl;}
};
void f(A &a){
    a.show();
}

int main()
{
    A a;
    AA aa;
    f(a);
    f(aa);
    return 0;
}
A
AA

如果去掉f(A &a)的&呢?
输出结果。

A
A

所以通过虚函数的实现的多态,必须要通过指针或引用

只有在程序的执行过程中,依据指针具体指向哪个类对象,或依据引用哪个类对象,才能确定调用哪一个版本,实现动态绑定

虚析构函数

如果要通过基类指针调用派生类对象的析构函数(里面包含delete),就需要让基类的析构函数成为虚函数。

class A{
public:
    A(){cout<<"A"<<endl;}
    ~A(){cout<<"~A"<<endl;}
};
class AA:public A{
public:
    int *p;
    AA(){cout<<"AA"<<endl;p = new int;}
    ~AA(){cout<<"~AA"<<endl;delete p;}
};

int main()
{
    A *pa = new AA;
    delete pa;
    return 0;
}
A
AA
~A

解决办法就是在基类的析构函数前加上关键字virtual

class A{
public:
    A(){cout<<"A"<<endl;}
    virtual ~A(){cout<<"~A"<<endl;}
};
class AA:public A{
public:
    int *p;
    AA(){cout<<"AA"<<endl;p = new int;}
    ~AA(){cout<<"~AA"<<endl;delete p;}
};

虚函数的几点说明:

  • 当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、参数的个数必须一一对应,函数的返回类型也相同。
    若函数名相同,但参数的个数不同或者参数的类型不同时,则属于函数重载而不是虚函数
    若函数名不同,显然是不同的成员函数。

  • 虚函数必须是类的一个成员函数,不能是友元函数也不能是静态的成员函数

  • 可把析构函数定义为虚析构函数。但是,不能将构造函数定义为虚函数

  • 在派生类中如果没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用基类中的虚函数

  • 实现动态多态性时,必须使用基类类型指针变量引用,指向不同的派生类对象,调用虚函数实现动态多态性。
    通过对象名访问虚函数时,只能静态绑定。即由编译器在编译的时候决定调用哪个函数。

  • 虚函数与一般的成员函数相比较,调用时的执行速度要慢一些。为了实现多态性,在每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现的。因此,除了要编写一些通用的程序,必须使用虚函数才能完成其功能要求外,通常不必使用虚函数。

  • 一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性(根据实际的类型来调用相应的函数,而不是声明的类型),以实现“一个接口,多个形态”。

但一定要注意virtual在类派生结构中的位置:

class Base0{
public:
    void v(){cout<<"Base0\n";}
};

class Base1:public Base0{
public:
    virtual void v(){cout<<"Base1\n";}
};

class A1:public Base1{
public:
    void v(){cout<<"A1\n";}
};

class A2:public A1{
public:
    void v(){cout<<"A2\n";}
};

class B1:private Base1{
public:
    void v(){cout<<"B1\n";}
};

class B2:public B1{
public:
    void v(){cout<<"B2\n";}
};

int main()
{
    Base0 *pb0;
    A1 a1;
    (pb0=&a1)->v();
    A2 a2;
    (pb0=&a2)->v();
    
    return 0;
}

原因在于pb0接受了它的派生类的赋值,只能调用自己的函数,virtual只是修饰了它的派生类。

Base0
Base0

纯虚函数(抽象类)

在基类中不对虚函数给出有意义的实现,只在派生类中有具体的意义。这时基类中的虚函数只是一个入口,具体的操作由派生类中的对象实现。这种虚函数称为纯虚函数(pure virtual)。

至少包含一个纯虚函数的类,称为抽象类(abstract classes)。抽象类只能作为派生类的基类,不能用来声明这种类的对象。

class <抽象类名>
{
	virtual <类型> <函数名>(<参数表>) =  0;
	……
}
  • 在定义纯虚函数时,不能定义虚函数的实现部分。

  • 把函数名赋于0,本质上是将指向函数的指针值赋为0。与定义空函数不一样,空函数的函数体为空,即调用该函数时,不执行任何动作。在没有重新定义这种纯虚函数之前,是不能调用这种函数的。

  • 因为纯虚函数没有实现部分,所以不能产生对象。但可以定义指向抽象类的指针,即指向这种抽象基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中给出纯虚函数的实现,否则会导致程序运行错误。

  • 抽象类的唯一用途是为派生类提供基类
    纯虚函数是作为派生类中的成员函数的基础,实现动态多态性。

demo

class A{
public:
    virtual void f()=0;
};
class A1:public A{
public:
    void f(){cout<<"A1\n";}
};
class A2:public A{
public:
    void f(){cout<<"A2\n";}
};

void fun(A *pa){
    pa->f();
}

int main()
{
    A1 a1;
    A2 a2;
    fun(&a1);
    fun(&a2);
    return 0;
}

虚函数与虚基类

  • 虚基类的用处:派生类只会有唯一一份基类的拷贝。
    要注意virtual修饰的是那唯一的一份基类,在派生的时候声明。

demo

class Base{
};
class A1:public Base{
};

class A2:public Base{
};

class B:public A1,public A2{
};

int main()
{
    B b;
    Base *base = &b; //error , Ambiguous conversion from derived class 'B' to base class 'Base':
    return 0;
}

要改为:

class Base{
};
class A1:virtual public Base{
};

class A2:virtual public Base{
};

class B:public A1,public A2{
};

int main()
{
    B b;
    Base *base = &b;
    return 0;
}
  • 虚函数的作用

用基类的指针或引用去根据实际对象的类型进行函数的调用。

这就是virtual关键字在c++中主要的两个用处。

派生类中重定义基类中的重载函数

C++中继承层次的函数调用遵循以下四个步骤:

  1. 首先确定进行函数调用的对象、引用或指针的静态类型
  2. 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
  3. 一旦找到了该名字,就进行**常规类型检查,**查看如果给定找到的定义,该函数调用是否合法。
  4. 假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。

下面的写法会报错。

class Base{
public:
    void print(){
        cout<<"print() in Base"<<endl;
    }
    
    void print(int x){
        cout<<"print(int x) in Base"<<endl;
    }
    void print(string s)
    {
        cout<<"print(string s) in Base"<<endl;
    }
    
};

class Derived:public Base{
public:
    void print(){
        cout<<"print() in Derived"<<endl;
    }
};

int main()
{
    Derived d;
    
    d.print();
    d.print("");  // error
    d.print(1);   // error
    
    return 0;
}

当仅重写了其中的一个重载函数时,在做函数名匹配时,在本类中就可以匹配到了,就不会向其父类查找了。在派生类中,仅记录了这个被重写的函数的信息,没有其他重载函数,就导致了上述错误的出现。即:派生类中的函数会将其父类中的同名函数屏蔽掉

  • 如果派生类想通过自身类型使用基类中重载版本,派生类最好重定义所有重载版本

解决方案1:
在派生类中通过using为父类函数成员提供声明,这样,派生类不用重定义所继承的每一个基类版本。

class Derived:public Base{
public:
    using Base::print;  // 使用这条语句即可继承父类所有print()函数的重载
    void print(){
        cout<<"print() in Derived"<<endl;
    }
};

解决方案2:
通过基类指针调用

int main() {  
  Derived d;  
  Base* bp = &d;   
  d.print();  
  bp->print(10);    // 这里可没有进行虚函数,执行的还是父类中的函数。
  bp->print("");    // 
  return 0;  
} 

解决方案3:
在派生类中需要重载的那个版本在基类中声明为virtual实现动态绑定,就能统一的使用基类指针来调用了:

class Base{
public:
     virtual void print(){
        cout<<"print() in Base"<<endl;
    }
    
    void print(int x){
        cout<<"print(int x) in Base"<<endl;
    }
    void print(string s)
    {
        cout<<"print(string s) in Base"<<endl;
    }
    
};
class Derived:public Base{
public:
    void print(){
        cout<<"print() in Derived"<<endl;
    }
};
int main()
{
    Derived d;
    Base *pb = &d;
    pb->print(); //动态绑定到Derived的函数
    pb->print(1);
    pb->print("");
    return 0;
}

个人觉得,第三种方法最为自然,并且最好还是自己显式地将函数全部进行重载(不要只重载一个)。

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

  • 多态类型与非多态类型

    • 有虚函数的类类型称为多态类型
    • 其它类型皆为非多态类型
  • 二者的差异
    多态类型支持运行时类型识别;多态类型对象占用额外的空间

  • 虚表(virtual table)
    每个多态类有一个虚表(virtual table),虚表中有当前类的各个虚函数的入口地址

  • 虚指针(virtual point)
    每个对象有一个指向当前类的虚表的指针(虚指针vptr)

  • 动态绑定的实现
    构造函数中为对象的虚指针赋值
    通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址,通过该入口地址调用虚函数。

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章