深入理解C++重载、多态、虚函数

来源:互联网 发布:电脑登录windows密码 编辑:程序博客网 时间:2024/04/29 10:03

对VC五年的使用经验,对于C++关键技术的探测和了解有所感悟,如果写的不对请尽快指出来,QQ:308393380

  重载:在相同的声明域中的函数名相同的,而参数表不同的,即通过函数的参数表而唯一标识并且来区分函数的一种非凡的函数。

当想定义一组函数,完成相同的操作,但是他们应用在不同的参数类型上,这时候使用到了重载:例如定义三个ADD函数:

  int add(int a,int b);

  double add(double a,double b);

  float add(float a,float b);

 

 

 这三个add函数在C++里面就叫做重载技术,那么编译器如何识别呢,当然是函数参数表。编译器对于重载并不像普通函数那样,轻易的从函数参数表中识别出他们。

编译器判定重载函数的第一步是确定该调用中所考虑的重载函数的集合,该函数集合被称为候选函数(candidant function)。所谓候选函数就是与被调用函数同名的函数。上面的三个函数就是候选函数。

那么第二步,首先第一步选出的候选函数中调出可行函数,可行函数指的是参数和候选函数参数相同的那些函数,上面三个都是。然后,根据参数类型的转换规则将被调用的函数实参转换(conversion)成候选函数的实参。这里本着充分利用参数类型转换的原则,换句话说,尽可能的使用上参数类型转换。当然转换要以候选函数为转换的目标。上面的函数中三个ADD都可以。

最后一步,第二步中选出的可行函数中选出最佳可行函数(best match situation)。在最佳可行函数的选择中,从函数实参类型到相应可行函数参数所用的转化都要划分等级,根据等级的划分(ranked),最后选出最佳可行函数。

 

 

如果使用上面三个add函数,编译器在最后一步才可以找到真正的调用函数。

 

 

 

多态:C++中的多态(虽然多态不是C++所特有的,但是C++中的多态确实是很特殊的)分为静多态和动多态(也就是静态绑定和动态绑定两种现象),静动的区别主要在于这种绑定发生在编译期还是运行期,发生在编译期的是静态绑定,也就是静多态;发生在运行期的则是动态绑定,也就是动多态。

静多态可以通过模板和函数重载来实现,结合上面对重载的介绍,重载发生在编译期。

 

1)函数模板
template <typename T>
T max(const T& lsh, const T& rhs)
{
       return (lsh > rhs) ? lsh : rhs;
}
返回两个任意类型对象的最大值(对象),前提是该类型能够使用>运算符进行比较,并且返回值是bool类型。
使用:
int a = 3; int b = 4;
cout << max(a, b) << endl;
float c = 2.4; float d = 1.2;
cout << max(c, d) << endl;
输出结果为:
         4
                2.4
这种绑定发生在编译期,这是由于模板的实例化是发生在编译期的,即在编译时编译器发现你调用max(a, b)时就自动生成一个函数。
int max(const int& lsh, const int& rhs)
{
       return (lsh > rhs) ? lsh : rhs;
}
即将所有的T替换成int;
当你调用max(c, d)时就自动生成一个函数
float max(const float& lsh, const float& rhs)
{
        return (lsh > rhs) ? lsh : rhs;
}
2)函数重载:
int max (int a, int b)
{
     return (a > b) ? a : b;
}
int max (int a, int b, int c)
{
     return max(max(a, b), c);
}
两个函数名称一样,参数类型或个数不完全相同,返回值一样(这个不重要)。
使用:
int a = 3, b = 4, c = 5;
cout << max(a, b) << endl;
cout << max(a, b, c) << endl;
输出结果为:
         4
                5
确定函数的过程也发生在编译器,当你使用max(a, b),编译器发现只有两个参数,那么就调用只有两个参数的函数版本,当使用max(a, b, c)时,编译器则使用有3个参数的版本。

动态指的是通过继承、虚函数(virtual)、指针来实现。

class A {
public:
    virtual void func() const {
         coust << “A::func()” << endl;
    }
}

class B : public A {
public:
    virtual void func() const {
         coust << “B::func()” << endl;
    }
}
使用:
A a* = B();
a->func();
输出:
     B::func()
编译期是不调用任何函数的,编译器编译到a->func()时只是检查有没有语法问题,经过检查没有。编译器并不知道调用的是A版本的func()还是B版本的func(),由于a是一个指向B对象的指针,所以a只知道它指向的是一个A类型(或者能转换成A类型)的对象。通常集成体系就说明了(由于是公有继承)B是一种A。在运行期,a要调用a所指向对象的func()函数,就对它指向的对象下达调用func()的命令,结果a所指向的是一个B对象,这个对象就调用了自己版本(B版)的func()函数,所以输出时B::func()

虚函数:必须是类的非静态成员函数,其访问权限可以是protected或public,在基类的类定义中定义虚函数的一般形式:

用这个类声明一个变量 A child;
 

 

 

 

 

 

         

那么编译器产生了一个虚函数表V TA B L E,他的形式如下:

child地址 -》Base::f()->Base::g()->Base::h()->A::f1()->A::g1()->A::h1()

结论1:

虚函数按照其声明顺序放于表中。

父类的虚函数在子类的虚函数前面。

 

如果我们在A中继承了Base的虚函数。

class A:public Base

{

public:

      void f() { cout << "Base::f" << endl; } 
      void g() { cout << "Base::g" << endl; } 

virtual void g1() { cout << "Base::g" << endl; } 
      

}

 

用这个类声明一个变量 A child;
 

那么编译器产生了一个虚函数表V TA B L E,他的形式如下:

child地址 -》A::f()->A::g()->Base::f()->Base::g()->Base::h()->A::g1()

 

结论2:

覆盖的函数被放到了虚表中原来父类虚函数的位置。

没有被覆盖的函数依旧。

 

如果我们有三个基类Base1/Base2/Base3

class A:public Base1,Base2,Base3

{

public:

      virtual  void f1() { cout << "Base::f" << endl; } 
      virtual void g1() { cout << "Base::g" << endl; } 
      virtual  void h1() { cout << "Base::h" << endl; }

}

用这个类声明一个变量 A child;

那么编译器产生了一个虚函数表V TA B L E,他的形式如下:

child地址 -》Base1::f()->Base1::g()->Base1::h()->A::f1()->A::g1()->A::h1()

             -》Base2::f()->Base2::g()->Base2::h()

             -》Base3::f()->Base3::g()->Base3::h()

结论3:

每个父类都有自己的虚表。

子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

 

如果我们集成三个基类的同名虚函数:

class A:public Base1,Base2,Base3

{

public:

      void f() { cout << "Base::f" << endl; } 
      virtual void g1() { cout << "Base::g" << endl; } 
      virtual  void h1() { cout << "Base::h" << endl; }

}

用这个类声明一个变量 A child;

那么编译器产生了一个虚函数表V TA B L E,他的形式如下:

child地址 -》A::f()->Base1::f()->Base1::g()->Base1::h()->A::g1()->A::h1()

             -》A::f()->Base2::f()->Base2::g()->Base2::h()

             -》A::f()->Base3::f()->Base3::g()->Base3::h()

结论4:

三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了

虚函数和非虚函数调用方式有什么不同
   
非虚成员函数是静态确定的,换句话说,该成员函数在编译时就会被静态地选择。
    然而,虚成员函数是动态确定的,换句话说,成员函数在运行时才被动态地选择,该选择基于对象的类型,而不是指向该对象的指针或引用的类型。这被称作“动态绑定”。大多数的编译器使用以下的一些的技术:如果对象有一个或多个虚函数,编译器将一个隐藏的指针放入对象,该指针称为vptr。这个vptr指向一个全局表,该表称为vtbl。在分发一个虚函数时,运行时系统跟随对象的vptr找到类的vtbl,然后跟随vtbl中适当的项找到方法的代码。
    虚函数对象的空间开销:每个对象一个额外的指针,加上每个方法一个额外的指针。
    虚函数对象的时间开销:和普通函数调用比较,虚函数调用需要两个额外的步骤。
虚函数和重载有什么不同
    虚函数看来于函数重载有些共通之处,但是函数重载在编译期间就可以确定下来我们要
使用的函数,是可预测的;而虚函数在运行时刻才能确定到具体的函数,是不可预测的
,对于虚函数这一特性有一个专用术语----晚绑定,运用虚函数这种方法叫做函数覆盖。

 

虚函数遭遇内联
(1)虚函数是在运行时机制而内联函数特性是一个编译时的机制;
   (2)声明一个内联的虚函数会使程序在执行的时间的产生多个函数拷贝,这将导致大量的空间的浪费。

 我们再举一个例子例子:
class Shape
{
public:
 inline virual void draw()=0;
};
inline void Shape::draw()
{ cout<<"Shape::draw()"<<endl; }

class Rectangle:public Shape
{
public:
 void draw()
         { Shape::draw(); cout<<"Rectangle::draw()"<<endl; }
};
Shape* p=new Rectangle;
p->draw();

这个draw是内联的吗?不,当然不是。这要通过虚函数机制在运行时刻确定。这一调用被转换为类似于下面的一些东西:
   ( *p->vptr[ 1] )( p );
  1代表draw在虚函数列表中的位置。因为这个draw的调用通过函数指针_vptr[1]来实现,编译器不能再编译时刻确定调用函数的地址,所以函数不可为内联。

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

编译过程:

     编译器对每个包含虚函数的类创建一个表(称为V TA B L E)。在V TA B L E中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。

     用虚函数实现动态连接在编译期间,C++编译器根据程序传递给函数的参数或者函数返回类型来决定程序使用那个函数,然后编译器用正确的的函数替换每次启动。这种基于编译器的替换被称为静态连接,他们在程序运行之前执行。另一方面,当程序执行多态性时,替换是在程序执行期进行的,这种运行期间替换被称为动态连接。

虚函数定义的条件:

1、 必须把动态联编的行为定义为类的虚函数。

2、 类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来。

3、 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。

虚函数定义的注意事项:

1、非类内的成员函数不能被定义为虚函数
2、构造函数不能被定义为虚函数。

3、当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。

4、如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。

例子:

class Base {
     public:
            virtual void f() { cout << "Base::f" << endl; }
            virtual void g() { cout << "Base::g" << endl; }
            virtual void h() { cout << "Base::h" << endl; }
 
};

用这个类声明一个变量 Base p;

那么编译器为P产生了一个虚函数表V TA B L E,他的形式如下:

P地址 -》Base::f()->Base::g()->Base::h()

继续定义一个子类A派生于Base;

class A:public Base

{

public:

      virtual  void f1() { cout << "Base::f" << endl; } 
      virtual void g1() { cout << "Base::g" << endl; } 
      virtual  void h1() { cout << "Base::h" << endl; }

}

原创粉丝点击