一,如何理解多态?
多态(polymorphism)的字面意思是多种表现形式,多态性可以简单地概括为"一个接口,多种方法",程序在运行时才决定调用的函数,换句话说,方法的行为应取决于调用方法的对象,它是面向对象编程领域的核心概念。多态的目的是为了实现接口重用,也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到对应于各自对象的实现方法。
二,分不清对象类型的问题
class Person{
protected:
string name;
public:
Person():name("default"){
}
Person(const string &name):name(name){
}
void show(){
cout<<"name: "<<name<<endl;
}
};
class Student : public Person{
protected:
string number;
public:
Student():number("000000"){
}
Student(const string &name, const string &number):Person(name), number(number){
}
void show(){
cout<<"name: "<<name<<endl;
cout<<"number: "<<number<<endl;
}
};
void test(Person &p){
p.show();
}
int main(){
Person p("person");
Student s("student", "11041722");
test(p);
test(s);
return 0;
}
输出结果
name: person
name: student
程序分析
对象p与s分别是基类和派生类的对象,而函数test的形参是Person类的引用。按照类继承的特点,编译器把Student类对象看做是一个Person类对象。我们想利用test函数达到的目的是,传递不同类对象的引用,分别调用不同类的重载了的show成员函数,但是程序的运行结果却出乎人们的意料,编译器分不清传进来的是基类还是派生类对象,无论是基类对象还是派生类对象调用的都是基类的show成员函数。
三,使用多态解决上面的问题
为了要解决上述不能正确分辨对象类型的问题,c++提供了一种叫做多态性(polymorphism)的技术来解决问题,对于上面的程序,这种能够在编译时就能够确定哪个重载的成员函数被调用的情况被称做静态联编,而在系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性或叫动态联编。下面我们使用多态技术解决上面的问题,动态联编正是解决多态问题的方法。 把基类中的show成员函数声明为虚函数:
class Person{
protected:
string name;
public:
Person():name("default"){
}
Person(const string &name):name(name){
}
virtual void show(){
cout<<"name: "<<name<<endl;
}
};
class Student : public Person{
protected:
string number;
public:
Student():number("000000"){
}
Student(const string &name, const string &number):Person(name), number(number){
}
virtual void show(){
cout<<"name: "<<name<<endl;
cout<<"number: "<<number<<endl;
}
};
void test(Person &p){
p.show();
}
int main(){
Person p("person");
Student s("student", "11041722");
test(p);
test(s);
return 0;
}
输出结果
name: person
name: student
number: 11041722
四,虚函数
虚函数是实现多态的重要机制
a,声明与定义虚函数
class Person{
protected:
string name;
public:
Person():name("default"){}
Person(const string &name):name(name){}
virtual void show(){
cout<<"name: "<<name<<endl<<endl;
}
};
class Student : public Person{
protected:
string number;
public:
Student():number("000000"){}
Student(const string &name, const string &number):Person(name), number(number){}
virtual void show(){
cout<<"name: "<<name<<endl;
cout<<"number: "<<number<<endl<<endl;
}
};
class Worker : public Person{
private:
string job;
public:
Worker():job("worker"){}
Worker(const string &name, const string &job):Person(name), job(job){}
virtual void show(){
cout<<"name: "<<name<<endl;
cout<<"job: "<<job<<endl<<endl;
}
};
void test(Person &p){
p.show();
}
int main(){
Person p("person");
Student s("student", "11041722");
Worker w("worker", "programmer");
p.show();
s.show();
w.show();
return 0;
}
输出结果
name: person
name: student
number: 11041722
name: worker
job: programmer
Process returned 0 (0x0) execution time : 0.008 s
Press any key to continue.
b,虚函数的特点
1,如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法。如果使用了关键字virtual,程序将根据指针或引用实际指向的对象的类型来选择方法。
2,注意,关键字virtual只用于类声明的方法原型中,而没有用于方法的定义中。
3,在基类的方法声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的。
c,使用数组来表示多种类型的对象,这就是多态性
int main(){
vector<Person *> vect;
Person p("person");
Student s("student", "11041722");
Worker w("worker", "programmer");
vect.push_back(&p);
vect.push_back(&s);
vect.push_back(&w);
for(int i = 0; i < vect.size(); i ++){
vect[i]->show();
}
return 0;
}
输出结果
name: person
name: student
number: 11041722
name: worker
job: programmer
Process returned 0 (0x0) execution time : 0.015 s
Press any key to continue.
.
五,虚函数的工作原理
1,虚函数工作原理
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。如果派生类没有重新定义虚函数,该虚函数表将保存基类中同名虚函数的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中。调用虚函数时,程序将通过对象中的vptr指针,找到虚函数表,然后在虚函数表中查找要调用的函数的地址。
2,虚函数带来的额外开销
使用虚函数时,在内存与执行速度方面都有一定的开销。虽然非虚函数的效率比虚函数高,但是不具有动态编联功能。
a,每个对象都将增大,增大量为存储隐藏成员(是一个指针)的空间。
b,对于每一个类,编译器都将创建一个虚函数表。
c,对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
六,使用虚函数应注意的问题
1,构造函数不能是虚函数
创建派生类对象时,将调用派生类的构造函数,派生类的构造函数将调用基类的构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类的构造函数声明为虚的没什么意义。
2,析构函数应当是虚函数,除非类不用作基类。
3,友元不能是虚函数,友元不是类成员,而只有成员函数才能是虚函数。
4,重新定义将隐藏方法。
重新定义继承的方法并不是重载。如果重新定义派生类中的函数,无论参数列表是否相同,该操作将隐藏所有的同名基类方法
class Person{
protected:
string name;
public:
virtual void show(){
cout<<"name: "<<name<<endl;
}
};
class Student : public Person{
protected:
string number;
public:
virtual void show(int num){
cout<<"name: "<<name<<endl;
cout<<"number: "<<number<<endl;
}
};
a,重新定义覆盖基类的所有版本
Student s("student", "11041722");
s.show(1); //valid
s.show(); //invalid
b,重新定义派生类的虚函数
int main(){
Person *p;
Student s("student", "11041722");
p = &s;
p->show();
s.show(4);
return 0;
}
输出结果
name: student
name: student
number: 11041722
如果重新定义的虚函数的特征标与基类的析构函数,但是函数名相同。此时,在派生类的虚函数表中,将有两个虚函数,一个是从基类继承过来的,另一个是在派生类中重新定义的虚函数。
七,继承时的虚函数表的样子
1,一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d的虚函数表如下:
我们可以看到下面几点:
a,虚函数按照其声明顺序放于表中。
b,父类的虚函数在子类的虚函数前面。
2,一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
a,覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
b,没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了,这就实现了多态。
3,多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
a,每个父类都有自己的虚表。
b,子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
4,多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()