80%以上的應屆畢業生看了我的面經都拿到了心儀的大廠offer

面試系列文章:

點擊這裏直接跳轉面試經驗貼專欄

[1] C++軟件開發工程師概念手冊
[2] 從瀏覽器輸入一個URL(www.baidu.com)後執行全過程
[3] const指針和指向常量的指針
[4] C/C++預處理指令#define,#ifdef,#ifndef,#endif…
[5] 堆與棧的區別(經典講解)
[6] 堆棧、BSS段、代碼段、數據段、RO、RW、ZI等概念區分
[7] C語言中關鍵字auto、static、register、extern、volatile、restrict的作用

昨天面試完字節跳動頭條的測試開發,我更是想要規劃寫一篇應屆生校園招聘的面經,做一個總結。不管你面試哪個方向,主要考察內容都是C++軟件知識操作系統計算機網絡數據庫這幾類。

因此,我以字節跳動面試的主要內容按這幾類分類整理,供大家參考,之後也會在此基礎上進行整理完善。

建議收藏關注

字節跳動春招測試開發工程師面試3月18號(附參考答案)


一、基礎概念題

第一部分 C++知識

C++考點整理請參看我的另一篇文章,常考題帶參考答案。
C++軟件開發工程師概念手冊

1. 重載和覆蓋的區別

重載和覆蓋是面向對象多態性的不同的表現方式。其中,重載是在一個類中多態性的一種表現,是指在一個類中定義了多個同名的方法,他們或有不同的參數個數,或有不同的參數類型,或參數順序不同與訪問修飾符和返回值類型無關。在使用重載時,需要注意以下幾點:

  1. 重載是通過不同的方法參數來區分的,例如不同的參數個數,不同的參數類型或者不同的參數順序。

  2. 重載和方法的訪問修飾符、返回值類型、拋出的異常類型無關。

  3. 對於繼承來說,如果父類方法的訪問修飾符爲private,那麼就不能在子類對其重載;如果子類也定義了一個同名的函數,這只是一個新的方法,不會達到重載的效果。

覆蓋是指子類函數覆蓋父類函數。覆蓋一個方法並對其進行重寫,以達到不同的作用。在使用覆蓋時要注意以下幾點:

  1. 子類中的覆蓋方法必須要和父類中被覆蓋的方法有着相同的函數名和參數。

  2. 子類中覆蓋方法的返回值必須和父類中被覆蓋方法的返回值相同。

  3. 子類中覆蓋方法所拋出的異常必須要和父類中被覆蓋方法所拋出的異常一致。

  4. 父類中被覆蓋的方法不能爲private,否則其子類只是定義了一個方法,並沒有對其覆蓋。

覆蓋和重載的區別如下:

  1. 覆蓋是子類和父類之間的關係,是垂直關係;重載是同一個類中方法之間的關係,是水平關係。

  2. 覆蓋只能由一對方法產生關係,重載是多個方法之間的關係。

  3. 覆蓋要求參數列表相同,重載要求參數列表不同。

  4. 覆蓋關係中,調用方法是根據對象的類型來決定;而重載關係是根據調用時的實參表與形參表來選擇方法體的。

拓展:

  1. 重載: 函數重載是指在同一作用域內(名字空間),可以有一組具有相同函數名,不同參數列表的函數;

  2. 覆蓋override(也叫重寫):指在派生類中重新對基類中的虛函數(注意是虛函數)重新實現。即函數名和參數都一樣,只是函數的實現體不一樣;

  3. 隱藏:指派生類中的函數把基類中相同名字的函數屏蔽掉了,隱藏與另外兩個概念表面上看來很像,很難區分,其實他們的關鍵區別就是在多態的實現上。

真題示例:

下面那種情形下myfunc函數聲明是重載?
A. namespace IBM
{
    int myfunc(int a);
}
namespace SUN
{
    int myfunc(double b);
}
B. namespace IBM
{
    int myfunc(int a);
}
namespace SUN
{
    using IBM::myfunc;
    int myfunc(double b);
}
C. namespace IBM
{
    int myfunc(int a);
    namespace SUN
    {
        int myfunc(double b);
    }
}
D. class A
{
public:
    int myfunc(int a);
}
class SubA: public A
{
public:
    int myfunc(double b);
}

答案是B,A和C都是名字空間不同;D是隱藏,只有B是重載!

存在如下聲明:
void f (); //全局函數
class A
{
public:
 void f(int);
};

class B: public A
{
public:
 void f(int *);
 static void f(int **);
 void test();
 //...
};

那麼在如下B::test實現中哪幾個函數調用是合法的:
void B::test()
{
 int x = 0;
 int *p = NULL;
 f();    //(1)
 f(x);   //(2)
 f(&x);  //(3)
 f(&p);  //(4)
};

A.(1)(2)(3)(4) B.(1)(3)(4) C.(2)(3)(4) D.(3)(4)

答案是D,類成員函數重載:局部同名函數將隱藏而不是重載全局聲明,不引入父類名字空間時子類的同名函數不會和父類的構成重載,靜態成員函數可以和非靜態成員函數構成重載。

補充:
當模板函數與重載函數同時出現在一個程序體內時,C++編譯器的求解次序是:

  • 1.調用重載函數
  • 2.如果不匹配,則調用模板函數
  • 3.如果還不匹配則進行強制類型轉換調用重載函數
  • 4.報錯

2. C語言和C++的區別

在這裏插入圖片描述
在這裏插入圖片描述

  1. 函數默認值
    C89標準的C語言不支持函數默認值,C++支持函數默認值,且需要遵循從右向左賦初始值。

  2. inline內聯函數
    C89沒有,在調用點直接展開,不生成符號,沒有棧幀的開闢回退,僅在Release版本下生效。一般寫在頭文件中。

  3. 函數重載
    C語言不存在函數重載,C++根據函數名參數個數參數類型判斷重載,屬於靜多態,必須同一作用域下才叫重載。

  4. const
    C中的const叫只讀變量,只是無法做左值的變量;C++中的const是真正的常量,但也有可能退化成c語言的常量,默認生成local符號。

  5. 引用
    引用底層就是指針,使用時會直接解引用,可以配合const對一個立即數進行引用。

  6. malloc,free && new,delete
    見此部分第4點

  7. 作用域
    C語言中作用域只有兩個:局部,全局。C++中則是有:局部作用域,類作用域,名字空間作用域三種。

擴展

2.1 namespace

命名空間就是將多個變量和函數等包含在內,使其不會與命名空間外的任何變量和函數等發生重命名的衝突。

在其中的很多實例中,都有這麼一條語句:using namespace std;,即使用命名空間std,其作用就是規定該文件中使用的標準庫函數都是在標準命名空間std中定義的。

使用命名空間成員的方法
(1)使用命名空間別名

namespace TV=Television;
TV::func();

(2)使用using命名空間成員名

using TV::func();

以上語句聲明:在本作用域(using語句所在的作用域)中會用到命名空間TV中的成員函數func(),在本作用域中如果使用該命名空間成員時,不必再用命名空間限定。

(3)使用using namespace命名空間成員名

在用using namespace聲明的作用域中,命名空間的成員就好像在全局域聲明的一樣。因此可以不必用命名空間限定。顯然這樣的處理對寫程序比較方便。但是如果同時用using namespace聲明多個命名空間時,往往容易出錯,因爲兩個命名空間可能會有同名的類和函數。

(4)無名的命名空間

由於命名空間沒有名字,在其他文件中顯然無法引用,它只在本文件的作用域內有效。在本程序中的其他文件中也無法使用該fun函數,也就是把fun函數的作用域限制在本文件範圍中。在C浯言中可以用static聲明一個函數,其作用也是使該函數的作用域限於本文件。C++保留了用static聲明函數的用法,同時提供了用無名命名空間來實現這一功能。

(5)標準命名空間std

標準C++庫的所有的標識符都是在一個名爲std的命名空間中定義的,或者說標準頭文件(如iostream)中函數、類、對象和類模板是在命名空間std中定義的。在std中定義和聲明的所有標識符在本文件中都可以作爲全局量來使用。但是應當絕對保證在程序中不出現與命名空間std的成員同名的標識符。

由於在命名空間std中定義的實體實在太多,有時程序設計人員也弄不請哪些標識符已在命名空間std中定義過,爲減少出錯機會,有的專業人員喜歡用若干個using命名空間成員聲明來代替using namespace命名空間聲明。但是目前所用的C++庫大多是幾年前開發的,當時並沒有命名空間,庫中的有關內容也沒有放在std命名空間中,因而在程序中不必對std進行聲明。

參考:

2.2 結構體和類的區別

C語言中的結構體只有數據成員,無函數成員;C++語言中的結構可有數據成員和函數成員。

在缺省情況下,結構體中的數據成員和成員函數都是公有的,而在類中是私有的。

一般我們僅在描述數據成員時使用結構,當既有數據成員又有成員函數時使用類。

2.3 爲什麼說C語言一定要有main函數呢?

C語言中有數據和函數。函數部分放在代碼區,數據分爲兩類:局部的和全局的,它們的區別在於放在靜態數據區還是堆棧中。而且全局變量和靜態變量是在函數執行前就創建好的。

C語言又有一個規定:全局區不能有可執行代碼 ,可執行代碼必須進入函數中。但是C語言中的函數都是全局的,這就導致函數不能嵌套定義:嵌套定義導致函數內部定義的函數成了局部函數。所以要解決各個函數的執行問題只能通過函數的嵌套調用。這時就需要有一個函數首先被執行,來調用其他一系列的函數,完成程序的功能,而這個第一個調用的函數就是main函數。

3. 指針和引用的區別

(1)指針:指針是一個變量,只不過這個變量存儲的是一個地址,指向內存的一個存儲單元;而引用跟原來的變量實質上是同一個東西,只不過是原變量的一個別名而已。如:

int a=1;int *p=&a;

int a=1;int &b=a;

上面定義了一個整形變量和一個指針變量p,該指針變量指向a的存儲單元,即p的值是a存儲單元的地址。
而下面2句定義了一個整形變量a和這個整形a的引用b,事實上a和b是同一個東西,在內存佔有同一個存儲單元。

(2)可以有const指針,但是沒有const引用

(3)指針可以有多級,但是引用只能是一級(int **p;合法 而 int &&a是不合法的)

(4)指針的值可以爲空,但是引用的值不能爲NULL,並且引用在定義的時候必須初始化;

(5)指針的值在初始化後可以改變,即指向其它的存儲單元,而引用在進行初始化後就不會再改變了。

(6)"sizeof引用"得到的是所指向的變量(對象)的大小,而"sizeof指針"得到的是指針本身的大小;

(7)指針和引用的自增(++)運算意義不一樣;指針:地址後移(不一定是1),引用:內容/值加一

4. new 和 malloc

  1. 屬性
    new和delete是C++關鍵字,需要編譯器支持;malloc和free是庫函數,需要頭文件支持。

  2. 參數
    使用new操作符申請內存分配時無須指定內存塊的大小,編譯器會根據類型信息自行計算。而malloc則需要顯式地指出所需內存的尺寸

  3. 返回類型
    new操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符。而malloc內存分配成功則是返回void * ,需要通過強制類型轉換將void*指針轉換成我們需要的類型。

  4. 自定義類型
    new會先調用operator new函數,申請足夠的內存(通常底層使用malloc實現)。然後調用類型的構造函數,初始化成員變量,最後返回自定義類型指針。delete先調用析構函數,然後調用operator delete函數釋放內存(通常底層使用free實現)。

    malloc/free是庫函數,只能動態的申請和釋放內存,無法強制要求其做自定義類型對象構造和析構工作。

    new可以調用malloc(),但malloc不能調用new。

  5. 重載
    C++允許重載new/delete操作符,malloc不允許重載。

  6. 內存區域
    new做兩件事:分配內存和調用類的構造函數,delete是:調用類的析構函數和釋放內存。而malloc和free只是分配和釋放內存。

    new操作符從自由存儲區(free store)上爲對象動態分配內存空間,而malloc函數從堆上動態分配內存。自由存儲區是C++基於new操作符的一個抽象概念,凡是通過new操作符進行內存申請,該內存即爲自由存儲區。而堆是操作系統中的術語,是操作系統所維護的一塊特殊內存,用於程序的內存動態分配,C語言使用malloc從堆上分配內存,使用free釋放已分配的對應內存。自由存儲區不等於堆,如上所述,佈局new就可以不位於堆中。

  7. 分配失敗
    new內存分配失敗時,會拋出bac_alloc異常。malloc分配內存失敗時返回NULL。

  8. 內存泄漏
    內存泄漏對於new和malloc都能檢測出來,而new可以指明是哪個文件的哪一行,malloc確不可以。

5. 構造函數和析構函數

5.1 概念

構造函數是與類同名,沒有返回值的特殊成員函數。一般用於初始化類的數據成員,每當創建一個對象時(包括使用new動態創建對象,不包括創建一個指向對象的指針),編譯系統就自動調用構造函數,類的構造函數一般是公有的(public)。構造函數可以重載。

析構函數的功能是當對象被撤消時,釋放該對象佔用的內存空間。

  • 與構造函數相同的是在定義析構函數時,不能指定任何的返回類型,也不能使用void。
  • 與構造函數不同的是析構函數沒有參數,每個類只能有一個析構函數。析構函數的函數名爲類名前加~。

析構函數被自動調用的三種情況
(1) 一個動態分配的對象被刪除,即使用delete刪除對象時,編譯系統會自動調用析構函數;
(2) 程序運行結束時;
(3) 一個編譯器生成的臨時對象不再需要時。

5.2 拷貝構造函數

拷貝構造函數的功能是用一個已有的對象來初始化一個被創建的同類對象。拷貝構造函數的聲明形式爲:
類名(類名&對象名);

自動調用拷貝構造函數的四種情況:
① 用類的一個對象去初始化另一個對象

cat cat1; 
cat cat2(cat1); /*創建cat2時系統自動調用拷貝構造函數,用cat1初始化cat2*/

② 用類的一個對象去初始化另一個對象時的另外一種形式

cat cat2=cat1;   // 注意並非cat cat1,cat2; cat2=cat1;

③ 對象作爲函數參數傳遞時,調用拷貝構造函數。

f(cat a){ }      // 定義f函數,形參爲cat類對象
	cat b;           // 定義對象b
	f(b); // 進行f函數調用時,系統自動調用拷貝構造函數

④ 如果函數的返回值是類的對象,函數調用返回時,調用拷貝構造函數。

cat f()          // 定義f函數,函數的返回值爲cat類的對象
{ cat a;return a;
}

cat b;          // 定義對象b
b=f();       // 調用f函數,系統自動調用拷貝構造函數

5.3 如何訪問基類的非公有成員

(1) 在類定義體中使用保護成員
保護段成員可以被它的派生類訪問。

(2) 將派生類聲明爲基類的友元類,以訪問基類的私有成員

(3) 派生類使用基類提供的接口間接使用基類的私有成員。

6. 虛函數

簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而採用不同的策略。

說明: C++知識點這部分詳細參考我之前寫的一篇文章 C++軟件開發工程師概念手冊

7.爲什麼要有main()函數?

     <空缺>

在main函數執行之前,需要執行什麼程序?

  1. 建立stdin/stdout/stderr流
  2. 將main函數接受的兩個參數(argc,argv)壓入棧中,供main調用
  3. 不同的操作系統還可能要求的一些其他操作。

第二部分 計算機網絡

1. 從瀏覽器輸入一個URL(www.baidu.com)後執行全過程

從瀏覽器輸入一個URL(www.baidu.com)後執行全過程

總結:
執行過程:

(1) 瀏覽器獲取輸入的域名www.baidu.com
(2) 瀏覽器向DNS請求解析www.baidu.com的IP地址
(3) 域名系統DNS解析出百度服務器的IP地址
(4) 瀏覽器發出HTTP請求,請求百度首頁
(5) 瀏覽器與該服務器建立TCP連接(默認端口號80)
(6) 服務器通過HTTP響應把首頁文件發送給瀏覽器
(7) TCP連接釋放
(8) 瀏覽器將首頁文件進行解析,並將Web頁顯示給用戶。

DNS查找過程

(1)瀏覽器會檢查緩存中有沒有這個域名對應的解析過的IP地址,如果緩存中有,這個解析過程就將結束。


(2)如果用戶的瀏覽器緩存中沒有,瀏覽器會查找操作系統緩存(hosts文件)中是否有這個域名對應的DNS解析結果。


(3)若還沒有,此時會發送一個數據包給DNS服務器,DNS服務器找到後將解析所得IP地址返回給用戶。

2. TCP和UDP的區別

TCP和UDP的區別分析與總結

UDP TCP
是否連接 無連接 面向連接
是否可靠 不可靠傳輸,不使用流量控制和擁塞控制 可靠傳輸,使用流量控制和擁塞控制
連接對象個數 支持一對一,一對多,多對一和多對多交互通信 只能是一對一通信
傳輸方式 面向報文 面向字節流
首部開銷 首部開銷小,僅8字節 首部最小20字節,最大60字節
適用場景 適用於實時應用(IP電話、視頻會議、直播等) 適用於要求可靠傳輸的應用,例如文件傳輸

3. TCP三次握手、四次揮手過程

TCP三次握手、四次揮手及常見面試題全集

4. HTTP狀態碼100、200、300、400、500、600的含義

  • 1xx (臨時響應)表示臨時響應並需要請求者繼續執行操作的狀態代碼。
    100 (繼續) 請求者應當繼續提出請求。 服務器返回此代碼表示已收到請求的第一部分,正在等待其餘部分。

  • 2xx (成功)表示成功處理了請求的狀態代碼。
    200 (成功) 服務器已成功處理了請求。 通常,這表示服務器提供了請求的網頁。

  • 3xx (重定向) 表示要完成請求,需要進一步操作。 通常,這些狀態代碼用來重定向。
    300 (多種選擇) 針對請求,服務器可執行多種操作。 服務器可根據請求者 (useragent)選擇一項操作,或提供操作列表供請求者選擇。

  • 4xx (請求錯誤) 這些狀態代碼表示請求可能出錯,妨礙了服務器的處理。
    400 (錯誤請求) 服務器不理解請求的語法。
    401 (未授權) 請求要求身份驗證。 對於需要登錄的網頁,服務器可能返回此響應。
    402 該狀態碼是爲了將來可能的需求而預留的。
    403 (禁止) 服務器拒絕請求。
    404 (未找到) 服務器找不到請求的網頁。

  • 5xx (服務器錯誤)這些狀態代碼表示服務器在嘗試處理請求時發生內部錯誤。這些錯誤可能是服務器本身的錯誤,而不是請求出錯。
    500 (服務器內部錯誤) 服務器遇到錯誤,無法完成請求。

  • 600 源站沒有返回響應頭部,只返回實體內容

5. http與https的區別

  • 1、https協議需要到ca申請證書,一般免費證書較少,因而需要一定費用。

  • 2、http是超文本傳輸協議,信息是明文傳輸,https則是具有安全性的ssl加密傳輸協議。

  • 3、http和https使用的是完全不同的連接方式,用的端口也不一樣,前者是80,後者是443。

  • 4、http的連接很簡單,是無狀態的;HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,比http協議安全。

第三部分 操作系統(Linux)

1. 進程與線程的區別

  1. 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程依賴於進程而存在。

  2. 進程在執行過程中擁有獨立的內存單元,而多個線程共享進程的內存。(資源分配給進程,同一進程的所有線程共享該進程的所有資源。同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。)

  3. 進程是資源分配的最小單位,線程是CPU調度的最小單位;

  4. 系統開銷: 由於在創建或撤消進程時,系統都要爲之分配或回收資源,如內存空間、I/O設備等。因此,操作系統所付出的開銷將顯著地大於在創建或撤消線程時的開銷。類似地,在進行進程切換時,涉及到整個當前進程CPU環境的保存以及新被調度運行的進程的CPU環境的設置。而線程切換隻須保存和設置少量寄存器的內容,並不涉及存儲器管理方面的操作。可見,進程切換的開銷也遠大於線程切換的開銷。

  5. 通信: 由於同一進程中的多個線程具有相同的地址空間,致使它們之間的同步和通信的實現,也變得比較容易。進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。在有的系統中,線程的切換、同步和通信都無須操作系統內核的干預

  6. 進程編程調試簡單可靠性高,但是創建銷燬開銷大;線程正相反,開銷小,切換速度快,但是編程調試相對複雜。

  7. 進程間不會相互影響 ;線程一個線程掛掉將導致整個進程掛掉

  8. 進程適應於多核、多機分佈;線程適用於多核

2. 進程間通信方式

進程間通信主要包括管道pipe、有名管道FIFO、消息隊列MessageQueue、共享存儲、信號量Semaphore、信號Signal、套接字Socket。

(1)管道
管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。

特點:

  • 它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端。
  • 它只能用於具有親緣關係的進程之間的通信(也是父子進程或者兄弟進程之間)。
  • 它可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的read、write 等函數。但是它不是普通的文件,並不屬於其他任何文件系統,並且只存在於內存中。

(2)有名管道FIFO
FIFO,也稱爲命名管道,它是一種文件類型。

特點:

  • FIFO可以在無關的進程之間交換數據,與無名管道不同。
  • FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中。

(3)消息隊列
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。

特點:

  • 消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
  • 消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內-容並不會被刪除。
  • 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。

(4)信號量
信號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。

特點:

  • 信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
  • 信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。
  • 每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
  • 支持信號量組。

(5)共享內存
共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。

特點:

  • 共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。
  • 因爲多個進程可以同時操作,所以需要進行同步。
  • 信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。

(5)套接字
socket也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同主機之間的進程通信。

3. 線程間通信的方式

  • 臨界區: 通過多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問;
  • 互斥量Synchronized/Lock: 採用互斥對象機制,只有擁有互斥對象的線程纔有訪問公共資源的權限。因爲互斥對象只有一個,所以可以保證公共資源不會被多個線程同時訪問
  • 信號量Semaphore: 爲控制具有有限數量的用戶資源而設計的,它允許多個線程在同一時刻去訪問同一個資源,但一般需要限制同一時刻訪問此資源的最大線程數目。
  • 事件(信號),Wait/Notify:通過通知操作的方式來保持多線程同步,還可以方便的實現多線程優先級的比較操作

4. 請你說一說Linux虛擬地址空間

4.1 虛擬內存的概念

爲了防止不同進程同一時刻在物理內存中運行而對物理內存的爭奪和踐踏,採用了虛擬內存。

虛擬內存技術使得不同進程在運行過程中,它所看到的是自己獨自佔有了當前系統的4G內存。所有進程共享同一物理內存,每個進程只把自己目前需要的虛擬內存空間映射並存儲到物理內存上。 事實上,在每個進程創建加載時,內核只是爲進程“創建”了虛擬內存的佈局,具體就是初始化進程控制表中內存相關的鏈表,實際上並不立即就把虛擬內存對應位置的程序數據和代碼(比如.text .data段)拷貝到物理內存中,只是建立好虛擬內存和磁盤文件之間的映射就好(叫做存儲器映射),等到運行到對應的程序時,纔會通過缺頁異常,來拷貝數據。還有進程運行過程中,要動態分配內存,比如malloc時,也只是分配了虛擬內存,即爲這塊虛擬內存對應的頁表項做相應設置,當進程真正訪問到此數據時,才引發缺頁異常。

請求分頁系統請求分段系統請求段頁式系統都是針對虛擬內存的,通過請求實現內存與外存的信息置換。

4.2 虛擬內存的好處:

  1. 擴大地址空間
  2. 內存保護:每個進程運行在各自的虛擬內存地址空間,互相不能干擾對方。虛存還對特定的內存地址提供寫保護,可以防止代碼或數據被惡意篡改。
  3. 公平內存分配。採用了虛存之後,每個進程都相當於有同樣大小的虛存空間。
  4. 當進程通信時,可採用虛存共享的方式實現。
  5. 當不同的進程使用同樣的代碼時,比如庫文件中的代碼,物理內存中可以只存儲一份這樣的代碼,不同的進程只需要把自己的虛擬內存映射過去就可以了,節省內存
  6. 虛擬內存很適合在多道程序設計系統中使用,許多程序的片段同時保存在內存中。當一個程序等待它的一部分讀入內存時,可以把CPU交給另一個進程使用。在內存中可以保留多個進程,系統併發度提高
  7. 在程序需要分配連續的內存空間的時候,只需要在虛擬內存空間分配連續空間,而不需要實際物理內存的連續空間,可以利用碎片

4.3 虛擬內存的代價:

  1. 虛存的管理需要建立很多數據結構,這些數據結構要佔用額外的內存
  2. 虛擬地址到物理地址的轉換,增加了指令的執行時間。
  3. 頁面的換入換出需要磁盤I/O,這是很耗時的
  4. 如果一頁中只有一部分數據,會浪費內存。

5. 請你說一說死鎖發生的條件以及如何解決死鎖

死鎖是指兩個或兩個以上進程在執行過程中,因爭奪資源而造成的下相互等待的現象。死鎖發生的四個必要條件如下:

  1. 互斥條件:進程對所分配到的資源不允許其他進程訪問,若其他進程訪問該資源,只能等待,直至佔有該資源的進程使用完成後釋放該資源;
  2. 請求並保持條件:進程獲得一定的資源後,又對其他資源發出請求,但是該資源可能被其他進程佔有,此時請求阻塞,但該進程不會釋放自己已經佔有的資源
  3. 不可剝奪條件:進程已獲得的資源,在未完成使用之前,不可被剝奪,只能在使用後自己釋放
  4. 環路等待條件:進程發生死鎖後,必然存在一個進程-資源之間的環形鏈

解決死鎖的方法即破壞上述四個條件之一,主要方法如下:

  • 資源一次性分配,從而剝奪請求和保持條件
  • 可剝奪資源:即當進程新的資源未得到滿足時,釋放已佔有的資源,從而破壞不可剝奪的條件
  • 資源有序分配法:系統給每類資源賦予一個序號,每個進程按編號遞增的請求資源,釋放則相反,從而破壞環路等待的條件

6. 請你說一說操作系統中的結構體對齊,字節對齊

6.1 原因:

1)平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。

2)性能原因:數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。

6.2 規則

1)數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。

2)結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。

3)結構體作爲成員:如果一個結構裏有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲。

6.3 定義結構體對齊

可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是指定的“對齊係數”。

6.4 舉例

#pragma pack(2) 
struct AA { 
	int a;       //長度4 > 2 按2對齊;偏移量爲0;存放位置區間[0,3] 
	char b;  //長度1 < 2 按1對齊;偏移量爲4;存放位置區間[4] 
	short c;     //長度2 = 2 按2對齊;偏移量要提升到2的倍數6;存放位置區間[6,7] 
	char d;  //長度1 < 2 按1對齊;偏移量爲7;存放位置區間[8];共九個字節 
}; 
#pragma pack() 

7. 請你說一下虛擬內存置換的方式

比較常見的內存替換算法有:FIFO,LRU,LFU,LRU-K,2Q。

7.1 FIFO(先進先出淘汰算法)

思想:最近剛訪問的,將來訪問的可能性比較大。
實現:使用一個隊列,新加入的頁面放入隊尾,每次淘汰隊首的頁面,即最先進入的數據,最先被淘汰。
弊端:無法體現頁面冷熱信息

7.2 LFU(最不經常訪問淘汰算法)

思想:如果數據過去被訪問多次,那麼將來被訪問的頻率也更高。

實現:每個數據塊一個引用計數,所有數據塊按照引用計數排序,具有相同引用計數的數據塊則按照時間排序。每次淘汰隊尾數據塊。

開銷:排序開銷。

弊端:緩存顛簸。

7.3 LRU(最近最少使用替換算法)

思想:如果數據最近被訪問過,那麼將來被訪問的機率也更高。

實現:使用一個棧,新頁面或者命中的頁面則將該頁面移動到棧底,每次替換棧頂的緩存頁面。

優點:LRU算法對熱點數據命中率是很高的。

缺陷:

  • 1)緩存顛簸,當緩存(1,2,3)滿了,之後數據訪問(0,3,2,1,0,3,2,1。。。)。
  • 2)緩存污染,突然大量偶發性的數據訪問,會讓內存中存放大量冷數據。

7.4 LRU-K(LRU-2、LRU-3)

思想:最久未使用K次淘汰算法

LRU-K中的K代表最近使用的次數,因此LRU可以認爲是LRU-1。LRU-K的主要目的是爲了解決LRU算法“緩存污染”的問題,其核心思想是將“最近使用過1次”的判斷標準擴展爲“最近使用過K次”。

相比LRU,LRU-K需要多維護一個隊列,用於記錄所有緩存數據被訪問的歷史。只有當數據的訪問次數達到K次的時候,纔將數據放入緩存。當需要淘汰數據時,LRU-K會淘汰第K次訪問時間距當前時間最大的數據。

實現:

  • 1)數據第一次被訪問,加入到訪問歷史列表;
  • 2)如果數據在訪問歷史列表裏後沒有達到K次訪問,則按照一定規則(FIFO,LRU)淘汰;
  • 3)當訪問歷史隊列中的數據訪問次數達到K次後,將數據索引從歷史隊列刪除,將數據移到緩存隊列中,並緩存此數據,緩存隊列重新按照時間排序;
  • 4)緩存數據隊列中被再次訪問後,重新排序;
  • 5)需要淘汰數據時,淘汰緩存隊列中排在末尾的數據,即:淘汰“倒數第K次訪問離現在最久”的數據。

針對問題: LRU-K的主要目的是爲了解決LRU算法“緩存污染”的問題,其核心思想是將“最近使用過1次”的判斷標準擴展爲“最近使用過K次”。

7.5 2Q

類似LRU-2。使用一個FIFO隊列和一個LRU隊列。

實現:

  • 1)新訪問的數據插入到FIFO隊列;
  • 2)如果數據在FIFO隊列中一直沒有被再次訪問,則最終按照FIFO規則淘汰;
  • 3)如果數據在FIFO隊列中被再次訪問,則將數據移到LRU隊列頭部;
  • 4)如果數據在LRU隊列再次被訪問,則將數據移到LRU隊列頭部;
  • 5)LRU隊列淘汰末尾的數據。

針對問題:LRU的緩存污染
弊端: 當FIFO容量爲2時,訪問負載是:ABCABCABC會退化爲FIFO,用不到LRU。


第四部分 數據庫

這部分我面試的時候,問的較多且我掌握的也不多,我就不詳細總結寫了,我也在參考這幾篇學習。

數據庫常見面試題(附答案)

1. 索引的作用?和它的優點缺點是什麼?

索引就一種特殊的查詢表,數據庫的搜索可以利用它加速對數據的檢索。它很類似與現實生活中書的目錄,不需要查詢整本書內容就可以找到想要的數據。索引可以是唯一的,創建索引允許指定單個列或者是多個列。缺點是它減慢了數據錄入的速度,同時也增加了數據庫的尺寸大小。

2. 四個ACID基本性質

1.原子性:要麼都執行,要麼都不執行。
2.一致性:合法的數據纔可以被寫入。
3.隔離性:允許多個用戶併發訪問。
4.持久性:事務結束後,事務處理的結果必須得到固化。即一旦提交,對數據庫改變是永久的。

事務的四大特性是:

  1. 原子性(Atomicity):事務中所有操作是不可再分割的原子單位。事務中所有操作要麼全部執行成功,要麼全部執行失敗。
  2. 一致性(Consistency):事務執行後,數據庫狀態與其它業務規則保持一致。如轉賬業務,無論事務執行成功與否,參與轉賬的兩個賬號餘額之和應該是不變的。
  3. 隔離性(Isolation):隔離性是指在併發操作中,不同事務之間應該隔離開來,使每個併發中的事務不會相互干擾。
  4. 持久性(Durability):一旦事務提交成功,事務中所有的數據操作都必須被持久化到數據庫中,即使提交事務後,數據庫馬上崩潰,在數據庫重啓時,也必須能保證通過某種機制恢復數據。

二、在線編程題

推薦力扣劍指offer,需要電子版PDF的微信公衆號後臺回覆“劍指offer”獲取網盤鏈接。

1. 最長公共子串

題目描述:給出兩個字符串,求出這樣的一 個最長的公共子序列的長度:子序列 中的每個字符都能在兩個原串中找到, 而且每個字符的先後順序和原串中的 先後順序一致。

Sample Input  
abcfbc abfcab  
programming contest 
abcd mnp  
 
Sample Output  
4 
2 
0

最長公共子序列(POJ1458)

溫馨提示: 如果你點開這個鏈接,本題以下的內容你可以不看了,直接跳到擴展部分看“最長公共子序列(LCS問題)”。

算法思路:

  1. 把兩個字符串分別以行和列組成一個二維矩陣。

  2. 比較二維矩陣中每個點對應行列字符中否相等,相等的話值設置爲1,否則設置爲0。

  3. 通過查找出值爲1的最長對角線就能找到最長公共子串。

針對於上面的兩個字符串我們可以得到的二維矩陣如下:

在這裏插入圖片描述

從上圖可以看到,str1和str2共有5個公共子串,但最長的公共子串長度爲5。

爲了進一步優化算法的效率,我們可以再計算某個二維矩陣的值的時候順便計算出來當前最長的公共子串的長度,即某個二維矩陣元素的值由record[i][j]=1演變爲record[i][j]=1 +record[i-1][j-1],這樣就避免了後續查找對角線長度的操作了。修改後的二維矩陣如下:
在這裏插入圖片描述
遞推公式爲:

當A[i] != B[j],dp[i][j] = 0

當A[i] == B[j],

若i = 0 || j == 0,dp[i][j] = 1

否則 dp[i][j] = dp[i - 1][j - 1] + 1

實現源代碼:
暴力法:

string getLCS(string str1, string str2) {
	vector<vector<int> > record(str1.length(), vector<int>(str2.length()));
	int maxLen = 0, maxEnd = 0;
	for(int i=0; i<static_cast<int>(str1.length()); ++i)
		for (int j = 0; j < static_cast<int>(str2.length()); ++j) {
			if (str1[i] == str2[j]) {
				if (i == 0 || j == 0) {
					record[i][j] = 1;
				}
				else {
					record[i][j] = record[i - 1][j - 1] + 1;
				}
			}
			else {
				record[i][j] = 0;
			}
 
 
			if (record[i][j] > maxLen) {
				maxLen = record[i][j];
				maxEnd = i; //若記錄i,則最後獲取LCS時是取str1的子串
			}
		}
	return str1.substr(maxEnd - maxLen + 1, maxLen);
}

動態規劃法:

public int getLCS(String s, String t) {
        if (s == null || t == null) {
            return 0;
        }
        int result = 0;
        int sLength = s.length();
        int tLength = t.length();
        int[][] dp = new int[sLength][tLength];
        for (int i = 0; i < sLength; i++) {
            for (int k = 0; k < tLength; k++) {
                if (s.charAt(i) == t.charAt(k)) {
                    if (i == 0 || k == 0) {
                        dp[i][k] = 1;
                    } else {
                        dp[i][k] = dp[i - 1][k - 1] + 1;
                    }
                    result = Math.max(dp[i][k], result);
                } else {
                    dp[i][k] = 0;
                }
            }
        }
        return result;
    }

簡化一下遞推公式:

當A[i] != B[j],dp[i][j] = 0
否則 dp[i][j] = dp[i - 1][j - 1] + 1
全部都歸結爲一個公式即可,二維數組默認值爲0

行、列都多一行,更適應公式。

參考:最長公共子串(動態規劃)

擴充動態規劃經典例題——最長公共子序列和最長公共子串(python)

最長公共子序列(LCS問題)

題目描述:
給定兩個字符串A和B,長度分別爲m和n,要求找出他們最長的公共子序列,並返回其長度。例如:
A = “HelloWorld”;
B = “loop”

則A與B的最長公共子序列爲“loo”,返回的長度爲5.

動態規劃算法是面試時常考的內容,更多編程練習可參考如下幾個題目(附解析和答案)。

在這裏插入圖片描述
考慮到篇幅和冗餘,需要OneNote上的筆記的也可以私信我,我發給你。

2. 隨機生成[0.m]間n個正整數,不重複。時間複雜度是多少?

var nums = new int[100];
var random = new Random();
for (int i = 0; i < 100; i++)
{
    nums[i] = i;
}
for (int i = 0; i < 100; i++)
{
    var r = random.Next(i, 99);
    Swap(ref nums[i], ref nums[r]);
}

此題解析參考:

3. 數據庫:找出分數高於80分的學生名單

分析: 要查詢出每門課程都大於80分的學生姓名,因爲一個學生有多門課程,可能所有課程都大於80分,可能有些課程大於80分,另外一些課程少於80分,也可能所有課程都小於80分,那麼我們要查找出所有大於80分的課程的學生姓名,我們可以反向思考,找出課程小於80分(可以找出有一些課程小於80分,所有課程小於80分的學生)的學生姓名再排除這些學生剩餘的就是所有課程都大於80分的學生姓名了,

-- 查詢各科成績都大於90的學生姓名
id      name    course  score
1       小白      語文      91
2       小白      數學      88
3       小黑      語文      79
4       小黑      數學      92
5       小花      語文      99
6       小花      數學      95
7       小花      英語      96

實現源代碼:

--創建表aa
create table aa(
	name varchar(10),
	kecheng varchar(10),
	fengshu int
)
 
--插入數據到表aa中
insert into aa values('張三','語文',81)
insert into aa values('張三','數學',75)
insert into aa values('李四','語文',76)
insert into aa values('李四','數學',90)
insert into aa values('王五','語文',81)
insert into aa values('王五','數學',100)
insert into aa values('王五','英語',90)
 
 
--用一條SQL語句查詢出每門課都大於80分的學生姓名
select distinct name from aa where name not in (select distinct name from aa where fengshu<=80)

此題解析參考:

補充幾道j經典常考題目:
試題1: 8個試劑,其中一個有毒,最少多少隻小白鼠能檢測出有毒試劑
方法1:(二進制編碼)
用3只小鼠,能組合成8種狀態。

第一隻餵食【1、3、5、7】四隻試劑
第二隻餵食【2、3、6、7】四隻試劑
第三隻餵食【4、5、6、7】四隻試劑

  [3 2 1]
1  0 0 1 = 1  # 2、3沒死,1死了,說明第1支試劑有毒
2  0 1 0 = 2  # 1、3沒死,2死了,說明第2支試劑有毒
3  0 1 1 = 3  # 3沒死,1、2死了,說明第3支試劑有毒
4  1 0 0 = 4  # 1、2沒死,3死了,說明第4支試劑有毒
5  1 0 1 = 5  # 2沒死,1、3死了,說明第5值試劑有毒
6  1 1 0 = 6  # 1沒死,2、3死了,說明第6值試劑有毒
7  1 1 1 = 7  # 三隻都死了,說明第7值試劑有毒
8  0 0 0 = 0  # 三隻都沒死,說明第8值試劑有毒

方法2:(二分查找法)

二分法,每次把試劑分成兩堆,然後用兩隻小鼠測試,如果一隻死掉了,那麼就能確定哪一堆有毒。然後繼續分。因此,小鼠的數量就是試劑能被二分的次數。8只試劑能被二分3次,所以就需要3只小鼠。

同理,1000種藥劑要檢驗出有效的那一種,也至少需要10只。
這是一道典型的二分法查找的算法題,一般情況下,我們使用的都是串行的二分法,如果這道題沒有時間限制,我們就可以使用串行的二分法找到毒藥,步驟如下:

(1)首先,給試劑編號,1~1000 
(2)給第一隻小白鼠喂1~500號混合的試劑,等待24小時, 
(3)如果小白鼠死亡,則給第二隻喂1~250號混合的試劑,否則,喂501~750號試劑 
(4)依次進行二分,可以看出,這樣最多需要10只小白鼠就能找到毒藥。

但是,這道題有時間限制,所以我們要同時給一定的小白鼠喂藥,然後從小白鼠的死亡情況找出毒藥。步驟如下:
(1)第一隻小白鼠:1~500
(2)第二隻小白鼠:1~250 + 501~750
(3)第三隻小白鼠:1~125 + 251~500 + 501~625 + 751+875
……….
依次下去,由於2^9 < 1000 < 2^10,所以需要10只小白鼠才能找到毒藥。

試題2: 跳臺階問題

有n階臺階,你可以一次跳一階,也可以跳2階,請問有多少種跳法。

1 思路
首先我們考慮最簡單的情況。如果只有1級臺階,那麼顯然只一種跳法。如果有2級臺階,那就有兩種跳法:一種是分兩次跳,每次跳1級;另一種是一次跳2級。

接着,我們來討論一般情況。我們把n級臺階時的跳法看成是n的函數,記爲f(n)。當n>2時,第一次跳的時候就有兩種不同的選擇:一是第一次只跳1級,此時跳法數目等於後面剩下的n-1級臺階的跳法數目,即爲f(n-1);另外一種選擇是跳一次跳2級,此時跳法數目等於後面剩下的n-2級臺階的跳法數目,即爲f(n-2)。因此n級臺階的不同跳法的總數f(n)=f(n-1)+f(n-2)。分析到這裏,我們不難看出這實際上就是斐波那契數列了。

2 程序實現:
C++

class Solution {
public:
    int jumpFloor(int number) {
        if(number <= 0){
            return 0;
        }
        else if(number < 3){
            return number;
        }
        int first = 1, second = 2, third = 0;
        for(int i = 3; i <= number; i++){
            third = first + second;
            first = second;
            second = third;
        }
        return third;
    }

};

Python

# -*- coding:utf-8 -*-
class Solution:
    def jumpFloor(self, number):
        # write code here
        if number < 3:
            return number
        first, second, third = 1, 2, 0
        for i in range(3, number+1):
            third = first + second
            first = second
            second = third
        return third

3 舉一反三
在這裏插入圖片描述


關注微信公衆號:邁微電子研發社,獲取更多精彩內容,首發於個人公衆號。
在這裏插入圖片描述

△微信掃一掃關注「邁微電子研發社」公衆號

知識星球:社羣旨在秋招/春招準備攻略(含刷題)、面經和內推機會、學習路線、知識題庫等。

在這裏插入圖片描述

△掃碼加入「邁微電子研發社」學習輔導羣

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章