校招八股:C/C++开发工程师常见笔试、面试题目不完全汇总【很基础】
這里匯總一些C/C++開發崗的常見面試八股題,都屬于比較基礎、偏理論性的題目。換句話說,如果這些題目答不上來,可能會給面試官留下的基礎不好的印象,尤其是科班生哈。
廢話不多說,直接開始。
一、C/C++篇
1. 基礎中的基礎篇
簡述C和C++的區別
難度:? 被考頻率:???
如果你面試的是C++開發崗,有些面試官會把這個問題作為第一個問題。C和C++的區別細究起來的話太多了,因此,我們只要回答它們最大的,也是最主要的區別就好了。如果面試官想知道其余細節的話會繼續追問的。
C和C++的區別如下:
思想上:C是面向過程的,它的主要特點是函數。編程思想是將問題分解成不同的步驟,并調用函數來依次實現這些步驟。
C++是面向對象的,它的主要特點是類和對象。編程思想是將數據和數據操作封裝成不同的類,通過創建這些類的對象并調用對象的成員函數來實現對數據模型的操作。
應用上:C的應用更偏底層,常常用于嵌入式開發、驅動開發等直接與硬件交互的領域。
C++由于它優秀的面向對象機制,在大型應用程序的開發方面也表現出色。
C++是對C的擴充。除了添加了面向對象機制以外,C++還添加的機制常用到的有:類模板、異常處理、運算符重載、標準模板庫(STL)、命名空間(namespace)、流(stream)等等。
C程序從源程序到二進制機器代碼的過程和gcc指令
難度:?? 被考頻率:???
舉例:gcc編譯源代碼hello.c的過程。
1. 預處理(Preprocessing):gcc -E hello.c -o hello.i
處理所有預編譯指令,即源代碼文件中以“#”開頭的指令,具體為:
a. 展開宏定義#define。
b. 處理條件編譯指令#ifdef。
c. 處理文件包含指令#include。
d. 刪除注釋"//“和”/* */"。
e. 添加行號和文件標識,便于編譯器在編譯階段產生錯誤和警告提示時能夠顯示行號。
2. 編譯(Compilation):gcc -S hello.i -o hello.s
對預處理后的文件進行詞法分析、語法分析、語義分析、中間代碼的生成和優化,最后得到匯編代碼文件。
a. 詞法分析(Lexical analysis):詞法分析器會從左到右逐個字符讀入源程序,按照詞法規則將源代碼分割成一個個單詞(Token),檢查詞法錯誤,并輸出二元組<單詞類別,單詞屬性>方便后續編譯過程的引用。
b. 語法分析(Syntax analysis):識別由詞法分析器輸出的單詞符號序列,構造一棵語法樹。語法樹指出了詞法單元流的語法結構,可以判斷是否符合語法規范。常見的語法分析方法分為自下而上和自上而下兩大方法。
c. 語義分析(Semantic analysis):使用語法樹和單詞符號表中的信息,進一步檢查源程序是否滿足語言定義的語義約束。這一步分析的時靜態語義,也就是在編譯期間能分析的語義,而動態語義指在運行期間才能確定的語義。
d. 中間代碼生成:根據語義分析的輸出,生成類機器語言的中間代碼,如三地址代碼。
e. 中間代碼優化:改進中間代碼的質量。
f. 目標代碼生成:將中間代碼映射成目標語言(匯編語言)。
3. 匯編(Assembly):gcc -c hello.s -o hello.o
匯編器將匯編代碼翻譯成可執行的機器碼。每一條匯編語句對應一條機器指令,因此匯編器的工作就是根據匯編指令和機器指令的對照表一一翻譯過來即可。
4. 鏈接(Linking):gcc hello.o -o hello
鏈接器將多個匯編后得到的機器碼文件鏈接,從而生成一個可執行程序。分為靜態鏈接和動態鏈接兩種方式。
a. 靜態鏈接:在鏈接可執行文件時,鏈接器會找到所有被用到的源代碼,將它們復制并組合起來形成一個可執行文件。優點是由于可執行文件中已經具備了執行程序需要的全部內容,因此執行時運行速度較快。缺點是如果有多個可執行程序需要引用同一個目標文件,每個可執行程序都會復制一份該文件的副本,造成空間浪費;另外,如果某個目標文件被修改了,則所有引用該文件的可執行程序都需要重新編譯。
b. 動態鏈接:程序被拆分成獨立的部分存儲,只有運行時才鏈接在一起形成完整的程序。優點是即使多個程序依賴同一個動態鏈接庫,也不需要將這個庫復制多份,而是所有程序共享這個庫;此外在程序更新時只更新被修改的庫,不需要重新鏈接所有程序。缺點是因為將鏈接過程推遲到程序運行時期,所以會對程序性能產生損失。
C程序的內存管理
難度:?? 被考到頻率:????
C或C++語言編寫的程序在處理機上運行,通常被分成五段:
棧區:存放函數中聲明的局部變量、函數的形參和返回值。地址空間“向下減少”。
堆區:保存動態分配的內存區域,可由程序員向操作系統申請和自行釋放。
靜態區(全局區):存儲全局變量和靜態變量。靜態區內存程序開始時創建,直到程序運行結束后才會被釋放。
常量區:保存常量,在程序運行期間不能被修改的量,如字符串常量"abcd"。
代碼區:存放程序代碼,二進制機器指令形式,只讀。
補充: 一個由C語言程序編譯得到的可執行二進制文件,從硬盤上被加載到內存空間上運行的過程,內存的分配和管理機制。
一個C語言編譯的可執行二進制文件,通常被分為三段:代碼段(text)、數據段(data)、堆棧段(BSS)。
裝載到內存上被分為五段:棧區(stack)、堆區(heap)、未初始化變量(BSS)、初始化變量(data)、代碼段(text)。
可執行文件中,text段存放二進制程序;data段存放靜態初始化的數據,即賦初值的全局變量和static變量;BSS段存放未初始化的數據,即沒有賦初值的全局變量和static變量,在可執行文件中BSS段并不占用實際空間,而是只記載該區大小的數值,程序載入時菜實際分配空間,且該控件由系統初始化為零。
應用程序加載到內存空間時,操作系統根據可執行文件中header中的內容,為text段、data段、為BSS段分配相應的內存空間,將文件中
data段和text段內容拷貝到內存中,將BSS段初始化為零,同時為堆區和棧區分配空間并維護。
堆和棧的區別
難度:?? 被考到頻率:?????
| 用戶自己申請 | 系統分配 |
| 由不連續的內存塊構成的鏈表,大小可以調整 | 連續空間,大小固定 |
| 速度慢,會產生碎片 | 速度快,不會產生碎片 |
| 向高地址增長 | 向低地址增長 |
| 由庫函數(malloc/realloc/free、new/delete)提供服務 | 由系統提供服務 |
什么叫內存泄漏
難度:?? 被考到頻率:??
內存泄漏,通常指堆內存泄漏,程序員向系統申請任意大小的堆內存塊,使用完畢必須進行顯式的內存釋放,否則該內存不能再次被使用。即使用malloc、realloc、new申請的空間,必須使用free、delete進行釋放。
什么叫作用域
2. C++特性篇
C++的三大特征
難度:? 被考到頻率:??
繼承、多態、封裝。
繼承:一個對象可以繼承另一類對象的特征和能力。目的是避免公用代碼的重復開發,減少代碼和數據冗余。
多態:為不同數據類型的實體提供統一的接口,程序運行時,相同的消息可能會送給多個不同的類別之對象,而系統可依據對象所屬類別,引發對應類別的方法,而有不同的行為
封裝:把客觀的事物抽象成一個類,就是將數據和方法打包在一起,加以權限的區分,達到保護并安全使用數據的目的。
靜態多態和動態多態
難度:?? 被考到頻率:??
多態分為靜態多態和動態多態。
靜態多態:在編譯期間決定程序的執行過程。包括函數重載和泛型編程,泛型編程包括函數模板和類模板。
動態多態:在程序運行時根據被引用對象的實際類型判斷調用哪個方法。包括虛函數。
C++中重載、重寫(覆蓋)和隱藏的區別
難度:?? 被考到頻率:??
派生類中的函數覆蓋基類中的同名函數,要求兩個函數具有相同的參數個數、參數類型和返回值類型,且基類中的函數必須是虛函數。重寫指的是重寫基類函數中的函數體。
在某些情況下,派生類中的函數屏蔽了基類中的同名函數的現象。如果想調用基類的函數必須加作用域限定符。具體情況有:
(1)派生類和基類中具有同名函數,兩個函數參數列表相同,且基類的函數沒有被聲明稱虛函數。
class A { //父類 public:void fun(int a) {...} };class B : public A { //子類 public:void fun(int a) {...} //隱藏父類的fun函數 };int main() {B b;b.fun(2); //調用B中的fun函數b.A::fun(2); //調用A中fun函數return 0; }(2)派生類和基類中具有同名函數,兩個函數參數列表不同,無論基類的函數是不是虛函數都會被隱藏。
class A { //父類 public:void fun(int a) {...} };class B : public A { //子類 public:void fun(char* a) {...} //隱藏父類的fun函數 };int main() {B b;b.fun(2); //報錯,調用B中的fun函數,但參數類型出錯b.A::fun(2); //調用A中fun函數return 0; }虛函數
難度:?? 被考到頻率:???
基類和派生類中可以出現名字相同、參數個數和參數類型都相同的函數,直接調用時編譯器會讓派生類函數覆蓋基類函數,或者通過添加作用域限定符來調用基類函數。因此,
下面的例子中,基類和派生類中具有同名函數display()。
#include <iostream> using namespace std;//定義基類 class Point { protected:float x, y; public:Point(float x = 0, float y = 0); //構造函數void display(); };Point::Point(float a, float b) : x(a), y(b) {} void Point::display() {cout << "[x, y] = [" << x << ", " << y << "]" << endl; }//定義派生類,繼承自父類Point class Circle : public Point { private:float radius; public:Circle(float x = 0, float y = 0, float r = 0); //構造函數void display(); //與基類中同名的函數 };Circle::Circle(float a, float b, float r) : Point(a, b), radius(r) {} void Circle::display() {cout << "[x, y] = [" << x << ", " << y << "]; r = " << radius << endl;return; }int main() {Point p(1, 1); //定義一個基類對象Circle c(5, 5, 2.5); //定義一個派生類對象Point* pt; //定義一個基類指針pt = &p; //指針指向基類對象pt->display(); //調用同名函數pt = &c; //指針指向派生類對象pt->display(); //調用同名函數return 0; }輸出結果。
[x, y] = [1, 1]
[x, y] = [5, 5]
分析輸出結果可以發現,第一行調用的基類的成員函數,第二行雖然指針指向了派生類,但由于指針的類型時基類的,調用的依舊是基類的成員函數。如果我們希望指針指向什么類的對象,就調用該類的成員函數,則需要將基類的display函數聲明為虛函數。
修改代碼,其他地方不修改,只是在基類Point中的聲明display函數時添加關鍵字virtual,如下。
//定義基類 class Point { protected:float x, y; public:Point(float x = 0, float y = 0); //構造函數virtual void display(); }; ... ...運行結果如下。
[x, y] = [1, 1]
[x, y] = [5, 5]; r = 2.5
在設定虛函數之前,基類指針本應該指向基類對象,如果指向派生類對象,則必須進行指針類型轉換,將派生類的指針轉換成基類指針,因此通過該指針也只能調用派生類對象中的基類部分。
設定虛函數之后,派生類的基類部分取代了原本基類中的虛函數,因此即使基類指針指向派生類對象,也會調用派生類中的成員函數。
虛函數的原理
難度:??? 被考到頻率:??
實現原理:虛函數表+虛表指針
虛函數表(vtbl):是一個數組,每個元素都用來存儲虛函數的地址。每個類有一個自己的虛函數表,由于類中虛函數的個數可以在編譯時期就能確定,因此虛函數表的大小在編譯時期就可以確定。虛函數表是全局共享的,存儲在全局數據區,在編譯時完成構造,全局可用。
虛表指針(vptr):編譯器為每個類對象添加一個隱藏成員,隱藏成員保存了一個指向虛函數表的指針,
未完待續……
總結
以上是生活随笔為你收集整理的校招八股:C/C++开发工程师常见笔试、面试题目不完全汇总【很基础】的全部內容,希望文章能夠幫你解決所遇到的問題。