和多態(tài))
【C 面試真題】聊聊 C 的虛函數(shù)和多態(tài)虛函數(shù)是 C 面試的分水嶺——背得出virtual 實(shí)現(xiàn)多態(tài)只是及格真考你的是虛函數(shù)表長(zhǎng)什么樣、對(duì)象內(nèi)存里存了什么、構(gòu)造析構(gòu)里調(diào)虛函數(shù)會(huì)發(fā)生什么。一道題能從語(yǔ)法一路問(wèn)到內(nèi)存布局。本文把虛函數(shù)表、動(dòng)態(tài)多態(tài)、純虛函數(shù)串成一條線一次講透。一、先說(shuō)結(jié)論虛函數(shù) 運(yùn)行期才決定的調(diào)用? 什么是虛函數(shù)它和多態(tài)什么關(guān)系? 一句話(huà)虛函數(shù)是一種運(yùn)行期才決定調(diào)用哪個(gè)版本的成員函數(shù)它是 C 實(shí)現(xiàn)動(dòng)態(tài)多態(tài)的機(jī)制。普通成員函數(shù)的調(diào)用在編譯期就定死虛函數(shù)的調(diào)用要到運(yùn)行期根據(jù)對(duì)象實(shí)際類(lèi)型來(lái)決定——這就是多態(tài)。structAnimal{virtualvoidspeak(){}virtual~Animal()default;};structDog:Animal{voidspeak()override{std::cout汪;}};Animal*anewDog;// 運(yùn)行期才知道 a 指向 Doga-speak();// 打印汪// 這就是多態(tài)a的靜態(tài)類(lèi)型是Animal*但實(shí)際指向Dog對(duì)象。沒(méi)有 virtuala-speak()會(huì)調(diào)Animal::speak有了 virtual它會(huì)調(diào)Dog::speak。這就是動(dòng)態(tài)多態(tài)。二、虛函數(shù)表多態(tài)是怎么實(shí)現(xiàn)的? 虛函數(shù)到底是怎么實(shí)現(xiàn)運(yùn)行期決定調(diào)用的? 靠虛函數(shù)表vtable和虛表指針vptr。這是這題的硬核必須講清。機(jī)制分三層每個(gè)有虛函數(shù)的類(lèi)編譯器給它生成一張?zhí)摵瘮?shù)表——一個(gè)存放虛函數(shù)地址的數(shù)組每個(gè)該類(lèi)的對(duì)象內(nèi)存里多一個(gè)虛表指針 vptr指向自己所屬類(lèi)的虛表虛函數(shù)調(diào)用時(shí)通過(guò)對(duì)象的 vptr 找到虛表再查表調(diào)到正確版本。Dog 對(duì)象的內(nèi)存布局 ---------- | vptr ------- Dog 的虛函數(shù)表 ---------- ------------ | 成員... | | Dog::speak| ---------- ------------Animal*anewDog;// a-speak() 實(shí)際做的事// 1. 通過(guò) a 找到對(duì)象里的 vptr// 2. vptr 指向 Dog 的虛表// 3. 查表得到 Dog::speak 的地址// 4. 調(diào)用它加分點(diǎn)誰(shuí)建表、誰(shuí)設(shè) vptr虛表是編譯期就生成好的每個(gè)類(lèi)一張靜態(tài)數(shù)據(jù)vptr 是構(gòu)造函數(shù)里設(shè)置/更新的——對(duì)象構(gòu)造時(shí)vptr 指向自己正在構(gòu)造的那個(gè)類(lèi)。這就引出后面構(gòu)造函數(shù)里調(diào)虛函數(shù)的坑。三、靜態(tài)多態(tài) vs 動(dòng)態(tài)多態(tài)? 多態(tài)只有虛函數(shù)這一種嗎? 不是。C 有兩種多態(tài)類(lèi)型時(shí)機(jī)機(jī)制例子靜態(tài)多態(tài)編譯期函數(shù)重載、模板max(1,2)vsmax(1.0,2.0)動(dòng)態(tài)多態(tài)運(yùn)行期虛函數(shù)a-speak()// 靜態(tài)多態(tài)編譯期定死templatetypenameTTadd(T a,T b){returnab;}add(1,2);// 調(diào) int 版本add(1.0,2.0);// 調(diào) double 版本// 動(dòng)態(tài)多態(tài)運(yùn)行期才定Animal*anewDog;a-speak();// 運(yùn)行期查虛表區(qū)別靜態(tài)多態(tài)零開(kāi)銷(xiāo)編譯期就定沒(méi)查表但靈活性低動(dòng)態(tài)多態(tài)有開(kāi)銷(xiāo)要查虛表、間接調(diào)用但能在運(yùn)行期根據(jù)實(shí)際類(lèi)型決定行為。面試常問(wèn)用模板還是虛函數(shù)本質(zhì)就是這兩者的權(quán)衡。四、構(gòu)造/析構(gòu)里調(diào)虛函數(shù)經(jīng)典大坑? 在構(gòu)造或析構(gòu)函數(shù)里調(diào)用虛函數(shù)會(huì)怎樣?不會(huì)發(fā)生多態(tài)——調(diào)到的是當(dāng)前類(lèi)的版本不是子類(lèi)的。這是 C 最經(jīng)典的坑之一。原因?qū)ο蟮?vptr 在構(gòu)造過(guò)程中逐層設(shè)置。構(gòu)造基類(lèi)時(shí)vptr 指向基類(lèi)進(jìn)入子類(lèi)構(gòu)造后vptr 才更新指向子類(lèi)。析構(gòu)則反過(guò)來(lái)。structBase{Base(){// 此時(shí) vptr 指向 Baseinit();// 調(diào) Base::init// 不是 Der::init}virtualvoidinit(){}virtual~Base()default;};structDer:Base{voidinit()override{}};Der d;// 構(gòu)造時(shí) Base() 里調(diào)的是 Base::init??為什么這么設(shè)計(jì)構(gòu)造基類(lèi)時(shí)子類(lèi)部分還沒(méi)構(gòu)造此時(shí)調(diào)子類(lèi)的虛函數(shù)會(huì)用到未初始化的成員——很危險(xiǎn)。所以語(yǔ)言規(guī)定構(gòu)造/析構(gòu)期間的虛函數(shù)調(diào)用退化為當(dāng)前類(lèi)的普通調(diào)用。理解了這層保護(hù)就不會(huì)再踩這個(gè)坑。五、純虛函數(shù)與抽象類(lèi)? 什么是純虛函數(shù)抽象類(lèi)有什么用?純虛函數(shù)是只有聲明、沒(méi)有定義的虛函數(shù) 0。含有純虛函數(shù)的類(lèi)叫抽象類(lèi)不能實(shí)例化只能作為接口被繼承。// 抽象類(lèi)接口structShape{// 純虛函數(shù)必須由子類(lèi)實(shí)現(xiàn)virtualdoublearea()const0;virtual~Shape()default;};// 子類(lèi)必須實(shí)現(xiàn) area否則仍是抽象類(lèi)structCircle:Shape{doubler;doublearea()constoverride{return3.14*r*r;}};// Shape s; // ? 抽象類(lèi)不能實(shí)例化Circle c{2.0};Shape*pc;std::coutp-area();// 12.56加分點(diǎn)純虛函數(shù)也可以有定義在類(lèi)外寫(xiě)Shape::area() {...}子類(lèi)可顯式調(diào)用——用于提供默認(rèn)實(shí)現(xiàn)但仍強(qiáng)制子類(lèi) override抽象類(lèi)是 C 實(shí)現(xiàn)接口編程的標(biāo)準(zhǔn)手段基類(lèi)只定契約子類(lèi)提供實(shí)現(xiàn)。六、虛函數(shù)的代價(jià)? 用虛函數(shù)有什么開(kāi)銷(xiāo)? 三方面代價(jià)運(yùn)行期開(kāi)銷(xiāo)每次虛函數(shù)調(diào)用要查虛表 間接跳轉(zhuǎn)比普通調(diào)用慢且阻礙內(nèi)聯(lián)空間開(kāi)銷(xiāo)每個(gè)對(duì)象多一個(gè)vptr通常 8 字節(jié)每個(gè)類(lèi)多一張?zhí)摫砭彺嬗绊戦g接跳轉(zhuǎn)破壞指令緩存局部性。實(shí)踐建議虛函數(shù)的開(kāi)銷(xiāo)在絕大多數(shù)業(yè)務(wù)代碼里可以忽略——一次查表就幾納秒。只有性能敏感的熱路徑如每幀百萬(wàn)次調(diào)用才需要計(jì)較那時(shí)考慮用模板/CRTP 做靜態(tài)多態(tài)替代。別為了怕慢就不用虛函數(shù)那是過(guò)早優(yōu)化。七、核心規(guī)則速查表維度規(guī)則多態(tài)實(shí)現(xiàn)虛函數(shù) 虛表 vptr虛表生成編譯期每個(gè)類(lèi)一張vptr 設(shè)置構(gòu)造函數(shù)里逐層更新構(gòu)造/析構(gòu)里調(diào)虛函數(shù)退化為當(dāng)前類(lèi)版本不多態(tài)純虛函數(shù) 0使類(lèi)變抽象不可實(shí)例化開(kāi)銷(xiāo)查表間接調(diào)用每對(duì)象多一 vptr八、面試高頻追問(wèn)? Q1基類(lèi)析構(gòu)函數(shù)為什么要是虛函數(shù)? 當(dāng)你用基類(lèi)指針 delete 一個(gè)子類(lèi)對(duì)象時(shí)若析構(gòu)不是虛函數(shù)只會(huì)調(diào)基類(lèi)析構(gòu)子類(lèi)析構(gòu)不執(zhí)行——子類(lèi)資源泄漏。這是未定義行為的高發(fā)區(qū)。所以有虛函數(shù)的類(lèi)析構(gòu)幾乎一定要虛。? Q2虛函數(shù)能內(nèi)聯(lián)嗎? 語(yǔ)法上能加 inline但通常不會(huì)被內(nèi)聯(lián)——虛函數(shù)要查虛表編譯期定不下來(lái)。例外編譯期能確定實(shí)際類(lèi)型時(shí)直接通過(guò)對(duì)象而非指針/引用調(diào)用編譯器可能內(nèi)聯(lián)掉。? Q3虛函數(shù)表存在哪? 每個(gè)類(lèi)一張是編譯期生成的靜態(tài)只讀數(shù)據(jù)通常在 .rodata 段。所有該類(lèi)對(duì)象共享這一張?zhí)摫碇皇歉髯缘膶?duì)象里有一個(gè) vptr 指向它。? Q4構(gòu)造函數(shù)能是虛函數(shù)嗎?不能。虛函數(shù)依賴(lài) vptr而 vptr 要在構(gòu)造函數(shù)里設(shè)置——構(gòu)造還沒(méi)開(kāi)始vptr 還沒(méi)就位邏輯上矛盾。所以語(yǔ)言直接禁止虛構(gòu)造函數(shù)。? Q5override 和 virtual 寫(xiě)哪個(gè)? 派生類(lèi)重寫(xiě)虛函數(shù)優(yōu)先寫(xiě) override讓編譯器檢查簽名。virtual 性質(zhì)會(huì)沿繼承鏈自動(dòng)傳遞派生類(lèi)不寫(xiě) virtual 也是虛函數(shù)但 override 能擋住簽名寫(xiě)錯(cuò)導(dǎo)致沒(méi)重寫(xiě)的 bug見(jiàn)關(guān)鍵字篇 final/override。九、總結(jié)速查表概念一句話(huà)虛函數(shù)運(yùn)行期決定的成員函數(shù)虛表 vtable每類(lèi)一張存虛函數(shù)地址vptr每對(duì)象一個(gè)指向所屬類(lèi)虛表抽象類(lèi)含純虛函數(shù)不可實(shí)例化多態(tài)實(shí)現(xiàn)通過(guò) vptr 查 vtable一句話(huà)回顧虛函數(shù)的本質(zhì)是用虛表和 vptr 實(shí)現(xiàn)運(yùn)行期決定調(diào)用。記住每類(lèi)一張?zhí)摫怼⒚繉?duì)象一個(gè) vptr、構(gòu)造析構(gòu)里調(diào)虛函數(shù)會(huì)退化、基類(lèi)析構(gòu)要虛這題就穩(wěn)了。如果您覺(jué)得本篇內(nèi)容對(duì)你有幫助歡迎點(diǎn)贊 、收藏 ?、轉(zhuǎn)發(fā) 。下期我們聊聊運(yùn)算符重載敬請(qǐng)關(guān)注