面試真題 | B站C++渲染引擎
一、基礎(chǔ)與語法
-
自我介紹
- 請簡要介紹自己的背景、專業(yè)技能和工作經(jīng)驗。
-
實習(xí)介紹
- 詳細(xì)描述你在實習(xí)期間參與的項目、職責(zé)和成果。
二、智能指針相關(guān)問題回答
unique_ptr
是如何實現(xiàn)的?它有哪些特點和優(yōu)勢?
unique_ptr
是C++11引入的一種智能指針,用于管理動態(tài)分配的內(nèi)存資源。其實現(xiàn)基于獨占所有權(quán)的概念,即每個 unique_ptr
實例擁有對其所指向?qū)ο蟮奈ㄒ凰袡?quán)。
特點:
- 獨占所有權(quán):在任何給定的時刻,只能有一個
unique_ptr
實例管理特定的內(nèi)存資源。這確保了內(nèi)存資源的安全性和唯一性。 - 自動釋放內(nèi)存:當(dāng)
unique_ptr
超出作用域或被重新賦值時,它所管理的內(nèi)存會自動釋放,從而避免了內(nèi)存泄漏的問題。 - 指針語義:
unique_ptr
的使用方式與原始指針相似,可以通過指針操作符(->)和解引用操作符(*)來訪問所指向?qū)ο蟮某蓡T。
優(yōu)勢:
- 安全性:通過獨占所有權(quán)和自動釋放內(nèi)存的特性,
unique_ptr
提供了比原始指針更高的安全性。 - 易用性:
unique_ptr
的使用方式簡單直觀,減少了手動管理內(nèi)存帶來的復(fù)雜性和出錯率。 - 性能:由于
unique_ptr
不需要維護(hù)引用計數(shù),因此在某些情況下,它的性能可能比shared_ptr
更高。
shared_ptr
是如何實現(xiàn)的?它如何實現(xiàn)資源共享和自動管理?
shared_ptr
是C++11引入的另一種智能指針,用于管理動態(tài)分配的內(nèi)存資源,并允許多個指針共享同一個資源。
實現(xiàn):
shared_ptr
的實現(xiàn)基于引用計數(shù)的技術(shù)。每個 shared_ptr
對象都會維護(hù)一個引用計數(shù)器,用于記錄有多少個指針指向同一個對象。當(dāng)引用計數(shù)器為0時,表示沒有任何指針指向該對象,此時會自動釋放該對象的內(nèi)存空間。
資源共享和自動管理:
- 共享所有權(quán):多個
shared_ptr
實例可以同時指向同一個對象,并共享對所指向?qū)ο蟮乃袡?quán)。這通過引用計數(shù)器來實現(xiàn),每次復(fù)制或賦值shared_ptr
時,引用計數(shù)器都會增加。 - 自動釋放內(nèi)存:當(dāng)最后一個指向?qū)ο蟮?
shared_ptr
超出作用域或被重新賦值時,引用計數(shù)器會減為0,此時會自動釋放所管理的內(nèi)存資源。
shared_ptr
是否線程安全?為什么?(計數(shù)器是線程安全的,但指針本身不是)
shared_ptr
的引用計數(shù)器是線程安全的,這意味著在多線程環(huán)境中,多個線程可以安全地同時訪問和修改同一個 shared_ptr
的引用計數(shù)器。然而,shared_ptr
的指針本身并不是線程安全的。
線程安全性:
- 引用計數(shù)器線程安全:
shared_ptr
的實現(xiàn)通常會使用原子操作或互斥鎖來保護(hù)引用計數(shù)器的訪問和修改,從而確保線程安全。 - 指針本身非線程安全:雖然引用計數(shù)器是線程安全的,但
shared_ptr
所管理的對象指針本身并不是線程安全的。如果多個線程同時訪問和修改同一個對象,需要額外的同步機(jī)制來確保線程安全。
面試官追問:
-
追問一:
unique_ptr
和shared_ptr
在性能上有何差異?- 回答:
unique_ptr
通常比shared_ptr
性能更高,因為它不需要維護(hù)引用計數(shù)。而shared_ptr
需要維護(hù)引用計數(shù),并在每次復(fù)制或賦值時進(jìn)行原子操作或互斥鎖保護(hù),這會增加一定的開銷。然而,在需要共享所有權(quán)的場景中,shared_ptr
是更好的選擇,因為它可以自動管理內(nèi)存并避免內(nèi)存泄漏。
- 回答:
-
追問二:如何解決
shared_ptr
的循環(huán)引用問題?- 回答:循環(huán)引用是指兩個或多個對象相互持有
shared_ptr
,形成一個循環(huán)鏈,導(dǎo)致引用計數(shù)永遠(yuǎn)不會變?yōu)?,從而引發(fā)內(nèi)存泄漏。為了解決這個問題,可以使用weak_ptr
。weak_ptr
是一種不增加引用計數(shù)的智能指針,它只觀察對象而不擁有對象。通過weak_ptr
,可以破壞循環(huán)引用鏈,讓所有的shared_ptr
都能夠正常析構(gòu)并釋放所管理的內(nèi)存。
- 回答:循環(huán)引用是指兩個或多個對象相互持有
-
追問三:在什么情況下應(yīng)該使用
unique_ptr
而不是shared_ptr
?- 回答:在以下情況下應(yīng)該使用
unique_ptr
而不是shared_ptr
:- 當(dāng)只有一個所有者需要管理內(nèi)存資源時,使用
unique_ptr
可以提供更高的安全性和性能。 - 當(dāng)不希望多個指針共享同一個資源時,使用
unique_ptr
可以避免不必要的引用計數(shù)開銷。 - 當(dāng)對象的生命周期與單個所有者的生命周期緊密相關(guān)時,使用
unique_ptr
可以更直觀地表達(dá)這種關(guān)系。
- 當(dāng)只有一個所有者需要管理內(nèi)存資源時,使用
- 回答:在以下情況下應(yīng)該使用
三. 虛函數(shù)與構(gòu)造函數(shù)/析構(gòu)函數(shù)
-
在構(gòu)造函數(shù)中調(diào)用虛函數(shù)會發(fā)生什么?
在構(gòu)造函數(shù)中調(diào)用虛函數(shù)會導(dǎo)致靜態(tài)綁定(也稱為早綁定),而不是動態(tài)綁定(晚綁定)。這意味著即使虛函數(shù)在派生類中被重寫,在基類的構(gòu)造函數(shù)中調(diào)用該虛函數(shù)時,調(diào)用的仍然是基類中的版本。這是因為在對象構(gòu)造過程中,派生類的部分還沒有被完全構(gòu)造,對象的實際類型(即派生類類型)還沒有完全形成,因此無法正確調(diào)用派生類中的虛函數(shù)版本。
示例代碼:
class Base { public: Base() { VirtualFunction(); // 調(diào)用的是 Base::VirtualFunction } virtual void VirtualFunction() { std::cout << "Base class virtual function" << std::endl; } }; class Derived : public Base { public: void VirtualFunction() override { std::cout << "Derived class virtual function" << std::endl; } }; int main() { Derived d; // 構(gòu)造 Derived 對象時,Base 的構(gòu)造函數(shù)中調(diào)用的是 Base::VirtualFunction return 0; }
-
為什么析構(gòu)函數(shù)通常需要設(shè)置為虛函數(shù)?
析構(gòu)函數(shù)通常需要設(shè)置為虛函數(shù),以確保通過基類指針刪除派生類對象時,能夠正確調(diào)用派生類的析構(gòu)函數(shù),從而避免資源泄漏和未定義行為。如果析構(gòu)函數(shù)不是虛函數(shù),那么通過基類指針刪除派生類對象時,只會調(diào)用基類的析構(gòu)函數(shù),派生類特有的資源不會被正確釋放。
示例代碼:
class Base { public: virtual ~Base() { // 基類析構(gòu)函數(shù)設(shè)置為虛函數(shù) std::cout << "Base class destructor" << std::endl; } }; class Derived : public Base { public: ~Derived() { // 派生類析構(gòu)函數(shù) std::cout << "Derived class destructor" << std::endl; } }; int main() { Base* b = new Derived(); delete b; // 正確調(diào)用 Derived 的析構(gòu)函數(shù),然后調(diào)用 Base 的析構(gòu)函數(shù) return 0; }
如果基類析構(gòu)函數(shù)不是虛函數(shù),則只會調(diào)用基類的析構(gòu)函數(shù),派生類的資源不會被釋放:
class Base { public: ~Base() { // 基類析構(gòu)函數(shù)不是虛函數(shù) std::cout << "Base class destructor" << std::endl; } }; // Derived 類定義同上 int main() { Base* b = new Derived(); delete b; // 只調(diào)用 Base 的析構(gòu)函數(shù),不會調(diào)用 Derived 的析構(gòu)函數(shù) return 0; }
面試官追問及回答
追問1: 在構(gòu)造函數(shù)中調(diào)用虛函數(shù)的具體影響是什么?
回答: 在構(gòu)造函數(shù)中調(diào)用虛函數(shù)的具體影響是,會導(dǎo)致虛函數(shù)的靜態(tài)綁定(早綁定),即調(diào)用的是當(dāng)前正在構(gòu)造的對象的基類中定義的版本。這可能導(dǎo)致派生類特有的行為不被執(zhí)行,甚至可能產(chǎn)生邏輯錯誤或未定義行為,因為派生類的成員可能還沒有被初始化。
追問2: 如果析構(gòu)函數(shù)不是虛函數(shù),會導(dǎo)致什么問題?
回答: 如果析構(gòu)函數(shù)不是虛函數(shù),當(dāng)通過基類指針刪除派生類對象時,會導(dǎo)致只調(diào)用基類的析構(gòu)函數(shù),而派生類的析構(gòu)函數(shù)不會被調(diào)用。這會導(dǎo)致派生類特有的資源(如動態(tài)分配的內(nèi)存、文件句柄等)沒有被正確釋放,從而產(chǎn)生資源泄漏。此外,如果派生類析構(gòu)函數(shù)中有重要的清理邏輯(如關(guān)閉網(wǎng)絡(luò)連接、解鎖資源等),這些邏輯也不會被執(zhí)行,可能導(dǎo)致程序崩潰或未定義行為。
追問3: 在多繼承的情況下,虛析構(gòu)函數(shù)如何處理?
回答: 在多繼承的情況下,每個有虛函數(shù)的基類都應(yīng)該有一個虛析構(gòu)函數(shù)。如果一個類從多個有虛析構(gòu)函數(shù)的基類繼承,那么該類的析構(gòu)函數(shù)將自動成為虛函數(shù),并且會按照C++的析構(gòu)函數(shù)調(diào)用順序(先調(diào)用派生類的析構(gòu)函數(shù),然后依次調(diào)用各基類的析構(gòu)函數(shù),按照構(gòu)造的反向順序)來調(diào)用所有基類的析構(gòu)函數(shù)。這樣可以確保所有基類特有的資源都被正確釋放。
示例代碼(多繼承):
class Base1 {
public:
virtual ~Base1() {
std::cout << "Base1 destructor" << std::endl;
}
};
class Base2 {
public:
virtual ~Base2() {
std::cout << "Base2 destructor" << std::endl;
}
};
class Derived : public Base1, public Base2 {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base1* b1 = new Derived();
delete b1; // 正確調(diào)用 Derived 的析構(gòu)函數(shù),然后依次調(diào)用 Base1 和 Base2 的析構(gòu)函數(shù)
return 0;
}
四、 內(nèi)存地址問題
class A {
int a;
int d;
};
class B {
int b;
};
class C : public A, public B {
int b;
};
C* c = new C;
A* a = c;
B* b = c;
- 問
a
、b
、c
指向的地址是否相同?為什么?
回答
在C++中,對象指針的行為和它們所指向的對象的內(nèi)存布局密切相關(guān)。在你的代碼示例中,類 C
是通過多重繼承從類 A
和類 B
派生出來的。下面是對問題的詳細(xì)分析:
- 類A 和 類B 各自包含一個
int
成員,假設(shè)每個int
成員占用4個字節(jié)(實際大小依賴于編譯器和平臺)。 - 類C 作為
A
和B
的派生類,也包含一個int
成員b
。由于C
通過多重繼承自A
和B
,類C
的內(nèi)存布局通常會按如下方式排列(這取決于編譯器的具體實現(xiàn),但大多數(shù)現(xiàn)代編譯器遵循類似規(guī)則):- 首先是
A
的成員(a
和d
),假設(shè)每個int
4個字節(jié),則共8個字節(jié)。 - 然后是
B
的成員(b
),也是4個字節(jié)。 - 最后是
C
自己獨有的成員(b
),也是4個字節(jié)。
- 首先是
所以,假設(shè)沒有對齊或其他編譯器特性干擾,類 C
的對象 c
至少需要 8(A)+ 4(B)+ 4(C獨有)= 16
個字節(jié)。
接下來看指針:
C* c = new C;
:c
是一個指向C
對象實例的指針,指向?qū)ο蟮钠鹗嫉刂贰?/li>A* a = c;
:a
是一個指向A
的指針,并將其初始化為c
。由于A
是C
的第一個基類,a
指向的地址與c
相同。B* b = c;
:b
是一個指向B
的指針,并將其初始化為c
。這里b
指向的地址同樣是c
的起始地址,但是要通過B
的視角去訪問對象,實際上,從C
對象起始地址到B
的成員b
會有偏移(跳過A
的部分),但指針轉(zhuǎn)換僅改變了解釋內(nèi)存的方式,并沒有改變指向的內(nèi)存地址。
因此,a
、b
和 c
指向的地址是相同的,因為它們都指向同一個 C
對象。不過,當(dāng)通過 a
、b
和 c
訪問成員時,編譯器會根據(jù)它們的類型(A
、B
或 C
)來解釋該地址處的內(nèi)存內(nèi)容。
面試官追問及回答
追問1: 如果 A
和 B
中各自有多個成員變量,a
和 b
指針訪問的偏移會如何變化?
回答: 如果 A
和 B
中各自有多個成員變量,a
和 b
指針在訪問這些成員時仍然會指向同一個 C
對象的基礎(chǔ)地址。不過,通過 a
訪問 A
的成員時,會基于 A
的布局解釋內(nèi)存;通過 b
訪問 B
的成員時,會基于 B
的布局并考慮 A
的大小作為偏移來解釋內(nèi)存。編譯器會在編譯時處理這些偏移。
追問2: 如果在類 C
中,我們將 int b;
成員移動到類定義的開始位置,那么內(nèi)存布局會發(fā)生什么變化?
回答: 如果將 int b;
成員移動到類 C
的定義開始位置,則 C
的內(nèi)存布局將發(fā)生變化?,F(xiàn)在 C
的內(nèi)存布局會先放置 C
獨有的 int b
(4個字節(jié)),然后是 A
的成員(a
和 d
,共8個字節(jié)),最后是 B
的成員(b
,4個字節(jié))。這種情況下,雖然 c
指針仍然指向 C
對象的起始地址,但通過 a
和 b
訪問時,相對于 c
指針的偏移會不同,因為 A
和 B
的成員在 C
對象中的位置發(fā)生了改變。
追問3: 在多重繼承中,使用虛繼承會對內(nèi)存布局和指針轉(zhuǎn)換產(chǎn)生什么影響?
回答: 在多重繼承中,如果 A
或 B
(或兩者)通過虛繼承方式被繼承,那么編譯器會引入一個額外的指針(通常稱為虛基類指針)來管理 A
或 B
(或兩者)的實例。這個虛基類指針指向一個包含實際 A
或 B
對象的表(虛基類表),以確保正確訪問。這會導(dǎo)致對象 C
的內(nèi)存布局變得更大,因為需要額外的空間來存儲虛基類指針。在指針轉(zhuǎn)換時,編譯器會通過虛基類指針和虛基類表來處理偏移,確保訪問正確的成員。這可能會增加訪問基類成員的開銷,但避免了由于多重繼承引起的菱形繼承問題(鉆石問題)。
五、STL與數(shù)據(jù)結(jié)構(gòu)
STL 中使用 vector
要注意的問題
內(nèi)存分配與擴(kuò)容策略
在STL中,vector
是一個表示可變大小數(shù)組的序列容器,它采用連續(xù)存儲空間來存儲元素,這意味著可以采用下標(biāo)對vector
的元素進(jìn)行高效訪問。然而,當(dāng)新元素插入時,如果當(dāng)前內(nèi)存空間無法容納,vector
需要進(jìn)行內(nèi)存重新分配和元素復(fù)制。
內(nèi)存分配策略:
vector
在初始化時會分配一定的內(nèi)存空間,這個空間大小通常與元素的類型和容器的初始大小有關(guān)。- 當(dāng)元素數(shù)量增加到當(dāng)前內(nèi)存空間無法容納時,會按照特定的擴(kuò)容策略重新分配內(nèi)存。常見的
剩余60%內(nèi)容,訂閱專欄后可繼續(xù)查看/也可單篇購買
【C/C++面試必考必會】專欄,直擊面試核心,精選C/C++及相關(guān)技術(shù)棧中面試官最愛的必考點!從基礎(chǔ)語法到高級特性,從內(nèi)存管理到多線程編程,再到算法與數(shù)據(jù)結(jié)構(gòu)深度剖析,一網(wǎng)打盡。助你快速構(gòu)建知識體系,輕松應(yīng)對技術(shù)挑戰(zhàn)。希望專欄能讓你在面試中脫穎而出,成為技術(shù)崗的搶手人才。