多型二:帶有虛擬函式的虛擬繼承
1、普通函式的繼承
class Base { public: virtual void Test1() { cout << "B::Test1()" << endl; } virtual void Test2() { cout << "B::Test2()" << endl; } int _b; }; class Derived :public Base { public: int _d; }; int main() { cout << sizeof(Derived) << endl; Derived d; d._b = 1; d._d = 2; system("pause"); return 0; }
一、帶有虛擬函式的虛擬繼承:比普通的函式多了一個偏移量指標的地址
1、
class Base { public: virtual void Test1() { cout << "B::Test1()" << endl; } virtual void Test2() { cout << "B::Test2()" << endl; } int _b; }; class Derived :virtual public Base { public: int _d; }; int main() { cout << sizeof(Derived) << endl; Derived d; d._b = 1; d._d = 2; system("pause"); return 0; }
class Derived :virtual public Base { public: int _d; }; int main() { cout << sizeof(Derived) << endl; Derived d; d._b = 1; d._d = 2; system("pause"); return 0; }
首先把基類中的成員繼承下來,在加上派生類位元組的大小,多出來的4個位元組為一個指標,指向偏移量表格的地址。
2、把派生類中的函式改為虛擬函式,大小不發生改變:
class Base
{
public:
virtual void Test1()
{
cout << "B::Test1()" << endl;
}
virtual void Test2()
{
cout << "B::Test2()" << endl;
}
int _b;
};
class Derived :virtual public Base
{
public:
virtual void Test2()
{
cout << "Derived::Test2()" << endl;
}
int _d;
};
int main()
{
cout << sizeof(Derived) << endl;
Derived d;
d._b = 1;
d._d = 2;
system("pause");
return 0;
}
class Derived :virtual public Base
{
public:
virtual void Test2()
{
cout << "Derived::Test2()" << endl;
}
int _d;
};
int main()
{
cout << sizeof(Derived) << endl;
Derived d;
d._b = 1;
d._d = 2;
system("pause");
return 0;
}
3、在派生類中新增加一個虛擬函式之後,大小增加四個位元組,新增加的虛擬函式是單獨拿出來存放的。
class Base
{
public:
virtual void Test1()
{
cout << "B::Test1()" << endl;
}
virtual void Test2()
{
cout << "B::Test2()" << endl;
}
int _b;
};
class Derived :virtual public Base
{
public:
virtual void Test2()
{
cout << "Derived::Test2()" << endl;
}
virtual void Test3()
{
cout << "Derived::Test3()" << endl;
}
int _d;
};
int main()
{
cout << sizeof(Derived) << endl;
Derived d;
d._b = 1;
d._d = 2;
system("pause");
return 0;
}
3、採用如下列印方式,列印的都是派生類的內容
class Base
{
public:
virtual void Test1()
{
cout << "B::Test1()" << endl;
}
virtual void Test2()
{
cout << "B::Test2()" << endl;
}
int _b;
};
class Derived :virtual public Base
{
public:
virtual void Test2()
{
cout << "Derived::Test2()" << endl;
}
virtual void Test3()
{
cout << "Derived::Test3()" << endl;
}
int _d;
};
typedef void(*PVTF)();
void PrintVTP(Base &b, const string &str)
{
PVTF *pVTF = (PVTF*)(*(int *)&b);//指向表格第一個元素的地址
cout << str << endl;//引用之前先列印str
while (*pVTF)//因為表格最後的位置為0,為0則不進入,完成迴圈
{
(*pVTF)();//(*pVTF)拿到空間中的內容//(*pVTF)()呼叫這個函式
++pVTF;
}
cout << endl;
}
void PrintVTP(Derived &b, const string &str)
{
PVTF *pVTF = (PVTF*)(*(int *)&b);//指向表格第一個元素的地址
cout << str << endl;//引用之前先列印str
while (*pVTF)//因為表格最後的位置為0,為0則不進入,完成迴圈
{
(*pVTF)();//(*pVTF)拿到空間中的內容//(*pVTF)()呼叫這個函式
++pVTF;
}
cout << endl;
}
int main()
{
cout << sizeof(Derived) << endl;
Derived d;
d._b = 1;
d._d = 2;
PrintVTP(d,"Derived VFT---->Base");
PrintVTP(d, "Derived VFT---->Derived");
system("pause");
return 0;
}
因為兩個函式同時給過來之後會形成過載
、
改進後:
int main()
{
cout << sizeof(Derived) << endl;
Derived d;
Base &b = d;
d._b = 1;
d._d = 2;
PrintVTP(b, "Derived VFT---->Base");
PrintVTP(d, "Derived VFT---->Derived");
system("pause");
return 0;
}
即可順利列印。
4、帶有虛擬函式的菱形虛擬繼承
class Base
{
public:
virtual void Test1()
{
cout << "B::Test1()" << endl;
}
virtual void Test2()
{
cout << "B::Test2()" << endl;
}
int _b;
};
class C1 :virtual public Base
{
public:
virtual void Test1()
{
cout << "C1::Test1()" << endl;
}
virtual void Test3()
{
cout << "C1::Test3()" << endl;
}
int _c1;
};
class C2 :virtual public Base
{
public:
virtual void Test2()
{
cout << "C2::Test2()" << endl;
}
virtual void Test4()
{
cout << "C2::Test4()" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void Test1()
{
cout << "D::Test1()" << endl;
}
virtual void Test3()
{
cout << "D::Test3()" << endl;
}
virtual void Test4()
{
cout << "D::Test4()" << endl;
}
virtual void Test5()
{
cout << "D::Test5()" << endl;
}
int _d;
};
typedef void(*PVTF)();
void PrintVTP(C1 &b, const string &str)
{
PVTF *pVTF = (PVTF*)(*(int *)&b);//指向表格第一個元素的地址
cout << str << endl;//引用之前先列印str
while (*pVTF)//因為表格最後的位置為0,為0則不進入,完成迴圈
{
(*pVTF)();//(*pVTF)拿到空間中的內容//(*pVTF)()呼叫這個函式
++pVTF;
}
cout << endl;
}
void PrintVTP(C2 &b, const string &str)
{
PVTF *pVTF = (PVTF*)(*(int *)&b);//指向表格第一個元素的地址
cout << str << endl;//引用之前先列印str
while (*pVTF)//因為表格最後的位置為0,為0則不進入,完成迴圈
{
(*pVTF)();//(*pVTF)拿到空間中的內容//(*pVTF)()呼叫這個函式
++pVTF;
}
cout << endl;
}
typedef void(*PVTF)();
void PrintVTP(Base &b, const string &str)
{
PVTF *pVTF = (PVTF*)(*(int *)&b);//指向表格第一個元素的地址
cout << str << endl;//引用之前先列印str
while (*pVTF)//因為表格最後的位置為0,為0則不進入,完成迴圈
{
(*pVTF)();//(*pVTF)拿到空間中的內容//(*pVTF)()呼叫這個函式
++pVTF;
}
cout << endl;
}
int main()
{
cout << sizeof(D) << endl;
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
Base &b = d;//通過d列印D中的虛表
PrintVTP(b, "D VFT--->B");
C1& c1 = d;//通過c1列印c1中的虛表
PrintVTP(c1, "D VFT--->C1");
C2& c2 = d;
PrintVTP(c2, "D VFT--->C2");
system("pause");
return 0;
}
派生類把自己新增加的虛擬函式加到一個虛表的後面。
(2)菱形虛擬繼承的方式,當在C1或C2中分別加一個構造(析構)函式時,多出來了4個位元組,在C1和C2中同時加上構造(析構)函式也是多出了4個位元組。但是在基類中增加一個構造(析構)函式時,沒有增加四個位元組;在派生類增加一個構造(析構)函式時也會增加4個位元組。
class C1 :virtual public Base
{
public:
C1()
{}
virtual void Test1()
{
cout << "C1::Test1()" << endl;
}
virtual void Test3()
{
cout << "C1::Test3()" << endl;
}
int _c1;
};
C1()
{}
virtual void Test1()
{
cout << "C1::Test1()" << endl;
}
virtual void Test3()
{
cout << "C1::Test3()" << endl;
}
int _c1;
};
相當於在派生類和基類之間多插入了4個位元組的0。
多型的缺陷:a、物件裡面多出來了四個位元組;
b、程式執行效率降低了(需要在虛表中找虛擬函式的地址)
那麼,我們認識了這麼久的虛擬函式,你知道哪些函式不能作為虛擬函式麼?我們一起好好回顧一下吧。
(1)全域性作用域裡面的函式不能作為虛擬函式(普通函式不能作為虛擬函式):因為虛擬關鍵字只能出現在虛擬繼承(一種繼承方式);虛擬繼承只能出現在成員函式的位置,把普通函式給成虛擬函式不能實現多型,一般情況下形成多型是根據基類型別的指標或者引用來呼叫的(必須時是成員函式,普通函式和類沒有任何關係,不能作為虛擬函式)
(2)友元函式不能作為虛擬函式(友元函式不是類的成員函式):
(3)靜態成員函式不能作為虛擬函式
虛擬函式是通過基類型別的指標或者引用來呼叫的,而靜態成員的呼叫方式可以不通過物件來呼叫。虛擬函式一定要通過物件來呼叫,會產生矛盾。
(4)類的一些其他成原函式:
a、建構函式不能作為虛擬函式:(a1)呼叫虛擬函式通過基類物件的指標或者引用,建構函式呼叫結束之後才會產生物件,沒有執行建構函式就沒有產生物件。沒有物件就不能呼叫虛的建構函式。(a2)虛擬函式的呼叫必須通過虛表,找到虛擬函式,建構函式沒有執行,物件不完整,帶有虛擬函式的類要在建構函式裡面把虛表的地址放到物件的前四個位元組中去,但是此時建構函式沒有呼叫,則不能產生物件,會發生矛盾。(建構函式沒有執行,物件不完整)
b、拷貝建構函式不能作為虛擬函式,拷貝建構函式也是建構函式
c、解構函式可以作為虛擬函式:
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
virtual ~Base()
{
cout <<"Base()::~Base()" << endl;
}
};
一般建議將解構函式給成虛擬函式,因為在繼承體系下,有的情況下沒有把基類的解構函式給成虛構函式會產生記憶體洩漏:
該程式碼中構造派生類的物件,但是最後呼叫了基類中的解構函式而沒有呼叫派生類中的解構函式
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
~Base()
{
cout <<"Base()::~Base()" << endl;
}
};
class Derived :public Base
{
public:
Derived()
:_p(new int[10])
{
cout << "Derived::Derived()" << endl;
}
~Derived()
{
cout << "Derived::~Derived()" << endl;
if (_p)
delete[] _p;
}
int *_p;
};
int main()
{
Base *d = new Derived;//基類的指標可以指向派生類的物件
delete d;
system("pause");
return 0;
}
但是把基類的解構函式給成虛擬函式之後即可正確釋放
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
virtual ~Base()
{
cout <<"Base()::~Base()" << endl;
}
};
把派生類中的解構函式給成虛擬函式,在繼承體系裡面,會構成重寫。釋放d所指向的物件,需要呼叫解構函式,在繼承體系裡面,該解構函式是一個虛擬函式,派生類前四個位元組中放的地址,是虛擬函式的地址,那麼解構函式則是派生類的解構函式。。如果沒有給成解構函式,則多型的條件不成熟,基類和派生類不能構成重寫,虛構函式不能放到虛表中去,此時會把d當作Base*型別的指標,所以此時銷燬物件只會把物件當作基類的物件釋放,呼叫基類裡面的解構函式。所以在繼承體系裡面最好把基類中的解構函式給成虛擬函式。只要派生類裡面管理資源,基類裡面的解構函式一定要給成虛擬函式,防止出現記憶體洩漏。
d、賦值運算子的過載:不能實現多型呼叫,因為引數列表不一樣。
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
virtual ~Base()
{
cout <<"Base()::~Base()" << endl;
}
virtual Base& operator=(const Base& b)
{
return *this;
}
};
class Derived :public Base
{
public:
Derived()
:_p(new int[10])
{
cout << "Derived::Derived()" << endl;
}
virtual Derived& operator=(const Derived& d)
{
return *this;
}
~Derived()
{
cout << "Derived::~Derived()" << endl;
if (_p)
delete[] _p;
}
int *_p;
};
可以採用協變的方式來構成過載
virtual Derived& operator=(const Base& d)
{
return *this;
}
但是此時只能把派生類的物件當做基類型別的物件來使用,相當於基類型別的物件給派生類型別的物件賦值,會產生問題。(基類物件比較小,派生類物件比較大,用基類物件給派生類物件賦值,只能賦值一部分,賦值不完整;而且此時不是同類物件給同類物件的賦值)此時只能通過基類物件賦值,可能發生混亂。雖然賦值運算子的過載可以給成虛擬函式,建議不要把賦值運算子的過載給成虛擬函式採用這種方法,引數列表必須給為基類物件的引用,若是給成派生類物件的引用,不能構成重寫,不能給位派生類物件的引用。給成基類物件的引用,相當於基類部分給基類部分賦值。採用這種方式,構成多型,無論採用基類物件還是派生類物件賦值方式都是用基類賦值。