多态
什么是多态?
多态性( polymorphism )是面向对象设计语言的基本特征之一。 多态性可以简单地概括为“一个接口,多种方法”。 通俗的说是指对于同一个消息、同一种调用,在不同的场合,不同的情况下,执行不同的行为 。
多态的两种形式
C++支持两种多态性:编译时多态和运行时多态。
编译时多态:也称为静态多态,像函数重载、运算符重载就是采用的静态多态,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,又称为先期联编(early binding)。
运行时多态:在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫晚期联编(late binding)。
C++通过虚函数来实现动态联编。
虚函数的定义
虚函数就是在基类中被声明为virtual,并在一个或多个派生类中被重新定义的成员函
数。其形式如下:
// 类内部
class 类名
{
virtual 返回类型 函数名(参数表)
{ /*...*/ }
};
//类之外
virtual 返回类型 类名::函数名(参数表)
{ /*... */}
如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。派生类要对虚函数进行中可根据需重定义,重定义的格式有一定的要求:
- 与基类的虚函数有相同的参数个数;
- 与基类的虚函数有相同的参数类型;
- 与基类的虚函数有相同的返回类型。
class Base
{
public:
virtual void display()
{
cout << "Base::display()" << endl;
}
virtual void print()
{
cout << "Base::print()" << endl;
}
};
class Derived
: public Base
{
public:
virtual void display()
{
cout << "Derived::display()" << endl;
}
};
void test(Base * pbase)
{
pbase->display();
}
int main()
{
Base base;
Derived derived;
test(&base);
test(&derived);
return 0;
}
上面的例子中,对于 test() 函数,如果不管测试的结果,从其实现来看,通过类 Base 的指针 pbase只能调用到 Base 类型的 display 函数;但最终的结果是24行的 test 调用,最终会调用到 Derived 类 的 display 函数,这里就体现出虚函数的作用了,这是怎么做到的呢,或者说虚函数底层是的怎么实现 的呢?
虚函数的实现机制
虚函数的实现是怎样的呢?简单来说,就是通过一张虚函数表( Virtual Fucntion Table )实现的。具体地讲,当类中定义了一个虚函数后,会在该类创建的对象的存储布局的开始位置多一个虚函数指针( vfptr ),该虚函数指针指向了一张虚函数表,而该虚函数表就像一个数组,表中存放的就是各虚函数的入口地址。如下图当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖( override ). 这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被覆盖。
虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?从上面的例子,可以得出结论:
-
基类定义虚函数 -
派生类重定义(覆盖、重写)虚函数 -
创建派生类对象 -
基类的指针指向派生类对象 -
基类指针调用虚函数
哪些函数不能被设置为虚函数?
-
普通函数(非成员函数):定义虚函数的主要目的是为了重写达到多态,所以普通函数声明为虚函数没有意义,因此编译器在编译时就绑定了它。 -
静态成员函数:静态成员函数对于每个类都只有一份代码,所有对象都可以共享这份代码,他不归某一个对象所有,所以它也没有动态绑定的必要。(静态函数发生在编译时,虚函数体现多态发生在运行时) -
内联成员函数:内联函数本就是为了减少函数调用的代价,所以在代码中直接展开。但虚函数一定要创建虚函数表,这两者不可能统一。另外,内联函数在编译时被展开,而虚函数在运行时才动态
绑定。
- 构造函数:这个原因很简单,主要从语义上考虑。因为构造函数本来是为了初始化对象成员才产生的,然而虚函数的目的是为了在完全不了解细节的情况下也能正确处理对象,两者根本不能“ 好好相处 ”。因为虚函数要对不同类型的对象产生不同的动作,如果将构造函数定义成虚函数,那么对
象都没有产生,怎么完成想要的动作呢
- 友元函数:当我们把一个函数声明为一个类的友元函数时,它只是一个可以访问类内成员的普通函数,并不是这个类的成员函数,自然也不能在自己的类内将它声明为虚函数。(当友元函数是成员函数的时候是可以设置为虚函数的,比如在自己类里面设置为虚函数,但是作为另一个类的友元)
虚函数的访问
指针访问使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对
象类型;
使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本
身的类型无关。
引用访问
使用引用访问虚函数,与使用指针访问虚函数类似,表现出动态多态特性。不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。因此在使用上有一定限制,但这在一定程度上提高了代码的安全性,特别体现在函数参数传递等场合中,可以将引用理解成一种 “受限制的指针” 。
对象访问
和普通函数一样,虚函数一样可以通过对象名来调用,此时编译器采用的是静态联编。通过对象名访问虚函数时, 调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。
成员函数中访问
在类内的成员函数中访问该类层次中的虚函数,采用动态联编,要使用 this 指针。
构造函数和析构函数中访问
构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用 “this->虚函数名” 的形式来调用,编译器仍将其解释为静态联编的 “本类名::虚 函数名” 。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。
|