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



























