多态:不同类的对象对同样的消息可能调用不同的方法。“一个接口,多种实现”。
1 基类指针指向派生类对象
根据C++指针的定义,一种类型的指针不能指向另一种类型的变量(void*是通用变体,没有特定类型),但对于基类的指针和派生类的对象是个例外。
实现虚函数的前提条件:基类的指针可以指向其派生类的对象,反过来不可以。
实际上,这种情况下,基类的指针通过指针也只能访问派生类中从基类继承而来的公有成员,不能访问派生类中新增的成员,除非通过强制类型转换把基类指针转换成派生类指针。
#include <iostream>
using namespace std;
class A
{
private:
int a;
public:
void setA(int i){ a = i;}
void showA(){cout << "a = "<<a<<endl;}
};
class B : public A{
private:
int b;
public:
void setB(int i){b = i;};
void showB(){cout<<"b="<<b<<endl;}
};
int main()
{
A a, *pa; // pa为基类对象指针
B b, *pb; // pb为派生类对象指针
pa = &b; // 基类指针pa指向派生类对象b
pa->setA(100); // 通过基类指针访问访问派生类B从基类A继承来的公有成员
pa->showA();
pb = (B*)pa; // 将基类指针强制转换为派生类指针
pb->setB(200); // 不能通过基类指针访问派生类自己定义的成员
pb->showB();
return 0;
}
输出:
a = 100
b = 200
不允许这样:
pa->showB()
pb = &pa;
另外
A a, *pa; // pa为基类对象指针
B b, *pb; // pb为派生类对象指针
pa = &a; // OK
pb = (*B)pa; // OK.将基类指针强制转换为派生类指针
pb->showB(); // 异常,pa指向的是基类对象,就算强制转换为派生类指针,访问对象本来不存在属于派生类新增的showB()方法会导致异常
2. 虚函数
问题,如上例,如果基类指针想要访问派生类新定义的成员怎么办?只能用虚函数。不然基类指针是无法访问派生类中的新内容的,访问的都是派生类里面继承于基类的成员。
派生类总会有与基类不同的特性,这是派生的意义所在,这些特性可能是新添加的,也可能是在原有基础上修改部分行为而作为一个新成员函数。也就是说,派生类会覆盖基类的成员,派生类的成员函数可以覆盖/屏蔽基类的成员函数(派生类具有和基类的成员函数相同的函数名以及形参声明)。
但要访问覆盖后的新成员,也只能是派生类对象的指针(编译器检查的是指针的类型,基类的指针只能访问继承基类的部分)。覆盖以后要访问基类原有的成员(继承部分),使用基类名和权限域控制符::
class A
{
private:
int a;
public:
void setA(int i){ a = i;}
void show(){cout << "a = "<<a<<endl;}
};
class B : public A{
private:
int b;
public:
void setB(int i){b = i;};
void show(){cout<<"b = "<<b<<endl;}
};
int main()
{
A a, *pa; // pa为基类对象指针
B b, *pb; // pb为派生类对象指针
pa = &b; // 基类指针pa指向派生类对象b
pa->setA(100);// 换成pb->setB(); 出错:setB is not a member of A
pa->show();// 换成pb->B::show(); 出错:B is not a base class of A
pb = (B*)pa;
pb->setB(200);
pb->show(); // 换成pb->A::show();输出是 a = 100
return 0;
}
所以如果一定要基类的指针访问派生类中覆盖基类的成员,那就只能通过虚函数。
虚函数:关键字virtual,一个基类中的方法被定义为虚函数,意味着这个成员函数可能被派生类改写。
这种机制下和上面的不一样,调用这个接口具体产生什么行为,取决于基类指针所指向的对象。(动态绑定,只有在运行时才能确定绑定的是什么对象)
类体内声明方式: virtual <函数类型> <函数名>(<形参声明>);
类体外定义不能再加virtual
【在运行时,当通过基类指针调用一个虚函数时,程序能够根据指针当前所指向的对象(基类或者派生类)动态地自动调用对应的类的成员函数】
PS: 去掉virtual和不去掉virtual差别是很大的。
虚函数作为基类的非私有成员,也可以被继承,在派生类中如果没有重新定义虚函数,则派生类继承基类的虚函数;如果派生类重新定义虚函数,则函数原型(返回类型、函数名、形参个数和类型)必须和基类完全一致。
虚函数必须通过对象或者其指针调用,不能定义为静态成员函数(动态绑定和静态函数不相容)
静态绑定:编译时期进行绑定,早期绑定。【静态多态/编译时多态】
动态绑定:运行时绑定(编译阶段不确定要调用的函数),后期绑定。【动态多态/运行时多态】
静态多态:在函数名(包括运算符)相同的情况下,编译器能够根据参数类型的不同(尽管函数名相同)确定要调用的函数。这种静态多态机制是通过重载来实现的。
动态多态:函数名和参数类型都相同情况下,程序运行时才能确定要调用的函数。通过虚函数机制实现。根据基类指针所指对象的类型确定要调用的虚函数。
虚函数机制必须是基类指针和派生类对象成员函数之间的,同一层次的类不能通过指针访问同一层次的另一个类的成员函数。
class Animal
{
public:
string name;
void setName(string s){ name = s;}
virtual void sound(){cout << name <<endl;}
};
class Dog : public Animal{
private:
int bone;
public:
void setName(){ name = "Dog";}
void sound(){cout<<name<<": Woof!"<<endl;}
};
int main()
{
Animal a, *pa; // pa为基类对象指针
Dog d, *pb; // pb为派生类对象指针
pa = &d; // 基类指针pa指向派生类对象d
pa->setName("Doggie");
pa->sound();
pb = (Dog*)pa;
pb->sound();
pb->setName();
pb->sound();
return 0;
}
输出
Doggie: Woof!
Doggie: Woof!
Dog: Woof!