虽然在开始的时候一直觉得C++的多态是一个很神秘的概念,但是从网上查了一些资料之后,发现,多态是一个不难理解的概念。昨天整理了一些资料拿出来和小组一块讨论一下。感觉效果很好,现在就讨论的几个要点做一下总结。
1、首先说明多态的概念:
“一个接口,多种方法”,而且是在运行时动态链接的。它与普通的函数调用的不同之处是,它依赖在运行的时候查看调用的对象的类型,从而进一步决定调用的函数。多态概念是面向对象里面非常典型的概念。
2、多态与虚函数:
虚函数:在类的成员函数中有关键字 virtual标识的,可以在继承父类的子类中重新改写的函数。
多态与虚函数的关系非常密切,没有虚函数就不存在多态,多态的形成是依赖虚函数的。
多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而在运行时候通过确定调用函数的类对象是子类或者父类来决定调用哪个函数,从而呈现的一个函数名,多种形态,多种效果。
3、说一下,重载(重写)的概念:
在虚函数的重载中,子类重新定义父类的做法叫做重载,又被称为覆盖。重载分为静态重载和动态重载。
静态重载:就是普通函数(非虚函数)的重新改写,同一函数名称实现的功能相似,只是形参不同,这样的形式也是重载,
动态重载:对应的就是虚函数。就是刚刚说的多态性的体现,下面用代码来说明下:
class Base
{
public:
virtual void f() { std::cout << "Base::f" << std::endl;}
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
void i(){std::cout << "Base::i" <<std::endl; std::cout << this << '\n'; }
};
class Derived : public Base
{
public:
void f(){std::cout << "Derived::f"<<std::endl;}
void i(){std::cout << "Derived::i"<<std::endl;}
virtual void M()
{
std::cout << "Derived::M" << std::endl;
}
};
在上面的代码中对于函数f()就是典型的多态性,动态重载,在Derived类中覆盖掉了虚函数Base::f()替换成为Derived::f()。而对于函数i()就是普通普通的重载,两个不同的函
数Base::i()和Derived::i(),他们的调用是在编译阶段就决定了使用哪个函数,而对于函数f()是在执行阶段决定的。
4、对于普通函数的重载不具备多态。其实现有自己的特点:
int main()
{
Base *b1 = new Base;
Base *b2 = new Derived;
Derived *d1 = new Derived;
b1->i();
b2->i();
d1->i();
}
上面的实现中由于b1,b2都是Base类的指针,其函数的地址的偏移量是固定的,他们调用函数i()输出都是Base::i,而d1他是Derived指针,他的输出是Derived::i().
5、虚函数的多态性:
首先通过对比上面的代码来看多态:
int main()
{
Base *b1 = new Base;
Base *b2 = new Derived;
Derived *d1 = new Derived;
b1->f();
b2->f();
d1->f();
return 0;
}
这里由于b1指向的是Base对象,所以输出是Base::f,而b2,b3都是指向的Derived对象,所以输出是Derived::f。这里对于b2->f()输出的不同来体会多态。
6、多态的实质,虚函数表(vtable)
每个不同的类,(如果存在虚函数的话)都有自己的一个虚函数表,而且所有类的对象实例是公用一个虚函数表的。
在类的实例化对象中,首先要存储一个4个字节的虚函数表指针,然后才存储类的其他成员。该指针指向虚函数表,例如:如果声明Base::b,那么&b就是虚函数表的地址,可以通过指针函数来取虚函数表中的各个虚函数的地址来调用,不过这里不多加讨论。
7、再看重载(理解覆盖):
继承父类的子类他的虚函数表如下图所示:Derived d,首先将从父类中继承的虚函数的所有的地址在自己的虚函数表里面复制一份,然后再加上自己定义的虚函数。
上面的图对应的代码形式如下:
class Base
{
public:
virtual void f() { std::cout << "Base::f" << std::endl;}
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
};
class Derived : public Base
{
public:
virtual void f1(){std::cout << "Derived::f1" << std::endl;}
virtual void g1(){std::cout << "Derived::g1" << std::endl;}
virtual void h1(){std::cout << "Derived::h1" << std::endl;}
};
而如果发生了虚函数的重载,子类的虚函数表是要被覆盖的。代码如下:
class Base
{
public:
virtual void f() { std::cout << "Base::f" << std::endl;}
virtual void g() { std::cout << "Base::g" << std::endl; }
virtual void h() { std::cout << "Base::h" << std::endl; }
};
class Derived : public Base
{
public:
void f(){std::cout << "Derived::f"<<std::endl;}
virtual void f1(){std::cout << "Derived::f1" << std::endl;}
virtual void g1(){std::cout << "Derived::g1" << std::endl;}
virtual void h1(){std::cout << "Derived::h1" << std::endl;}
};
对应上面的代码,其形式如下图所示,这样大家应该对多态有了一定的了解了。
8、当然还有一些复杂点的情况,我们小组讨论过的,有关虚函数表的存储地址的情况,还有一个子类继承于多个父类的虚函数表的情况,在这里不再赘述。
9、对于虚函数表,是编译时候建立的,在执行的时候根据类对象来确定调用哪个函数。而普通的函数调用是在编译时期就确定了调用哪个函数。
10、大家一块讨论知识的感觉很棒,昨天的讨论挺成功,期望以后的小组讨论会越来越给力。