一、什么是多态
多态就是“多种状态”,具体到面向对象语言中,就是调用一套接口,根据不同的情况会产生不同的结果。这听起来似乎很神奇,以下代码就是对多态最好的解释。
代码:
# include <iostream>
using namespace std;
class Base {
public:
int a;
int b;
public:
virtual void func1() {
cout << "I'm Base" << endl;
}
};
class Child1 : public Base {
public:
int a;
int b;
public:
virtual void func1() {
cout << "I'm Child1" << endl;
}
};
class Child2 : public Base {
public:
void func1() {
cout << "I'm Child2" << endl;
}
};
int main() {
Base* base = dynamic_cast< Base* > (new Child1);
base->func1();
delete base;
base = dynamic_cast< Base* >(new Child2);
base->func1();
delete base;
base = NULL;
getchar();
return 0;
}
结果
I'm Child1
I'm Child2
现象:Child1和Child2继承自同一个父类Base,Base中有一个虚函数func1(),子类Child1和Child2分别重写了父类Base中的虚函数func(),然后在主函数中父类指针分别指向了子类Child1和Child2两个子类对象,在父类指针调用func1()函数时,由于指向不同的子类对象,调用了不同子类对象的func1()函数。这就是多态,但是一般程序中不这样用,一般用法如下:
# include <iostream>
# include <string>
using namespace std;
class Animal {
private:
string name;
string color;
string eating;
public:
Animal() {}
Animal(string name, string color, string eat) {
this->name = name;
this->color = color;
this->eating = eat;
}
public:
virtual void DoSth() {
eat();
this->run();
}
virtual void eat() {
cout << color + name + eating << endl;
}
virtual void run() {
cout << color + name + "is running" << endl;
}
};
class Cat : public Animal {
private:
string name;
string color;
string eating;
public:
Cat() {}
Cat(string name, string color, string eat) {
this->name = name;
this->color = color;
this->eating = eat;
}
public:
virtual void eat() {
cout << color + name + eating << endl;
}
virtual void run() {
cout << color + name + "is running" << endl;
}
};
class Dog : public Animal {
private:
string name;
string color;
string eating;
public:
Dog() {}
Dog(string name, string color, string eat) {
this->name = name;
this->color = color;
this->eating = eat;
}
public:
virtual void eat() {
cout << color + name + eating << endl;
}
virtual void run() {
cout << color + name + "is running" << endl;
}
};
void Do_Things( Animal* an ) {
an->DoSth();
delete an;
}
int main() {
Do_Things(new Dog("小狗", "黑色", "吃狗粮"));
Do_Things(new Cat("小猫", "花色", "吃猫粮"));
getchar();
return 0;
}
上示代码中,对于函数 void Do_Things( Animal* an ); 虽然传入参数类型是 Animal ,但是如果传入的是 Dog 类的实例化,那么执行的是 dog 对象的方法,如果传入的是 Cat 类的实例化,那么执行的是 cat 对象的方法。当然,为了明显,您可以尝试在 Cat 类中加入新的方法,在Cat继承自 Animal 类的虚函数中去调用,这样会更明显一些。
二、总结多态成立的条件
①子类继承父类
②子类重写父类虚函数
③父类指针指向子类对象
三、多态的实现原理
1、类的内存布局图
多态的实现依赖于编译器的提前布局,这要从继承说起。以上述 Animal 类为例:
class Animal {
private:
string name;
string color;
string eating;
public:
Animal() {}
Animal(string name, string color, string eat) {
this->name = name;
this->color = color;
this->eating = eat;
}
public:
virtual void DoSth() {
eat();
this->run();
}
virtual void eat() {
cout << color + name + eating << endl;
}
virtual void run() {
cout << color + name + "is running" << endl;
}
};
以下附上 Animal 类的内存布局图:
1> class Animal size(88):
1> +---
1> 0 | {vfptr}
1> 4 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ name
1> 32 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ color
1> 60 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ eating
1> +---
1>
1> Animal::$vftable@:
1> | &Animal_meta
1> | 0
1> 0 | &Animal::DoSth
1> 1 | &Animal::eat
1> 2 | &Animal::run
1>
1> Animal::DoSth this adjustor: 0
1> Animal::eat this adjustor: 0
1> Animal::run this adjustor: 0
—————————————————————————————
class Animal size(88)
表示该类占用内存字节数
—————————————————————————————
1> 0 | {vfptr}
1> 4 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ name
1> 32 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ color
1> 60 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ eating
表示该类中包含哪些成员(注意:类中不包含成员方法),Animal 类中将要占用内存的是 指向虚表 的虚函数指针和成员属性(那几个string)。类是抽象的概念,不占用内存,只有当类实例化为某个对象才会占用内存。左边的0、4、32、60表示内存地址偏移。
—————————————————————————————
1> Animal::$vftable@:
1> | &Animal_meta
1> | 0
1> 0 | &Animal::DoSth
1> 1 | &Animal::eat
1> 2 | &Animal::run
表示 Animal 类的虚表。所有该类的虚函数入口地址都放在虚表中,左侧的0、1、2表示虚函数编号。
—————————————————————————————
最后那些是关于内存对齐的。
2、基类中包含有virtual函数时子类继承过程(多态机制)
①基类中,只要有virtual函数,那么基类的开始处必然有一个虚指针,该虚指针指向该类对应的虚表,虚表中存放了该类所有虚函数的入口地址。
②当有子类继承基类时,子类中也一定包含一个虚指针,这个虚指针是从父类中继承过来的(子类的虚指针起初指向父类虚表),之后,子类重建自己的虚表,并且在虚表放入了父类的虚函数入口地址(当然如果子类声明了自己的虚函数,也会再加入到子类的虚表中),当子类重写父类虚函数时,子类虚表中对应入口地址将变为子类重写的虚函数入口地址。这就是多态发生的基础。
③调用时传入的是子类对象,但是接收参数的是父类指针,对于指针来说,两大属性,首地址和步长,子类对象指针步长一般大于等于父类对象,将子类对象指针转化成父类对象指针是安全的。当以父类指针的类型调用成员方法时,如果子类对象重写了父类虚函数,实际调用的是传入的子类对象重写父类虚函数后的方法,产生多态;若子类对象没有重写父类虚函数的方法,那么子类虚表中有的是克隆来的父类的方法,没有多态产生。
综上所述,C++中的多态本质应该是以函数指针实现的:父类每一个虚函数,都以一个确定的函数指针类型存入虚表中,并且对应该函数的入口地址;当子类继承该父类时,继承了父类的虚表,虚表中有父类虚函数类型的函数指针,当重写父类方法时,实际相当于改变了子类虚表中函数指针的指向。于是在调用时多态发生。
3、以下附上上述 Cat 类的内存布局图
class Cat : public Animal {
private:
string name;
string color;
string eating;
public:
Cat() {}
Cat(string name, string color, string eat) {
this->name = name;
this->color = color;
this->eating = eat;
}
public:
virtual void eat() {
cout << color + name + eating << endl;
}
virtual void run() {
cout << color + name + "is running" << endl;
}
};
1> class Cat size(172):
1> +---
1> 0 | +--- (base class Animal)
1> 0 | | {vfptr}
1> 4 | | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ name
1> 32 | | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ color
1> 60 | | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ eating
1> | +---
1> 88 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ name
1> 116 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ color
1> 144 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ eating
1> +---
1>
1> Cat::$vftable@:
1> | &Cat_meta
1> | 0
1> 0 | &Animal::DoSth
1> 1 | &Cat::eat
1> 2 | &Cat::run
1>
1> Cat::eat this adjustor: 0
1> Cat::run this adjustor: 0
四、总结
多态是面向对象最重要的特性,多态主要为了提高代码的可扩展性。多态的产生三大条件缺一不可,这也是区分多态、函数重载等的精辟总结(这是前辈总结,万分感激)。
多态的作用
提高代码可扩展性
形成多态三要素
有继承、子类重写父类虚函数、父类指针指向子类对象