多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。
利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。
接下来详细说明联编过程。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。
也叫做早期绑定(early binding),静态联编(static binding)。
通过函数的重载和运算符的重载来实现的。
int Max(int a, int b) {
return a > b ? a : b;
}
char Max(char a, char b) {
return a > b ? a : b;
}
double Max(double a, double b) {
return a > b ? a : b;
}
int main(void)
{
int x = Max(12, 23);
char ch = Max('a', 'b');
double dx = Max(12.23, 34.45);
return 0;
}
这种在编译时期就确定好了调用哪些函数的,就是早期绑定。
也叫做晚期绑定(late binding),(dynamic binding)
运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定。
它是通过类继承关系public和虚函数来实现的。目的也是建立一种通用的程序。通用性是程序追求的主要目标之一。
虚函数是一个类的成员函数,定义格式为:
virtual 返回类型 函数名(参数表);
关键字virtual指明该成员函数为虚函数。
virtual仅用于类定义中,如果虚函数在类外定义,不可以加关键字。
这里的基类为Animal类,派生类为Dog类和Cat类。
在派生类中,分别重写了基类的虚函数。
class Animal
{
private:
string name;
public:
Animal(const string& na) : name(na) {}
public:
virtual void eat() {}
virtual void walk() {}
virtual void PrintInfo() {}
string& get_name() { return name; }
const string& get_name()const { return name; }
};
class Dog : public Animal
{
private:
string owner;
public:
Dog(const string& ow, const string& na) : Animal(na), owner(ow) {}
virtual void eat() { cout << "Dog eat: bone" << endl; }
virtual void walk() { cout << "Dog walk: run" << endl; }
virtual void PrintInfo()
{
cout << "Dog owner 's name: " << owner << endl;
cout << "Dog name: " << get_name() << endl;
}
};
class Cat : public Animal
{
private:
string owner;
public:
Cat(const string& ow, const string& na) : Animal(na), owner(ow) {}
virtual void eat() { cout << "Cat eat: fish" << endl; }
virtual void walk() { cout << "Cat walk: silent" << endl; }
virtual void PrintInfo()
{
cout << "Cat owner 's name: " << owner << endl;
cout << "Cat name: " << get_name() << endl;
}
};
运行示例:
如果将派生类传递给基类的引用或指针,再以基类指针调用虚方法,就会发生动态联编。
void fun(Animal& animal)
{
animal.eat();
animal.walk();
animal.PrintInfo();
}
int main(void)
{
Dog dog("Srh", "二哈");
Cat cat("Sauron", "汤姆猫");
fun(dog);
fun(cat);
return 0;
}
可以看出,虽然fun()函数里是拿基类的引用来调用虚方法,打印结果却不同,这就是动态联编。
注意点:
为什么要使用公有继承?
因为公有继承代表 is-a 关系,即猫是动物的一种。不能使用私有继承。
1的示例:这种情况下是函数重载
class Object
{
public:
virtual Object* fun() {}
};
class Base : public Object
{
public:
virtual Base* fun() {}
}
实际上,运行时的多态是因为虚函数表的存在,如果设计的类里面有虚函数,那么在编译时期就会生成虚函数指针和虚函数表,里面存放各个虚函数的函数指针;
如果派生类重写了基类的虚函数,那么派生类的虚函数就覆盖虚函数表里面的基类虚函数。
示例:
class Object
{
private:
int value;
public:
Object(int x = 0) : value(x) {}
virtual void add() { cout << "Object::add" << endl; }
virtual void fun() { cout << "Object::fun" << endl; }
};
class Base : public Object
{
private:
int sum;
public:
Base(int x = 0) : Object(x + 10), sum(x) {}
virtual void add() { cout << "Base::add" << endl; }
virtual void fun() { cout << "Base::fun" << endl; }
};
int main()
{
Base base(10);
Object* op = &base;
return 0;
}
运行结果:
vfptr为虚表的指针,指向虚函数表,里面存放虚函数的指针。
注意点:
如果拿对象名加(.)调用方法,那么不管该方法是否为虚函数,都调用该对象的方法。
如果是以基类的指针或引用指向派生类,那么调用虚方法时就会采用动态联编,调用派生类的虚方法。
示例:
int main()
{
Base base(10);
Object* op = &base;
op->add();
op->fun();
return 0;
}
如图,在调用op->add()时,先将op的地址加入到eax中,再从eax中取出虚表的地址给edx;
eax再从edx中取地址,这时取得的地址为第一个虚函数的地址,调用该函数。
在执行到op->fun()时,和刚才的区别就是eax取地址时是 [edx + 4]。
原因是虚函数表里存放的都是虚函数指针,每个指针都占四字节,对其加4,就是取第二个虚函数指针。
示例:
class Object
{
private:
int value;
public:
Object(int x = 0) : value(x) {}
virtual void add() { cout << "Object::add" << endl; }
virtual void fun() { cout << "Object::fun" << endl; }
virtual void Print() { cout << "Object::Print" << endl; }
};
class Base : public Object
{
private:
int sum;
public:
Base(int x = 0) : Object(x + 10), sum(x) {}
virtual void add() { cout << "Base::add" << endl; }
virtual void fun() { cout << "Base::fun" << endl; }
virtual void show() { cout << "Base::show" << endl; }
};
class Test : public Base
{
private:
int num;
public:
Test(int x = 0) : Base(x + 10), num(x) {}
virtual void add() { cout << "Test::add" << endl; }
virtual void Print() { cout << "Test::Print" << endl; }
virtual void show() { cout << "Test::show" << endl; }
};
虚表内存模型:
基类Object的虚表为:
派生类Base的虚表为:
因为Base重写了add()和fun()这两个虚方法,所以会覆盖基类的。没有重写Print(),那么基类的虚函数还会存在虚表中。
派生类Test的虚表为:
重写了Base的add(),和show()这两个虚方法,所以替换为Test的虚方法;Base的fun()未重写,继续留在虚表;重写了Object的Print()方法,所以替换为Test的虚方法。
在上述代码不变的情况下,如果基类Object中有一个普通方法,里面调用虚方法:
class Object
{
private:
int value;
public:
Object(int x = 0) : value(x) {}
virtual void add() { cout << "Object::add" << endl; }
virtual void fun() { cout << "Object::fun" << endl; }
virtual void Print() { cout << "Object::Print" << endl; }
void function()
{
fun();
}
};
那么在通过对象调用该方法时,都分别调用什么?
int main(void)
{
Test t1;
Base b1;
Object obj;
t1.function();
b1.function();
obj.function();
return 0;
}
实际上,虽然是以对象调用,很多人会认为这个就是普通的静态联编。但这种调用方式是通过this指针调用,所以函数内部调用的虚函数也是通过this指针。
运行示例:
首先记住这三个对象的虚表
第一步
在对象t1调用function()时,this指针里的虚函数指针指向的是Test类的虚表,那么他调用fun()函数会在自己的虚表里找,此时的fun()函数并未重写,所以会调用它的基类Base的fun()函数。
第二步
通过b1对象调用,那么虚表指针此时指向Base的虚表,会调用Base::fun()。
第三步
通过对象obj来调用,那么虚表指针此时指向Object的虚表,就会调用Object::fun()。