一.多态性
多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。 利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。
接下来详细说明联编过程。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。
1.静态联编——编译时的多态
也叫做早期绑定(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;
}
这种在编译时期就确定好了调用哪些函数的,就是早期绑定。
2.动态联编——运行时的多态
也叫做晚期绑定(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() {}
}
- 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。
- 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
- 实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。
- 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
- 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
- 函数执行速度要稍慢一些。为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
- 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。
四.虚函数表和虚表指针的概念
实际上,运行时的多态是因为虚函数表的存在,如果设计的类里面有虚函数,那么在编译时期就会生成虚函数指针和虚函数表,里面存放各个虚函数的函数指针; 如果派生类重写了基类的虚函数,那么派生类的虚函数就覆盖虚函数表里面的基类虚函数。
示例:
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,就是取第二个虚函数指针。
六.习题:多重继承时的虚表
1.多重继承时虚表的内存模型
示例:
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的虚方法。
2. 以对象调用普通方法时产生的动态联编
在上述代码不变的情况下,如果基类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()。
运行结果:
end
|