C++ 基礎
- 引用和指針之間的區別?
- 堆棧和堆中的內存分配有何區別?
- 存在哪些類型的智能指針?
- unique_ptr 是如何實現的?我們如何強制在 unique_ptr 中僅存在一個對象所有者?
- shared_ptr 如何工作?對象之間如何同步引用計數器?
- 我們可以復制unique_ptr或者將其從一個對象傳遞到另一個對象嗎?
- 什么是右值和左值?
- 什么是 std::move 和 std::forward()
面向對象編程(OOP)
- 訪問某些類的私有字段的方法?
- 一個類可以繼承多個類嗎?
- 靜態字段是否在類構造函數中初始化?
- 構造函數/析構函數中會拋出異常嗎?如何防止這種情況發生?
- 什么是虛方法?
- 為什么我們需要虛擬析構函數?
- 抽象類和接口的區別?
- 構造函數可以是虛擬的嗎?
- 關鍵字 const 如何用于類方法?
- 如何保護對象不被復制?
STL 容器
- 向量和列表之間的區別?
- 地圖和無序地圖之間的區別?
- 調用 push_back() 時向量中的迭代器無效嗎?
如何修改你的類以便與 map 和 unordered_map 一起使用?
主題
- 進程和線程之間的區別?
- 同一根線程可以重復運行嗎?
- 同步線程的方法?
- 什么是死鎖?
字節跳動中臺、后端研發面試題
一面經常提到的問題
1.手撕lru
2.并發編程
3.函數式編程
4.無鎖隊列
5.CAP
6.線程池,進程池
7.大文件如何對字符串排序
8.線程同步
9.進程狀態
10.進程調度、多進程開發,多線程開發
11.線程通信 + 2道場景題選擇進程通信方式
12.raws
13.ocket與socket區別
14.設計高性能服務
15.nginx反向代理 epoll,nginx工作模式實現原理
16.pmtu,sendfile,io status等等
17.手撕memcpy 考慮內存重疊與copy效率
二面
1.自我介紹+項目介紹
2.mmap
3.軟鏈接與硬鏈接
4.free命令 cache與buffer區別
5.數據庫 列與行數據庫區別
6.線程安全
7.單核線程安全
8.事務
9.隔離性,隔離級別
10.索引,合并
11.http cookie與session
12.跨域請求
13.最大上升子序列個數
14.函數調用過程
15.static c/c++區別
16.項目中的一些問題
17.tcp與udp
18.tcp深挖
19.進程與線程
20.字符串匹配
小米C++相關崗位面試題
1.自我介紹
2.項目介紹
3.實習介紹
4.C++11的新特性
5.懂rapidjson的底層原理?rapidjson使用的過程中遇到過那些bug?
6.判斷鏈表是否有環,求環的入口節點
7.多態實現原理?虛函數指針具體怎么找虛函數表?
8.如何創建進程?
9.進程布局?堆棧全局代碼那些區
10.函數壓棧過程,包括函數帶參數
11.”hello world"存在那個區?
12.線程私有區和共享區
13.是個全局變量但我又想是線程獨有的怎么辦?
14.tcp三次握手、流量控制、擁塞控制、MSS那一套
15.timewait
16.客戶端握手發送SYN,TCP狀態機,client變成什么狀態?
17.相交鏈表
C++ 基礎
1、引用和指針之間的區別?
引用和指針是C++中兩種重要的間接訪問機制。引用實質上是一個別名,它在聲明時必須初始化,一旦綁定就不能改變。引用不能為空,也不能建立引用的引用,這使得它比指針更安全。引用在內存中實際上是作為一個常量指針實現的,但它對程序員隱藏了指針的復雜性。指針則是一個變量,它存儲了另一個變量的內存地址。指針可以為空,可以改變它所指向的對象,也可以進行指針運算。指針的靈活性使它成為一個強大的工具,但同時也容易導致程序錯誤,如懸空指針和內存泄漏。
2、堆棧和堆中的內存分配有何區別?
棧內存的分配和釋放是由編譯器自動完成的。當變量超出其作用域時,棧內存會自動釋放。棧內存的分配和釋放速度很快,因為它使用簡單的指針移動來實現。棧內存的大小是固定的,在程序運行時就已確定。
堆內存則是動態分配的,由程序員手動管理或使用智能指針等工具管理。堆內存的生命周期不受作用域限制,可以在需要時分配,在不需要時釋放。堆內存的大小理論上只受系統可用內存的限制。但堆內存的分配和釋放相對較慢,且容易產生內存碎片。
3、存在哪些類型的智能指針?
智能指針類型: C++11定義了三種智能指針。unique_ptr實現了獨占式擁有概念,它保證一個對象只被一個指針擁有。shared_ptr允許多個指針指向同一個對象,通過引用計數機制來管理對象的生命周期。weak_ptr是一種弱引用,它不會增加引用計數,主要用于解決shared_ptr可能產生的循環引用問題。
auto_ptr是C++98引入的智能指針,但由于其危險的拷貝語義已在C++11中被棄用。每種智能指針都有其特定的使用場景,選擇合適的智能指針可以大大降低內存管理的復雜性。
4、unique_ptr 是如何實現的?我們如何強制在 unique_ptr 中僅存在一個對象所有者?
unique_ptr的核心思想是獨占式擁有。它通過刪除拷貝構造函數和拷貝賦值運算符來保證一個對象只能被一個unique_ptr擁有。它支持移動語義,允許在保證安全的情況下轉移對象的所有權。
unique_ptr內部包含一個原始指針和一個刪除器。刪除器是一個可調用對象,負責在unique_ptr析構時釋放所管理的資源。unique_ptr的實現非常輕量,在大多數情況下不會帶來任何額外的開銷。
5、shared_ptr 如何工作?對象之間如何同步引用計數器?
shared_ptr使用引用計數來追蹤有多少個shared_ptr共享同一個對象。當引用計數增加時(比如拷貝構造),計數器加一;當引用計數減少時(比如shared_ptr析構),計數器減一。當計數器降為零時,管理的對象會被刪除。
為了保證線程安全,引用計數的更新必須是原子操作。shared_ptr內部實際上包含兩個指針:一個指向管理的對象,另一個指向控制塊。控制塊包含引用計數、刪除器等信息。這種實現方式使shared_ptr的大小是原始指針的兩倍。
6、我們可以復制unique_ptr或者將其從一個對象傳遞到另一個對象嗎?
由于unique_ptr強制獨占所有權,所以它不能被復制。但它可以通過std::move來轉移所有權。在轉移后,原來的unique_ptr會變為空指針,而目標unique_ptr獲得對象的所有權。
這種設計使得unique_ptr可以方便地在函數間傳遞對象的所有權。例如,factory函數可以返回一個unique_ptr,調用者就獲得了返回對象的所有權。unique_ptr也可以存儲在容器中,但必須使用移動語義來操作。
7、什么是右值和左值?
在C++中,左值是一個位置值,它標識一個持久的對象。左值有一個地址,可以取地址操作符作用于它。典型的左值包括變量名、解引用的指針等。
右值表示臨時值,它不能取地址。右值可以分為純右值(pr-value)和將亡值(x-value)。純右值是臨時的、不具名的值,如字面常量。將亡值是即將被銷毀的值,比如即將被移動的對象。
8、什么是 std::move 和 std::forward()
std::move和std::forward: std::move是一個工具函數,它將一個左值強制轉換為右值引用,使得我們可以調用移動構造函數或移動賦值運算符。這在需要顯式地指明要進行移動操作時非常有用。
std::forward用于完美轉發,它保持參數的值類別(左值或右值)不變。在模板編程中,std::forward可以確保參數按照其原始類型被轉發,這對于通用庫的實現非常重要。
9、訪問某些類的私有字段的方法?
訪問私有字段的方法: 在C++中,有幾種合法的方式可以訪問類的私有成員。最常用的是通過友元(friend)聲明,可以是友元函數或友元類。友元聲明允許外部代碼訪問類的私有成員。
另一種方式是通過類的公共接口方法來間接訪問私有成員。這是面向對象設計中推薦的方式,因為它維護了類的封裝性。還可以使用嵌套類,因為嵌套類可以訪問外部類的所有成員。
10、一個類可以繼承多個類嗎?
類的多重繼承: C++支持多重繼承,即一個類可以同時繼承多個基類。但這可能導致菱形繼承問題,即一個類通過不同的路徑繼承了同一個基類的多個實例。
為了解決這個問題,C++引入了虛繼承。虛繼承確保共同基類只有一個實例。雖然C++支持多重繼承,但在實際開發中應該謹慎使用,因為它可能增加代碼的復雜性。
11、靜態字段是否在類構造函數中初始化?
靜態字段的初始化: 靜態成員變量屬于類而不是對象,它們不在構造函數中初始化。靜態成員需要在類外進行定義和初始化,除非是整型或枚舉類型的const static成員,這種情況可以在類內初始化。
這樣設計的原因是為了避免靜態成員被多次初始化。在程序的整個生命周期中,靜態成員只需要初始化一次。靜態成員的初始化發生在程序開始執行之前。
12、構造函數/析構函數中會拋出異常嗎?如何防止這種情況發生?
構造函數/析構函數的異常: 構造函數可以拋出異常,這通常用于指示初始化失敗。但析構函數應該避免拋出異常,因為如果在異常處理過程中析構函數拋出異常,程序會立即終止。
為了防止構造函數拋出異常,可以使用try-catch塊包裝可能拋出異常的代碼,或者使用初始化列表來確保資源獲取。對于析構函數,應該將其聲明為noexcept,并在內部處理所有可能的異常。
13、什么是虛方法?
虛方法: 虛方法是C++實現運行時多態的機制。當基類中聲明一個函數為virtual時,派生類可以重寫這個函數。在運行時,程序會根據對象的實際類型來調用適當的函數版本。
虛函數通過虛函數表(vtable)來實現。每個包含虛函數的類都有一個vtable,其中存儲了該類虛函數的地址。每個對象都包含一個指向vtable的指針(vptr)。這種機制允許在運行時動態綁定函數調用。
14、為什么我們需要虛擬析構函數?
虛析構函數的必要性: 當通過基類指針刪除派生類對象時,如果析構函數不是虛函數,則只會調用基類的析構函數,而不會調用派生類的析構函數。這會導致資源泄漏。
因此,當一個類可能作為基類時,其析構函數應該聲明為虛函數。這確保了在刪除對象時,無論使用什么類型的指針,都能正確調用整個繼承鏈上的析構函數。
15、抽象類和接口的區別?
抽象類和接口的區別: 抽象類是包含至少一個純虛函數的類。它不能被實例化,只能作為基類使用。抽象類可以包含普通成員函數和數據成員,這些可以在派生類中直接使用。
接口是一種特殊的抽象類,它只包含純虛函數。在C++中,接口通常被實現為所有函數都是純虛函數的抽象類。接口定義了一個對象能夠做什么,而不規定如何做。
16、構造函數可以是虛擬的嗎?
構造函數的虛擬性: 構造函數不能是虛函數。這是因為在調用構造函數時,對象還沒有被完全構造,vptr還沒有被初始化。因此不能使用虛函數機制。
如果需要根據運行時條件創建不同類型的對象,可以使用工廠模式。工廠模式通過一個靜態成員函數來創建對象,這個函數可以根據參數返回不同類型的對象。
17、關鍵字 const 如何用于類方法?
const關鍵字在類方法中的使用: const成員函數承諾不會修改對象的狀態。它們可以被const對象調用,這提供了一種編譯時的類型安全機制。const成員函數不能調用非const成員函數。如果需要在const成員函數中修改某些成員變量,可以將這些變量聲明為mutable。mutable允許在const成員函數中修改特定的成員變量,這通常用于緩存等不影響對象邏輯狀態的場合。
18、如何保護對象不被復制?
防止對象被復制: 有幾種方式可以防止對象被復制。最現代的方式是使用=delete來刪除拷貝構造函數和拷貝賦值運算符。這會在編譯時阻止任何復制操作。
另一種方式是將拷貝構造函數和拷貝賦值運算符聲明為private,并且不提供實現。這也可以防止復制,但錯誤信息可能不如=delete清晰。
19、向量和列表之間的區別?
vector和list的區別: vector是一種連續存儲的容器,它在內存中分配一塊連續的空間。這使得vector支持隨機訪問,但在中間插入或刪除元素時需要移動后續元素。當vector需要更多空間時,它會重新分配一個更大的連續空間。
list是一種鏈式存儲的容器,它的元素可以分散在內存的不同位置。list不支持隨機訪問,但在任何位置插入或刪除元素都很快,因為只需要修改相關節點的指針。list的每個元素都需要額外的內存來存儲指針。
20、map和unordered_map之間的區別?
map和unordered_map的區別: map基于紅黑樹實現,它保持鍵值對按鍵的順序存儲。這使得map的查找、插入和刪除操作的時間復雜度都是O(log n)。map占用的內存較少,而且可以按序遍歷。
unordered_map基于哈希表實現,它不保持任何順序。在理想情況下,查找、插入和刪除操作的時間復雜度都是O(1)。但unordered_map需要額外的內存來存儲哈希表,而且可能需要處理哈希沖突。
21 調用 push_back() 時向量中的迭代器無效嗎?
vector的push_back和迭代器:
當向vector調用push_back時,迭代器的有效性取決于是否發生了內存重新分配。如果vector當前的capacity足夠容納新元素,那么push_back不會導致迭代器失效。但如果capacity不足,vector會分配一個更大的內存塊(通常是當前大小的1.5或2倍),并將所有元素復制到新位置,這時之前的所有迭代器都會失效。
為了避免迭代器失效的問題,有以下幾種方法:
- 預先使用reserve分配足夠的空間
- 在push_back后重新獲取迭代器
- 使用索引而不是迭代器來遍歷vector
- 如果必須使用迭代器,可以先完成迭代器操作,再進行push_back
在實際編程中,建議在知道vector大致容量的情況下,先調用reserve預分配空間。這不僅能避免迭代器失效,還能提高程序性能,因為減少了內存重新分配的次數。
22、如何修改你的類以便與 map 和 unordered_map 一起使用?
修改類使其可用于map和unordered_map:
要讓自定義類可以作為map的鍵,必須為該類實現小于運算符(operator<)。這是因為map內部使用紅黑樹來組織數據,需要通過比較運算來確定元素的位置。operator<必須滿足嚴格弱序關系,即具有傳遞性且不能出現等價關系。
對于unordered_map,要求更復雜:
- 需要實現相等運算符(operator==)
- 需要提供一個特化的std::hash模板,或者自定義哈希函數
實現示例:
class MyClass {int id;string name;
public:// 為map實現小于運算符bool operator<(const MyClass& other) const {if (id != other.id) return id < other.id;return name < other.name;}// 為unordered_map實現相等運算符bool operator==(const MyClass& other) const {return id == other.id && name == other.name;}
};// 為unordered_map特化hash模板
namespace std {template<>struct hash<MyClass> {size_t operator()(const MyClass& obj) const {return hash<int>()(obj.id) ^ hash<string>()(obj.name);}};
}
哈希函數的質量會直接影響unordered_map的性能。好的哈希函數應該能夠均勻分布鍵值,避免產生過多的哈希沖突。在選擇容器時,如果需要保持元素順序就用map,如果需要最快的查找性能就用unordered_map。
23、同步線程的方法?
在C++中,有多種線程同步機制可以確保多線程程序的正確性:
- 互斥鎖(mutex):
最基本的同步機制,用于保護共享資源。C++提供了多種互斥鎖:
- std::mutex:標準互斥鎖
- std::recursive_mutex:允許同一線程多次加鎖
- std::timed_mutex:支持超時的互斥鎖
- std::shared_mutex:讀寫鎖,允許多個讀者或一個寫者
- 條件變量(condition_variable):
用于線程間的通信和同步,典型用法是等待某個條件成立:
std::mutex mtx;
std::condition_variable cv;
bool ready = false;// 消費者線程
{std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, []{ return ready; });// 處理數據
}// 生產者線程
{std::lock_guard<std::mutex> lock(mtx);ready = true;cv.notify_one();
}
-
原子操作:
對于簡單的數據類型,使用std::atomic可以避免顯式的鎖定:
std::atomic counter{0};
counter++; // 原子操作,線程安全 -
讀寫鎖:
當讀操作遠多于寫操作時,使用std::shared_mutex可以提高并發性:
std::shared_mutex rwlock;
// 讀者
{std::shared_lock<std::shared_mutex> lock(rwlock);// 讀取數據
}
// 寫者
{std::unique_lock<std::shared_mutex> lock(rwlock);// 修改數據
}
選擇合適的同步機制取決于具體場景:
- 簡單的共享資源保護用mutex
- 需要線程間通信用condition_variable
- 簡單數據類型的并發訪問用atomic
- 讀多寫少的場景用shared_mutex
5.避免死鎖一般通過以下方法:
- 始終按照相同的順序獲取鎖
- 使用std::lock同時獲取多個鎖
- 避免在持有鎖時調用用戶代碼
- 使用RAII風格的鎖管理