1:子類不要覆寫父類的非虛函數。
2:子類不要覆寫從父類繼承過來的默認參數
3:子類與父類之間的賦值問題
1:子類不要覆寫父類的非虛函數。
為了解釋方便,先看一個簡單的例子。
[cpp]
class A
{
public:
A(int d):data(d){ }
void print()
{
cout<<"A print..."<<data<<endl;
}
virtual void test(int i=2)
{
cout<<"A test..."<<i<<endl;
}
private:
int data;
};
class B:public A
{
public :
B(int d):A(d){ }
void print()
{
cout<<"B print..."<<endl;
}
virtual void test(int i=4)
{
cout<<"B test..."<<i<<endl;
}
};
//測試代碼
int main() {
{
B b(5);
b.print();
A *a=&b;
a->print();
cout<<endl;
b.test();
a->test();
cout<<endl;
A a1=b;
a1.test();
}
getchar();
return 0;
}
class A
{
public:
A(int d):data(d){ }
void print()
{
cout<<"A print..."<<data<<endl;
}
virtual void test(int i=2)
{
cout<<"A test..."<<i<<endl;
}
private:
int data;
};
class B:public A
{
public :
B(int d):A(d){ }
void print()
{
cout<<"B print..."<<endl;
}
virtual void test(int i=4)
{
cout<<"B test..."<<i<<endl;
}
};
//測試代碼
int main() {
{
B b(5);
b.print();
A *a=&b;
a->print();
cout<<endl;
b.test();
a->test();
cout<<endl;
A a1=b;
a1.test();
}
getchar();
return 0;
}
運行截圖:
例子中指針a是指向對象b的,但是他們調用的print方法卻不是同一個。這裡涉及到靜態綁定和動態綁定的問題。a的靜態類型是A,a的動態的類型卻是B,b的靜態類型和動態類型都是B,因為靜態類型就是申明時的類型,動態類型是其真正指向的類型。還有一點就是非虛方法是靜態綁定,虛擬方法是動態綁定。Print是非虛方法,它是靜態綁定,調用的是自己的對象申明類型的方法,所以a調用的是A的print,b調用的是B的print方法。我想我們更想知道C++是怎麼實現動態綁定。我們都知道含有虛方法的類都有一個虛擬方法表,每個對象的實例都有一個指針指向這個虛擬方法表,子類會繼承父類的virtual方法,也可以覆寫父類的虛擬方法,如果子類覆寫父類的虛擬方法,那麼在虛擬表中對應的指針就指向子類覆寫父類的方法,如果子類不覆寫父類的虛擬方法,則還是指向父類的方法,這樣就形成了動態綁定。不同的子類按照自己的方式覆寫父類的虛擬方法,表現出不同的行為這就是多態。在多重繼承中,每個對象可能有多個虛擬表,那麼它的實例就會有多個指向虛擬表的指針,如果多個父類有一個相同的方法,那麼你就不能直接用這個實例調用這個方法,因為編譯器根本不知道它該調用哪個方法,你要指定是那個父類的方法,當你指明了哪個父類,編譯就可以通過對應的指針調用對應的虛擬表中對應的方法。那麼實例調用虛擬方法的過程是怎麼樣的呢,你有沒有想過?其實上面也提到一點,大致三步:
1:根據對象的vptr指針找到其虛擬方法表vtbl;
2:找到被調用方法在vtbl中對應的指針;
3:調用2中指針指向的方法。
2:子類不要覆寫從父類繼承過來的默認參數
這一條其實還是涉及到靜態綁定和動態綁定的問題,關於這個問題我想上面已經說得比較清楚了,默認值也是靜態綁定,這是毫無疑問的,因為它在編譯期就已經確定了,而虛擬方法確實動態綁定,你把靜態綁定的東西和動態綁定的東西攪在一起沒有問題,但是你還有得寸進尺的在子類中覆寫靜態的東西就會出問題,對不起,父類不管子類中靜態的東西,它只管自己靜態的東西,所以當子類不要覆寫從父類繼承過來的默認參數時,子類就可能出現精神分裂的行為,上面那個列子就是證明。
上面更多提到的都是關於虛擬方法的,那麼非虛擬方法呢,對象實例時怎麼調用非虛擬方法的呢?非虛擬方法是怎麼實現的呢?非虛擬方法就像一般的C函數那樣被實現的,所以他們的調用不需要像虛擬方法一樣先要找到一個指針,然後在通過這個指針調用對應的方法。
3:子類與父類之間的賦值問題
首先將父類轉換成子類的事最好不要做,因為子類的很多特性父類根本沒有,當你把一個從父類轉換過來的子類,當做子類來用的話,很可能出問題。接下來我們重點討論將子類轉換成父類。還是通過上面例子來說明問題。
B b(2);
A a=b;//調用copy constructor
a=b;//調用 operator=
上面兩行代碼,第一行先實例化了一個對象b,第二行將b賦給a,那麼是怎麼將b賦給a的呢,這裡其實調用的不是operator=,而是copy constructor,因為構造一個對象必須調用constructor,或是copy constructor,那麼這裡肯定是調用copy constructor,operator=只是一個賦值動作,一個對象還沒有構造出來怎麼給他賦值呢,在operator=可不是用來幫你構造對象的哦,在第三行的時候a已經被構造出來了,那麼這裡真的就是賦值了調用的就是operator=。總之一句話,一個對象作為左值時,第一次肯定調用的是copy constructor,被初始化後(分配了內存),之後的操作才是賦值。一個對象作為by value形式的參數,那麼每次調用的都是copy constructor,而不是operator=,我們一般都會說將實參賦給形參,其實是用實參構造一個形參。
將b賦給a,就是將b的A部分賦給a,a就是一個完全的A了,它對B一無所知,更不會表現出B的任何行為,所以by value是很暴力並且很耗性能的,也不會出現多態的行為。所以要避免使用by value,盡量用by reference。
作者:chentaihan