抖音C++二面挑戰(zhàn):如何限制對象創(chuàng)建位置的方法?
棧上創(chuàng)建對象,猶如在自家規(guī)整有序的倉庫中取用物品,由編譯器自動管理內(nèi)存。其過程迅速高效,就像直接從倉庫近在咫尺的貨架上拿取,無需額外尋找空間,直接在??臻g為對象分配內(nèi)存并調(diào)用構(gòu)造函數(shù),函數(shù)結(jié)束時還會自動調(diào)用析構(gòu)函數(shù)清理內(nèi)存。但??臻g有限,如同倉庫空間大小固定,若對象過多或過大,可能導(dǎo)致棧溢出。而堆上創(chuàng)建對象,則好似在廣闊的外部市場獲取資源,需程序員手動操作。使用 new 運(yùn)算符時,先調(diào)用 operator new 函數(shù)在堆空間搜索、分配內(nèi)存,再調(diào)用構(gòu)造函數(shù)初始化對象。這一過程靈活自由,如同在市場能按需挑選不同大小的場地,但也要求程序員精心管理內(nèi)存,否則易引發(fā)內(nèi)存泄漏,就像在市場租了場地卻忘記歸還,造成資源浪費(fèi)。
在實(shí)際開發(fā)中,諸多場景迫切需要限制對象創(chuàng)建位置。比如在資源管理類中,若期望對象像盡職盡責(zé)的管家,自動管理資源,避免內(nèi)存泄漏,那么將其限制在棧上創(chuàng)建是明智之舉;在一些對靈活性要求極高的場景,如需要動態(tài)創(chuàng)建大量不同大小的對象時,限制對象只能在堆上創(chuàng)建則能更好地滿足需求。那么,具體有哪些方法可以實(shí)現(xiàn)對對象創(chuàng)建位置的精準(zhǔn)限制呢?
Part1引言
在 C++ 的編程世界里,對象創(chuàng)建是構(gòu)建程序大廈的基礎(chǔ)操作,而對象創(chuàng)建又分為靜態(tài)建立(棧上)和動態(tài)建立(堆上)這兩種主要方式 ,它們在內(nèi)存分配和構(gòu)造函數(shù)調(diào)用上存在顯著差異。
先來說說棧上對象創(chuàng)建,當(dāng)我們在代碼塊中定義一個對象時,比如ClassName obj;,這就是在棧上創(chuàng)建對象。棧內(nèi)存就像是一個提前準(zhǔn)備好的儲物箱,由編譯器自動管理。當(dāng)對象被創(chuàng)建時,編譯器會在棧上為其分配一塊連續(xù)的內(nèi)存空間,就像從儲物箱里拿出一塊固定大小的區(qū)域來存放物品一樣。而且,棧上對象的構(gòu)造函數(shù)調(diào)用是自動進(jìn)行的,無需額外的手動操作。在這個對象的生命周期內(nèi),只要其所在的代碼塊沒有結(jié)束,它就一直存在。一旦代碼塊執(zhí)行完畢,對象就會自動銷毀,這時候編譯器會自動調(diào)用析構(gòu)函數(shù)來清理對象占用的內(nèi)存空間,就像把物品從儲物箱中拿走,把空間騰出來一樣,整個過程無需我們操心。
再看看堆上對象創(chuàng)建,它和棧上創(chuàng)建有著明顯的不同。堆內(nèi)存更像是一個大型的自由市場,沒有固定的分配模式,由程序員手動管理。當(dāng)我們使用new操作符創(chuàng)建對象時,例如ClassName* ptr = new ClassName();,首先new操作符會在堆上尋找合適的內(nèi)存空間進(jìn)行分配,這就像是在自由市場里尋找一塊合適的攤位,這個過程相對復(fù)雜,需要花費(fèi)一定的時間。
找到合適的內(nèi)存后,才會調(diào)用構(gòu)造函數(shù)來初始化對象,對這個攤位進(jìn)行布置。而當(dāng)對象使用完畢后,我們需要手動調(diào)用delete操作符來釋放內(nèi)存,即delete ptr;,這一步很關(guān)鍵,如果忘記釋放,就會造成內(nèi)存泄漏,就像在自由市場租了攤位卻不歸還,浪費(fèi)了資源。而且堆上對象的生命周期不受代碼塊的限制,只要不手動釋放,它就會一直占用內(nèi)存。
Part2只能在堆上生成的對象類
在實(shí)際的編程場景中,有時候我們需要對對象的創(chuàng)建位置進(jìn)行精細(xì)控制,比如只能在堆上創(chuàng)建對象。這并非是一個簡單的任務(wù),讓我們一步步來探索實(shí)現(xiàn)的方法。
2.1最初的嘗試:構(gòu)造函數(shù)私有化
當(dāng)我們最初思考如何限制對象只能在堆上創(chuàng)建時,很容易想到將構(gòu)造函數(shù)設(shè)為私有。因?yàn)樵?C++ 中,構(gòu)造函數(shù)是創(chuàng)建對象的關(guān)鍵入口,將其私有化似乎就能阻止在棧上直接創(chuàng)建對象,只能通過new在堆上創(chuàng)建 。比如下面這段代碼:
class OnlyHeap1 {
private:
OnlyHeap1() {}
public:
static OnlyHeap1* create() {
return new OnlyHeap1();
}
};然而,這種方法存在一個嚴(yán)重的問題。雖然它確實(shí)阻止了在棧上直接創(chuàng)建對象,但是當(dāng)我們使用new操作符創(chuàng)建對象時,new操作符的執(zhí)行過程分為兩步:第一步是執(zhí)行operator new()函數(shù),在堆空間中搜索合適的內(nèi)存并進(jìn)行分配;第二步是調(diào)用構(gòu)造函數(shù)構(gòu)造對象,初始化這片內(nèi)存空間。而 C++ 提供new運(yùn)算符的重載,其實(shí)是只允許重載operator new()函數(shù),這個函數(shù)僅用于分配內(nèi)存,無法提供構(gòu)造功能 。也就是說,即使我們將構(gòu)造函數(shù)私有化,new操作符在調(diào)用構(gòu)造函數(shù)時仍然會因?yàn)樵L問權(quán)限問題而失敗,所以這種方法并不可行。
2.2析構(gòu)函數(shù)的 “秘密使命”
既然構(gòu)造函數(shù)私有化這條路走不通,我們不妨換個思路,從析構(gòu)函數(shù)入手。在 C++ 中,編譯器在為類對象分配??臻g時,會先檢查類的析構(gòu)函數(shù)的訪問性,其實(shí)不光是析構(gòu)函數(shù),只要是非靜態(tài)的函數(shù),編譯器都會進(jìn)行檢查。如果類的析構(gòu)函數(shù)是私有的,編譯器就無法調(diào)用析構(gòu)函數(shù)來釋放內(nèi)存,也就不會在??臻g上為類對象分配內(nèi)存。這就為我們實(shí)現(xiàn)只能在堆上創(chuàng)建對象提供了一種可行的方法。
class OnlyHeap2 {
public:
OnlyHeap2() {}
void destroy() {
delete this;
}
private:
~OnlyHeap2() {}
};在這段代碼中,我們將析構(gòu)函數(shù)設(shè)為私有,這樣對象就無法在棧上創(chuàng)建。因?yàn)楫?dāng)我們嘗試在棧上創(chuàng)建對象,比如OnlyHeap2 obj;時,編譯器在對象生命周期結(jié)束時無法調(diào)用私有的析構(gòu)函數(shù),從而導(dǎo)致編譯錯誤。而使用new在堆上創(chuàng)建對象時,由于delete操作是在代碼中顯式調(diào)用的,并且在類的成員函數(shù)destroy中,所以可以訪問私有的析構(gòu)函數(shù)。例如:
OnlyHeap2* ptr = new OnlyHeap2();
ptr->destroy();不過,這種方法也并非完美無缺。當(dāng)這個類作為基類被繼承時,析構(gòu)函數(shù)通常要設(shè)為virtual,然后在子類重寫,以實(shí)現(xiàn)多態(tài)。但如果析構(gòu)函數(shù)是私有的,就無法在子類中重寫,這會導(dǎo)致繼承和多態(tài)的功能無法正常實(shí)現(xiàn)。
2.3完美方案:protected 的巧妙運(yùn)用
為了解決上述方法中存在的問題,我們可以將析構(gòu)函數(shù)設(shè)為protected。這樣,類外無法直接訪問析構(gòu)函數(shù),對象不能在棧上創(chuàng)建,同時子類可以訪問析構(gòu)函數(shù),能夠滿足繼承和多態(tài)的需求。
class OnlyHeap3 {
protected:
~OnlyHeap3() {}
public:
OnlyHeap3() {}
static OnlyHeap3* create() {
return new OnlyHeap3();
}
void destroy() {
delete this;
}
};進(jìn)一步優(yōu)化,我們可以將構(gòu)造函數(shù)也設(shè)為protected,然后通過public的static函數(shù)create()來創(chuàng)建對象,destory()函數(shù)來釋放對象。這樣不僅實(shí)現(xiàn)了對象只能在堆上創(chuàng)建,還統(tǒng)一了對象的創(chuàng)建和釋放方式,使代碼更加優(yōu)雅和安全。
class OnlyHeap {
protected:
OnlyHeap() {}
~OnlyHeap() {}
public:
static OnlyHeap* create() {
return new OnlyHeap();
}
void destory() {
delete this;
}
};使用時,我們只需要調(diào)用create()函數(shù)來創(chuàng)建對象,調(diào)用destory()函數(shù)來釋放對象,例如:
OnlyHeap* ptr = OnlyHeap::create();
// 使用對象
ptr->destory();通過這種方式,我們成功地實(shí)現(xiàn)了一個只能在堆上生成對象的類,并且解決了繼承和多態(tài)相關(guān)的問題,讓代碼在功能和安全性上都得到了保障。
Part3只能在棧上生成的對象類
與只能在堆上生成對象的類相對應(yīng),在某些編程場景中,我們也需要確保對象只能在棧上生成。實(shí)現(xiàn)這一目標(biāo)的關(guān)鍵在于禁用在堆上創(chuàng)建對象的方式。
我們知道,只有使用new運(yùn)算符,對象才會建立在堆上。因此,只要禁用new運(yùn)算符就可以實(shí)現(xiàn)類對象只能建立在棧上。而new運(yùn)算符在執(zhí)行時,總是先調(diào)用operator new()函數(shù)來分配內(nèi)存,所以我們可以將operator new()設(shè)為私有,這樣在類外就無法調(diào)用該函數(shù),從而不能在堆上分配內(nèi)存,也就無法使用new創(chuàng)建對象。同時,為了保證內(nèi)存釋放操作的一致性,delete對應(yīng)的operator delete()函數(shù)也需要設(shè)為私有。以下是具體的代碼實(shí)現(xiàn):
class StackOnly {
private:
void* operator new(size_t size) {}
void operator delete(void* ptr) {}
public:
StackOnly() {}
~StackOnly() {}
};在這段代碼中,StackOnly類將operator new()和operator delete()設(shè)為私有,當(dāng)我們在類外嘗試使用new創(chuàng)建對象時,例如StackOnly* ptr = new StackOnly();,編譯器會因?yàn)闊o法訪問私有成員函數(shù)而報錯,從而確保對象只能在棧上創(chuàng)建,如StackOnly obj;。通過這種簡單而有效的方式,我們成功地實(shí)現(xiàn)了一個只能在棧上生成對象的類,滿足了特定的編程需求 。
Part4相關(guān)面試題
4.1簡述如何設(shè)計一個只能在堆上創(chuàng)建對象的類
回答:有兩種常見方式。其一,把析構(gòu)函數(shù)設(shè)為私有。編譯器分配棧內(nèi)存時需確認(rèn)能調(diào)用析構(gòu)函數(shù),析構(gòu)函數(shù)私有會阻止其在棧上分配。但需提供如 destroy() 的公有函數(shù)釋放內(nèi)存。其二,將構(gòu)造函數(shù)設(shè)為 protected,并提供公有靜態(tài)創(chuàng)建函數(shù),像 static YourClass* create(),在其中用 new 創(chuàng)建并返回對象指針。
4.2怎樣設(shè)計一個僅允許在棧上創(chuàng)建對象的類
回答:對象用 new 會在堆上創(chuàng)建,其底層依賴 operator new 分配內(nèi)存。把類的 operator new 函數(shù)聲明為私有,外部便無法用 new 創(chuàng)建它的對象,從而對象通常只能在棧上聲明。
4.3將析構(gòu)函數(shù)設(shè)為私有讓對象僅在堆上創(chuàng)建時,用 delete 釋放對象會怎樣?如何正確釋放
回答:用 delete 會編譯出錯,因 delete 需調(diào)用析構(gòu)函數(shù),私有析構(gòu)使其無法訪問。正確做法是類內(nèi)定義類似 void destroy() 的公有函數(shù),在其中用 delete this; 語句或手動處理資源后調(diào)用析構(gòu)函數(shù)(析構(gòu)函數(shù)可訪問情況)釋放堆內(nèi)存。
4.4限制只能在堆上創(chuàng)建對象的類,若被繼承會遇到什么問題,怎么處理
回答:若析構(gòu)函數(shù)私有,子類無法訪問以完成析構(gòu)。通常把基類析構(gòu)函數(shù)改為 protected 解決。其能防棧上創(chuàng)建,子類析構(gòu)函數(shù)也能正常調(diào)用它,保障繼承與多態(tài)場景下,借基類指針釋放堆對象不出錯。
4.5把 operator new 設(shè)為私有讓對象僅棧上創(chuàng)建,為何難以限制其在靜態(tài)存儲區(qū)創(chuàng)建
回答:將 operator new 私有可禁用 new,阻止堆創(chuàng)建。但定義全局或靜態(tài)成員對象時,其在靜態(tài)存儲區(qū)分配內(nèi)存,不走 operator new 流程,故該方法無法阻止類對象于靜態(tài)存儲區(qū)聲明創(chuàng)建。
4.6能否用友元函數(shù)突破限制對象創(chuàng)建位置的限制?
回答:能部分突破。如析構(gòu)函數(shù)私有讓對象僅堆上創(chuàng)建時,聲明特定友元函數(shù)可在外部調(diào)析構(gòu)函數(shù),或用 delete 釋放。不過,這違背限制設(shè)計初衷,友元需謹(jǐn)慎使用,避免破壞類封裝與既定內(nèi)存管理規(guī)則。
4.7限制對象創(chuàng)建位置的機(jī)制,與單例模式的創(chuàng)建邏輯有何相似之處?
回答:單例模式常將構(gòu)造函數(shù)設(shè)為 private 或 protected 防隨意創(chuàng)建,通過靜態(tài)方法按特定邏輯供唯一實(shí)例,類似限制堆上創(chuàng)建用靜態(tài) create 函數(shù)借 new 控制創(chuàng)建的思路,都借限制構(gòu)造途徑,按期望邏輯于指定位置(單例的固定存儲區(qū) / 僅堆上)創(chuàng)建對象。
4.8如何確保一個類的對象只能在指定的內(nèi)存池中創(chuàng)建?
回答:可重載 operator new,令其僅從目標(biāo)內(nèi)存池獲取內(nèi)存。內(nèi)存池存可分配內(nèi)存塊列表,重載版本按對象大小,從列表取對應(yīng)內(nèi)存塊并返回地址,供構(gòu)造函數(shù)初始化對象。也可將構(gòu)造函數(shù)保護(hù)化,配合接收內(nèi)存池指針的靜態(tài)創(chuàng)建函數(shù)達(dá)成目標(biāo)。
4.9如果限制了對象只能在棧上創(chuàng)建,對象需要動態(tài)擴(kuò)容該怎么處理?
回答:可設(shè)計類支持 “移動語義”。必要時,棧上對象可調(diào)用轉(zhuǎn)移資源函數(shù),于堆分配更大內(nèi)存,轉(zhuǎn)移內(nèi)部資源至堆空間,并更新自身成員指向堆內(nèi)存。或提前估算合理?xiàng)?臻g,借自定義內(nèi)存管理結(jié)構(gòu),如棧上存儲固定大小鏈表 / 數(shù)組,按特定規(guī)則復(fù)用空間避免溢出與動態(tài)擴(kuò)容需求。
4.10限制對象創(chuàng)建位置對程序的內(nèi)存碎片問題有幫助嗎?
回答:有幫助。如限制于特定內(nèi)存池創(chuàng)建,能按池管理規(guī)則分配 / 回收內(nèi)存,降低碎片化。僅棧上創(chuàng)建可避免堆碎片化,因棧內(nèi)存連續(xù)分配、自動釋放。僅堆上創(chuàng)建也便于借統(tǒng)一釋放邏輯,像內(nèi)存池或自定義 operator delete 優(yōu)化釋放順序,減少外部碎片。




























