多态性和虛函數(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());
} ///:~