多态意思是”多种形态“。多态性是面向对象编程的关键思想。
C++通过以下方式支持多态
(1)经由一组隐式的转化操作。例如把一个派生类的指针转化为一个指向公共基类的指针:
shape *ps = new circle();
(2)经由dynamic_cast和typeid运算符:
if (Derived *derivedPtr = dynamic_cast<Derived*>(basePtr) { .....}
通过运行时类型识别(RTTI),程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型。
通过下面两个操作符提供RTT1:
a. typeid操作符,返回指针或引用所指对象的实际类型;
b. dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类的指针或引用。
(3)经由虚函数机制
ps->rotate();
多态可以简单地概况为“一个借口,多种方法”,在程序运行的过程中才决定调用哪个函数。多态性是面向对象编程的核心概念。
在C++中多态性仅用于通过继承而相关联的类型的引用或指针,多态通过虚函数实现。当声明基类的指针,利用该指针指向任意子类对象,调用相应的虚函数,可以根据指向的子类不同而实现不同的方法。通过基类指针或引用调用虚函数时,发生动态绑定。引用或指针既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用或指针调用的虚函数在运行时确定,被调用的函数时引用或指针所指对象实际类型所定义的。
如果没有使用虚函数的话,则肯定没有利用C++的多态性。C++中,定义为Virtual的函数是基类希望派生类重定义的函数;如果基类希望派生类继承的函数不能定义为虚函数。因此,基类应该将派生类需要重新定义的任意函数定义为虚函数。
重载与覆盖
成员函数被重载的要求:
(1)成员函数是同一个类的成员函数,即成员函数之间重载;
(2)函数名相同,函数参数(参数个数或参数类型)不同。
(3)此时virtual不是必须的,可有可无。
覆盖(或称为重写)【指的是子类重新定义父类的虚函数的做法】:
(1)函数分别位于派生类和基类中;
(2)函数名相同,参数相同;
(3)基类必须有virtual关键字。
这时,子类重定义了基类中的同名函数。
重载和覆盖的区别:
重载(overload)是同名但是参数列表不同的函数。重载是一种语法规则,重载函数的调用在编译时就确定了,是静态的,与多态无关。
覆盖(override)是派生类重写了基类的虚函数,重写的函数必须有一致的参数列表和返回值,覆盖与多态相关的;当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的函数,这样的函数调用在编译时无法确定,是在运行时动态绑定的。
隐藏规则
”隐藏“是指派生类的函数屏蔽了与其同名的基类函数。
(1)如果派生类与基类函数同名,但参数不同,此时无论有无关键字virtual,基类函数都将被隐藏;
(2)如果派生类与基类函数同名,且参数相同,但基类函数没有关键字virtual,则基类函数被隐藏。
看如下代码:
class Base{
public:
void f(void) {
cout << "Base::f(void) " << endl;
}
void f(float x) { // 这里的两个f函数间是重载关系
cout << "Base::f(float) " << x << endl;
}
void h(float x) {
cout << "Base::h(float) " << x << endl;
}
virtual void g(void){
cout << "Base::g(void) " << endl;
}
};
class Derived : public Base{
public:
void f(int x) {
cout << "Derived::f(int) " << x << endl;
}
void h(float x) {
cout << "Derived::h(float) " << x << endl;
}
void g(void) {
cout << "Derived::g(void) " << endl;
}
};
void main(void) {
Derived d;
Base base;
Base *pb = &d;
pb->f(); //1 Base::f(void)
pb->f(3.14f); //2 Base::f(float) 3.14
pb->g(); //3 Derived::g(void)
pb->h(3.14); //4 Base::h(float) 3.14
cout << endl;
Derived *pd = &d;
pd->f(42); //5 Derived::f(int) 42
pd->f(3.14f); //6 Derived::f(int) 3
pd->g(); //7 Derived::g(void)
pd->h(3.14); //8 Derived::h(float) 3.14
cout << endl;
Derived *p = (Derived *)&base;
p->h(3.4f); //9 Derived::h<float> 3.4
p->g(); //10 Base::g(void)
}
上述代码中,基类Base定义了两个f()成员函数,这两个函数参数不同,它们之间是重载关系。运行的函数(虚函数或非虚函数)是由对象的类型决定的;由于f函数是非virtual的,在编译时确定函数调用。所以main函数中注释1,2对f函数的调用中,无论运行时pb所指的对象是什么,编译时其类型是Base类型的,所以此处调用基类的f函数。
正如上面提到的隐藏的第一种情况:子类定义了与基类同名的f函数,参数不同,因此不管是否有virtual,子类的f函数都隐藏了基类的f函数。因此main函数中注释5,6调用的均是子类Derived中的f函数。即这里的函数调用取决于指针的类型。
正如上面提到的隐藏的第二种情况:子类与父类定义了同名的h函数,参数相同,此时没有virtual,子类的h函数隐藏了父类的h函数。
在父类中定义了虚函数g,子类重定义了g函数(子类重定义基类的虚函数时,virtual关键字不是必须的)。子类覆盖了父类的虚函数。因此在main函数注释3处,根据多态性:virtual函数的调用在运行时才确定,此时虽然pb是基类指针但是它指向子类对象,因此调用的是子类的g函数。
在此处讨论两种假定的情况:
(1)如果子类中不重定义g函数(去掉g函数定义部分),那么注释3处将调用的是子类从父类继承的g函数。
(2)如果子类中定义了g函数,但是其参数与基类的参数不同,例如在子类中定义的g函数为:
void g(int x) {
cout << "Derived::g(int) " << x << endl;
}
如果这样的话,这个子类的g函数将要隐藏(屏蔽)基类中的g函数,正如屏蔽规则的第一种情况;这样就不能通过子类的对象(引用或指针)调用从基类继承的虚函数g。
接着,p指针所指向的是一个强制转换为Derived类型的base对象。这时,由于p是一个Derived类型指针,因此注释9对非虚函数h的调用,编译器在编译时直接调用的是Derived类中的函数。而【注释10处还不太理解】,将Base类型的对象转化为Drived类型并赋值给Derived的指针,不过base对象始终没有发生改变,因此derived指针也指向了base对象所对应的Base::g(void)函数。
再看例子:
class A {
public:
void virtual f() {
cout << "A" << endl;
}
};
class B: public A {
public:
void virtual f() {
cout << "B" << endl;
}
};
int main() {
A* pa = new A(); //1
pa->f();
B* pb = (B*)pa; //2
pb->f();
if (B *pc = static_cast<B*>(pa)) { // 3
cout << "static_cast: ";
pc->f();
}
delete pa, pb;
pa = new B(); //4
pa->f();
pb = (B*)pa; //5
pb->f();
}
注释1毫无疑问,基类指针指向基类对象,因此调用基类的函数。
注释2处,将基类指针pa转换为派生类指针,并赋值给派生类指针pb,但是注意,pa所指的内容并没有改变,还是基类部分;因此pb指向了基类函数。
注释3,通过static_cast将pa转换为派生类指针,但是和注释2一样,pa所指向的仍然是基类,所以pc指向基类。
注释4,delete了pa,pb所指向的地址,但是这两个指针并没有删除,而是成为了垂悬指针。现在将派生类对象赋值给pa,那么pa指向派生类的成员。
注释5,此时将pa转换为派生类指针,pa所指的仍不变(pa指向的是派生类成员),所以,pb此时也指向派生类。
再看下面这个例子:
class A {
public:
virtual void fun() {
cout << "A" << endl;
}
};
class B : public A {
public:
void fun(int) {
cout << "B" << endl;
}
};
class C : public B {
public:
void fun(int) {
cout << "C(int)" << endl;
}
void fun() {
cout << "C" << endl;
}
};
int main() {
A a;
B b;
C c;
A *bp1 = &a, *bp2 = &b, *bp3 = &c;
bp1->fun(); // A
bp2->fun(); // A
bp3->fun(); // C
}
B中fun函数没有重定义A的虚函数fun,相反,它隐藏(屏蔽)了基类A的fun。结果B中有两个名为fun的函数:从A中继承来的fun虚函数,类自己定义的fun非虚函数。但是,从A继承来的虚函数不能通过B对象(或B的指针或引用)调用,因为该函数被fun(int)的定义屏蔽了。C类重定义了它继承的两个函数:重定义了A中定义的fun原始版本(覆盖了A中的虚函数),重定义了B中定义的非虚函数。
三个指针都是基类指针,因此通过在A中查找fun来确定这三个调用,所以这些调用是合法的。另外,因为fun是虚函数,所以编译器 会生成代码,在运行时基于引用或指针所绑定的实际对象类型进行调用。在bp2情况下,基本对象是B,B没有重定义不接受参数的虚函数版本,因此在B中找不到fun(),所以在基类A中查找,最终在运行时调用A中的无参版本fun()。
数组名做形参的例子
在下面的例子中涉及到了数组名做形参时,sizeof(数组名)的内容
class Base
{
public:
virtual int foo(int x){
cout << "base virtual foo" << endl;
return x*10;
}
//这里的数组作为形参,数组名是指针,因此sizeof结果为指针4
int foo(char x[]){
cout << "base int foo " << sizeof(x) << endl;
return sizeof(x)+10;}
};
class Derived:public Base
{
int foo(int x){
cout << "derived foo" << endl;
return x*20;}
virtual int foo(char x[10]){
cout << "derived virtual foo " << endl;
return sizeof (x)+20;}
};
int main(void)
{
Derived stDerived;
Base * pstBase= &stDerived;
char x[10];
printf("%d\n",pstBase->foo(100)+pstBase->foo(x));
return 0;
}
结果:
纯虚函数
通过在函数形参表后写上”=0“来指定纯虚函数:void fun() = 0;
将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。用户不能创建该类的对象。
含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。