
2017年秋天我在圖書館刷到愛奇藝的校招筆試通知點進去做完那套C開發工程師筆試卷整整花了一晚上復盤。說起來這套卷子不算偏但也正因為不偏它特別能暴露基礎功底的薄弱點。指針、內存、STL、算法、多線程基本把C方向校招筆試的高頻考點都覆蓋了一遍題目難度呈現明顯的梯度前面還能靠記憶應付越往后越考驗真功夫。我當時做完最大感受是這套卷子篩的不是誰刷題多而是誰對C這門語言有真正體系化的理解。今天我把這類卷子的出題邏輯、高頻考點和答題策略拆開講一遍給準備秋招的同學做個參考。1. 這套卷子的出題邏輯它到底想篩什么樣的人1.1 筆試形式與C崗位的考察范圍先說形式。這類大廠C方向的秋招筆試卷通常會分為三個部分選擇題、簡答題和編程題。選擇題大概20到30道覆蓋語言基礎、操作系統、網絡、數據結構簡答題一般2到3道考設計思路或問題分析編程題有2到3道要求在線編碼跑通測試用例。整套卷子時間給得不多有些公司甚至是90分鐘到120分鐘完成所有題目這就要求答題節奏非常緊湊。愛奇藝2017年這套卷子的整體風格是基礎題量大、編程題有區分度。選擇題里的C語法和內存題數量不少而且選項之間非常接近如果平時只是眼熟而沒有真正理解很容易掉坑。編程題里既有可以直接套模板的經典題也有需要現場推理的變種題靠死記硬背過不了最后那幾道。從崗位屬性來說C開發工程師在互聯網公司主要做的是底層基礎設施、音視頻處理、網絡服務和高性能組件。愛奇藝這類視頻平臺對C的需求尤其集中在播放器內核、CDN調度、流媒體傳輸、轉碼服務這一條鏈路上。筆試考的東西其實就是在為這些業務方向篩選候選人。1.2 從愛奇藝的業務屬性反推考點偏好視頻平臺的核心技術棧里C主要出現在三個方面一是流媒體服務端需要處理高并發連接和海量數據轉發二是音視頻處理涉及編解碼、轉碼、畫質增強對內存管理和性能極度敏感三是播放器內核和客戶端組件要求代碼在資源受限的終端設備上穩定運行。這三個方向對應的C技能要求其實很明確內存管理和對象生命周期要扎實這塊直接決定線上服務穩定性多線程和并發控制要熟練因為流媒體服務本質就是并發系統STL和算法要熟海量數據的排序、查找、去重是日常工作網絡編程和系統調用要懂這涉及傳輸協議和IO模型。所以這套卷子里出現的知識點并不是出題人隨手抓的而是業務倒推考點的結果。你在復習的時候如果能站在這個角度理解題目就不容易覺得某些偏怪問題沒有意義。比如虛函數考察的是你對對象內存布局的理解放到實際場景里就是基類指針能不能安全地操作派生類對象這在組件化架構中天天遇到。1.3 這套卷子的時間節奏與丟分重災區我當時給自己模擬了真實考試的時間分配發現最耗時間的不是編程題而是前面的選擇題和簡答題。選擇題里很多是下列哪個說法是錯誤的這類否定式提問每個選項都很像是對的需要逐個排查。一道這樣的題就可能花掉3到5分鐘20道基礎題做完40分鐘就沒了。結合周圍同學反饋這套卷子丟分比較多的位置通常有三處內存和指針相關的題目尤其涉及二維指針、指針數組、數組指針的辨析拷貝構造與移動語義的調用時機判斷題目改一個參數傳遞方式答案就完全不同編程題里的邊界條件處理測試用例隱藏了空指針、越界和溢出導致看似正確的代碼只有部分用例通過。搞清楚這些丟分點后你會發現復習重心其實很明確。基礎題靠體系化復習編程題靠高頻題型訓練兩者缺一不可。下面我把每類考點的答題思路拆開細講。2. 語言基礎題拆解指針、內存和對象生命周期2.1 指針與引用的組合陷阱C筆試題里指針和引用是一個怎么考都不過時的主題。這套試卷里涉及指針的部分主要不是讓你寫指針運算而是給你一段代碼讓判斷輸出結果或指出錯誤。比如下面這類問題int a[5] {1, 2, 3, 4, 5}; int *p a; cout *(p) endl; cout *p endl; cout *p 1 endl;很多人會在這種題上翻車原因是把p和p的副作用時機搞混了。*(p)先把p指向的值取出來然后p后移一位所以輸出1*p先把p前移一位再取值此時p指向a[2]輸出3*p 1由于運算符優先級等價于(*p) 1輸出4。這類題考的不是你記沒記住優先級表而是你對表達式求值過程中指針狀態如何變化有沒有清晰認知。指針數組和數組指針也是一個經典區分點。int *p[3]表示一個數組數組元素是三個int*指針int (*p)[3]表示一個指針指向含有三個int元素的數組。在二維數組傳參時才分得清這兩種寫法的差異。不少同學在簡化代碼時把int (*p)[3]錯寫成int *p[3]編譯雖然能過但語義完全錯了。2.2 構造、析構、拷貝與移動的調用時機C筆試里考察對象生命周期的方式很多最常見的是輸出題讓你數一個類被構造了幾次、析構了幾次、拷貝了幾次。比如傳值、傳引用、返回臨時對象的情況class Test { public: Test() { cout ctor endl; } Test(const Test t) { cout copy endl; } Test(Test t) { cout move endl; } ~Test() { cout dtor endl; } }; Test func() { Test t; return t; } int main() { Test a func(); }在C11之前這段代碼可能會觸發兩次構造和多次拷貝在C11之后由于返回值優化和移動語義的存在實際輸出可能簡化。這里的關鍵點是編譯器優化RVO/NRVO)并不是強制行為而移動構造的優先級高于拷貝構造當你有右值引用時return t會優先走移動而不是拷貝。當時這套卷子在移動語義上挖了一個很細的坑類里如果手動實現了析構函數或者拷貝構造函數編譯器就不會自動生成移動構造函數。這意味著即使你的類滿足移動條件也只能退回到拷貝性能差異在上千萬元素容器里非常明顯。這類題表面考語法實際考的是你知不知道C11規則中的隱式函數生成條件。2.3 虛函數與多態的實現機制虛函數是C筆試的必考內容考察方向一般有兩種一種是讓你描述虛表vtable的實現機制另一種是給一段多態代碼讓判斷輸出。描述虛表時關鍵是講清楚三點每個包含虛函數的類都有一張虛函數表表中存放虛函數地址每個對象內部有一個虛表指針vptr指向所屬類的虛表構造對象時vptr會被設置為指向當前正在構造的類的虛表所以在構造函數里調用虛函數不會觸發多態。關于最后一點經常有人踩坑。因為父類構造過程中子類部分還沒有初始化如果這時候調用虛函數走的是子類實現子類成員可能還沒構造程序就會出問題。C選擇了在構造期間把vptr指向當前類從機制上避免這個風險。筆試題里讓你判斷new Derived()之后構造函數中虛函數調用的輸出答案就是父類版本。這套卷子還考過一個很容易錯的知識點析構函數為什么要聲明為虛函數。如果基類析構函數不是虛的通過基類指針刪除派生類對象時只會調用基類析構函數派生類資源無法釋放造成內存泄漏。代碼層面對應的場景是所有組件類、接口類、抽象基類這類體系設計中的基類析構函數幾乎都應該是虛的。3. 算法與編程題實戰字符串、排序和數學計算的拿分策略3.1 字符串類題目從字符數組的轉換說起C筆試題里字符串處理是編程題常客而且經常結合字符數組和std::string互相轉換來考。很多同學對string用得很熟一旦碰到C風格字符串就懵。比如要求按指定分隔符把字符串拆成數組有人會用strtok但這個函數會修改原字符串并且不是線程安全的在多線程題目場景下容易被扣分。更好的做法是使用std::string的find和substr組合實現分詞std::vectorstd::string split(const std::string s, char delim) { std::vectorstd::string result; std::string::size_type start 0; auto pos s.find(delim); while (pos ! std::string::npos) { result.push_back(s.substr(start, pos - start)); start pos 1; pos s.find(delim, start); } result.push_back(s.substr(start)); return result; }寫這類代碼時注意find的第二個參數表示從哪個位置開始搜索避免漏掉最后一個分隔符后面的內容。字符串轉數字也是高頻題std::stoi和std::to_string雖然方便但筆試題更愛考察手寫轉換邏輯因為你得自己處理符號位、溢出和非法字符。3.2 排序算法冒泡和選擇之外的考察角度排序算法在筆試里的考法分為兩種一種是直接讓你實現某個排序算法另一種是考察算法特性和復雜度。選做題里經常出現冒泡排序和選擇排序因為代碼短、易于驗證。但這里有個容易被忽略的知識點冒泡排序是穩定排序選擇排序是不穩定排序。為什么冒泡排序只在相鄰元素之間做交換相等元素的相對順序不會改變選擇排序在每一輪把一個元素放到最終位置如果當前輪發現一個更小值會和前面的元素交換這個交換可能跨過相等的元素導致相對順序改變。看似只是八股文實際在排序對象的屬性有序性要求上很關鍵比如按分數降序、分數相同按學號升序的場景。快速排序在筆試中出現頻率也很高但多數不是讓你寫基本版而是考察如何優化。比如三數取中、小區間插入排序、尾遞歸優化。我見過一道題是要求手寫快速排序的partition函數并保證把所有等于pivot的元素集中在中間這就是三路快排的思路。寫三路快排的要點是維護lt、gt兩個邊界把小于pivot、等于pivot、大于pivot分成三個區域避免重復元素的性能退化。3.3 數學類題目快速冪與最小公倍數的邊界處理數學類題目看著不起眼但往往是編程題里的送分題前提是你能快速寫出無Bug的版本。快速冪是高頻考點中的高頻描述很簡單計算a的b次方模mod。如果直接循環乘b次b到1e9量級就超時了所以要用二分思想long long quickPow(long long a, long long b, long long mod) { long long result 1; a % mod; while (b 0) { if (b 1) result result * a % mod; a a * a % mod; b 1; } return result; }寫這個代碼有三個容易出錯的地方第一步要a % mod否則a可能溢出乘法時要用long long接收中間結果因為兩個1e9量級的數相乘會超過int范圍循環條件是while (b 0)而不是while (b)雖然效果一樣但顯式比較更清晰不容易讓閱卷人誤解。最小公倍數的題目也不少見核心公式是lcm(a, b) a / gcd(a, b) * b。注意這里要先除后乘否則a * b可能溢出。如果是多個整數求最小公倍數就兩兩迭代計算。筆試里常給幾個較大的數比如6、8、12、15很多人能算對但如果換成包含質數的長列表并且在代碼里要求處理就需要確保gcd函數在遞歸和迭代兩種寫法下都能正確工作尤其注意gcd中a或b為零的邊界情況。3.4 進階結構單調棧與鏈表操作的常見考法這套卷子或者同類互聯網公司的筆試卷編程題里偶爾會拔高到單調棧這種進階數據結構。單調棧的典型應用是尋找每個元素下一個更大或更小的元素位置比如每日溫度類問題。核心理解是元素入棧時保持棧內單調性出棧時就可以確定某些答案。寫單調棧的代碼初學者最常犯的錯誤是混淆棧中存值和存下標的區別。大多數情況下應該存儲下標因為最終要求的往往是位置距離或者需要根據下標去原數組取值。如果存值當數組里有重復元素時索引信息會丟失。鏈表操作中反轉鏈表、合并有序鏈表、判斷鏈表是否有環是三個標配題。反轉鏈表迭代寫法要維護三個指針順序上容易錯ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* curr head; while (curr ! nullptr) { ListNode* next curr-next; curr-next prev; prev curr; curr next; } return prev; }這道題的關鍵是在改變curr-next之前先把next保存下來否則鏈表斷掉。很多面試官會追問如果鏈表有環會怎樣、如果要求遞歸實現呢所以現場編碼時要把邊界情況一并考慮清楚。4. 多線程、設計模式與C新特性容易拖后腿的半基礎題4.1 多線程同步從互斥鎖到ABA問題多線程是互聯網公司C筆試里繞不開的題因為它直接對應真實服務端的高并發場景。基礎考察方向包括互斥鎖、條件變量、讀寫鎖、原子操作以及死鎖的形成條件。一個經典考題是多個線程對同一個變量進行自增操作如何保證結果正確。最直接的回答是加互斥鎖但要注意自增操作count不是原子的它包含讀、加、寫三步。用C11的std::atomicint可以解決但如果你在代碼里用了兩次原子操作做比較并交換就會引入ABA問題。ABA問題的場景是線程A讀取到值X線程B把X改成Y又改回X線程A的CAS操作無法察覺中間的變動。這在無鎖數據結構中特別危險比如無鎖棧中可能因為ABA問題導致重復釋放同一塊內存。解決辦法一般是引入版本號或者使用帶有標簽的指針每次修改帶上遞增的標簽CAS時比較值的同時比較標簽。筆試中能說出這層說明你真的理解并發場景下的內存安全問題。另外還有個細節容易被忽略std::mutex類型的變量不能拷貝所以包含互斥鎖的類不能直接放進std::vector或使用默認拷貝構造。如果類里需要互斥鎖又必須實現拷貝只能自定義拷貝邏輯這一般在筆試簡答題里出現。4.2 設計模式的經典問法與答題思路C方向筆試涉及設計模式時最常考的是單例模式其次是觀察者模式、工廠模式。單例模式問你如何實現線程安全的單例這道題有一個標準的進化路線懶漢式加鎖在獲取實例的成員函數里加鎖但性能差雙重檢查鎖先判斷指針是否為空為空才加鎖加鎖后再判斷一次注意防止指令重排序要加內存屏障C11之后的靜態局部變量初始化編譯器保證線程安全。class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() {} };這個寫法的核心在static局部變量C11標準規定它的初始化是線程安全的所以最簡潔高效的實現方式就是靠它。把構造函數設為私有并把拷貝構造刪除防止外部創建實例。觀察者模式的考察方式一般是用一句話說明模式中Subject和Observer的關系或者讓你寫一個簡化版本。答題時不需要硬背UML圖只要說清主題對象維護一個觀察者列表狀態變化時遍歷列表通知觀察者更新即可寫代碼時用std::vectorstd::functionvoid()來存放回調比定義抽象觀察者基類更符合現代C的做法。4.3 C11之后的新特性constexpr、智能指針與回調C11是C發展史的分水嶺筆試中涉及新特性的題目逐年增加。一個常見的送分題是constexpr是哪個C版本引入的。答案是C11引入C14放寬了函數內可以包含的邏輯C17又允許在if和switch中聲明變量C20進一步支持constexpr虛函數和constexpr動態分配。這個問題不難但很多人會答錯成C14或C17。智能指針也是重點。unique_ptr是獨占所有權語義不可拷貝只能移動shared_ptr是共享所有權語義通過引用計數管理生命周期weak_ptr用于打破循環引用它不會增加引用計數。筆試里經常給一段代碼問你shared_ptr管理的對象什么時候析構。關鍵是要能發現循環引用兩個對象互相持有對方的shared_ptr引用計數永遠不為零析構函數永遠不會被調用。解決辦法就是把其中一個方向的指針改為weak_ptr。回調函數的題目這幾年越來越多主要問法有回調函數是什么、在C里怎么實現。從最傳統C函數指針到std::functionstd::bind再到C11的lambda表達式三層遞進。答題時寫出lambda版本是最討巧的因為代碼簡潔且捕獲列表能控制捕獲方式std::functionint(int, int) add [](int a, int b) { return a b; };如果深究還可以講一下回調在異步IO、定時器、事件循環中的底層作用。理解到這個層面筆試的簡答題分數基本穩了。4.4 現場答題的代碼風格與踩坑預防編程題除了算法正確性代碼風格也會影響整體評價。我當時總結出幾條實用原則變量命名要有含義i、j、k可以用在循環里但最好不要出現tmp1、tmp2這種寫完代碼要自查邊界條件空數組、單元素數組、全相同元素數組、指針為空的情況涉及數組下標的地方留意是不是會越界尤其是while循環里同時訪問i和i1的情況能用常量引用做參數的就用常量引用避免無意義拷貝這既是性能優化也是代碼習慣的體現。還有一個小技巧筆試環境沒有本地IDE的自動糾錯代碼寫完之后自己是沒法編譯運行的所以要靠人工編譯自查。我會在腦中模擬一次執行過程用一個小例子從頭走一遍比如排序算法用一個5元素數組模擬基本能發現絕大多數低級錯誤。5. 復盤與延伸這套卷子留給今天的備考建議5.1 筆試后的復盤方法不管是這套愛奇藝的卷子還是其他公司的筆試卷考完以后最重要的事是復盤不是看分數。復盤第一步是把每道題按會做但錯了蒙對的完全不會三個標簽分類。第二步是回來查每一個錯題背后的知識點找到知識盲區后補一輪系統學習而不是只看這道題的解析。我當時會把所有錯題對應的知識點整理成一份清單比如指針數組和數組指針辨析移動構造的生成條件單調棧應用場景。每收集一個新知識點就往清單里加到秋招結束時這份清單已經覆蓋了上百個考點。筆試前重看一遍這份清單比刷一大堆新題更有針對性。5.2 從筆試到技術面試的知識銜接筆試通過后緊接著是技術面試很多在筆試里考的知識點會在面試中以追問形式出現。比如筆試考了單例模式面試就會追問靜態局部變量初始化為什么線程安全或者如果我需要提前釋放單例對象怎么辦。因此筆試復盤的內容其實也是技術面試的復習素材你需要從知道怎么回事升級到能講清楚原理。編程題也一樣筆試里寫了快速冪面試時可能會讓你口頭分析復雜度并說明為什么取模要分布在整個計算過程中。準備面試的時候把筆試中遇到的每個知識點都往深處想一想基本就能覆蓋大部分問題。5.3 幾條具體的刷題和復習建議結合我自己的經驗給準備秋招C方向的同學幾條實操建議語言基礎要系統過一遍推薦找一本講C原理的書完整讀下來做筆記、寫示例代碼不要只看博客碎片算法題按專題刷數組、字符串、鏈表、樹、動態規劃、數學計算各找20到30道代碼手寫到熟練為止多線程、設計模式這類半基礎題一定要動手寫demo光看概念記不住比如自己實現一個線程安全的單例再用兩個線程并發調用測試每次筆試完都要把錯題整理到個人題庫里標記好錯誤原因和正確思路。如果時間有限優先保證基礎和常見算法題的正確率這兩項撐起了筆試的大部分分數。我之前見過不少同學在偏題怪題上花了很多時間結果基礎選擇題錯一片編程題也沒寫完非常可惜。把握住自己能穩穩拿分的部分再逐步提升難度這才是校招筆試最務實的策略。