C++学习之继承篇
今天通過對實驗二繼承,重載,覆蓋的學習,讓我更深一步理解了這些概念的區別。
首先來明確一個概念,函數名即地址,也就是說函數名就是個指針。
編譯階段,編譯器為每個函數的代碼分配一個地址空間并編譯函數代碼到這個空間中,函數名就指向這個地址空間。
也即每個函數名都有自己唯一的代碼空間。
同理,類的成員函數也是如此。
但是,有一點大家一定要記住,C++編譯器編譯CPP文件時,會根據"C++編譯器的函數名修飾規則" 對函數名進行修飾。
(修飾規則大家自己去搜吧,我就不敘述了),前面講到函數名稱的作用是指向函數真實代碼的指針。
知道了以上規則,那么我們對函數覆蓋便不難理解了。
首先來看看百度百科中函數覆蓋的中文描述是:
函數覆蓋發生在父類與子類之間,其函數名、參數類型、返回值類型必須同父類中的相對應被覆蓋的函數嚴格一致,
覆蓋函數和被覆蓋函數只有函數體不同,當派生類對象調用子類中該同名函數時會自動調用子類中的覆蓋版本,
而不是父類中的被覆蓋函數版本,這種機制就叫做函數覆蓋。
我們來寫一段函數覆蓋的代碼
class father
{
public:
void fun()
{cout<<"father's fun"<<endl;}
};
class son:public father
{
public:
void fun()
{cout<<"son's fun"<<endl;}
};
void main()
{
father Father,*pFather;
son Son,*pSon;
int i = sizeof(Father); //此行代碼過后,i=1,此行代碼無意義只是讓大家知道普通成員函數不占用類的實例空間。
// Father.fun(); //此行注釋與下行注釋是正常的調用函數覆蓋相信大家都能理解,所以不在解釋
// Sun.fun();
pSon = (son*)&Father; //子類指針指向父類實例是危險的,此例中并沒涉及到任何越界,并且為了展示區別
//所以才這樣使用,但大家要明白,這樣做是危險的。
pFather = (father*)&Son;
pSon->fun(); //函數調用執行了father's fun
pFather->fun(); //函數調用執行了son's fun
}
此時有些人可能就不能理解為什么會出現這種調用結果了,那么大家是否還記得我上面曾提到的"C++編譯器的函數名修飾規則"?
我們來根據"C++編譯器的函數名修飾規則"再來想想原因。
根據規則,編譯器把父類father中的fun函數名編譯為"?fun@father@@QAEZ",子類son中的fun函數名編譯為"?fun@son@@QAEZ"。
當pSon->fun();調用時,編譯器會把pSon所存地址值的類型轉化成當前指針類型,而pSon的當前類型為son(這句話不多余,
因為類型是可以隨意轉換的),
所以表達式"pSon->fun()"全部展開以后得到的函數名稱即"?fun@father@@QAEZ"
同理表達式"pFather->fun()"全部展開以后得到的函數名稱即"?fun@son@@QAEZ",
既然得出了函數名,那么也就可以根據函數名稱,跳轉到真實的函數代碼實體位置了。
根據以上分析,我覺得"函數覆蓋(英文名不知到叫什么只能用中文了)"這個詞匯真的容易把人帶入歧途,
我的語文又不好,所以還希望哪位語文好的兄弟,來重新翻譯下"函數覆蓋"這個詞匯。
呃,真費勁啊,函數覆蓋算是講完,不知道大家有每有看懂,如果還看不懂的話,我是真沒招了。
下面在來講個虛函數吧。
?
有了前面的基礎,大家應該對函數有了充分的了解。
那么問大家一個問題,為什么一個類實力化后普通成員函數不影響實例的大小?
呵呵,如果一個新手能回答出來,那我這篇東西就不算白寫。
正確答案嘛,是因為不需要它來影響實例大小,因為編譯器會根據"C++編譯器的函數名修飾規則"與"表達式的地址類型",
自動的把成員函數展開成完整的函數名,也就找到了函數的真實地址,所以普通成員函數是不影響實力大小的。
我再來問大家一個問題,你們認為虛函數需不需要影響類的實例大小?
哈哈,這次的答案是需要。
這次我們來寫一段虛函數的代碼瞧瞧 因為只有在實例內部添加一個指針,才能夠完成例如,
class father
{
public:
virtual void fun()
{cout<<"father's fun"<<endl;}
};
class son:public father
{
public:
void fun()
{cout<<"son's fun"<<endl;}
};
void main()
{
father Father,*pFather;
son Son,*pSon;
int i = sizeof(Father); //此行代碼過后,i=4,此行代碼無意義只是讓大家知道虛函數占用類的實例空間。
Father.fun(); //函數執行結果 "father's fun"
Son.fun(); //函數執行結果 "son's fun" 這句簡單點說,就是很多人說的動態綁定,我們下面會具體分析。
pSon = (son*)&Father; //再次強調,子類指針指向父類實例是危險的,一旦越界操作,就會引發異常。
pFather = (father*)&Son;
pSon->fun(); //函數執行結果 "father's fun"
pFather->fun(); //函數執行結果 "son's fun" 這兩句也是動態綁定,相信還是有不少人不理解,下面具體分析。
}
恩,大家需要調式一下上面的程序,對Father添加監視,你會發現Father實例中莫名其妙的多了一個vftable類型指針對象vfptr。
是了,虛函數的實現靠的就是這個東西了。
IED幫我們在我們的實例外部實例化了一個vftable對象(我知道這句話很繞,但是我不知道該怎么更好的解釋了),
同時為我們的實例Father添加了一個指向vftable對象的指針vfptr。
我們繼續把監視中的指針vfptr展開,可以看到一個叫[0]的函數指針(別問我這名為啥長成這樣,我也沒搞清楚),
哈哈,找到了,這里存儲了一個地址,這個地址就是一個函數真實地址(如果用的VS2010編譯器,你會直觀的看到這個地址
所對應的是father::fun這個函數)。
然后,我再來明確一個虛函數規則,就是當你的實例調用虛函數時,最終調用的就是這個vftable類型的成員[0]所存儲地址。
那么好了,我們知道子類是完全繼承父類的,所以那個vftable類型指針對象vfptr也同時被繼承了下來。
IDE同樣為我們實例化一個vftable對象,讓vfptr來指向這個vftable對象。
而如果我們的子類重寫了虛函數,那么IDE在實例化vftable對象時,就會把[0]這個指針重寫為新的子類中那個虛函數地址。
如果我們的子類沒有重寫這個虛函數,那么IDE就會找到距離這個子類關系最近的一個實現了虛函數的父類,
把這個父類中的虛函數地址,寫入到子類的[0]中。
這樣子類在調用虛函數的時候,就可以實現動態綁定了。
另外父類指針指向子類實例時,因為有了vfptr指針占位,所以當父類指針調用虛函數時,尋址到的vfptr是子類實例的。
而子類的vfptr指向子類自己的vftable對象,所以父類最終調用的會是子類對象的中[0],所以[0]中存的是哪個函數地址。
父類指針最終調用的就會是哪個函數了。
轉載于:https://www.cnblogs.com/luoyunjian/p/4480712.html
總結
- 上一篇: Java 基本类型相互转换
- 下一篇: C#随机数