多态性和虚函数(Polymorphism&Virtual Functions)
一、相关日志
多态性与虚函数
http://blog.163.com/zhoumhan_0351/blog/static/3995422720100290234430
二、多态性与虚函数
1、关键点和概念
把函数体与函数调用相联系称为捆绑。晚捆绑只对虚函数起作用。为了达到这个目的,编译器对每个包含虚函数的类创建一个表(VTABLE)。在表中,编译器放置特定的虚函数的地址。在每个带有虚函数的类中,编译器秘密的放置一个指针,称为vpointer(VPTR),指向这个类对象的VTABLE。当通过基类指针做虚函数调用时(多态),编译器静态地插入能取得这个VPTR并在VTABLE表中查找函数地址的代码。这样就能调用正确的函数并引起晚捆绑的发生。
//: C15:Sizes.cpp
// Object sizes with/without virtual functions
#include <iostream>
using namespace std;
class NoVirtual {
int a;
public:
void x() const {}
int i() const { return 1; }
};
class OneVirtual {
int a;
public:
virtual void x() const {}
int i() const { return 1; }
};
class TwoVirtuals {
int a;
public:
virtual void x() const {}
virtual int i() const { return 1; }
};
int main() {
cout << "int: " << sizeof(int) << endl;
cout << "NoVirtual: "
<< sizeof(NoVirtual) << endl;
cout << "void* : " << sizeof(void*) << endl;
cout << "OneVirtual: "
<< sizeof(OneVirtual) << endl;
cout << "TwoVirtuals: "
<< sizeof(TwoVirtuals) << endl;
} ///:~
由上面的例子我们可以看出,有虚函数的类的长度是成员变量的总长度加上了一个void指针的长度。它反映出,如果有一个或多个虚函数,编译器都只在这个结构中插入一个单指针VPTR。
每当创建一个包含虚函数的类或从包含虚函数的类派生一个类时,编译器就为这个类创建一个惟一的VTABLE。
对于所有基类对象,或从基类派生类对象,它们的VPTR都在对象的相同位置(常常在对象的开头)。所以编译器可以取出这个对象的VPTR。VPTR指向VTABLE的开始位置。拥有虚函数的同一类簇的所有的VTABLE均有相同的顺序。
注意:不论我们在派生类中是以什么次序重载这些虚函数,它们在VTABLE中的所有函数指针都以相同的次序出现。
在向上类型转换时,只是处理地址。我们还应当意识到,早绑定比晚绑定效率更高。
2、抽象类和纯虚函数
virtual void f() = 0;
告诉编译器在VTABLE中为函数保留一个位置,但在这个特定位置中不放地址。只要有一个函数在类中声明为纯虚函数,VTABLE就是不完全的。纯虚函数禁止对抽象类的函数以传值调用,也是防止对象切片(object slicing)。通过抽象类,可以保证在向上类型转换期间总是使用指针或引用(因为抽象类不能定义对象,所以不能用对象向上类型转换)。
3、纯虚定义
我们可以给纯虚函数进行定义一段公共代码,这样就不需要在每个派生类中都分别定义了(按照如下的形式)。然而,虽然这样定义,纯虚类仍然不能定义对象,且在派生类仍然需要重新定义。
//: C15:PureVirtualDefinitions.cpp
// Pure virtual base definitions
#include <iostream>
using namespace std;
class Pet {
public:
virtual void speak() const = 0;
virtual void eat() const = 0;
// Inline pure virtual definitions illegal:
//! virtual void sleep() const = 0 {}
};
// OK, not defined inline
void Pet::eat() const {
cout << "Pet::eat()" << endl;
}
void Pet::speak() const {
cout << "Pet::speak()" << endl;
}
class Dog : public Pet {
public:
// Use the common Pet code:
void speak() const { Pet::speak(); }
void eat() const { Pet::eat(); }
};
int main() {
Dog simba; // Richard's dog
simba.speak();
simba.eat();
} ///:~
对于可被创建的每个对象(它的类不含有纯虚函数),在它的VTABLE中总有一个函数地址全集。在派生类中没有定义的用基类的地址。
如果在派生类中增加虚函数,而用基类的指针调用,则碰到派生类的虚函数调用时会非法。
//: C15:AddingVirtuals.cpp
// Adding virtuals in derivation
#include <iostream>
#include <string>
using namespace std;
class Pet {
string pname;
public:
Pet(const string& petName) : pname(petName) {}
virtual string name() const { return pname; }
virtual string speak() const { return ""; }
};
class Dog : public Pet {
string name;
public:
Dog(const string& petName) : Pet(petName) {}
// New virtual function in the Dog class:
virtual string sit() const {
return Pet::name() + " sits";
}
string speak() const { // Override
return Pet::name() + " says 'Bark!'";
}
};
int main() {
Pet* p[] = {new Pet("generic"),new Dog("bob")};
cout << "p[0]->speak() = "
<< p[0]->speak() << endl;
cout << "p[1]->speak() = "
<< p[1]->speak() << endl;
//cout << "p[1]->sit() = "
//<< p[1]->sit() << endl; // Illegal
} ///:~
当然可以类型转换:
((Dog*)p[1])->sit()
4、RTTI
是在关向下类型转换基类指针到派生类指针的问题。向上类型转换是自动发生的,向下类型转换则是不安全的。
当使用对象向上类型转换(不是指针或是引用)时,将发生对象切片,如下图所示,应当比避免使用:
5、对于虚函数,如果我们在派生类中重写,编译器要求我们不能改变基类虚成员函数的返回值(如果不是虚函数是允许的)。但是允许我们改变参数列表的类型和个数,同样,改变了以后基类中成员函数将被隐藏不可调用。但是如果把派生类向上类型转换到基类,则只基类的成员函数可用,而派生类中的方法又将不可行。
//: C15:NameHiding2.cpp
// Virtual functions restrict overloading
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
virtual int f() const {
cout << "Base::f()\n";
return 1;
}
virtual void f(string) const {}
virtual void g() const {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Overriding a virtual function:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Cannot change return type:
//! void f() const{ cout << "Derived3::f()\n";}
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//! d2.f(s); // string version hidden
Derived4 d4;
x = d4.f(1);
//! x = d4.f(); // f() version hidden
//! d4.f(s); // string version hidden
Base& br = d4; // Upcast
//! br.f(1); // Derived version unavailable
br.f(); // Base version available
br.f(s); // Base version abailable
} ///:~
6、特例
在上面我们说了,虚成员函数在派生类中不能改变类型,但是如果我们定义虚函数的返回类型是一个指向基类的指针或引用,则在派生类中,我们可以定义该虚函数的返回类型是一个指向其基类的派生类。如下所示:
//: C15:VariantReturn.cpp
// Returning a pointer or reference to a derived
// type during ovverriding
#include <iostream>
#include <string>
using namespace std;
class PetFood {
public:
virtual string foodType() const = 0;
};
class Pet {
public:
virtual string type() const = 0;
virtual PetFood* eats() = 0;
};
class Bird : public Pet {
public:
string type() const { return "Bird"; }
class BirdFood : public PetFood {
public:
string foodType() const {
return "Bird food";
}
};
// Upcast to base type:
PetFood* eats() { return &bf; }
private:
BirdFood bf;
};
class Cat : public Pet {
public:
string type() const { return "Cat"; }
class CatFood : public PetFood {
public:
string foodType() const { return "Cat Food"; }
};
// Return exact type instead:
CatFood* eats() { return &cf; }
private:
CatFood cf;
};
int main() {
Bird b;
Cat c;
Pet* p[] = { &b, &c, };
for(int i = 0; i < sizeof p / sizeof *p; i++)
cout << p[i]->type() << " eats "
<< p[i]->eats()->foodType() << endl;
// Can return the exact type:
Cat::CatFood* cf = c.eats();
Bird::BirdFood* bf;
// Cannot return the exact type:
//! bf = b.eats();
// Must downcast:
bf = dynamic_cast<Bird::BirdFood*>(b.eats());
} ///:~