日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++校招常见面试题(2019年校招总结)

發布時間:2024/7/23 c/c++ 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++校招常见面试题(2019年校招总结) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

總結了語法、數據結構、常見排序算法、操作系統、網絡五大塊常見校招面試題。歡迎補充與修正。

★★語法知識★★

一、C++與C的區別

面向對象與面向過程的區別

面向過程

面向過程編程是就分析出解決問題題的步驟,然后把這些步驟一步一步的實現,使用的時候一個一個的一次調用就可以了。

面向對象

面向對象編程就是把問題分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描述某個市委在整個解決問題的步驟中的行為。

舉個例子(玩五子棋)

使用面向過程的思想來考慮就是:開始游戲,白棋先走、繪制畫面、輪到黑子、繪制畫面、判斷輸贏、重復之前的過程,輸出最終結果。

使用面向對象的思想來考慮就是:玩家系統、棋盤系統、判定系統、輸出系統。

面向對象就是高度的將實物抽象化,也就是功能的劃分,面向過程就是自頂向下編程,也就是步驟的劃分

具體語言的區別

1、關鍵字不同

C99有32個關鍵字

C++98有63個關鍵字

一些關鍵字細微的區別

1、struct:在C語言猴子那個struct定義的變量中不能由函數,在C++中可以有函數

2、malloc:malloc的返回值是void*,在C語言中可以賦值給任意類型的指針,在C++中必須要進行強制類型轉換,否則會報錯。

3、class和struct:class是對struct的擴展,struct的默認訪問權限是public,而class的默認訪問全顯示private

2、后綴名不同

C源文件的后綴是.c,C++源文件的后綴是.cpp,在VS中,如果在創建源文件的時候什么都不給,默認的就是.cpp

3、返回值不同

在C語言中,如果一個函數沒有指定返回值得類型,默認的返回值為int類型,并且會返回一個隨機數,一般為0xCCCCCCCC,C++中如果一個函數沒有返回值,則必須要指定為void,否則編譯不會通過。

4、參數列表不同

在C語言中,函數沒有指定參數列表的時候,默認可以接受多個參數,但是不支持無名參數,在C++中,因為嚴格的參數類型檢測,沒有參數列表的函數,默認為void,不接受任何參數,但是他支持無名參數。

5、缺省參數

缺省參數的聲明或定制函數時的參數指定一個默認值。在調用該函數時,如果沒有指定實參則可以采用該默認值,則使用指定的參數。但是這在C語言中是不支持的。

6、函數重載

函數重載是函數的一種特殊情況,指的是在同一作用域中,聲明幾個功能類似的同名函數,這些同名函數的形參列表必須不同,或者是在類中使用const修飾的函數和沒有使用const修飾的函數,常用來處理實現功能類似但是數據類型不同的問題。在C語言中沒有函數重載,是因為C語言對函數名的修飾只是在函數名前添加一個下劃線,但是C++對函數名的修飾會添加上該函數的返回值和參數列表。

7、標準輸入輸出

在C語言中使用的是scanf()和printf()來實現的,但是C++中是使用類來實現的。cin、cout對象,他們本身并不是C++語言的組成部分,在C++中不提供內在的輸入輸出運算符,這時與其他語言不相同的地方,他的輸入和輸出是通過C++中的類來實現的,cin和cout都是這些類的實例,是在C++語言的外部實現的。

8、動態內存管理

C語言使用的是malloc/free函數,C++在此基礎上還添加了new/delete兩個關鍵字。

9、const修飾的變量

C語言中const修飾的變量不可以用在定義數組時的大小,并且在定義的時候可以不設定初始值,但是在C++中修飾的變量在定義的時候必須要設定初始值,并且可以用在定義數組的大小,,如果不進行取地址或解引用的話,是存放在符號表中的,不開辟內存。

二、C++面向對象

面向對象的特點

維護性、復用性、擴展性。

封裝體現了維護性,按照信息屏蔽的原則,把對象的屬性和操作結合在一起,構成一個獨立的對象。通過限制對屬性和操作的訪問權限,可以將屬性隱藏在對象內部,對外提供一定的接口,在對象之外只能通過接口對對象進行操作,這樣增加了對象的獨立性,外部的對象不能直接操作對象的屬性,只能使用對象提供的服務,從而保證了數據的可靠性。

繼承體現了復用性,當定義了一個類后,又需要定義一個新類但是這個類與原來的類相比只是增加或修改了部分屬性和操作,這時可以引用原來的類派生出新的類,新類中只需要描述自己特有的屬性和操作,這樣就大大的簡化了對問題的描述,提高了程序的復用性。

多態體現了擴展性,多態就是一個接口多種實現,當需要添加新的模塊功能的時候,不需要改變原來的功能,只需要添加新的即可,這樣就實現了擴展性。

面向對象的優點

易于維護:可讀性比較高,如果有改變的需求,由于繼承的存在,維護也只是局部模塊,所以說維護起來是非常方便和較低成本的。

質量高:可重用現有的,在以前的項目的領域中一杯測試過的類使系統滿足業務需求并具有較高的質量。

效率高:在軟件開發時,根據設計的需要對現實事件的事務進行抽象,產生類。這樣結局問題的方法接近于日常生活和自然的思考方式,必定會提高軟件開發的效率和質量。

1、c語言是面向過程的結構化 語言,易于調試和維護。

2、表現能力和處理能力極強,可以直接訪問內存的物理地址。

3、C語言實現了對硬件的編程操作,也適合于引用軟件的開發。

概述

封裝可以使得代碼模塊化,繼承可以擴展已經存在的代碼,他們的目的是為了代碼重用。而多態的目的是為了接口重用。

封裝

封裝是設計類的一個基本原理,是將抽象得到的數據和行為相結合,形成一個有機的整體,也就是將數據與對數據進行的操作進行有機的結合,從而形成類,其中的數據和函數都是類的成員。

繼承

如果B是繼承了A,那么就把這個B稱為是A的子類,把A稱為B的父類。繼承可以使子類具有父類的各種屬性和方法和方法,就不用再次編寫相同的代碼。子類繼承父類的同時,可以重新定義某些屬性,并重定義其中的一些方法,也就是隱藏父類中原有的屬性和方法,使其獲得于父類不同的功能。

單繼承

單繼承就是一個派生類繼承一個基類。單繼承的繼承規則為:所有繼承下來的基類成員變量存放在派生類添加的成員變量之前,也就是基類的成員變量的內存地址低于派生類的內存地址,可以看做是將基類的內存空間進行了一次拷貝,并且在拷貝的內存空間后面加上派生類自己的成員。

多繼承

菱形繼承

菱形繼承存在的問題就是數據二義性,相應的解決方案就是虛擬繼承。多繼承的繼承規則是:以單繼承的方式按照父類聲明的順序繼承每個父類,可以看做是按照聲明的順序將每個父類的內存空間拷貝到一起,并且在后面添加上派生類自己的成員。

虛擬繼承

虛擬繼承是解決C++中多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷貝。這樣會存在兩個問題,一個是對于存儲空間的浪費,還有就是數據的二義性。虛擬繼承就是針對這兩個問題出現的,虛擬繼承的底層實現原理與編譯器相關,一般通過虛基類和虛基表實現,每個虛繼承的子類都有一個虛基類指針和虛基表,當虛擬繼承的子類被當做父類繼承時,虛基類指針也會被繼承。實際上vbptr指的是虛基類表指針,這個指針指向虛基類表,在虛基類表中記錄了虛基類與本類的偏移地址,通過偏移地址,可以找到虛基類的成員,虛擬繼承和虛函數有著相同之處,都是利用了虛指針和虛表,虛基類表中存儲的是虛基類相對于直接繼承類的便宜,而虛函數表中存儲的時候虛函數的地址。

繼承中的訪問權限

父類的私有成員在子類中無論以什么方式繼承都是不可見的,這個的不可見指的是private成員仍然被繼承到了子類中,但是在語法上限制子類對象不管實在類內還是在類外都是無法訪問它的。

子類以公有方式繼承父類時,父類的成員在子類中保持原有的屬性。

子類以保護方式繼承父類時,父類中的公有成員在子類中成了保護成員。

子類以私有方式繼承父類時,父類中所有成員在子類中都是私有的。

使用class時默認的繼承方式時私有的,使用struct時則默認的繼承方式是共有的。

還有一點就是友元是類級別的,不存在繼承的問題,也就是子類不能繼承父類的友元關系。

多態

多態可以簡單的概括為“一個接口,多種方法”,字面意思是多種形態。多態分為靜態多態和動態多態。

靜態多態

靜態多態也稱作靜態綁定或者是早綁定。地址的綁定是編譯器在編譯的時候完成的,編譯器根據函數實參的類型,可以推斷出要調用那個函數,這里可能會進行隱式的類型轉換,如果有對應的函數就調用了,否則編譯報錯。靜態多態又分為函數重載和泛型編程。

函數重載

函數重載是在相同的作用域中,只有函數的名稱相同,參數個數或參數類型不同。編譯器根據函數不同的參數表,對同名函數的名稱修飾,然后這些同名函數就成了不同的函數。這個在C語言中是不支持的,因為c語言中對函數的名稱修飾較為簡單,在VS2013編譯器中,c語言對函數名稱修飾的處理只關注到了函數名,對函數名的修飾只是簡單的在函數名前添加_,而c++語言除了函數名,還關注了函數的參數,對函數名的修飾時候要加上參數,通過對函數名稱的修飾不同,編譯器調用函數時所找的符號就不同。

泛型編程

泛型編程指的是編寫獨立于特定類型的代碼,泛型編程在C++中的主要是實現為函數模板和類模板。泛型編程的特性有如下幾點:

1、函數模板并不是真正的函數,他只是C++編譯器生成具體的函數的一個模子。

2、函數模板本身并不生成函數,實際生成的函數是替換函數模板的那個函數,這種替換在編譯期就綁定了。

3、函數模板不是只編譯一份滿足多重需要,而是為每一種替換他的函數編譯一份。

4、函數模板不允許自動類型轉換。

5、函數模板不可以設置默認模板參數。

動態多態

C++中的動態多態是基于虛函數的。對于相關的對象類型,確定他們之間的一個共同的功能集,然后在父類中把這些共同的功能聲明為多個公共的虛函數接口。各個子類重寫這些虛函數,完成具體的功能。操作函數通過指向基類的引用或指針來操作這些對象,對虛函數的調用會自動綁定到實際提供的子類對象上去。

虛函數

虛函數之所以叫做虛函數,是因為他的推遲聯編和動態聯編,一個類的虛函數的調用并不是在編譯的時候確定的,而是在運行的時候確定的,虛函數通過基類的指針或者引用指向派生類對象實現多態。

純虛函數

純虛函數指的是在基類中聲明的虛函數,但是沒有在基類中定義,要求在任何派生類中都要定義自己實現方法。如果一個類中有純虛函數,則這個類被稱為抽象類,由于這個類的構建并不完成,所以不能生成一個對象。繼承了抽象類的派生類必須要將純虛函數實現,否則同樣是抽象類,不能生成對象。

虛函數表

在C++中虛函數通話四通過虛函數表來實現的,這個表中存放的是虛函數的地址,他是屬于類的,不屬于某個具體的對象,在一個類中只有一個虛表,所有對象共享同一份虛表。為了指定對象的虛表,在對象構造的時候就在對象的內部包含了虛表指針_vfptr,一般是放在頭部。

關于虛函數表有兩種情況是要分清楚的,多繼承和多重繼承中的虛表是不一樣的。

多繼承指的是有一個子類繼承了兩個基類,比如說有A,B,C三個類,在A和B類中都有虛函數,C類依次繼承了A類和B類,這時候C類中的虛表就有了A類和B類兩個虛表,并且C類中的虛表指針是以A類虛表地址為基礎的,如果想要獲取到B類虛表的地址可以讓指針向后偏移A類的大小或者給出一個B類的指針指向C類對象發生一個天然的轉換,需要注意的是在C類中的重寫的虛函數會覆蓋A類和B類中的同名虛函數,如果C類中的虛函數在A類和B類中沒有,就添加到A類的虛函數表中,但是A類指針不可以調用,如果是只在A類或者B類中有的虛函數,在C類中沒有,那么只能是擁有虛函數的父類和C類可以調用。

多重繼承就是B類繼承了A類,C類繼承了B類,在B類中的重寫的虛函數會在虛函數表中覆蓋A類的同名虛函數,并將B類新添加的虛函數放在B類虛函數表的末尾,C類也是如此,C類的虛表是從B類中繼承的,在C類中的重寫的虛函數會在虛函數表中覆蓋B類的同名虛函數,并將C類新添加的虛函數放在C類虛函數表的末尾。

靜態多態多態的比較

靜態多態

優點

1、靜態多態通過模板編程為C++帶來了泛型設計的概念,比如STL。

2、靜態多態是在編譯期完成的,所以效率很高,編譯器可以對其進行優化。

缺點

由于模板是實現靜態多態,所以模板的不足也是靜態多態的劣勢,比如調試困難、編譯耗時、代碼膨脹。

動態多態

優點

1、實現與接口分離,可復用。

缺點

1、運行時綁定,導致一定程度上的運行時開銷。

2、編譯器無法對虛函數進行優化。

3、笨重的類繼承體系,對接口的修改影響整個類層次。

不同點

本質不同

早晚綁定,靜態多態是在編譯期決定的,由模板實現完成,而動態多態是在運行期間決定的,由繼承、虛函數實現。

接口方式不同

動態多態的接口是顯式的,以函數名為中心,通過虛函數在運行期間實現,靜態多態的接口是隱式的,以有效表達為中心,通過模板在編譯期間完成。

應用形式上

靜多態是發散式的,讓相同的實現代碼應用于不同的場合。

動多態是收斂式的,讓不同的實現代碼應用于相同的場合。

思維方式上

靜多態是泛型式編程風格,它看重的是算法的普適性。

動多態是對象式編程風格,它看重的是接口和實現的分離度。

相同點

夠可以實現多態性,靜態多態/編譯期多態,動態多態/運行期多態。

都可以是使接口和實現分離,一個是模板定義接口,類型參數定義實現,一個是基類定義接口,繼承類負責實現。

虛函數面試題

  • inliine函數可以實虛函數碼?
  • 不可以,因為inline函數沒有地址,無法將他存放到虛函數表中。

  • 靜態成員可以是虛函數嗎?
  • 不能,因為靜態成員函數中沒有this指針,使用::成員函數的嗲用用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

  • 構造函數可以是虛函數嗎?
  • 不可以,因為對象中的虛函數指針是在對象構造的時候初始化的。

  • 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
  • 可以,最好將析構函數設置為虛函數,因為這樣可以避免內存泄漏的問題,如果一個父類的指針指向了子類的的對象,如果子類對象中的虛函數沒有寫成多態的,他只會調用父類的析構函數,不會調用自己的析構函數,但是他創建對象的時候調用了構造函數,所以說就用子類的構造函數就應該該取調用他的析構函數,這樣才能保證所有的必須釋放的資源都是放了,才可以保證不會有內存泄漏。如果是多態的,就會先去調用子類的析構函數,然后再取調用父類的析構函數,這樣子類和父類的資源就都可以釋放。

  • 對象訪問普通函數快還是虛函數快?
  • 如果是普通對象,是一樣快的,如果是指針對象或者是引用對象,調用普通函數更快一些,因為構成了多態,運行時調用虛函數要先到虛函數表中去查找。這樣然后才拿到韓式的地址,這樣就不如直接可以拿到函數地址的普通函數快。

  • 虛函數表時再什么階段生成的?他存放在哪里?
  • 虛函數時再編譯階段生成的,他一般存放再代碼段,也就是常量區。

  • 是否可以將類中的所有成員函數都聲明稱為虛函數,為什么?
  • 虛函數是在程序運行的時候通過尋址操作才能確定真正要調用的的函數,而普通的成員函數在編譯的時候就已經確定了要調用的函數。這個兩者的區別,從效率上來說,虛函數的效率要低于普通成員函數,因為虛函數要先通過對象中的虛標指針拿到虛函數表的地址,然后再從虛函數表中找到對應的函數地址,最后根據函數地址去調用,而普通成員函數直接就可以拿到地址進行調用,所以沒必要將所有的成員函數聲明成虛函數。

  • 虛函數表指針被編譯器初始化的過程怎么理解的?
  • 當類中聲明了虛函數是,編譯器會在類中生成一個虛函數表VS中存放在代碼段,虛函數表實際上就是一個存放虛函數指針的指針數組,是由編譯器自動生成并維護的。虛表是屬于類的,不屬于某個具體的對象,一個類中只需要有一個虛表即可。同一個類中的所有對象使用同一個虛表,為了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在每個對象的頭添加了一個指針,用來指向虛表,并且這個指針的值會自動被設置成指向類的虛表,每一個virtaul函數的函數指針存放在虛表中,如果是單繼承,先將父類的虛表添加到子類的虛表中,然后子類再添加自己新增的虛函數指針,但是在VS編譯器中我們通常看不到新添加的虛函數指針,是編譯器故意將他們隱藏起來,如果是多繼承,在子類中新添加的虛函數指針會存放在第一個繼承父類的虛函數表中。

  • 多態的分類?
  • 靜態綁定的多態的是通過函數的重載來實現的。動態綁定的多態是通過虛函數實現的。

  • 為什么要引入抽象類和純虛函數?
  • 為了方便使用多態特性,在很多情況下由基類生成對象是很不合理的,純虛函數在基類中是沒有定義的,要求在子類必須加以實現,這種包含了純虛函數的基類被稱為抽象類,不能被實例化,如果子類沒有實現純虛函數,那么它他也是一個抽象類。

  • 虛函數和純虛函數有什么區別?
  • 從基類的角度出發,如果一個類中聲明了虛函數,這個函數是要在類中實現的,它的作用是為了能讓這個函數在他的子類中能被重寫,實現動態多態。純虛函數,只是一個接口,一個函數聲明,并沒有在聲明他的類中實現。對于子類來說它可以不重寫基類中的虛函數,但是他必須要將基類中的純虛函數實現。虛函數既繼承接口的同時也繼承了基類的實現,純虛函數關注的是接口的統一性,實現完全由子類來完成。

  • 什么是多態?他有什么作用?
  • 多態就是一個接口多種實現,多態是面向對象的三大特性之一。多態分為靜態多態和動態多態。靜態多態包含函數重載和泛型編程,進程多態是程序調用函數,編譯器決定使用哪個可執行的代碼塊。靜態多態是由繼承機制以及虛函實現的,通過指向派生類的基類指針或者引用,訪問派生類中同名重寫成員函數。墮胎的作用就是把不同子類對象都當作父類來看,可以屏蔽不同子類之間的差異,從而寫出通用的代碼,做出通用的編程,以適應需求的不斷變化。

    三、虛函數和純虛函數

    概述

    虛函數,它虛就虛在所謂的“推遲聯編”和“動態聯編”上,一個類虛函數的調用并不是在編譯時刻確定的,而是在運行的時候被確定。由于編寫代碼的時候并不能確定被調用的是基類函數還是那個派生類的函數,所以被稱為虛函數。

    虛函數只能借助指針或引用來達到多態的效果。常用的方式是基類指針或引用指向子類的對象。當有多個子類的繼承時,可以統一用父類指針來表示各子類對象,但事實上所指的對象具體是哪一個,或者調用的函數是哪個子類中的函數,要在運行的時候才知道,這就實現了多態。

    class parent {public:vritual void fun(){cout <<"parent::fun" << endl;} };class child : public parent {public:void fun(){cout << "child :: fun" << endl;} };int main() {parent *a = new child;a->fun(); //這里的指針雖然是parent類型,但是指向的是child的fun函數。構成了多態。return 0; }

    語法

    virtual void fun()=0; //純虛函數 virtual void fun(); //虛函數

    純虛函數

    純虛函數是指在基類中聲明的虛函數,并沒有在基類中定義,要求在任何派生類中都要定義自己的實現方法。在類中有純虛函數的類被稱為抽象類,由于他的構建并不完整,所以不能用抽象類來生成對象。繼承了抽象類的派生類必須要將純虛函數實現,否則同樣是抽象類,不能生成對象。

    虛函數表

    在C++中虛汗是是通過虛函數表來實現的(以下稱為虛表),在這張表中存放的是虛函數的地址,它是屬于類的,不屬于某個具體的對象,一個類中只有一個虛表,在同一個類中的所有對象都是用類中唯一這個虛表。為了指定對象的虛表,在對象構造的時候就在對象內部包含了虛表指針。為此編譯器在類中添加了一個*_vptr來指向虛表,并且這個指針的值會自動被設置為指向該類的虛表。在C++編譯器中,虛表指針在存放在每個對象的頭四個字節,并且虛函數表的末尾是以空指針結束。

    關于虛函數表有兩種情況是要分清楚的,多繼承和多重繼承這兩種繼承中的虛表是不一樣的。

    多繼承指的是有一個子類繼承了兩個基類,比如說有A,B,C三個類,在A和B類中都有虛函數,C類依次繼承了A類和B類,這時候C類中的虛表就有了A類和B類兩個虛表,并且C類中的虛表指針是以A類虛表地址為基礎的,如果想要獲取到B類虛表的地址可以讓指針向后偏移A類的大小或者給出一個B類的指針指向C類對象發生一個天然的轉換,需要注意的是在C類中的重寫的虛函數會覆蓋A類和B類中的同名虛函數,如果C類中的虛函數在A類和B類中沒有,就添加到A類的虛函數表中,但是A類指針不可以調用,如果是只在A類或者B類中有的虛函數,在C類中沒有,那么只能是擁有虛函數的父類和C類可以調用。

    多重繼承就是B類繼承了A類,C類繼承了B類,在B類中的重寫的虛函數會在虛函數表中覆蓋A類的同名虛函數,并將B類新添加的虛函數放在B類虛函數表的末尾,C類也是如此,C類的虛表是從B類中繼承的,在C類中的重寫的虛函數會在虛函數表中覆蓋B類的同名虛函數,并將C類新添加的虛函數放在C類虛函數表的末尾。

    虛函數面試題

  • inliine函數可以實虛函數碼?
  • 不可以,因為inline函數沒有地址,無法將他存放到虛函數表中。

  • 靜態成員可以是虛函數嗎?
  • 不能,因為靜態成員函數中沒有this指針,使用::成員函數的嗲用用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

  • 構造函數可以是虛函數嗎?
  • 不可以,因為對象中的虛函數指針是在對象構造的時候初始化的。

  • 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
  • 可以,最好將析構函數設置為虛函數,因為這樣可以避免內存泄漏的問題,如果一個父類的指針指向了子類的的對象,如果子類對象中的虛函數沒有寫成多態的,他只會調用父類的析構函數,不會調用自己的析構函數,但是他創建對象的時候調用了構造函數,所以說就用子類的構造函數就應該該取調用他的析構函數,這樣才能保證所有的必須釋放的資源都是放了,才可以保證不會有內存泄漏。如果是多態的,就會先去調用子類的析構函數,然后再取調用父類的析構函數,這樣子類和父類的資源就都可以釋放。

  • 對象訪問普通函數快還是虛函數快?
  • 如果是普通對象,是一樣快的,如果是指針對象或者是引用對象,調用普通函數更快一些,因為構成了多態,運行時調用虛函數要先到虛函數表中去查找。這樣然后才拿到韓式的地址,這樣就不如直接可以拿到函數地址的普通函數快。

  • 虛函數表時再什么階段生成的?他存放在哪里?
  • 虛函數時再編譯階段生成的,他一般存放再代碼段,也就是常量區。

  • 是否可以將類中的所有成員函數都聲明稱為虛函數,為什么?
  • 虛函數是在程序運行的時候通過尋址操作才能確定真正要調用的的函數,而普通的成員函數在編譯的時候就已經確定了要調用的函數。這個兩者的區別,從效率上來說,虛函數的效率要低于普通成員函數,因為虛函數要先通過對象中的虛標指針拿到虛函數表的地址,然后再從虛函數表中找到對應的函數地址,最后根據函數地址去調用,而普通成員函數直接就可以拿到地址進行調用,所以沒必要將所有的成員函數聲明成虛函數。

  • 虛函數表指針被編譯器初始化的過程怎么理解的?
  • 當類中聲明了虛函數是,編譯器會在類中生成一個虛函數表VS中存放在代碼段,虛函數表實際上就是一個存放虛函數指針的指針數組,是由編譯器自動生成并維護的。虛表是屬于類的,不屬于某個具體的對象,一個類中只需要有一個虛表即可。同一個類中的所有對象使用同一個虛表,為了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在每個對象的頭添加了一個指針,用來指向虛表,并且這個指針的值會自動被設置成指向類的虛表,每一個virtaul函數的函數指針存放在虛表中,如果是單繼承,先將父類的虛表添加到子類的虛表中,然后子類再添加自己新增的虛函數指針,但是在VS編譯器中我們通常看不到新添加的虛函數指針,是編譯器故意將他們隱藏起來,如果是多繼承,在子類中新添加的虛函數指針會存放在第一個繼承父類的虛函數表中。

  • 多態的分類?
  • 靜態綁定的多態的是通過函數的重載來實現的。動態綁定的多態是通過虛函數實現的。

  • 為什么要引入抽象類和純虛函數?
  • 為了方便使用多態特性,在很多情況下由基類生成對象是很不合理的,純虛函數在基類中是沒有定義的,要求在子類必須加以實現,這種包含了純虛函數的基類被稱為抽象類,不能被實例化,如果子類沒有實現純虛函數,那么它他也是一個抽象類。

  • 虛函數和純虛函數有什么區別?
  • 從基類的角度出發,如果一個類中聲明了虛函數,這個函數是要在類中實現的,它的作用是為了能讓這個函數在他的子類中能被重寫,實現動態多態。純虛函數,只是一個接口,一個函數聲明,并沒有在聲明他的類中實現。對于子類來說它可以不重寫基類中的虛函數,但是他必須要將基類中的純虛函數實現。虛函數既繼承接口的同時也繼承了基類的實現,純虛函數關注的是接口的統一性,實現完全由子類來完成。

  • 什么是多態?他有什么作用?
  • 多態就是一個接口多種實現,多態是面向對象的三大特性之一。多態分為靜態多態和動態多態。靜態多態包含函數重載和泛型編程,進程多態是程序調用函數,編譯器決定使用哪個可執行的代碼塊。靜態多態是由繼承機制以及虛函實現的,通過指向派生類的基類指針或者引用,訪問派生類中同名重寫成員函數。墮胎的作用就是把不同子類對象都當作父類來看,可以屏蔽不同子類之間的差異,從而寫出通用的代碼,做出通用的編程,以適應需求的不斷變化。

    四、引用和指針的區別

    指針

    對于一個類型T,T* 就是一個指向T的指針類型,也就是一個T*類型的變量能夠保存一個T對象的地址,而類型T可以添加一些限定詞,如const、volatile等等。

    volatile提醒編譯器它后面所定義的變量隨時有可能改變。精確地說就是,遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再優化,從而可以提供對特殊地址的穩定訪問;如果不使用volatile,則編譯器對所聲明的語句進行優化。

    <1>中斷服務程序中修改的供其他程序檢測的變量需要加volatile;

    <2>多任務環境下各任務間共享的標志應該加volatile;

    <3>多存儲器映射的硬件寄存器通常也要加volatile,因為每次對它的讀寫都可能有不同意義。

    需要注意的是:頻繁地使用volatile很可能會增加代碼尺寸和降低代碼性能,因此要合理地使用volatile。

    引用

    引用就是一個對象的別名,主要用于函數參數和返回值類型,類型+&+變量名 = 被引用的對象。

    不同點

    1、引用是某塊內存的別名,而指針指向的是一塊內存,他的內容是所指內存的地址。

    2、引用在創建時必須被初始化,但是指針可以為空,在C語言中NULL,在C++中是nullptr。所以指針在使用的時候必須要判空,引用就沒有必要。

    3、引用不可以改變指向,一旦初始化了,就不能改變。指針可以改變自己的指向,可以指向其他對象。

    4、對于引用來說,const int &a 和 int& const a沒有區別,因為都表示指向的對象是常量。對于指針來說,const int *p 說明p是指向常量的指針, int * const p 說明p本身就是一個常量。

    5、引用的大小是指向對象的大小,而指針在32位機上是四字節,在64位機上是8字節,是指針本身的大小。

    6、對引用++操作就是對引用指向的對象進行++操作,但是對指針++操作,表示的是地址的變化,向后移動一個指針的大小。

    7、指針傳遞和引用傳遞

    ? 指針傳遞傳遞的是地址,在函數中定義了一個局部的指針變量,存放的是實參的地址,消耗了內存,可以對地址進行加減操作,指向另一個變量,由于傳遞的是地址,所以不需要返回值,因為實際修改的就是實參的值。

    ? 引用傳遞同樣是地址,但是不會在函數中消耗內存,直接對地址進行使用,對函數中的引用變量的加減操作直接影響外部的實參,并且不能指向另一個變量。在實際使用中傳遞引用的時候如果不希望實參被改變,通常要用const將其修飾。

    五、深拷貝和淺拷貝

    ? 淺拷貝指的是將原始對象中的數據型字段拷貝到新對象中,將引用型對象的引用賦值到新對象中去,不把引用的對象復制進去,所以原始對象和新對象引用同一對象,新對象中的引用型字段發生變化會導致原始對象中對應的字段發生變化。

    ? 深拷貝是在引用方面不同,深拷貝就是重新創建一個新的和原始字段內容相同的字段,所以兩者的引用是不同的。其中一個對象發生變化并不會影響另一個對象。

    編譯系統會在我們自己沒有定義拷貝構造的時候調用默認的拷貝構造函數,進行淺拷貝,也就是兩個對象使用同一份資源。當兩個對象調用析構函數的時候,就會析構兩次,導致內存泄漏。所以對含有指針成員的對象或類中存在資源的對象進行拷貝的時候,必須要自己定義拷貝構造函數,實現深拷貝,避免內存泄漏。

    六、常見關鍵字的作用

    static

    static有靜態局部變量、靜態全局變量和靜態方法三種使用方法,他們的共同點就是在本文件中聲明的靜態變量和靜態方法是不能被其他文件所使用的,和對應的extern關鍵字,extern關鍵字聲明的全局變量和函數在整個工程中都是可以被使用的。

    全局變量

    有static聲明的全局變量,只能在函數體外部被定義,并且只能在本文件中有效,這點就是區別于普通的全局變量,普通的全局變量在其他的文件中也是可見的。在函數體重可以定義同名的局部變量,這時會隱藏這個靜態的,如果要使用靜態的全局變量,需要在變量名前添加::作用域運算符。

    局部變量

    static局部變量同樣只能在本文件中使用,靜態局部變量的生命周期不隨著函數的結束而結束,只能在第一調用函數的時候回他進行初始化,之后調用就會跳過初始化,他會在函數結束之后在內存中保存當前的結果,而不會像普通的局部變量在清棧的時候銷毀,在內存中他區別與局部變量的是局部變量每次調用函數時分配的內存空間可能是不一樣的,但是靜態局部變量具有全局唯一性的特點,每次調用使用的時候用的都是同一塊內存空間,但是這也造成了一個不可重入的問題。(現在有兩個進程A、B都要去調用這個函數fun(),如果是A先調用函數fun,在運行函數的時候突然失去了運行權,但是已經將局部變量修改成了自己要試用的值,由于使用的是同一塊內存空間,進程B調用函數的時候也將局部變量修改成了自己要使用的值,當進程A需要繼續執行的時候,由于這塊內存空間中的值已經被修改了,所有進程A就得不到自己想要的結果)。

    方法

    static數據成員和成員函數

    在C++中繼承了C語言中的static這個關鍵字,并且在類中給了第三種定義方法,表示只屬于一類而不是屬于類的某個對象的變量和函數。這個和普通的成員最大的區別就是在類中是唯一的,并且在內存中只有一份,普通成員函數調用的時候需要傳入this指針,但是靜態成員函數調用的時候是沒有this指針的,只能在調用的時候使用類名加作用域來調用。在設計多線程操作的時候,有POSIX庫下的線程函數要求是全局的,所以普通的成員函數是無法直接作為線程函數的,但是靜態的成員函數是可以做線程函數的。

    static函數和普通函數

    普通函數的定義和聲明默認是extern的,在同一個工程中的其他文件中是可見的,如果在另一個文件中定義了相同的函數就會穿線重定義錯誤,當然這個重定義和繼承中的 重定義是不一樣的,這里的重定義指定的命名沖突。靜態函數在內存中只有一份,但是普通的函數在每個被調用中都會維護一份拷貝。

    extern

    extern置于變量或函數前,用于標示變量或函數的定制在別的文件中,提示編譯器遇到這個變量或函數要在其他的模塊中查找。

    extern “C”

    如果是extern“C” void fun(int a, int b);這樣是高數編譯器在編譯fun這個函數的時候要按照C的規則去編譯,而不是按照C++的,這一點主要是與C++支持重載,C語言不支持重載和函數被C++編譯器編譯后在苦衷的名字與C語言的不同有關。

    當extern不與“C”在一起修飾變量或者函數時,比如extern int a;他的作用就是聲明函數或者全局變量的作用范圍和關鍵字,其生命的函數和變量可以在本工程中的所有文件中使用。需要注意的是他只是一個聲明,并不是定義。

    const

    使用const修飾類的成員變量的時候,必須要在初始化列表進行初始化,并且引用類型的成員變量和沒有默認默認構造函數的對象成員也必須要在初始化列表進行初始化,如果有繼承的關系,如果父類沒有默認的構造函數,也必須要在初始化列表進行初始化,初始化列表對數據成員的初始化順序時按照數據成員的聲明順序嚴格執行的。

    const修飾成員函數的時候,一般是放在成員函數的最后面,修飾的類的成員函數中隱藏的this指針,代表不可以通過this指針修改類的數據成員,這個使用方法也可以與普通的相同的成員函數構成重載。

    關于const還有一個問題就是傳參和賦值的問題,一般來說使用const修飾的變量是安全的,沒有使用const修飾的變量是不安全的,在傳參的時候可以讓非const修飾的變量傳給const修飾的,但是const修飾的變量不可以傳給非const修飾的形參,這就相當于將安全的變量交給了不安全的變量。

    volatile

    volatile一般用來修飾變量,他的存在是因為我們的程序在進行編譯的時候編譯器會進行一系列的優化,比如說某個變量被修飾為const,編譯器就會認為這個值是只讀的,就會在寄存器中保存這個變量的值,每次需要的時候直接從寄存器中讀取,但是有的時候會在不經意間修改了這個變量的值,那么編譯器是并不知道的,還是從寄存器中進行讀取,這樣就會造成結果不匹配。但是如果使用volatile聲明后,就是相當與告訴編譯器這個變量隨時會給變,需要每次都要從內存中讀取,不需要優化,從而避免了這個問題,volatile的應用場景最多的是多線程對

    define、const、inline區別

    define作用域程序的預處理節點,而預處理主要的工作是宏替換、去注釋以及條件編譯,而define起作用的地方就在宏替換階段,只是單純的將宏替換為代碼。但是define只是單純的代碼替換,不會進行類型的檢查,很容易出錯。在C++中建議使用const、枚舉定義常量,這樣就會有類型檢查。于是C++中有提供了一個inline關鍵字,可以實現和define相同的功能,并且支持類型檢查和調試,一般生命在函數的定義前面,但是inline只是對編譯器的一種建議,一般建議代碼為3-5航左右,并且沒有復雜的邏輯結構,例如循環、遞歸之類的。

    七、malloc/free和new/delete的區別

    1、malloc是從堆上開辟空間,而new是從自由存儲區開辟空間。自由存儲區是C++抽象出來的概念,不僅可以是堆,還可以是靜態存儲區。

    2、malloc是函數,而new是關鍵字。

    3、malloc對開辟的空間大小需要嚴格的指定,而new只需要對象名。

    4、malloc開辟的空間既可以給單個對象使用也可以給數組使用,釋放的方式都是free();而new開辟對象數組需要使用new[size],釋放是使用delete[]。

    5、malloc成功的返回值是void*,需要用戶進行強轉,申請空間失敗會返回NULL,所以在使用的時候需要進行判空處理,new成功返回的是對象指針,不需要強轉,失敗拋出異常,但是為了最大程度的兼容C,C++的new也支持失敗返回NULL,但是一般不使用。

    6、new不僅負責開辟空間,還會去調用對象的構造函數和析構函數。

    7、new申請空間的效率要低于malloc,因為new的底層是通過malloc實現的。

    八、類中的成員函數占空間嗎?怎么調用?

    類中的普通成員函數和靜態成員函數是不占用類的內存的,只有在類中函數有虛函數的時候才會在類中添加一個虛函數指針,增加一個指針的大小。類中的成員函數實際上與普通的全局函數一眼,只不過是在編譯的時候在成員函數中國添加了一個指向當前對象的this指針,成員函數的地址是全局已知的,所以對象的內存空間中是沒有必要去保存成員函數的地址的。在編譯的時候就已經綁定了,類的屬性值得是類中的數據成員,他們是實例化一個對象的時候就為數據成員分配了內存,但是成員函數是所有對象多公有的。

    但是空類的大小是1個字節,這是為了保證兩個不同對象的地址不同。類的實例化是在內存中分配一塊地址,每個勢力在內存中歐擁有獨一無二的地址。同樣的,空類也會實例化,所以編譯器會給類隱含的添加一個字節,這樣空類實例化后就有了獨一無二的地址了。在一個空類中,在第一次實例化對象的時候就創建了默認的成員函數,并且這個成員函數是public和inline的。

    九、NULL和nullptr的區別

    傳統意義上來說,c++把NULL、0視為同一種東西,有些編譯器將NULL定義為 ((void*)0),有些將其定義為0 c++不允許直接將void隱式的轉化為其他類型,但是如果NULL被定義為 ((void*)0),當編譯char *p = NULL,NULL只好被定義為0。 還有:void func(int);void func(char*); 如果NULL被定義為0,func(NULL)會去調用void func(int),這是不合理的 所以引入nullptr,專門用來區分0、NULL。nullptr的類型為nullptr_t,能夠隱式的轉換為任何指針。所以用空指針就盡可能的使用nullptr。

    十、智能指針

    什么是智能指針?

    智能指針最主要的作用就是管理一個指針,因為有可能申請的空間在函數結束的時候沒有及時的釋放,從而造成的了內存泄漏。使用智能指針是要是針對這個問題,因為只能指針是一個類,在他生命周期結束的時候回去調用析構函數進行資源的釋放,不需要進行手動的釋放資源。智能指針一共有auto_ptr、unique_ptr、shard_ptr、week_ptr四種。

    auto_ptr:

    是C++98提出來的智能指針,它采用的是所有權模式。就是將智能指針A賦值給智能指針B,就是將A的所有權交給了B,如果再想訪問A就會出錯,auto_ptr的缺點就是存在潛在的內存崩潰問題。

    unique_ptr

    他解決了auto_ptr的問題,從名字可以知道他是獨有的智能指針,也就是說他不能進行賦值和拷貝操作,在unique_ptr中將拷貝構造函數和賦值運算符重載私有,并且將其刪除。

    shared_ptr

    shared_ptr實現了共享的概念,多個智能指針可以指向同一份資源。他的原理是多個智能指針共同維護一份引用計數,當有第二個進行賦值和拷貝構造的時候回將引用計數+1,每當一個智能指針使用完需要調用析構函數的時候就會檢查是不是最后一個使用資源的,如果不是不進行釋放,否則要進行資源釋放。但是在使用的時候會出現這么一個情況,就是相互引用產生的死鎖問題,那么這兩個智能指針的引用計數將永遠不會下降為0,也就是資源不會得到釋放。這時就需要week_ptr了。

    week_ptr

    week_ptr是一種不控制對象生命周期的智能指針,他指向一個sheard_ptr管理的對象,進行該對象的內存管理的是那個強引用的shared_ptr。week_ptr只是提供了對管理對象的一各訪問的手段,他只能從一個shared_ptr或另一個weak_ptr對象構造,他的構造函數和析構函數是不會引起引用計數的改變。weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那么這兩個指針的引用計數永遠不可能下降為0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr。week_ptr只能是協助shared_ptr,他不能直接指向對象。

    十一、默認構造函數

    默認構造函數我認為是在調用時不需要顯式地傳入實參的構造函數。如果定義一個對象時沒有使用初始化式,編譯器就會使用默認的構造函數。編譯器生成默認構造函數的情況用一句話可以進行概括,就是唯有默認構造函數被編譯器需要的時候,編譯器才會生成默認的構造函數。關于這個被編譯器需要分為幾種情況:當該類的類對象數據成員有默認構造函數時,當類的基類有默認的構造函數時、當該類的基類為虛基類時、當該類有虛函數時這四種情況。

    默認構造函數中有這么幾點需要注意:

    1、不能讓無參的默認構造函數和帶缺省的默認構造函數同時存在,這樣就會讓編譯器產生二義性,從而生成編譯錯誤。

    2、在使用無參默認構造函數的時候不能再對象名后面加括號,否則會產生警告,讓編譯器誤認為是要聲明一個函數,但是又沒有找到該函數的定義,所以就產生了警告。

    十二、前置++和后置++區別

    1、從使用的角度來說:c++是先使用,后自增,++c是先自增,后使用

    2、從系統的角度來說:c++表示取c的地址,把他放入寄存器中,然后增加內存中的c,使用寄存器中的值,對于++c表示取c的地址,自增后把他放入寄存器中使用,很顯然++c的效率要比c++高,因為c++會生成一個臨時變量。

    3、如果不需要返回自增后之前的值,那么c++和++c的效果是一樣的,但是要優先使用++c,效率高。

    十三、lambda表達式

    [函數對象參數](操作符重載函數參數)mutable或exception聲明 ->返回值類型{函數體}

    函數對象參數

    空:沒有使用任何的函數對象參數。 = :函數體內可以使用lambda所在作用域范圍內所有可見的局部變量,并且是值傳遞。 & :函數體內可以使用lambda所在作用域范圍內所有可見的局部變量,并且是引用傳遞。 a :將a按值傳遞,但是a的默認是const的,可以添加mutable修飾后變為普通的。 &a:將a按引用傳遞。 a,&b:將a按值傳遞,b按引用傳遞。 =,&a,&b:除了a和b,其他參數均按值傳遞。 &,a,b:除了a和b,其他參數均按引用傳遞。 this:函數體重可以使用lambda所在類中的所有成員變量

    操作符重載函數參數

    表示重載的()操作符的參數,沒有參數的時候這個可以省略。

    mutable或exception聲明

    按值傳遞函數對象參數時,加上mutable修飾可以修改值傳遞進來的拷貝。exception聲明用于指定函數拋出的異常,如拋出整形的異常可以使用throw進行捕獲。

    ->返回值類型

    標識函數返回值的類型,當返回值為void或者函數同種只有一處return的地方,這部分可以省略。

    {函數體}

    表示函數的實現,這部分不可以省略,但是函數體可以為空。

    十四、什么是右值?

    左值和右值是編譯器和程序中經常出現的詞匯,在C++中被廣泛認同的說法就是可以取地址的、有名字的就是左值,沒有地址的,不能取取名字的就是右值。

    在C++11中,右值由純右值和將亡值組成。

    純右值,用于辨識臨時變量和一個不跟對象有關聯的值,比如說非引用返回的函數返回值,運算表達式、不跟對象關聯的字面量值(true、false、常量等)、類型轉換函數的返回值,lambda表達式。

    將亡值,是C++11新增的跟右值引用相關的表達式,這樣表達式通常是將要被移動的對象,比如說返回右值引用T&&的函數的返回值,move庫函數的返回值,轉換為T&&的類型轉換函數的返回值。

    除了純右值和將亡值剩余的所有值都是左值。

    十五、右值引用

    右值引用:

    C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,沒有名字的就是右值。而在指C++11中,右值是由兩個概念構成,將亡值和純右值。純右值是用于識別臨時變量和一些不跟對象關聯的值,比如1+3產生的臨時變量值,2、true等,而將亡值通常是指具有轉移語義的對象,比如返回右值引用T&&的函數返回值等。

    C++11中,右值引用就是對一個右值進行引用的類型。由于右值通常不具有名字,所以我們一般只能通過右值表達式獲得其引用,比如:T && a=ReturnRvale();

    假設ReturnRvalue()函數返回一個右值,那么上述語句聲明了一個名為a的右值引用,其值等于ReturnRvalue函數返回的臨時變量的值。基于右值引用可以實現轉移語義和完美轉發新特性。

    十六、 說一說c++中四種cast轉換

    C++中四種類型轉換是:static_cast, dynamic_cast, const_cast, reinterpret_cast

    1、const_cast

    用于將const變量轉為非const

    2、static_cast

    用于各種隱式轉換,比如非const轉const,void*轉指針等, static_cast能用于多態向上轉化,如果向下轉能成功但是不安全,結果未知;

    3、dynamic_cast

    用于動態類型轉換。只能用于含有虛函數的類,用于類層次間的向上和向下轉化。只能轉指針或引用。向下轉化時,如果是非法的對于指針返回NULL,對于引用拋異常。要深入了解內部轉換的原理。

    向上轉換:指的是子類向基類的轉換

    向下轉換:指的是基類向子類的轉換

    它通過判斷在執行到該語句的時候變量的運行時類型和要轉換的類型是否相同來判斷是否能夠進行向下轉換。

    4、reinterpret_cast

    幾乎什么都可以轉,比如將int轉指針,可能會出問題,盡量少用;

    5、為什么不使用C的強制轉換?

    C的強制轉換表面上看起來功能強大什么都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。

    十七、數組和指針的區別

    1、指針中保存的是數據的地址,而數組中保存的是數據。

    2、指針是間接訪問數據,要訪問一個數據要先獲得指針的內容,然后通過這個地址提取出數據,而數組是直接訪問數據。

    3、指針通常用于動態的數據結構,鏈表、鏈式二叉樹等,數組通常用語固定數目并且數據類型相同的數據結構,順序表等。

    4、指針通過malloc或new申請內存,并且使用完要進行釋放,數組則是隱式的分配和刪除。

    十八、函數指針與指針函數

    十九、請你來說一下一個C++源文件從文本到可執行文件經歷的過程?

    對于C++源文件,從文本到可執行文件一般需要四個過程:

    預處理階段:對源代碼文件中文件包含關系(頭文件)、預編譯語句(宏定義)進行分析和替換,生成預編譯文件。

    編譯階段:將經過預處理后的預編譯文件轉換成特定匯編代碼,生成匯編文件后綴為.s

    匯編階段:將編譯階段生成的匯編文件轉化成機器碼,生成可重定位目標文件.o

    鏈接階段:將多個目標文件及所需要的庫連接成最終的可執行目標文件.exe

    二十、內存泄漏

    內存泄漏(memory leak)是指由于疏忽或錯誤造成了程序未能釋放掉不再使用的內存的情況。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費。

    內存泄漏的分類:

    1、堆內存泄漏 (Heap leak)。對內存指的是程序運行中根據需要分配通過malloc,realloc new等從堆中分配的一塊內存,再是完成后必須通過調用對應的 free或者delete 刪掉。如果程序的設計的錯誤導致這部分內存沒有被釋放,那么此后這塊內存將不會被使用,就會產生Heap Leak.

    2、系統資源泄露(Resource Leak)。主要指程序使用系統分配的資源比如 Bitmap,handle ,SOCKET等沒有使用相應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能降低,系統運行不穩定。

    3、沒有將基類的析構函數定義為虛函數。當基類指針指向子類對象時,如果基類的析構函數不是virtual,那么子類的析構函數將不會被調用,子類的資源沒有正確是釋放,因此造成內存泄露。

    二十一、C++對象模型

    ★★數據結構★★

    一、STL總結

    什么是STL

    STL是一套高效的C++程序庫,采用泛型編程的思想對常見的數據結構和算法進程封裝,里面處處體現著泛型編程的設計思想以及設計模式,現在已經被集成到了C++標準庫中。STL里面包含了容器、適配器、空間配置器、迭代器、仿函數和算法。

    六大組件

    容器:就是各種數據結構,根據底層的實現由分為序列式容器和關聯式容器。

    適配器:是一種用來修飾容器或者仿函數或迭代器接口的東西。比如queue和stack。

    空間配置器:負責空間配置與管理。從實現角度看,配置器是一個實現了動態空間配置、空間管理、空間釋放額class template。

    迭代器:扮演容器與算法之間的膠合劑,是所謂的“泛型指針”。

    算法:各種常見算法,如sort,search,copy,erase等。

    仿函數:行為類函數,可作為算法的某種策略,從實現角度看,仿函數是一種重載了operator()的class或class template。一般函數指針可視為狹義的仿函數。

    他們之間的關系:空間配置器給容器分配存儲空間,算法通過迭代器獲取容器中的內容,仿函數可以協助算法完成各種操作,配接器用來套接適配仿函數

    容器

    其中容器分為序列容器和關聯式容器兩大類,序列容器包括靜態數組array、動態數組vector、動態二維數組deque、帶頭結點的循環單鏈表forward_list、帶頭結點的雙向循環鏈表list、字符串string。關聯式容器根據地層的實現紅黑樹實現和哈希表實現兩大類,關聯式容器中的兩大類主要區別于底層的實現,紅黑樹和哈希表,

    紅黑樹是一種二叉搜索樹,在每個節點上都增加了一個存儲位表示結點的顏色,可以紅色或者是黑色,通過對任何一條從根到葉子結點的路徑上各個節點的顏色限制,確保沒有一條路徑會比其他路徑長出2倍,所以是接近平衡的,是比較穩定的二叉搜索樹,由于二叉搜索樹的任意根節點總是大于它左子樹的所有節點,小于他的右子樹的所有節點,所以在查找的時候可以采用類似于二分查找的思想,快速找到某個節點,紅黑樹是一個平衡二叉樹,他的查找時間復雜度也是logN。

    哈希表是根據關鍵碼值直接進行訪問的數據結構。通過關鍵碼值可以直接映射到哈希表中的一個位置來訪問對應的值,以加快查找的速度,查找的時間復雜度是O(1)。哈希表主要解決的是哈希函數和哈希沖突,哈希函數也叫散列函數,要根據不同的輸入值得到一個固定長度的消息摘要。理想的哈希函數要對不同的輸入產生不同的不同的輸出結果,同時還要滿足同一性和雪崩效應。哈希沖突就是不同的關鍵碼值通過哈希函數計算出相同的哈希地址。

    解決哈希沖突的常見方法有閉散列和開散列兩種,閉散列采用的是線性探測法,當發生哈希沖突時,如果哈希表沒有被裝滿,就說明還可以將關鍵碼值存放到下一個空位置,然后從沖突位置一次向后探測,直到空位置為止,閉散列可以緩解哈希沖突,但是不能徹底解決。開散列使用的是拉鏈法,先將關鍵碼通過哈希函數計算得到相應的哈希地址,然后將相同的哈希地址的關鍵碼放在同一個子集合中,這個子集合通常稱之為桶,每個桶中的元素通過單鏈表的方式連接起來,然后將各個鏈表的頭結點存放在哈希表中。

    但是對于哈希表來說通的個數是固定,如果某個桶中的元素超過了8個,就要將這個桶中的單鏈表轉換為紅黑樹,如果桶中的元素小于6個的時候要將紅黑樹結構再次轉換為單鏈表。這個轉換是由于桶中的元素是由鏈表保存的,鏈表的查找時間復雜度是O(N),而紅黑樹的查找時間復雜度是logN,但是當鏈表中的長度很小的時候,查找也是很快的,當鏈表不斷變長了他的查找性能就會降低,需要轉換成紅黑樹。一開始不將桶的結構初始化為樹是因為一個紅黑樹的節點占用的空間是單鏈表節點的二倍,為了時間和空間的權衡只有當鏈表長度到8才進行單鏈表向紅黑樹的轉換。但一個哈希表的離散性很好的情況下,將單鏈表轉換為紅黑樹的概率很小,因為數據均勻分布在每個哈希桶中,幾乎不會有哈希桶中的鏈表長度達到8,理想情況下哈希表中的所有哈希桶中節點分布頻率會遵循泊松分布,長度為8的概率是6*10^-8,幾乎是不可能的。

    hash和紅黑都是性能非常高的兩個數據結構,但是他們還是有區別的,hash的查找速度比紅黑樹快,并且查找速度基本上是和數據量無關的,屬于常數級別,但是紅黑樹的查找速度是logN的,但是hash還有hash函數,并且它的構造速度也是比較慢的,并且還需要實現分配足夠的內存存儲散列表,并且hash是無序的。紅黑樹所需要的內存較小,只需要為節點分配內存,并且紅黑樹中的節點是有序,它的查找時間復雜度是LogN,但是不能說就比常數大。基于這兩個數據結構各自的特點和性能,STL將關聯式容器分成了基于紅黑樹的map、set、multimap、multiset和基于hash的unordered_map、unordered_map、unordered_set、unordered_multimap、unordered_multiset。

    vector

    vector是線性容器,他的元素嚴格按照線性序列排序和動態數組很相識,他和數組一樣,所有的元素存儲在一塊連續的存儲空間中,這也意味著我們不僅可以使用迭代器訪問元素,還可以使用指針偏移的方式訪問,和常規數組不一樣的是,vector能夠自動存儲元素,可以自動增長和縮小存儲空間。和數組相比,雖然vector在在自動處理容量的大小時會消耗更多的內存,但是容器可以提供和數組一樣的性能,而且可以很好的調整存儲空間的大小。相比其他的序列容器,能更有效地訪問容器中的元素和在末尾添加和刪除元素,在其他位置添加和刪除元素就不如其他序列容器了,并且在迭代器當面也不如list支持的好。

    vector的迭代器,無論在迭代器位置進行增加元素還是刪除元素都會導致所有的迭代器失效,因為vector中刪除和添加元素后可能會改變容器的大小,所以要更新所有的迭代器,關于vector的空間這里需要注意的是size和capacity這兩個成員函數的區別,size指的是當前容器中擁有元素的個數,而capacity指的是當前容器可以存放的元素個數。vector在分配空間這塊他會分配一些額外的空間來適應可能的增長。不同的庫采用的不同的策略權衡空間的使用和重新分配,在vs中PJ版本的STL是以1.5倍的方式進行擴容的,在gcc中的SGI版本的STL是按2倍的方式進行擴容的。

    list

    list是可以在常數范圍內在任意位置進行插入和刪除的序列式容器,并且該容器可以向前向后雙向迭代。list的地層是一個雙向鏈表結構,雙向鏈表中每個元素存儲在互不相關的獨立節點中,在節點中通過指針指向其前一個元素和后一個元素。他與forward_list非常相似,最主要的區別就是forward_list是單鏈表,只能向后迭代,而list是雙向鏈表,可以向前迭代也可以向后迭代。與其他容器相比較,list和forward_list最大的缺陷就是不支持在任意位置的隨機訪問,比如:要訪問list的第6個元素,必須從頭部或者尾部迭代到該位置,這段位置上的迭代需要線性的時間開銷,list還需要開辟一些額外的空間,以保存每個節點相關聯的信息。

    list中也存在迭代器失效的問題,因為list的底層結構是雙向循環鏈表,所以在list中插入時不會導致list的迭代器失效,只有在刪除的時候才會導致迭代器失效,但是失效的僅是被刪除節點的迭代器,其他的迭代器并不會受到影響。

    vector和list的區別

    底層結構

    vector是動態的順序表,在內存中是一段連續的空間,list使用的是帶頭結點的雙向循環鏈表。

    隨機訪問

    vector支持隨機訪問,訪問某個元素的時間復雜度是O(1),list不支持隨機訪問,訪問某個元素的時間復雜度是O(n)

    插入和刪除

    vector在任意位置的插入和刪除的效率較低,需要搬移元素,時間復雜度為O(N),插入的時候還有可能會增容,增容操作中有開辟新空間、拷貝元素、釋放舊空間,導致效率更低。list在任意位置插入和刪除效率比較高,不需要搬移元素,只需要更改上下兩個節點的指針指向,時間復雜度是O(1)。

    空間利用率

    vector的底層為一段連續的空間,不容易造成內存碎片,空間利用率高,緩存利用率高。list底層節點動態開辟,容易造成內存碎片,空間利用率低,緩存利用率低。

    迭代器

    vector使用的是原生態的指針,list是將原生態的指針進行了封裝。vector在插入元素是要給所有的迭代器重新賦值,因為插入元素有可能會導致擴容,致使原來的迭代器會失效,并且刪除元素的時候也會對迭代器進行重新賦值,同樣也會使原來的迭代器失效。對弈list來說,他的底層是雙向循環鏈表,插入元素時并不會導致迭代器失效,刪除元素是只會導致當前的迭代器失效,其他的迭代器并不受影響。

    使用場景

    vector適用于需要高效存儲,需要隨機訪問,并且不關心插入和刪除的效率。list適用于大量的插入和刪除操作,不需要隨機訪問,也不關心存儲的場景。

    set

    set是按照一定次序進行存儲的容器。set中元素只有一個value,并且每個value必須是唯一的,set中的元素不能再容器中進行修改,元素總是const的,但是可以容容器中對他們進行刪除或者插入。并且元素總是按照其內部的比較對象所指是的特定的嚴格弱排序標準進行排序的,set訪問元素的速度要比unordered_set慢,但是它允許直接迭代,并且是有序的,set的底層實現是紅黑樹。他與map/multimap不同的是set中只存放的value,但是他的底層存放的是由構成的鍵值對,由于set中不允許有重復的元素,所以可以使用set去重,

    map

    map是關聯式容器,他按照特定的次序來存儲由建值key和值value組合而成的元素。在map中,建值key通常用于排序和唯一標識元素,而值value中存儲的是與建值key相關聯的內容。建值key和值value的類型可能不同,并且在map中,key與value通過成員類型value_type綁定在一起。在map中,元素總是按照建值key進行比較排序,并且他訪問某個元素時通常要比unordered_map慢,但是map中的元素直接進行迭代是有序的。他的底層實現是紅黑樹結構。map與multimap唯一的不同就是map中的key值是唯一的,但是multimap中的key是可以重復的。set于multiset的區別就是multiset中的元素是可以重復的。

    list、set、map的區別

    從結構上來說

    list和set是存儲的單列數據的集合,map中存儲的是鍵值對這樣的雙列數據的集合。list的底層結構是雙向循環列表,set和map的底層結構是紅黑樹。

    從數據上來說

    list中存儲的數據是有順序的,并且是允許有重復的,查找的時間復雜度是O(N),每個節點只有一個值value;map中的存儲的數據是無序的,他的鍵值是不允許重復的,但是他的值是允許重復的,并且他的鍵值是有序的,查找時間復雜度是logN,每個節點是由鍵值key,值value構成的鍵值對,set中存儲的數據是有序的,不允許重復,查找時間復雜度是logN,每個節點是值value,但是他的底層節點是value,value的鍵值對。

    STL面試題

    一、STL常用的容器有哪些以及各自的特點是什么?

    1.vector:底層數據結構為數組 ,支持快速隨機訪問。

    2.list:底層數據結構為雙向鏈表,支持快速增刪。

    3.stack:底層一般用順序表和鏈表實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時

    4.queue:底層一般用順序表和鏈表實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時(stack和queue其實是適配器,而不叫容器,因為是對容器的再封裝)

    5.set:底層數據結構為紅黑樹,有序,不重復。

    6.multiset:底層數據結構為紅黑樹,有序,可重復。

    7.map:底層數據結構為紅黑樹,有序,不重復。

    8.multimap:底層數據結構為紅黑樹,有序,可重復。

    9.hash_set:底層數據結構為hash表,無序,不重復。

    10.hash_multiset:底層數據結構為hash表,無序,可重復 。

    11.hash_map :底層數據結構為hash表,無序,不重復。

    12.hash_multimap:底層數據結構為hash表,無序,可重復。

    二、什么情況下用vector,什么情況下用list。

    vector可以隨機存儲元素(即可以通過公式直接計算出元素地址,而不需要挨個查找),但在非尾部插入刪除數據時,效率很低,適合對象簡單,對象數量變化不大,隨機訪問頻繁。

    list不支持隨機存儲,適用于對象大,對象數量變化頻繁,插入和刪除頻繁。

    三、什么時候需要用hash_map,什么時候需要用map?

    總體來說,hash_map 查找速度會比map快,而且查找速度基本和數據數據量大小,屬于常數級別;而map的查找速度是log(n)級別。并不一定常數就比log(n)小,hash還有hash函數的耗時,明白了吧,如果你考慮效率,特別是在元素達到一定數量級時,考慮考慮hash_map。但若你對內存使用特別嚴格,希望程序盡可能少消耗內存,那么一定要小心,hash_map可能會讓你陷入尷尬,特別是當你的hash_map對象特別多時,你就更無法控制了,而且hash_map的構造速度較慢。

    四、請你來說一說STL迭代器刪除元素

    這個主要考察的是迭代器失效的問題。1.對于序列容器vector,deque來說,使用erase(itertor)后,后邊的每個元素的迭代器都會失效,但是后邊每個元素都會往前移動一個位置,但是erase會返回下一個有效的迭代器;2.對于關聯容器map set來說,使用了erase(iterator)后,當前元素的迭代器失效,但是其結構是紅黑樹,刪除當前元素的,不會影響到下一個元素的迭代器,所以在調用erase之前,記錄下一個元素的迭代器即可。3.對于list來說,它使用了不連續分配的內存,并且它的erase方法也會返回下一個有效的iterator,因此上面兩種正確的方法都可以使用。

    五、請你來說一下STL中迭代器的作用,有指針為何還要迭代器

    1、迭代器

    Iterator(迭代器)模式又稱Cursor(游標)模式,用于提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示。或者這樣說可能更容易理解:Iterator模式是運用于聚合對象的一種模式,通過運用該模式,使得我們可以在不知道對象內部表示的情況下,按照一定順序(由iterator提供的方法)訪問聚合對象中的各個元素。

    由于Iterator模式的以上特性:與聚合對象耦合,在一定程度上限制了它的廣泛運用,一般僅用于底層聚合支持類,如STL的list、vector、stack等容器類及ostream_iterator等擴展iterator。

    2、迭代器和指針的區別

    迭代器不是指針,是類模板,表現的像指針。他只是模擬了指針的一些功能,通過重載了指針的一些操作符,->、*、++、–等。迭代器封裝了指針,是一個“可遍歷STL( Standard Template Library)容器內全部或部分元素”的對象, 本質是封裝了原生指針,是指針概念的一種提升(lift),提供了比指針更高級的行為,相當于一種智能指針,他可以根據不同類型的數據結構來實現不同的++,–等操作。

    迭代器返回的是對象引用而不是對象的值,所以cout只能輸出迭代器使用*取值后的值而不能直接輸出其自身。

    3、迭代器產生原因

    Iterator類的訪問方式就是把不同集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。

  • 說說你所知道的容器都有哪些?
  • map與set的區別?使用map有哪些優勢?
  • map的底層實現,說下紅黑樹?
  • map的迭代器會失效嗎?什么情況下回失效?
  • AVLTree和RBTree的對比,為什么map和set使用了紅黑樹?紅黑樹的優勢是什么?
  • AVLTree和RBTree所達到的平衡有什么區別?
  • RBTree節點的顏色是紅或者黑色?其他顏色行不行?
  • RBTree是如何插入?如何旋轉的?
  • 二、哈希

    哈希表是根據關鍵碼值而直接進行訪問的數據結構。通過關鍵碼值映射到表中的一個位置來訪問記錄,以加快查找的速度。哈希表主要是解決兩個問題,哈希函數和哈希沖突。

    哈希函數

    哈希函數也叫散列函數,他對不同的輸出值得到一個固定長度的消息摘要。理想的哈希函數對不同的輸入要產生不同的結構,同時散列結果也應當具有同一性和雪崩效應(微小的輸入值發生的變化使得輸出值發生巨大的變化)。

    哈希沖突

    哈希沖突就是不同的關鍵字通過相同的哈希函數計算出了相同的哈希地址。解決哈希沖突最常見的方法是閉散列和開散列,閉散列使用的是線性探測法,當發生哈希沖突時,如果哈希表沒有被裝滿,說明可以將key值保存到下一個空位置中,從沖突發生的位置開始,依次向后探測,直到找到下一個空位置為止。開散列使用的是拉鏈法,先對關鍵碼使用哈希函數計算哈希地址,具有相同的哈希地址的關鍵碼歸于同一個子集,稱子集合為一個桶,每個桶中的元素通過一個單鏈表連接起來,然后將各個鏈表的頭結點存放在哈希表中。由于哈希表中桶的個數是固定的,如果某個桶中的元素非常多,這樣會影響真個哈希表的性能。一般來說如果桶中的元素過多,會將單鏈表的存儲方式換成紅黑樹,或者是對哈希表進行增容。

    十、二叉樹的先序遍歷、中序遍歷和后序遍歷

    //二叉樹的遞歸遍歷

    void PreOrde(BTree* root)
    {
    if(root)
    {
    cout << root->data << " ";
    PreOrde(root->left);
    PreOrde(root->right);
    }
    }

    void InOrde(BTree* root)
    {
    if(root)
    {
    PreOrde(root->left);
    cout << root->data << " ";
    PreOrde(root->right);
    }
    }

    void PostOrde(BTree* root)
    {
    if(root)
    {
    PreOrde(root->left);
    PreOrde(root->right);
    cout << root->data << " ";
    }
    }

    void PreOrde1(BTree* root)
    {
    stack<BTree*> s;
    BTree* cur = root;

    while(!s.empty() || cur) {while(cur){cout << cur->data << " ";s.push(cur);cur = cur->left;}if(!s.empty()){BTree* tmp = s.top();s.pop();cur = tmp->right;} }

    }

    void InOrde1(BTree* root)
    {
    stack<BTree*> s;
    BTree* cur = root;

    while(!s.empty() || cur) {while(cur){s.push(cur);cur = cur->left;}if(!s.empty()){BTree* tmp = s.top();cout << tmp->data << " ";s.pop();cur = tmp->right;} }

    }

    void PasrOrde1(BTree* root)
    {
    stack<BTree*> s;
    stack flag;
    BTree* cur = root;

    while(!s.empty() || cur) {while(cur){s.push(cur);flag.push(false);cur = cur->left;}if(!s.empty()){if(flag.top()){cout << s.top()->data << " ";s.pop();flag.pop();}else{cur = s.top()->right;flag.top() = true;}} }

    }

    三、鏈表環

    判斷是否有環

    定義一個快指針和一個慢指針,快指針一次走兩步,慢指針一次走兩步,會出現兩種情況,情況一指針走到了空的位置,那就說明這個鏈表不帶環。情況二兩個指針相遇,說明這個鏈表帶環。

    獲得入環節點

    如果不考慮空間復雜度,可以使用一個map來記錄走過的節點,這個指針一直向后遍歷如果遇到空,說明這個鏈表不帶環,也就沒有入環節點,如果沒有遇到空,如果遇到第一個在map中存在的節點,就說明回到了出發點,這個節點就是環的入口節點。

    如果不建立額外的空間,先使用快慢指針判斷這個鏈表是否有環,如果有環將相遇節點記錄,然后一個指針從鏈表的起始位置開始一次走一步,另一個指針從記錄的節點開始一次走一步,當兩個節點再次相遇,這個相遇節點就是環的入口節點。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7uf3JqSR-1637763958108)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1565675547490.png)]

    四、棧和隊列

    兩個棧模擬實現隊列

    隊列的特性就是只能在隊尾進行插入操作,在隊頭進行刪除操作,兩個棧實現一個隊列,一個棧s1負責入隊列,另一個棧s2負責出隊列,當刪除隊列的時候,如果s2中有元素,直接取棧頂元素,如果s2是空棧,將s1中的所有元素搬移到s2中,然后再取棧頂元素。

    兩個隊列模擬實現棧

    棧的特性就是只能在一端進行插入和刪除操作,用兩個隊列一個用來進行入棧操作,另一個進行刪除的時候才會用到,將有元素的棧的size-1個元素全部搬移到另一個空隊列中,然后將最后一個元素刪除,就完成了刪除操作。

    五、hashmap和map的區別?

    map是STL中的一個關聯式容器,它提供一對一的K-V的數據處理能力,由于這個特性,在我們需要完成Key-Value數據處理的時候可以很方便的調用。map的底層結構是紅黑樹,這棵樹對數據有自動排序的功能,所以map中的數據都是有序的,并且查找的時間復雜度基本是LogN。他的特點是增加和刪除節點對迭代器的影響很小,只對操作的節點有影響,但是對于迭代器來說,可以修改節點對應的V值,不能修改K值。

    HashMap是基于哈希表的Map,它具有著map的特性。當我們將K值傳遞給put()方法時,它調用對象的hashCode()方法來計算hashcode,然后找到對應的位置來存放value。hashmap使用開散列的方法來解決哈希沖突。他是線程不安全的,hashmap中的初始容量和裝填因子會影響他的性能。

    1、他們的底層實現不同,map使用的是紅黑樹來實現,Hashmap使用的哈希表來實現。

    2、他們的查找時間復雜度不同,map的時間復雜度是log(n),hashmap的時間復雜度O(1)。

    3、map不允許有NULL值,但是hashmap允許有NULL。

    ★★排序算法★★

    一、冒泡排序

    ??冒泡排序時通過無序區中相鄰記錄的關鍵字間的比較和位置的交換,使關鍵字最小的元素如氣泡似的逐步上浮直水面。有序區逐漸擴大,無序區逐漸縮小。
    ??冒泡排序算法的原理如下:

  • 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  • 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最后一對。在這一點,最后的元素應該會是最大的數。
  • 針對所有的元素重復以上的步驟,除了最后一個。
  • 持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較
  • 動畫演示

    冒泡排序是一種非常容易理解的排序
    時間復雜度:O(N^2)
    空間復雜度:O(1)
    穩定性:穩定

    普通冒泡

    void bubble0(vector<int>& arr){ int size = arr.size(); for(int i = 0; i < size; ++i) { for(int j = 0; j < size-i-1; ++j) { if(arr[j] > arr[j+1]) { swap(arr[j+1], arr[j]); } } }}

    優化一
    ??第一種優化就是在交換的地方加一個標記,如果某一趟排序沒有交換元素,說明這組數據已經有序,不用再繼續下去。這樣對部分連續有序而整體無序的數據大大提升了效率。

    void bubble1(vector<int>& arr){ int size = arr.size(); for(int i = 0; i < size; ++i) { bool flag = true; for(int j = 0; j < size-i-1; ++j) { if(arr[j] > arr[j+1]) { flag = false; swap(arr[j+1], arr[j]); } } if(flag) return; }}

    優化二
    ??對于優化一來說,僅僅對部分連續有序而整體無序的數據效率高一些,如果這組數據時前面無序,但是后面的部分有序,效率就不是那么高了。因此可以在優化一的基礎上記下最后一次交換的位置,這個位置后沒有交換,必然是有序的,然后下一次排序從第一個比較到上次記錄的位置結束即可。

    void bubble2(vector<int>& arr){ int size = arr.size() -1; int pos = size; for(int i = 0; i < size; ++i) { bool flag = true; int k = 0; for(int j = 0; j < pos; ++j) { if(arr[j+1] < arr[j]) { flag = false; swap(arr[j+1], arr[j]); k = j; } } if(flag) return; pos = k; }}

    優化三
    ??冒泡算法經過優化二后效率有很大的提升,但是效率還可以繼續優化,就是在一趟排序中確定兩個最值,也就是一個正向查找最大值,另一個反向查找最小值

    void bubble3(vector<int>& arr){ int size = arr.size() -1; int pos = size; int pos1 = 0; for(int i = 0; i < size; ++i) { bool flag = true; int k = 0; //正向冒最大值 for(int j = 0; j < pos; ++j) { if(arr[j+1] < arr[j]) { flag = false; swap(arr[j+1], arr[j]); k = j; } } if(flag) return; pos = k; //反向冒最小值 int n = pos1; for(int j = k; j > pos1; --j) { if(arr[j-1] > arr[j]) { swap(arr[j-1], arr[j]); n = j-1; flag = false; } } if(flag) return; pos1 = n; }}

    二、選擇排序

    ??選擇排序是一種簡單直觀的排序算法。
    ??選擇排序原理

  • 初始時設第一個元素為最大值,并記錄其下標為maxpos
  • 從剩余未排序元素中繼續尋找最大元素,如果當前元素比下標為maxpos的元素大,將maxpos更新到當前位置
  • 一次遍歷后,將下標為maxpos的元素與最后一個元素交換位置
  • 以此類推,直到整個數組有序。
  • ??注意:選擇排序與冒泡排序是區別的,冒泡排序通過依次交換相鄰兩個順序不合法的元素位置,從而將當前最大元素放到合適的位置,而選擇排序每遍歷一次都記住了當前最大元素的位置,最后僅需一次交換操作即可將其放到合適的位置。

    直接選擇排序思考非常好理解,但是效率不是很好。實際中很少使用
    時間復雜度:O(N^2)
    空間復雜度:O(1)
    穩定性:不穩定

    void slectSort(vector<int>& arr){ int size = arr.size(); for (int i = 0; i < size - 1; ++i) { int maxPos = 0; for (int j = 1; j < size - i; ++j) { if (arr[j] > arr[maxPos]) maxPos = j; } if (maxPos != size-i-1) swap(arr[maxPos], arr[size - i-1]); }}

    優化
    ??選擇排序的優化就是在原來的一次只標記一個最值,優化為一個標記兩個最值,這樣效率可以提升原來的一半。

    void SlectSort(vector<int>& arr){ int size = arr.size(); int begin = 0; int end = size - 1; while (begin < end) { int minPos = begin; int maxPos = begin; int index = begin + 1; while (index <= end) { if (arr[minPos] > arr[index]) minPos = index; if (arr[maxPos] < arr[index]) maxPos = index; ++index; } if (maxPos != end) swap(arr[maxPos], arr[end]); //這段小代碼在在下面介紹其作用 if (minPos == end) minPos = maxPos; if (minPos != begin) swap(arr[begin], arr[minPos]); ++begin; --end; }}

    ??上面代碼標記的一小段代碼是為了防止minpos在最大值要插入的位置。比如序列(2,15,4,1)這時候的maxpos為1,minpos為3,調整過最大值后,序列就成了(2,1,4,15),這時候的minpos還是3,如果直接進行最小值交換,就恢復到之前的位置了,所以要加上這段判斷代碼。這樣如果最小值在最大值要交換的位置,最大值交換后要將最小值的位置更新到maxpos的位置。

    三、插入排序

    ??直接插入排序是一種簡單的插入排序法,其基本思想是:把待排序的記錄按其關鍵碼值的大小逐個插入到一個已經排好序的有序序列中,直到所有的記錄插入完為止,得到一個新的有序序列 。實際中我們玩撲克牌時,就用了插入排序的思想
    ??當插入第i(i>=1)個元素時,前面的arr[0],arr[1],…,arr[i-1]已經排好 序,此時用arr[i]與arr[i-1],arr[i-2]進行比較,找到插入位置即將array[i]插入,原來位置上的元素順序后移。
    元素集合越接近有序,直接插入排序算法的時間效率越高,其時間效率在O(n)與O(n^2)之間。直接插入排序的空間復雜度為O(1),它是一種穩定的排序算法。

    元素集合越接近有序,直接插入排序算法的時間效率越高
    時間復雜度:O(N^2)
    空間復雜度:O(1),它是一種穩定的排序算法
    穩定性:穩定

    void InsertSort(vector<int>& arr){ int size = arr.size(); for (int i = 1; i < size; ++i) { //待插入元素 int key = arr[i]; //找插入位置 int end = i - 1; while (end >= 0 && arr[end] > key) { //向后搬移數據 arr[end + 1] = arr[end]; end--; } //開始插入 arr[end + 1] = key; }}

    優化
    ??普通的插入排序就是不斷的依次將元素插入前面已排好序的序列中。由于前半部分為已排好序的數列,這樣我們不用按順序依次尋找插入點,可以采用折半查找的方法來加快尋找插入點的速度。
    ??折半插入排序算法是一種穩定的排序算法,比普通插入算法明顯減少了關鍵字之間比較的次數,因此速度比直接插入排序算法快,但記錄移動的次數沒有變,所以折半插入排序算法的時間復雜度仍然為O(n^2),與直接插入排序算法相同。

    void InsertSort1(vector<int>& arr){ int size = arr.size(); for (int i = 1; i < size; ++i) { //待插入元素 int key = arr[i]; //找插入位置 int right = i - 1; int left = 0; if(arr[right] > key) { while (left <= right ) { int mid = left+((right-left)>>1); if(arr[mid] < key) left = mid+1; else right = mid-1; } } //開始搬移數據 int end = i -1; while(end >= left) { arr[end+1] = arr[end]; --end; } //開始插入數據 arr[end + 1] = key; }}

    四、希爾排序

    ??希爾排序法又稱縮小增量法。希爾排序法的基本思想是:先選定一個整數,把待排序文件中所有記錄分成個組,所有距離為的記錄分在同一組內,并對每一組內的記錄進行排序。然后,重復上述分組和排序的工作。當到達=1時,所有記錄在統一組內排好序。

    希爾排序是對直接插入排序的優化。
    當gap > 1時都是預排序,目的是讓數組更接近于有序。當gap == 1時,數組已經接近有序的了,這樣就會很快。這樣整體而言,可以達到優化的效果。我們實現后可以進行性能測試的對比。
    希爾排序的時間復雜度不好計算,需要進行推導,推導出來平均時間復雜度: O(N^1.3 - N^2)
    穩定性:不穩定

    void ShellSort(vector<int>& arr){ int size = arr.size(); int gap = size; while (gap > 0) { gap = gap / 3 + 1; for (int i = gap; i < size; ++i) { //待插入元素 int key = arr[i]; //找插入位置 int end = i - gap; while (end >= 0 && arr[end] > key) { arr[end + gap] = arr[end]; end -= gap; } //開始插入 arr[end + gap] = key; } if(gap == 1) break; }}

    五、快速排序

    ??快速排序快速排序是Hoare于1962年提出的一種二叉樹結構的交換排序方法,其基本思想為:任取待排序元素序列中的某元素作為基準值,按照該排序碼將待排序集合分割成兩子序列,左子序列中所有元素均小于基準值,右子序列中所有元素均大于基準值,然后最左右子序列重復該過程,直到所有元素都排列在相應位置上為止。

    快速排序整體的綜合性能和使用場景都是比較好的,所以才敢叫快速排序
    時間復雜度:平均O(N*logN) 最壞:O(n^2)【每次的關鍵值都是最大值或者最小值】

    空間復雜度:O(logN)
    穩定性:不穩定

    普通快排:
    普通快排就是將當前序列的最后一個值作為標志,然后開始分割序列。

    int Partion(vector<int>& arr, int left, int right){ int key = arr[right-1]; int begin = 0; int end = right-1; while (begin < end) { while (begin < end && arr[begin] <= key) begin++; while (begin < end && arr[end] >= key) end--; if (begin != end) swap(arr[begin], arr[end]); } if (begin != right - 1) swap(arr[begin], arr[right - 1]); return begin;}

    優化一:挖坑法
    ??挖坑法的原理就是在左邊找到不符合條件的元素時,將這個元素放在end處,這時候begin位置就成了一個“坑”,在右邊找到不符合條件的元素時,將這個元素放到begin位置,將之前的“坑”填好,以此類推,最后將標志key保存的值放在begin位置,將最后一個“坑”填滿。

    int Partion1(vector<int>& arr, int left, int right){ int end = right - 1; int begin = left; int key = arr[right - 1]; while (begin < end) { while (begin < end && arr[begin] <= key) begin++; if (begin < end) { arr[end] = arr[begin]; end--; } while (begin < end && arr[end] >= key) end--; if (begin < end) { arr[begin] = arr[end]; begin++; } } arr[begin] = key; return begin;}//齊頭并進法int Partion2(vector<int>& arr, int left, int right){ int key = arr[right - 1]; int cur = left; int pre = cur - 1; while (cur < right) { if (arr[cur] < key && ++pre != cur) { swap(arr[pre], arr[cur]); } cur++; } if (++pre != right-1) swap(arr[pre], arr[right - 1]); return pre;}//三數取中法int MidNum(vector<int>& arr,int left, int right){ int mid = left + ((right - left) >> 1); if (arr[left] > arr[right]) { if (arr[mid] > arr[left]) mid = left; if (arr[mid] < arr[right]) mid = right; } else { if (arr[mid] < arr[left]) mid = left; if (arr[mid] > arr[right]) mid = right; } return mid;}int Partion3(vector<int>& arr, int left, int right){ int end = right - 1; int begin = left; int mid = MidNum(arr, left, end); swap(arr[mid], arr[right - 1]); int key = arr[right - 1]; while (begin < end) { while (begin < end && arr[begin] <= key) begin++; if (begin < end) { arr[end] = arr[begin]; end--; } while (begin < end && arr[end] >= key) end--; if (begin < end) { arr[begin] = arr[end]; begin++; } } arr[begin] = key; return begin;}void QuickSort(vector<int>& arr, int left, int right){ if (right - left > 1) { int key = Partion3(arr, left, right); QuickSort(arr, left, key); QuickSort(arr, key + 1, right); }}//非遞歸快排void QuickSortNor(vector<int>& arr){ int right = arr.size(); int left = 0; stack<int> s; s.push(right); s.push(left); while (!s.empty()) { left = s.top(); s.pop(); right = s.top(); s.pop(); if (right - left > 1) { int key = Partion3(arr, left, right); //保存右值 s.push(right); s.push(key + 1); //保存左值 s.push(key); s.push(left); } }}

    六、堆排序

    ??堆排序就是利用堆(堆的詳細介紹)這種數據結構進行排序的算法,堆排序屬于選擇排序

    時間復雜度:O(nlogn)
    空間復雜度:O(1)
    穩定性:不穩定
    堆排序的步驟為:
    1、基于所給元素創建一個大堆
    2、使用堆刪除的思想從最后一個結點向前調整

    typedef struct Heap{ HPData* _array; int _size; int _capacity;}Heap;void HeapAdjust(HPData* array, int size, int root){ int child = root * 2 + 1; while (child < size) { if (child + 1 < size && array[child] < array[child + 1]) child += 1; if (array[child] > array[root]) { swap(array[child], array[root]); root = child; child = root * 2 + 1; } else return; }}void HeapSort(HPData* array, int size){ //建大堆 //找倒數第一個非葉子節點 int root = (size - 2) >> 1; for (; root >= 0; --root) HeapAdjust(array, size, root); //開始排序,使用刪除節點的思想 int end = size - 1; while (end) { swap(array[0], array[end]); HeapAdjust(array, end, 0); end--; }}

    七、歸并排序

    ??歸并排序是簡歷在歸并操作上的一中有效的排序算法,該算法是采用分治法的一個非常典型的應用。將已有序的子序列合并,得到完全有序的序列。顯示每個子序列有序,再使子序列段間有序。若兩個有序列表合并成一個有序列表,陳偉二路歸并。

    歸并的缺點在于需要O(N)的空間復雜度,歸并排序的思考更多的是解決在磁盤中的外排序問題。
    時間復雜度:O(N*logN)
    空間復雜度:O(N)
    穩定性:穩定

    void _MergeData(vector<int>& arr, int left, int mid, int right, vector<int>& temp){ int begin1 = left; int end1 = mid; int begin2 = mid; int end2 = right; int index = left; while (begin1 < end1 && begin2 < end2) { if (arr[begin1] < arr[begin2]) temp[index++] = arr[begin1++]; else temp[index++] = arr[begin2++]; } while (begin1 < end1) temp[index++] = arr[begin1++]; while (begin2 < end2) temp[index++] = arr[begin2++];}void _MergeSort(vector<int>& arr, int left, int right, vector<int>& temp){ if ((right - left) > 1) { int mid = left + ((right - left) >> 1); _MergeSort(arr, left, mid, temp); _MergeSort(arr, mid, right, temp); _MergeData(arr, left, mid, right, temp); copy(temp.begin() + left, temp.begin() + right, arr.begin() + left); }}

    ★★系統知識★★

    一、進程

    進程間通信

    為什么要為用戶提供程間通信方式?

    因為進程的獨立性,每個進程操作的都是自己虛擬地址空間中的虛擬地址,無法訪問別人的地址,所以無法直接通信。

    管道(半雙工)

    本質:內核中的一塊緩沖區

    原理:人多個進程訪問到相同的緩沖區來實現通信,管道通信使用的系統調用的IO接口,遵循一切皆文件的思想。

    匿名管道

    一個進程創建匿名管道,操作系統再內核中重建一塊緩沖區,并返回兩個人文件描述符作為管道的操作句柄(一個用于讀,一個用于寫,方向的選擇權交給用戶),但是這個緩沖區在內核中沒有標識。

    操作接口
    創建管道
    int pipe(int pipefd[2]);pipefd:至少有兩個int型元素的數組創建一個管道,通過pipefd獲取系統返回的管道操作句柄,其中pipefd[0]: 用于從管道中讀取數據pipefd[1]: 用于向管道中寫入數據返回值成功返回 0 失敗返回-1

    ipc:進程間通信方式

    ? 管道必須創建于子進程之前,子進程這樣才能復制到管道的操作句柄

    管道的讀寫特性

    ? 1、若管道中沒有數據,則read會阻塞等待,直到數據被寫入

    ? 2、若管道中數據滿了,則write會阻塞等待,直到數據被讀取

    ? 3、若管道中的所有讀端被關閉,則wirte會觸發異常,進程退出

    ? 4、若管道的所有寫端被關閉,則read會返回0

    ? 5、管道的read返回0,不僅僅指的是沒有讀到數據,還有可能是寫端被關閉

    grep make:從標準輸入循環讀取數據,對讀到的數據進行過濾匹配

    匿名管道的實現就是創建兩個進程,一個運行ls,另一個運行grep

    讓ls這個進程標準輸出,重定向到管道寫入端

    命名管道

    ? 在命名管道中,同一主機上的任意進程之間通信,命名管道在內核中這塊緩沖區是由標識的,一維著所有的進程都可以通過這個標識找到這塊緩沖區來實現通信。命名管道的標識符實際上是一個文件,可見于文件系統,意味著所有進程都可以通過打開文件來訪問到內核中的緩沖區。

    命名管道的打開特性:

    ? 若文件當前沒有被已讀的方式打開,則以O_WRONLY打開時會阻塞

    ? 若文件當前沒有被已讀的方式打開,則以O_RDONLY打開時會阻塞

    命名管道的讀寫特性:類同于匿名管道

    命名管道和匿名管道

    匿名管道只能用于具有親緣關系的進程間通信

    命名管道可以作用于同一主機上任意進程間通信

    管道特性

    1、管道是半雙工通信

    2、管道的讀寫特性+(命名管道的打開特性)

    3、管道的生命周期隨進程(所有管道的操作句柄被關閉)

    4、管道自帶同步與互斥(管道的讀寫大小不超過PIPE_BUF時是安全的)

    5、管道提供字節流服務,但是存在粘包問題

    管道通信

    管道通信的本質是通過內核中的緩沖區來實現通信,

    進程1將數據從用戶態緩沖區拷貝到內核態緩沖區,然后進程2將數據沖內核態緩沖區拷貝到用戶態緩沖區,涉及兩次用戶態與內核態之間的數據拷貝。

    共享內存

    最快的進程間通信方式

    共享內存原理

    1、在物理內存中開辟一塊內存空間

    2、將這塊空間通過頁表映射到進程的虛擬地址空間中

    3、進程可以直接通過進程虛擬地址空間訪問這塊物理內存,進行操作,若多個進程映射同一塊物理內存,就可以實現相互通信,這樣就可以通過虛擬地址改變內存中的數據,其他進程也會隨之改變。

    4、相較于其他進程間通信方式,少了兩步內核態和用戶態之間的數據拷貝

    5、釋放映射關系

    6、刪除共享內存

    1、創建共享內存int shmget(key_t key, size_t size, int shmflg); key: 共性內存在操作系統中的標識符 size: 共享內存大小 shmflag: IPC_CREAT 共享內若存在則打開,否則創建 IPC_EXCL 與IPC_CREAT同時使用,若共享內存村在則報錯返回 mode 貢獻內存的操作權限 返回值: 正整數-->共享內存的操作句柄 2、將共享內存映射到虛擬地址空間void *shmat(int shmid, const void *shmaddr, int shmflg); shmid 創建共享內存返回的操作句柄 shmaddr 共享內存在虛擬地址空間中的首地址--通常置空(NULL),有操作系統來指定 shmflg SHM_RDONLY--映射之后,共享內存只讀,通常置0,可讀可寫 返回值 映射首地址 失敗(void*) -1

    3、通過虛擬地址進行操作

    mencpy

    4、解除映射關系

    int shmdt(const void *shmaddr); shmaddr shmat建立映射關系是返回的映射首地址

    5、刪除共享內存

    int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmid 共享內存操作句柄 cmd 共享內存操作()即將進行的操作 IPC_RMID buf 用于獲取/設置共享內存信息

    共享內存的刪除流程:共向內存在刪除的時候,首先會判斷當前映射鏈接數是否為0,若為0直接刪除,否則表示現在還有其他進程在使用,則共享內存不能立即被刪除,但是會拒絕后續進程的映射鏈接,等待映射鏈接數為0時刪除這塊共享內存。

    消息隊列

    操作系統在內核中為用戶闖將一個隊列,其他進程通過訪問相同的隊列進行通信,消息隊列傳輸的是有類型的數據塊,共享內存、消息隊列的生命周期隨內核

    用于多個進程間有類型的數據塊傳輸

    ipcs 查看 -m 共享內存 -s 信號量 -q 消息隊列ipcrm -m shmid 刪除對應的共享內存 -m 共享內存 -s 信號量 -q 消息隊列

    信號量

    實現進程間的同步與互斥,

    本質:在內核中是一個計數器+喚醒+等待.

    同步:保證多個進程之間對臨界資源訪問的時序合理性-—-等待與喚醒

    互斥:保存多個進程之間同一時間對臨界資源的唯一訪問型性

    原理:

    進程對資源進行訪問操作之前,先進行臨界資源計數判斷,

    若計數<=0,則阻塞,等待創建資源 --計數-1 等待在等待隊列中

    若計數> 0,則直接返回,按照程序流程就可以直接擦做資源了,計數-1

    有進程創建了資源,計數+1, 并喚醒等待隊列上的那些進程

    二、線程

    線程是在Linux中使用PCB模擬實現的輕量級進程,進程從表面來說是一個運行起來的程序,但是從操作系統角度來說,進程就是操作系統堆為一個正在執行的程序而創建的描述符,操作系統通過對這個描述來對程序進行控制,這個描述信息就是PCB,所以說線程就是一個輕量級的進程,是一個進程等的子任務,線程共享進程中部分資源,包括數據段、代碼段和擴展段,每個線程擁有自己的線程描述符、數據棧、用于存放上下文數據的寄存器、錯誤碼、信號屏蔽字。線程是進程中的一個執行流,是操作系統調度和執行的最小單位。

    線程間通信(同步)的方式

    臨界區:通過多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問;

    互斥量:采用互斥對象機制,只有擁有互斥對象的線程才有訪問公共資源的權限。因為互斥對象只有一個,所以可以保證公共資源不會被多個線程同時訪問

    信號量:為控制具有有限數量的用戶資源而設計的,它允許多個線程在同一時刻去訪問同一個資源,但一般需要限制同一時刻訪問此資源的最大線程數目。

    事件(信號):通過通知操作的方式來保持多線程同步,還可以方便的實現多線程優先級的比較操作

    三、進程與線程

    什么是線程?

    線程是在Linux中使用PCB模擬實現的輕量級進程,進程從表面來說是一個運行起來的程序,但是從操作系統角度來說,進程就是操作系統堆為一個正在執行的程序而創建的描述符,操作系統通過對這個描述來對程序進行控制,這個描述信息就是PCB,所以說線程就是一個輕量級的進程,是一個進程等的子任務,線程共享進程中部分資源,包括數據段、代碼段和擴展段,每個線程擁有自己的線程描述符、數據棧、用于存放上下文數據的寄存器、錯誤碼、信號屏蔽字。線程是進程中的一個執行流,是操作系統調度和執行的最小單位。

    線程和進程的區別?

    1、一個線程只能屬于一個進程,而一個進程可以有一個或多個線程,線程是依賴于進程存在的。

    2、進程在執行過程中擁有一個獨立的內存單元,而多個線程共享同一個進程的內存。也就是說資源分配給進程,在這個進程中的所有線程共享其中的所有資源,同一個進程中所有線程共享代碼段、數據段、擴展段。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。棧寄存器(上下文數據)信號屏蔽字errno(錯誤碼)線程標識符

    3、進程是資源分配的最小單元,線程是CPU調度的最小單元

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

    5、通信:由于同一個進程中的多個線程具有相同的地址空間,所以他們之間的同步和通信的實現也變得比較簡單。進程間通信IPC有管道、共享內存、消息隊列、信號量進行通信。線程間可以直接讀寫進程數據段來進行通信。在有的系統中,線程的切換、同步和通信都無需操作系統內核干預。

    6、進程編程調試簡單可靠性高,但是創建和銷毀的開銷較大,相對于線程來說正好相反,開銷小、切換速度快,但是編程調試相對復雜。

    7、進程之間不會相互影響,但是同一個進程中只有一個線程掛掉了將導致整個進程掛掉。

    8、多線程適合于I/O密集的工作場景、多進程適合于CPU密集型的工作場景。

    怎么實現線程池?

    1、設置一個生產者消費者隊列,作為臨界資源。

    2、初始化n個線程,并讓其運行起來,以加解鎖的方式去隊列取任務運行。

    3、當任務隊列為空的時候所有的線程阻塞。

    4、當生產者隊列來了一個任務后,先對隊列加鎖,把任務掛在隊列上,然后使用條件變量去通知阻塞中的一個線程。

    四、線程安全

    什么是線程安全

    多線程對臨界資源進程操作二不會產生二義性。

    保證線程安全的機制

    使用同步與互斥保證線程安全。

    線程同步

    同步就是協同步調,按預定的先后次序進行運行。線程同步是指多線程通過特定的設置(如互斥量,事件對象,臨界區)來控制線程之間的執行順序(即所謂的同步)也可以說是在線程之間通過同步建立起執行順序的關系,如果沒有同步,那線程之間是各自運行各自的!

    線程互斥是指對于共享的進程系統資源,在各單個線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。線程互斥可以看成是一種特殊的線程同步。

    線程同步有四種方式:臨界區、互斥對象、信號量、事件對象。

    臨界區、互斥對象:主要用于互斥控制,都具有擁有權的控制方法,只有擁有互斥對象的線程才能執行任務,所以擁有互斥對象的線程,執行完任務后一定要釋放該對象。

    信號量、事件對象:事件對象是以通知的方式進行控制,主要用于同步控制。

    臨界區

    通過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問。在任意時刻只允許一個線程對共享資源進行訪問,如果有多個線程試圖訪問公共資源,那么在有一個線程進入后,其他試圖訪問公共資源的線程將被掛起,并一直等到進入臨界區的線程離開,臨界區在被釋放后,其他線程才可以搶占。它并不是核心對象,不是屬于操作系統維護的,而是屬于進程維護的。

    互斥對象

    互斥對象和臨界區很像,采用互斥對象機制,只有擁有互斥對象的線程才有訪問公共資源的權限。因為互斥對象只有一個,所以能保證公共資源不會同時被多個線程同時訪問。當前擁有互斥對象的線程處理完任務后必須將線程交出,以便其他線程訪問該資源。

    信號量

    信號量也是內核對象。它允許多個線程在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大線程數目。

    條件變量

    條件變量是一種同步機制,允許線程掛起,直到共享數據上的某些條件得到滿足。條件變量要和互斥鎖相結合,避免出現條件競爭,就是一個線程預備等待一個條件變量,當它在真正等待之前,另一個線程恰好觸發了該條件。

    互斥鎖

    互斥從表面上理解就是相互排斥。所以互斥鎖從字面上理解就是一個進程擁有了這個鎖,他將排斥其他所有的進程訪問被鎖住的東西,其他的進程如果需要鎖只能阻塞等待,等擁有鎖的進程解鎖后才能繼續運行。在使用互斥鎖的時候要注意一點就是解鈴還須系鈴人,如果擁有鎖的進程不解鎖,那么其他進程將永遠不能得到互斥鎖。

    讀寫鎖

    互斥鎖是排它鎖,條件變量出現后和互斥鎖配合能夠有效地節省系統資源并提高線程之間的協同工作效率。互斥鎖的目的是為了獨占,條件變量的目的是為了等待和通知。對于文件來說,最常見的操作就是讀和寫,讀文件不會修改文件的內容,所以多個進程同時讀也是可以的,但是當寫進程需要寫數據的時候為了保證數據的一致性,所有讀的進程都不能讀數據,否則讀到的數據可能是一半是舊的,一半是新的,這樣就亂了。所以為了防止讀數據的時候寫入新的數據,在讀數據的時候就必須對文件加鎖,但是如果有兩個進程要同時讀,另一個進程就只能等待,從性能上講,是浪費時間。所以這是要用到讀寫鎖,讀寫鎖的出現有效地解決了多進程并行讀的問題,這樣每個進程在讀的時候需要申請讀鎖,進程之間相互不干擾。如果有進程要寫數據,需要申請寫鎖,如果有讀鎖或者寫鎖存在,那么只能等待所有的鎖都解鎖了才可以進行寫操作。有一點值得注意的是,讀鎖是所有進程共享的,但是寫鎖是互斥的。

    記錄鎖

    為了增加同步的性能,可以在讀寫鎖的基礎上進一步細分被鎖對象的粒度。比如說寫操作是針對文件的前1k字節,但是讀操作是針對文件的后1k字節,這樣就可以對文件的前1k上寫鎖,后1k上讀鎖,這樣讀和寫就可以并發進行了,文件鎖是記錄鎖的一個特例,記錄鎖針對的是文件中的某一部分內容,如果記錄鎖將整個文件上鎖,這時候的記錄鎖就是一個文件鎖。

    線程互斥

    五、死鎖的條件以及解決的辦法

    什么死鎖

    死鎖的條件

    必須要滿足以下的四個條件才可以發生死鎖。

    1、互斥條件

    指的是某個資源同時只能讓一個進程占有,比如說打印機。必須要在占有該資源的進程主動釋放資源后其他進程才可以占有。這時有資源本身屬性決定的。

    2、不可搶占資源

    進程獲得的資源在沒有使用完的情況下,這份資源不能被資源申請者強行占有,只能等該資源釋放候才可以申請。

    3、占有且申請條件

    一個進程至少占有了一個資源,他又要申請新的資源,但是申請的資源被另一個進程所占有,這時進程阻塞,在他阻塞的時候仍然占有已有的資源。

    4、循環等待條件

    可以說是一個進程等待隊列,p1等待p2所占有的資源,同時p1不對自己的資源進行釋放,p2在等待p1所占有的資源,同樣的p2也不對自己所占有的資源進行釋放,這樣就形成了一個進程的循環等待。

    死鎖的預防

    死鎖的預防就是保證系統不進入死鎖的狀態的一種策略。

    1、破壞互斥條件

    但是有些資源確實不能被共享,這是由資源的屬性決定的。

    2、破壞不可搶占條件

    將資源的申請設置為可搶占式,也就是要求申請失敗的進程要釋放自己占有的資源給其他進程使用,但是會降低系統性能。

    3、破壞占有且申請條件

    直接一次性將自己所需要的資源申請完。當時這樣有兩個問題,一個是有時候不可預知需要什么資源,另一個是資源的利用率低,進程可能會長期占有自己可能用不到的資源。

    4、破壞循環等待條件

    將資源進行分類、編號,讓進程按照排好的序號進行申請。存在的問題是對資源的編號可能是困難的,維護相應的序列也可能是困難的。

    死鎖的避免

    死鎖的避免是指不限制進程有關申請資源的命令,而是對進程所發出的每一個申請資源命令甲乙動態的檢查,并且根據檢查結果來判斷是否進行資源分配。

    銀行家算法:我們可以把操作系統看作是銀行家,操作系統管理的資源相當于銀行家管理的資金,進程向操作系統請求分配資源相當于用戶向銀行家貸款。
    為保證資金的安全,

    銀行家規定:

    當一個顧客對資金的最大需求量不超過銀行家現有的資金時就可接納該顧客;
    顧客可以分期貸款,但貸款的總數不能超過最大需求量;
    當銀行家現有的資金不能滿足顧客尚需的貸款數額時,對顧客的貸款可推遲支付,但總能使顧客在有限的時間里得到貸款;
    當顧客得到所需的全部資金后,一定能在有限的時間里歸還所有的資金.

    操作系統按照銀行家制定的規則為進程分配資源,當進程首次申請資源時,要測試該進程對資源的最大需求量,如果系統現存的資源可以滿足它的最大需求量則按當前的申請量分配資源,否則就推遲分配。當進程在執行中繼續申請資源時,先測試該進程本次申請的資源數是否超過了該資源所剩余的總量。若超過則拒絕分配資源,若能滿足則按當前的申請量分配資源,否則也要推遲分配。

    六、IO多路轉接

    select

    對大量的IO事件是否就緒進行監控,并且可以告訴進程哪一個IO就緒了,然后就可以對就緒的IO進行具體的操作。事件是否就緒指的是文件描述符可讀/可寫/異常。他的流程是這樣的:用戶將需要監控的文件描述符添加到一個描述符集合中,然后select將描述符集合拷貝到內核中,對集合中的描述符進行輪詢判斷,如果有描述符事件就緒了,select調用返回,并且返回描述符的個數。否則隔一會再進行輪詢判斷。在調用返回之前會將集合中沒有就緒的時間描述符移除,在集合中只剩下就緒的文件描述符。返回之后進程判斷有哪個文件描述符在集合中,然后對其進行操作。

    select的缺點

    select所能監控的文件描述符最多只有1024個,他每次都需要將文件描述符集合拷貝到內核中進行監控,在用戶態和內核態之間進行數據的拷貝,在內核中要對所有的文件描述符進行輪詢遍歷判斷,性能會隨著描述符的增加而降低,每次返回的時候都需要將文件描述符集合中非就緒的描述符進行移除,再次監控的時候需要添加新的描述符,編碼復雜,select雖然返回給用戶文件描述符集合,但并不會告訴用戶哪些文件描述符是就緒的,需要用戶自己去遍歷判斷,性能同樣會隨著描述符的增多而降低。

    select的優點

    select遵循posix標準,可以跨平臺使用,并且他監控超時的等待時間可以精細到微秒。

    poll

    poll的功能也是對大量的文件描述符事件進行監控,用戶為每一個關心的文件描述符定義一個事件結構,內容為文件描述符+所關心的事件,poll將描述符事件結構數組拷貝到內核中進行監控,同樣是輪詢遍歷數組中的描述符判斷事件是否就緒,如果就緒了就將事件放到事件結構revents中,調用返回,否則會隔一段時候后繼續遍歷判斷,當調用返回后,用戶遍歷事件結構數組,判斷結構中的revents事件中是否包含了所關心的事件。

    poll缺點

    每次監控需要將所有的事件結構信息拷貝到內核中,在內核中進行輪詢判斷描述符事件是否就緒,會隨著描述符的增多而降低性能,poll的返回同樣不會直接告訴用戶哪些描述符就緒了,需要用戶輪詢去遍歷事件結構數組,判斷哪個是用戶所關心的,并且對其進行操作,poll相對于select來說沒有大的改變,性能并沒有提升多少,并且poll沒有遵循POSIX標準,所以不能跨平臺。

    poll優點

    poll采用事件結構的方式對描述符進行監控,簡化了多個描述符集合的監控編碼流程,poll監控的文件描述符的數量沒有上限。

    epoll

    epoll可以說是Linux中最優秀的多路轉接,他的性能是最高的,他在內核中創建eventpoll結構體,這個結構體是由紅黑樹(存放事件節點)和雙向鏈表(存放就緒的文件描述符)實現。在linux2.6.8之前需要對epoll監控的文件描述符數量上限進行設置,但是在Linux2.6.8之后就忽略了這一參數,只要大于0就可以。epoll是一個異步操作,epoll開始監控后,告訴操作系統開始進程監控eventpoll結構體中紅黑樹中的所有事件節點,操作系統為每一個文件描述符就定義了 一個事件回溯,當文件描述符指定的事件就緒后,就將這個描述符指定的事件接收信息節點添加到eventpoll中的雙向鏈表中。相當于直接告訴了用戶哪些文件描述符就緒了。

    epoll觸發方式

    水平觸發

    對于可讀事件來說,文件描述符接收緩沖區數據大小只要大于低水位標記就會觸發,對于可寫事件來說只要是文件描述符的發送緩沖區空間大小只要大于低水位標記就會觸發。水平觸發是epoll的默認觸發方式,EPOLLLT宏;

    邊緣觸發

    對于可讀事件來說,描述符緩沖區中每當有新數據到來時才會觸發一次,對于可寫事件來說描述符發送緩沖區從沒有剩余空間到有剩余空間時才會觸發一次。邊緣觸發需要手動設置,EPOLLET。邊緣觸發每條數據值觸發一次,這就意味著這次觸發的過程中需要用戶將所有的數據存取完畢,因為這條數據不會觸發第二次,只有等到下一條數據到來才會觸發下一次。由于邊緣觸發需要一次性將所有的數據都處理完,但是用戶有可能也不知道數據有多長,所以要使用循環才可以將所有的數據處理完,但是循環會出現一個就是讀到沒有數據的時候就會阻塞。相應的解決方案是將IO操作設置為非阻塞的。

    將描述符設置為非阻塞:int flag = fcntl(fd, F_GETFD,0) ----獲取描述符屬性fcntl(fd, F_GETFD, flag | O_NONBLOCK)---設置描述符屬性為增加一個非阻塞屬性

    epoll優點

    監控的文件描述符沒有上限,采用事件結構對描述符進行監控,簡化了多個描述符集合的監控流程,每條epoll的事件信息只向內核中拷貝一次,是一個異步阻塞操作,操作系統僅對描述符事件進行監控,并且使用的是事件回調方式,描述符就緒后直接拷貝到對應的位置,不需要輪詢遍歷判斷,所以描述符的增加不會影響性能,在回調的過程中是將就緒的描述符添加到雙向鏈表中,epoll_wait只需要判斷雙向鏈表就知道當前是否有描述符事件就緒,當epoll_wait返回時,是直接將就緒信息拷貝到之前給予的事件結構數組中,這就相當于直接將就緒的描述符告訴給了用戶。

    epoll缺點

    epoll沒有遵循POSIX標準,所以不能跨平臺,他監控超時只能精確到毫秒。

    同步異步和阻塞非阻塞的區別

    阻塞是為了完成一個功能發起調用,如果當前不滿足條件,要等待滿足條件了才調用返回。非阻塞恰恰相反,如果不滿足條件直接報錯返回,阻塞和非阻塞強調的是發起一個調用后是否立即返回。同步指的是如果不滿足條件需要進程進行等待,知道條件滿足了才會調用返回,對于異步來說,他為了完成某個功能只發起一個調用,進程自身并不去完成,功能由操作系統完成后調用進程,所以說同步和異步強調的是發起調用后,功能是否有進程自己完成。同步功能是由進程自身完成的,通常是阻塞的,異步功能是由系統完成的,有阻塞也有非阻塞。在多路轉接中select和poll是同步的,需要進程自己來實現輪詢的遍歷監控,epoll是異步阻塞的,因為功能的完成是操作系統完成的,而進程是調用epoll_wait阻塞等待操作系統完成監控。

    場景

    多路轉接模型實現高并發模型相較于多線程/多進程消耗資源較少,但并不是說所有的場景都使用多路轉接模型,它只是用于有大量的文件描述符需要監控,但是同一時間只有少量活躍,并且每個描述符的處理過程時間不能太長。

    七、用戶態和內核態

    在inter x86結構的cpu中一共有四個級別,0-3級,0級特權最高,3級特權最低,這樣做是為了讓最關鍵的工作交給特權級最高的進程去執行,可以做到集中管理,減少有限資源的訪問和使用沖突。

    當一個進程在執行用戶自己的代碼時處于用戶態,此時特權級最低,為3級,是普通用戶進程運行的特權級,大部分用戶直接面對的程序都是運行在用戶態,并且Ring3狀態不能訪問Ring0狀態的地址空間,包括代碼和數據,當一個進程因為系統調用陷入內核代碼中執行時處于內核運行態,此時特權級最高,為0級。執行的內核代碼會使用當前進程的內核棧,每個進程都有自己的內核棧。內核態的進程執行完又會切換到Ring3,回到用戶態。這樣用戶態的程序就不能隨意操作內核地址空間,具有一定的安全保護作用。用戶態切換到內核態通常有三種方式:系統調用、異常、外圍設備的斷。

    系統電泳是用戶態進程主動要求切換內核態的一種方式。用戶態進程通過系統調用申請使用操作系統的服務程序完成工作。比如說fork()就是執行了一個創建新進程的系統調用。

    異常是當cpu在執行運行在用戶態的程序時,發生了一些沒有預知的異常,這時會觸發由當前運行進程切換到處理詞異常的內核相關進程中,也就是切換到了內核態。

    外圍設備的中斷指的是當外圍設備完成用戶請求的操作后,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令而轉到與中斷信號對應的處理程序去執行,如果當前執行的執行時用戶態下的程序,那么轉換的過程自然就是由用戶態轉換到內核態。

    這三種方式是系統在運行時有用戶態切換到內核態的主要方式,其中系統調用可以認為是用戶進程主動發起的,異常和外圍設備中斷是被動的。

    八、什么是POSIX標準?

    posix表示可移植操作系統接口,它定義了操作系統應該為應用程序提供的接口,這一標準帶來的好處就是在一個POSIX兼容的操作系統中編寫的符合其標準的應用程序可以直接在其他的POSIX支持的操作系統中無需修改而能夠直接編譯運行。

    九、協程★★☆☆☆

    協程可以認為是比線程更小的執行單元,它自帶CPU上下文,只要在合適的時機,就可以把一個協程切換到另一個協程,只要在這個過程中保存或恢復CPU上下文那么程序還是可以運行的。
    線程與協程的區別就是系統為了程序運行的高效性每個線程都有自己緩存Cacge等數據,操作系統還會幫助做這些數據的恢復操作,可以說線程的切換時非常耗性能,但是協程的切換只是單程的操作CPU的上下文,所以一秒鐘切換上百萬次系統都扛得住。但是協程有一個問題就是系統并不感知,所以操作系統不會對其做切換。在設計的時候可以是一個線程作為容器,然后再里面放置多個協程,構成一個協程池,在協程池中有一個被動的調度器,當有協程執行不了的時候,就要去通知協程調度器通過算法找到當前最需要CPU的協程,然后去執行。

    十、Linux程序典型內存布局

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YdIAz6xP-1637763958109)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1566374936321.png)]

    十一、內存管理

    虛擬內存

    什么是虛擬內存

    內存是一個程序運行的物質基礎,由于內存空間總是有限的,所以讓有限的空間運行較大的程序這個問題就出現了,Linux中就使用了虛擬內存技術解決了這個問題。虛擬內存是計算機系統管理的一種技術,可以讓應用程序認為自己有擁有連續的可用內存,讓系統看上去具有比實際物理內存大得多的內存空間,而實際上是被分隔成多個物理內存的碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數據交換。由于程序是逐段被運行的,虛擬內存技術是先將一段程序導入內存中,執行完后導入下一段程序,給我們的感覺是內存變大了,實際上物理內存并沒有發生變化。

    每個進程都有自己獨立的4G內存空間,各個進程的內存空間具有類似的結構,這個空間知識虛擬內存空間,每次訪問內存空間的某個地址,都需要吧地址翻譯成實際的物理內存地址。每一個進程都會建立自己的內存空間,用來存放這個進程的數據、代碼等,這些內存空間都與對應的磁盤空間映射。所有進程都共享同一物理內存,每個進程只把自己目前需要的虛擬內存并保存到物理內存中。進程通過頁表來記錄哪些數據在物理內存上,哪些不在,如果在是在物理內存的哪里。頁表一般分為兩個部分,一部分記錄此頁是否在物理內存中,另一部分用來記錄在物理內存中的地址,當一個進程需要訪問某個虛擬地址的時候,要先去看頁表,如果發絲昂對象的數據不在物理內存中,就會缺頁異常,然后通過解決缺頁異常處理后,訪問對應的物理內存。

    虛擬內存的優點

    1、每個進程的內存空間都是一致并且是固定的,所以鏈接器在鏈接可執行文件的時候,可以設定內存地址,就可以不用去管這些數據最終實際的內存地址,這是獨立內存空間的好處。當不同的進程使用的同樣的代碼的時候,比如說庫文件中的代碼,物理內存中可以只存儲一份這樣的代碼,不同的進程只需要將自己的虛擬內存映射到對應的位置就可以,這樣節省了內存。在程序需要分配連續的內存空間的時候,只需要在虛擬內存空間中分配連續的空間,而不需要實際的物理內存空間連續,這樣可以利用物理內存中的內存碎片,提高了內存的利用率。

    2、虛擬內存保證了讀寫內存的安全性。物理內存本身是不限制訪問的,任何地址都可以讀寫,而操作系統要求不同的頁面要具有不同的訪問權限,這是利用CPU模式和MMU的內存保護機制實現的。

    3、如果一個操作系統同時運行著很多進程,并且為各個進程分配的內存之和大于實際可用的物理內存,虛擬內存管理就可以使這種情況各個進程仍然可以正常運行。因為各個進程分配的只不過是虛擬內存的頁面,這些頁面的數據可以映射到物理頁面,也可以臨時保存到磁盤上而不占用物理頁面,在磁盤上臨時保存的虛擬內存頁面可能是一個磁盤分區,也可能是一個磁盤文件,被稱為交換設備。當物理內存不夠用的時候,可以將一些不常用的物理頁面中的數據臨時保存到交換設備中,然后這個物理頁面就可以被認為是空閑的,可以重新分配給進程使用,這個過程稱為換出。如果進程要用到被換出的頁面,就從交換設備再加載回物理內存,這個過程稱為換入。換出和換入在操作系統中統稱為換頁,所以系統中可分配的內存總量為:物理內存的大小+交換設備的大小。

    4、虛擬內存作為內存管理工具,大大的簡化了內存管理,并且為每個進程提供了一個獨立的虛擬地址空間,多個虛擬頁面可以映射到同一個共享的物理頁面上。按照需要進行頁面的調度和獨立的虛擬地址空間的結合,讓虛擬內存簡化了連接和加載,代碼和數據共享,以及應用程序的內存分配。

    簡化鏈接:說的是獨立的地址空間允許每個進程的內存映射使用相同的基本格式,不用考慮代碼和數據在物理內存的地址。當不用的進程使用相同的代碼的時候,比如說庫文件中的代碼,在物理內存中只存放了一份這樣的代碼,不同的進程只需要將自己的虛擬內存映射到對應的位置就可以。

    簡化加載:指的是虛擬內存讓向內存中中加載可執行文件和共享對象文件變得容易。將一組連續的虛擬頁面映射到任意一個文件中的任意位置的表示法稱為內存映射。

    簡化共享:指的是獨立地址空間為操作系統提供了一個管理用戶進程和操作系統自身之間共享的機制。一般情況下每個進程都有自己獨立的內存空間,里面存放著該進程的代碼、數據、堆棧,這些內容都是不與其他進程所共享的。在操作系統中為了多個進程之間通訊的考慮,預留出了一塊內存區,多個進程如果要實現共享,由操作系統創建頁表,將相應的虛擬頁映射到不連續的物理頁面,然后將這個頁表映射到進程的這塊內存中,這樣多個進程就可以對同一塊物理內存進行讀寫,實現數據的共享。

    簡化內存分配:指的是虛擬內存向用戶進程提供一個簡單的分配額外空間的機制。當一個用戶程序要求額外空間的時候,操作系統分配K個適當的連續的虛擬內存頁面,并且將他們映射到物理內存中的K個任意頁面,操作系統沒有必要分配K個連續的物理內存頁面。

    鑒于《深入理解計算機系統》龔奕利·譯

    缺頁異常解決辦法

    什么是缺頁異常

    在分頁系統中,可以通過查詢頁表的狀態為來確定要訪問的頁面時候存在內存中,每當所有訪問的頁面不在內存時,或者訪問的頁面在內存中但是時不可寫的,會產生一次缺頁中斷。缺頁本身就是一種中斷,它與一般的中斷一樣,需要經過4個處理步驟

    1、保護CPU現場。

    2、分析中斷的原因

    3、轉入缺頁中斷處理程序中進行處理

    4、恢復CPU現場,繼續執行

    但是缺頁中斷時可能由于要訪問的頁面不在內存中,這時就會由硬件產生一種特殊的中斷,缺頁中斷與一般的中斷區別在于:在指令執行期間產生和處理缺頁中斷信號、一條指令在執行期間可能會產生多次缺頁中斷、缺頁中斷返回時執行的是產生中斷的那一條指令,而不是像一般中斷去執行下一條指令。

    頁面置換算法

    進程運行過程中,如果發生缺頁中斷,但是內存中沒有空間的物理塊,為了能夠把所缺的頁面裝入內存,系統必須要從內存中選擇一頁調出到磁盤的對換區。但此時應該吧那個頁面換出,則需要根據一定的頁面置換算法來確定。通常有最佳置換、先進先出置換、最近最久未使用置換、時鐘置換算法。

    最佳置換算法(OPT)

    從主存中移除永遠不需要的頁面,如果沒有這樣的頁面存在,則選擇最長時間不需要的訪問的頁面。由于所選擇的被淘汰的頁面是以后永不使用的,或者是在長時間內不再被訪問的頁面,這樣就可以保證獲得最低的缺頁率。

    先進先出算法(FIFO)

    是最簡單的頁面置換算法。這種算法的思想是當需要淘汰一個頁面的時候,總是選擇駐留主存事件最長的頁面進行淘汰,也就是先進入主存的頁面會被先淘汰。這是因為最早調入主存的頁面的不再被使用的可能性最大。

    FIFO算法還會產生當所分配的物理塊樹增大而頁故障數不減少反而增加的異常現象,這是由Belady于1969年發現,所以稱為Belady異常,只有FIFO算法會出現Belady異常,而LRU和OPT算法永遠不會出現Belady異常。

    需要注意的是內存的頁面中最先進入隊列的頁面會被來的頁面直接覆蓋,而不是先出隊,然后再從大隊尾入隊。

    最近最久未使用算法(LRU)

    這種算法的思想是:利用局部性原理,根據一個作業在執行中過去的頁面訪問歷史來推測未來的行為。他認為過去一段時間里不曾被訪問的頁面,在最近的將來也不會再被訪問。所以這種算法的實質就是當需要淘汰一個頁面的時候,總是選擇在最近一段時間內最久沒有使用的頁面淘汰。

    時鐘置換算法(CLOCK)

    由于LRU算法的性能接近于OPT,但是實現起來比較困難,并且開銷大,FIFO算法實現簡單,但是性能差。所以師徒用比較小的開銷接近LRU的性能,這類算法都是CLOCK算法的變體。

    簡單的CLOCK算法是給每一幀關聯一個附加位,稱為使用位。當某一頁首次裝入主存時,該幀的使用位設置為1,當給爺雖有在被訪問到時,它的使用位也置為1。對于也替換算法來說,用于替換的候選幀集合看做一個循環緩沖區,并且有一個指針與之關聯。當某一頁被替換時,給指針被設置成指向緩沖區中的下一幀。當需要替換一頁時,操作系統掃描緩沖區,以查找使用位被置為0的一幀。每當遇到一個使用位為1的幀時,操作系統就會將該位重新置為0,如果在這個過程開始時,緩沖區中所有的使用位均為0,則選擇遇到的第一個幀替換;如果所有的幀使用位均為1,則指針在緩沖區中完整的循環一周,把所有的使用位都置為0,并且停留在最初始的位置上,替換該幀中的頁。由于這個算法循環地檢測各頁面的情況,所以被稱為CLOCK算法,又稱為最近未使用算法。

    MMU

    內存管理單元簡稱MMU,他杜澤虛擬地址到物理地址的映射,并提供硬件機制的訪問權限檢查。MMU使得每個用戶進程擁有自己獨立的地址空間,并通過內存訪問權限的檢查保護每個進程所用的內存不被其他的進程破壞。

    物理內存

    在內核態申請內存比在用戶態申請內存更為直接,他沒有采用用戶態的延遲分配內存技術。內核認為一旦有內核函數申請內存就必須立即滿足該申請的請求,并且這個請求一定是合理的。相反,對于用戶態申請內存的請求,內核總是盡量延后分配內存,用戶進程總是先獲得一個虛擬內存區的使用權,最終通過缺頁異常獲得一塊真正的物理內存。

    十二、動態庫和靜態庫

    什么是庫?

    平時在寫代碼的時候會經常添加一些頭文件,添加這些頭文件其實是讓編譯器從一個目錄下去尋找這個文件,這個目錄就是我們常說的庫。在Linux中庫一般存放在user/lib目錄。庫就是將一些常用的函數的目標文件打包在一起,提供相應的函數接口,以便于使用。

    什么是靜態庫?

    靜態庫就是在編譯連接的時候,將庫中的代碼連接直接復制到可執行文件中,這樣程序在運行的時候就不用去連接動態庫了。靜態庫的這個連接過程就是靜態鏈接。

    什么是動態庫?

    動態庫就是程序在運行的時候才去連接動態庫的代碼,可以多個程序共享動態庫中的代碼,這個動態庫的連接過程就是動態鏈接,也就是在執行文件開始之前將外部函數的機器碼有系統從磁盤上對應的動態庫中向內存復制一份。

    靜態庫和動態庫的區別?

    1、動態庫是在運行時有系統調用庫函數實現鏈接,代碼較小巧。而靜態庫是在鏈接是復制到代碼中,代碼量比較龐大,冗余度高。
    2、由于靜態庫是通過復制的方式,所以他在編譯連接之后就不再需要靜態庫,代碼的可以執行強,但是動態庫由于是利用本地的庫函數,如果將代碼移植到其他電腦會出現運行bug等,可移植性差。
    3、動態庫必須放在指定的目錄下完成連接,但是靜態庫只需要給出鏈接文件的路徑就可以。
    4、他們的相同點就是在庫文件中不能出現main函數,庫都是用來提供函數接口的,不暴露源代碼,所有的庫的目的都是為了增加代碼的復用,可共享性,減小冗余。
    5、在windows中靜態庫是后綴是.lib,動態庫是.dll,在Linux中靜態庫是.a,動態庫是so。
    6、使用ar創建靜態庫

    生成動態和靜態庫

    ? gcc -fPIC -c child.c -o child.o 生成目標文件

    ? gcc --share child.o -o lid+庫名稱+.so 動態庫

    ? gcc -ar rcd lib+庫名稱+.a 靜態庫

    ? windows下的動態庫 .dll 靜態庫 .lib

    先生成二進制的.o文件gcc -fPIC -c child.c -o child.o生成靜態庫ar -rc libchild.a child.o生成動態庫gcc --shared -fpic libchild.so child.o鏈庫-l + 庫名,但是庫必須保存在lib、usr/lib、usr/local/lib目錄下,否則要用-L + 文件路徑 + -l + 庫名

    十三、軟連接和硬鏈接

    十四、線程的切換

    ★★網絡知識★★

    一、TCP和UDP的區別及應用場景

    區別

    1、面向連接和無連接角度

    tcp建立一個鏈接需要三次握手,斷開的時候需要進行四次揮手。TCP在斷開是主動方要進入一個TIME_WAIT狀態,要等待2MSL才會對連接(端口)釋放,這個時間要根據系統來定,Windows一般為120秒。

    但是UDP不需要建立連接,直接發起。

    2、可靠和不可靠角度

    TCP利用三次握手、四次揮手、ACK確認應答、重傳機制等來保證可靠性,UDP沒有。

    TCP中保證可靠性的機制有:

    校驗和:用來校驗數據是否損壞

    定時器:用來確認分組是否丟失,丟失需要重傳

    序列號:用來檢測丟失的分組和重傳的分組。

    確認應答ACK:用來讓接收方通知發送方已經正確接受,以及期望的下一個分組。

    否定確認:用來讓接收方通知發送方沒有被正確接受的分組。

    窗口和流水線:用于增加信道的吞吐量。

    3、有序性

    TCP利用seq序列號來對包進行排序,UDP沒有

    4、面向字節流和面向報文

    面向報文

    面向報文的傳輸方式是應用層交給UDP多少報文,UDP就按原樣發送,也就是一個發送一個報文。所以應用層就要選擇合適的大小的報文交給UDP,如果報文太長,在IP層就會進行分片。

    面向字節流

    面向字節流的傳輸方式是雖然應用層交給TCP的是一次一個數據塊,但是TCP會將這個數據塊看成一連串無結構的字節流。在TCP的發送端有一個發送緩沖區,當數據塊太大,TCP就將它劃分的小一些在發送,如果數據塊太小了,TCP就會等待累計夠足夠多的字節后再發送。

    5、TCP有流量控制機制,UDP沒有
    6、TCP頭部為20字節,UDP頭部為8字節

    應用場景

    TCP的應用場景

    對效率要求較低,但是對準確要求很高的場景。因為在使用TCP傳輸的時候需要對數據進行確認、重發、排序等操作,相對UDP來說效率降低了許多。這些場景都有:文件傳輸、遠程登錄。

    UDP應用場景

    對效率要求較高,對準確要求較低的場景,因為在使用UDP傳輸的時候沒有對數據進行任何的檢驗操作,相對于TCP效率提升了很多。這些應用場景有:QQ聊天、在線視頻、廣播通信等。

    二、TCP連接的建立和斷開

    TCP建立連接

    tcp采用三次握手建立連接,

    1、客戶端向服務端發起SYN連接請求,這條連接請求中還包含了客戶端的窗口大小

    2、服務端收到連接、請求后,為客戶端申請資源同時向客戶端回復一個SYN+ACK,還有自己的窗口大小。

    3、如果這條消息丟失,客戶端沒有收到這條消息,那么客戶端不會做任何響應,也就不會向服務端發送ACK確認,由于服務端遲遲沒有發送確認的ACK,服務器就會將之前申請的資源釋放。如果客戶端收到了這條消息,開始申請資源,同時回復服務端一個ACK確認,開始進行收發數據。

    4、如果最后這條ACK消息丟失,導致服務端沒有收到,服務端就認為連接失敗,將之前的資源釋放,但是客戶端認為連接已經建立好了,就會向服務端發送數據。服務端由于沒有收到最后一條ACK消息,就會以RST包對客戶端進行響應,重新建立連接。

    TCP斷開連接

    TCP采用四次揮手斷開連接,

    1、主動關閉方向被動方發送FIN請求,進入FIN_WAIT_1狀態,等待被動方的ACK確認。

    2、被動接收到FIN請求后,回復一個ACK確認,這時被動方進入CLOSE_WAIT狀態,進入這個狀態是為了將自己的所有要發送的數據都發送完,將自己申請的空間都釋放完。這時候主動方接受到ACK請求沒有直接斷開連接,而是進入FIN_WAIT_2狀態,在這個狀態下,主動方只能接受數據,不能進行發送。

    3、等被動方將自己的事情都處理完,向主動方發送FIN請求,進入LAST_ACK狀態,等待主動方的ACK確認。

    4、主動方接收到被動方的FIN請求后,進入TIME_WAIT狀態,向被動方發送ACK,等待2MSL后,進入CLOSED的狀態。這里的MSL是一個報文在網絡中的最大生命周期,進入TIME_WAIT狀態,并且等待2MSL是為了防止由于網絡原因,最后一個ACK沒有到達被動方,如果被動方一直沒有收到ACK確認,會再次向主動方發送FIN請求,這時主動方重新進入TIMR_WAIT狀態。將等待時間設置為2MSL還有一個原因就是要將網絡中所有殘留的數據消失,防止由于網絡原因某些數據阻塞在網絡中,如果沒有等待2MSL而是立即重新建立連接,很有可能之前的滯留在網絡中的數據到達了,這時就會造成數混亂。如果在2MSL之內主動方沒有接收到被動方FIN請求,則認為對方已經接收到ACK,主動方進入CLOSED狀態,也就是斷開連接。對于被動方來說,一旦接收到了來自主動方的ACK確認,就會進入CLOSED狀態,關閉連接。

    三、socket編程過程

    基于TCP的socket:

    服務端程序:

    創建一個socket,調用socket()函數,返回綁定IP地址、等信息到socket上,調用函數bind()設置允許的最大連接數,調用函數listen(),通常設置為5接受來自客戶端的連接,調用accept(),使用send和recv開始收發數據 返回值: 成功返回實際接收字節數,錯誤返回的-1,對端已經關閉了連接返回0關閉網絡連接

    客戶端程序

    創建一個socket,調用socket()函數設置要連接的對方的IP地址和端口等屬性連接服務器,調用函數connect()使用send和recv開始收發數據關閉網絡連接

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nzGXRIl7-1637763958110)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1566711302816.png)]

    /************************************************************************* -> 文件名: tcp_socket.hpp -> 作者: bean -> 郵箱: 17331372728@163.com -> 創建時間: Fri 24 May 2019 06:05:41 AM PDT -> 備注: 封裝一個TcpSocket類,向外提供輕便的socket接口 1.創建套接字 2.為套接字綁定地址 3.客戶端向服務端發起連接請求 4.服務端開始監聽 4.服務端獲取一個已經連接成功的客戶端的新建socket 5.客戶端與服務端開始收發數據 6.關閉套接字 *************************************************************************/#include<iostream>#include<string>#include<stdlib.h>#include<stdio.h>#include<unistd.h>#include <sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<string.h>using namespace std;#define CHECK_RET(q) if((q) == false){return -1;}class TcpSocket{ public: TcpSocket():_sockfd(-1) {} ~TcpSocket(){} bool Socket() { _sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(_sockfd < 0) { cout << "socket error" << endl; return false; } return true; } bool Bind(string &ip, uint16_t port) { struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = bind(_sockfd, (struct sockaddr*)&addr, len); if(ret < 0) { cout << "bind error" << endl; return false; } return true; } //開始監聽,并且設置服務端的同一時間最大并發連接數 bool Listen(int baklog = 5) { //int listen(int sockfd, int backlog); //sockfd: 套接字描述符 //backlog: 同一時間最大連接數,設置內核中已完成連接的最大節點數 int ret = listen(_sockfd, baklog); if(ret < 0) { cout << "Listen error" << endl; return false; } return true; } //獲取連接成功客戶端socket,并且返回客戶端的地址信息 bool Accept(TcpSocket &sock, struct sockaddr_in *addr = NULL) { //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //sockfd: 套接字描述符 //addr: 用于存儲客戶端地址信息 //addrlen: 用于設置想要的地址長度和保存實際的地址長度 //返回值: 為客戶端連接新建的socket描述符 //接下來與客戶端的通信都是通過這個新的描述符實現的 struct sockaddr_in _addr; socklen_t len = sizeof(struct sockaddr_in); int newfd = accept(_sockfd, (struct sockaddr*)&_addr, &len); if(newfd < 0) { cout << "Accept error" << endl; return false; } sock.SetFd(newfd); if(addr != NULL) { memcpy(addr, &_addr, len); } return true; } //客戶端向服務端發起連接請求 bool Connect(string &ip, uint16_t port) { //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //sockfd: 套接字描述符 //addr: 服務端監聽地址信息 //addrlen: 地址信息長度 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = connect(_sockfd,(struct sockaddr*)&addr, len); //cout << ip << " "<< port << " " << ret<< " " <<"1" << endl; if(ret < 0) { cout << "connect error" << endl; return false; } cout << "來了,老弟!!!" << endl; return true; } //通信接口:tcp服務端也可以直接發送數據(因為連接已經發送成功) bool Recv(string &buff) { //ssize_t recv(int sockfd, void *buf, size_t len, int flags); //sockfd: 套接字描述符 //buf: 接收的數據存儲 //len: 想要接收的數據長度 //flags: 0 默認是阻塞接收 // MSG_PEEK 接收數據但是數據不從接收緩沖區中移除 探測性接收 //返回值: 成功返回實際接收字節數,錯誤返回的-1,對端已經關閉了連接返回0 char tmp[1024] = {0}; int ret = recv(_sockfd, tmp, 1024, 0); if(ret == 0) { cout << "peer shutdown" << endl; return false; } else if(ret < 0) { cout << "recv error" << endl; return false; } buff.assign(tmp,ret); return true; } ssize_t Send(string &buff) { //ssize_t send(int sockfd, const void *buf, size_t len, int flags); //返回值: 成功就是實際發送的字節數 失敗 -1 //如果連接已經斷開,發送就會觸發異常,導致進程退出 int ret = send(_sockfd, buff.c_str(), buff.size(), 0); if(ret < 0) { cout << "send error" << endl; return false; } return true; } bool Close() { close(_sockfd); _sockfd = -1; return true; } void SetFd(int sockfd) { _sockfd = sockfd; } private: int _sockfd;};/************************************************************************* *文件名: tcp_ser.cpp *作者: bean *郵箱: 17331372728@163.com * 創建時間: Fri 24 May 2019 07:27:34 AM PDT * 備注:通過封裝的TcpSocket類實現服務端程序 * 1、創建套接字 * 2、綁定地址信息 * 3、開始監聽 * 4、獲取連接成功的客戶端新建socket * 5、通過新建的socket與客戶端進行通信 * 6、關閉套接字 *************************************************************************/#include"tcpsocket.hpp"int main(int argc, char* argv[]){ if(argc != 3) { cout << "./tcp_ser ip port" << endl; return -1; } string ip = argv[1]; uint16_t port = atoi(argv[2]); TcpSocket sock; CHECK_RET(sock.Socket()); CHECK_RET(sock.Bind(ip, port)); CHECK_RET(sock.Listen()); while(1) { //程序會卡在accept是應為用戶無法獲知客戶端的新連接什么時候到來 //如果能知道什么時候盜壘就不會阻塞,只需要在來的時候調用一次就行 TcpSocket client; if(sock.Accept(client) == false) { continue; } while(1) { string buff; client.Recv(buff); cout << "Tcp->Client say:" << buff << endl; buff.clear(); cout << "Tcp->Server say:"; fflush(stdout); getline(cin,buff); client.Send(buff); } } sock.Close(); return 0;}/**************************************************************************文件名: tcp_ser.cpp*作者: bean*郵箱: 17331372728@163.com* 創建時間: Fri 24 May 2019 07:27:34 AM PDT* 備注:通過封裝的TcpSocket類實現服務端程序* 1、創建套接字* 2、綁定地址信息* 3、開始監聽* 4、獲取連接成功的客戶端新建socket* 5、通過新建的socket與客戶端進行通信* 6、關閉套接字*************************************************************************/#include"tcpsocket.hpp"#include<signal.h>#include<sys/wait.h>void sigcb(int no){ while(waitpid(-1, NULL, WNOHANG) > 0);}int main(int argc, char* argv[]) if(argc != 3) { cout << "./tcp_ser ip port" << endl; return -1; } //如果多個進程同時退出了,要等他處理完了再退出 signal(SIGCHLD, sigcb); string ip = argv[1]; uint16_t port = atoi(argv[2]); TcpSocket sock; CHECK_RET(sock.Socket()); CHECK_RET(sock.Bind(ip, port)); CHECK_RET(sock.Listen()); while(1) { //程序會卡在accept是應為用戶無法獲知客戶端的新連接什么時候到來 //如果能知道什么時候盜壘就不會阻塞,只需要在來的時候調用一次就行 TcpSocket client; if(sock.Accept(client) == false) { continue; } //創建一個子進程負責聊天 int pid = fork(); if(pid == 0) { while(1) { string buff; client.Recv(buff); cout << "Client say:" << buff << endl; cout << "Server asy:"; fflush(stdout); cin >> buff; client.Send(buff); } } client.Close(); } sock.Close(); return 0;}

    基于UDP的socket

    服務端流程

    創建套接字文件描述符,調用socket();設置服務器地址和監聽關口,初始化要綁定的網絡地址結構。綁定監聽端口,調用bind()函數,接收客戶端數據,調用recvfrom()函數向客戶端發送數據,調用sendto()函數向客戶端發送數據關閉套接字,調用close()函數釋放資源。

    客戶端流程

    創建套接字文件描述符,調用socket()設置服務器地址和端口,struct sockaddr()向服務端發送數據,調用sendto(),接受服務端的數據,調用recvfrom()關閉套接字,調用close()函數釋放資源。 /*************************************************************************-> 文件名: udp_socket.hpp-> 作者: bean-> 郵箱: 17331372728@163.com-> 創建時間: Thu 23 May 2019 04:20:18 AM PDT-> 備注:分裝一個udp的套接字接口類 實現: 套接字創建、綁定地址、接收數據、發送數據、關閉套接字*************************************************************************/#include<iostream>#include<string>#include<sys/socket.h>#include<errno.h>#include<netinet/in.h>#include<arpa/inet.h>#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<string.h>#include<unistd.h>#define CHECK_RET(q) if((q) == false){return -1;}using namespace std;class UdpSocket{ public: UdpSocket() { } ~UdpSocket() { } bool Socket() //創建套接字 { //int socket(int domain, int type, int protocol); //domain: 地址域,確定通信范圍 // AF_INET 表示使用IPV4網絡協議 //type: 套接字類型 // SOCK_STREAM 流式套接字----默認使用TCP // SOCK_DGRSM 數據包套接字--默認使用UDP //protocol: // 0 使用默認協議 // IPPROTO_TCP 6 TCP協議 // IPPROTO_UDP 17 UDP協議 //返回值:套接字操作句柄---文件描述符 失敗:-1 _sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if(_sock < 0) { perror("socket error"); return false; } return true; } bool Bind(string &ip, uint16_t port) //為套接字綁定地址信息 { struct sockaddr_in addr; addr.sin_family = AF_INET; //因為網絡通信使用大端字節序,因此端口和IP地址信息都要轉換成網絡字節序 //uint16_t htons(uint16_t hostshort); // 將一個16為數據從主機字節序轉換為網絡字節序 //uint32_t htonl(uint32_t hostlong); //將一個32為數據從主機字節序轉換為網絡字節序 //不能使用htonl,因為端口只有2的字節,由于htonl是兩個字節來轉換的,所以經htonl后,還是原來的 addr.sin_port = htons(port); //in_addr_t inet_addr(const char *cp); //將字符串點分十進制IP地址,轉換為網絡字節序的整數IP地址 addr.sin_addr.s_addr = inet_addr(ip.c_str()); //int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //sockfd: 創建套接字返回套接字描述符 //addr: 要綁定的地址信息 //addrlen: 地址信息長度 //返回值: 成功:0 失敗:-1 socklen_t len = sizeof(struct sockaddr_in); int ret = bind(_sock, (struct sockaddr*)&addr, len); if(ret < 0) { perror("Bind error"); return -1; } return true; } bool Recv(string &buf, sockaddr_in *_addr = NULL) //接收數據 { //recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); //sockfd: 套接字描述符 //buf: 用于保存接收的數據 //len: 要接受的數據長度 //flags: 默認0,表示阻塞接收,沒有數據就阻塞 //src_addr: 發送端的地址信息 //addelen: 用于獲取地址信息長度(輸入輸出型參數) //返回值: 實際接收數據長度,錯誤返回-1 char tmp[1024] = {0}; struct sockaddr_in addr; socklen_t len = sizeof(struct sockaddr_in); int ret = recvfrom(_sock, tmp, 1024, 0,(struct sockaddr*)&addr, &len); if(ret < 0) { perror("recvfrom error"); return false; } buf.assign(tmp, ret); //從tmp中拷貝獲取的實際長度到buf中 if(_addr != NULL)//如果用戶想要獲取發送端的地址,這是_addr就不為空,這時就將獲取到的地址拷貝到_addr中 memcpy(_addr,&addr,len); return true; } bool Send(string &buf, struct sockaddr_in &addr) //發送數據 { //sendto(int sockfd, void *buf, size_t len, int flags,struct sockaddr *dest_addr, socklen_t addrlen); //dest_addr: 目的端地址信息 //addrlen: 發送地址信息長度 socklen_t len = sizeof(struct sockaddr_in); int ret = sendto(_sock, buf.c_str(), buf.size(), 0,(struct sockaddr*)&addr, len); if(ret < 0) { perror("sendto error"); return false; } return true; } bool Close() //關閉套接字 { close(_sock); _sock = -1; return true; } private: int _sock;};/*************************************************************************-> 文件名: udp_ser.cpp-> 作者: bean-> 郵箱: 17331372728@163.com-> 創建時間: Thu 23 May 2019 04:08:16 AM PDT-> 備注:使用UdpSock類完勝UDP服務端程序*************************************************************************/#include"udp_socket.hpp"int main(int argc, char *argv[]){ if(argc != 3) { cout << "./dup_srv ip port" << endl; return -1; } string ip = argv[1]; uint16_t port = atoi(argv[2]); UdpSocket sock; //創建套接字 CHECK_RET(sock.Socket()); CHECK_RET(sock.Bind(ip, port)); while(1) { string buff; struct sockaddr_in c_addr; sock.Recv(buff, &c_addr); cout << "client asy:" << buff.c_str() << endl; cout << "server say:"; fflush(stdout); getline(cin, buff); sock.Send(buff, c_addr); } sock.Close(); return 0;}/*************************************************************************-> 文件名: udp_cli.cpp-> 作者: bean-> 郵箱: 17331372728@163.com-> 創建時間: Fri 24 May 2019 05:06:34 AM PDT-> 備注: 通過udpsocket實現客戶端程序*************************************************************************/#include "udp_socket.hpp"int main(int argc, char* argv[]){ if(argc != 3) { cout << "./dup_cli ip port" << endl; return -1; } string ip = argv[1]; uint16_t port = atoi(argv[2]); UdpSocket sock; CHECK_RET(sock.Socket()); struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(port); s_addr.sin_addr.s_addr = inet_addr(ip.c_str()); while(1) { string buff; cout << "client say:"; getline(cin, buff); sock.Send(buff, s_addr); buff.clear(); sock.Recv(buff); cout << "server say:" << buff << endl; } sock.Close(); return 0;}

    四、應用層的協議

    基于TCP的有FTP、Telnet、SMTP、HTTP、POP3與DNS
    基于UDP的有TFTP、SNMP與DNS
    其中DNS既可以基于TCP,也可以基于UDP。

    FTP(File Transfer Protocol):文本傳輸協議 端口號: 20 和21一個端口用于控制,一個端口用于傳輸數據

    Telnet:端口號為23,功能:遠程管理,而在Linux中為SSH 端口號為22

    SMTP: 發送郵件 TCP:25

    POP3:接收郵件TCP:110

    HTTP:超文本傳輸協議 TCP:80
    HTTPS:相對于HTTP安全 對數據包進行加密 TCP:43

    DNS:域名解析服務 網絡下標如果出現談好很可能是DNS配置不對 TCP UDP:53

    TFTP:簡單文件傳輸協議 早先FTP服務器代碼太大了相對于服務器的容量來說,所以主要傳輸一些小文件的時候就是用TFTP,對于網絡設備的維護一直使用 端口號為69

    五、HTTP協議

    HTTP協議(超文本傳輸協議),是互聯網上引用最廣泛的一種網絡協議,所有的www文件都要必須遵守這個標準。HTTP和TCp是不沖突的,HTTP定義在應用層、TCP是在傳輸層,解決的是傳輸層的邏輯。HTTP使用TCP不使用UDP是因為打開一個網頁需要傳送很多的數據,而TCP提供傳輸控制,按順序數字數據和錯誤糾正,并且保證數據的可靠傳輸。

    URL、URI、URN的區別

    URI:用來唯一標識一個資源。由URL和URN組成。

    URL:統一資源定位:通過描述資源的位置來標識資源

    URN:通過名字來標識資源,與其位置沒有關系,及時資源的位置發生了變化URN也不會變化

    請求報文格式(請求行、請求頭部、請求正文)

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-TPGBDTCi-1637763958112)(D:/%E6%9C%89%E9%81%93%E7%AC%94%E8%AE%B0/%E6%9C%89%E9%81%93%E6%96%87%E4%BB%B6/qqFD408D4C0254F01133B894C843A4BA78/3bcc5219fdc64197a7027c676ad4a85f/clipboard.png)]

    請求行

    請求方法:GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE

    URL:通過描述資源的位置來標識資源。

    協議版本:常見有1.0和1.1

    常見的請求頭

    請求頭部

    請求頭部為請求報文添加了一些附加信息,由“名/值”組成,每行一對,名和值之間使用冒號分割

    常見的請求頭

    請求頭說明
    Host接受請求的服務器地址,可以是IP:端口號,也可以是域名
    User-Agent發送請求的應用程序名稱
    Connection指定與連接相關的屬性,如Connection:Keep-Alive
    Accept-Charset通知服務端可以發送的編碼格式
    Accept-Encoding通知服務端可以發送的數據壓縮格式
    Accept-Language通知服務端可以發送的語言

    請求頭部的最后會有一個空行,表示請求頭部結束,接下來為請求報文,這一行必不可少

    請求正文

    響應報文格式

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ppe4AVAq-1637763958113)(D:/%E6%9C%89%E9%81%93%E7%AC%94%E8%AE%B0/%E6%9C%89%E9%81%93%E6%96%87%E4%BB%B6/qqFD408D4C0254F01133B894C843A4BA78/e011fb9e44af4a808701792c293fb77c/clipboard.png)]

    狀態行

    協議版本

    狀態碼:100 ~ 199信息狀態碼(1.1),200~299表示成功,300 ~ 399資源重定向, 400 ~ 499 表示客戶端請求出錯, 500 ~ 599表示服務端出錯。

    常見狀態碼

    狀態碼狀態說明
    200響應成功
    301永久重定向,搜索引擎將刪除原地址,保留重定向地址
    302暫時重定向,重定向地址由響應投中的Locaition屬性指定,由于搜索引擎的判定問題,較為復雜的URL容易被其他的網站使用更為精簡的URL及302重定向劫持
    304緩沖文件并未過期,還可以繼續使用,無需再次從服務端獲取
    400客戶端請求語法錯誤,不能被服務器識別
    403服務器收到了請求,但是拒絕提供服務
    404請求的資源不存在
    500服務器的內部錯誤

    響應頭部

    與請求頭部類似,為響應報文添加了一些附加信息

    響應頭說明
    Server服務器應用程序軟件的名稱和版本
    Content-Type響應正文的類型(是圖片還是二進制字符串)
    Content-Length響應正文長度
    Content-Charset響應正文使用的編碼
    Content-Encoding響應正文使用的數據壓縮格式
    Content-Language響應正文使用的語言

    六、請求方法

    HTTP1.0中定義了三種請求方法:GET,POST,HEAD方法。

    HTTP1.1中新加了五種請求方法:OPTIONS,PUT,DELETE,TRACE,CONNECT

    常見的請求方法

    序號方法描述
    1GET請求指定的頁面信息,并返回實體主體
    2HEAD類似于GET請求,只不過返回的響應中沒有具體的內容,用于獲取報頭
    3POST向指定的資源提交數據進行處理請求。數據被包含在請求體中,POST請求可能會導致新資源的建立或已有資源的修改。
    4PUT從客戶端向服務器傳送的數據取代指定的文檔內容。
    5DELETE請求服務器刪除指定的頁面。
    6CONNECTHTTP/1.1協議中預留該能夠將連接改為管道方式的代理服務器
    7OPTIONS允許客戶端查看服務器的性能
    8TRACE回顯服務器收到的請求,主要用于測試或診斷

    七、GET與POST比較

    本質區別:

    get是從指定資源中獲取數據

    post是向指定資源中提交將要被處理的數據

    1、生成方式

    get可以直接在URL地址欄中輸入URL,還可以網頁中的超鏈接生成

    post只知道一種,就是將html中的form標簽的method屬性改為post

    2、數據的傳輸方式

    GET和POST只是HTTP協議中的兩種請求方式,而HTTP是基于TCP/IP的應用層協議,所以用的都是同一個傳輸層的協議,所以在傳輸上沒有區別。

    在報文格式上,不帶參數時最大的區別就是第一行方法名不同:POST方法請求報文第一行是這樣的POST/url HTTP/1.1 GET方法請求報文第一行是這樣的 GET/url HTTP/1.1

    帶參數的區別是GET請求的數據會附加在URL之后,以**?**分割URL,多個參數使用&連接,URL的編碼格式采用的是ASCII編碼,也就是說所有的非ASCII字符都要編碼之后才可以傳輸,POST請求:POST請求會把請求的數據放置在HTTP請求包的包體中。所以說GET請求的數據會暴露在地址欄中,但是POST請求不會,所以POST相對于GET來說相對安全,但是如果是使用HTTP,那么他們都是不安全的,因為HTTP是明文傳輸,沒有對數據進行加密,也就是說POST請求的數據也是可見的,但是使用HTTPS,POST的數據就是安全的。

    3、傳輸數據的大小

    在HTTP規范中,沒有對URL的長度和傳輸的數據進行限制,但是在實際的開發中特定的瀏覽器和服務器對URL的長度是有限制的,所以在使用GET請求的時候傳輸的數據就會受URL的長度限制,一般是不超過2KB,但是對于POST來說,理論上來說并不會受到URL的限制,但是實際上各個服務器都會規定對POST提交的數據大小進行限制。

    在實際開發中對URL的長度進行限制是因為對于服務器來說URL長了是一種負擔,如果一個沒有多少數據的會話,但是被人惡意的構造了一個幾M大小的URL,并且不停的訪問服務器,那么服務器的并發數顯然就會下降。還有一種攻擊就是將Content-Length設置為一個很大的數,然后只給服務器發送很短的數據,這時候服務器就會進行等待,只有等待超時了服務器才可以繼續做其他的,這樣也會降低服務器的效率,所以要給URL的長度進行限制。

    GET在瀏覽器回退時是無害的,POST會再次提交請求。。

    GET請求只能進行url編碼,而POST支持多種編碼方式。

    GET請求參數會被完整保留在瀏覽器歷史記錄里,而POST中的參數不會被保留。

    GET只接受ASCII字符的參數的數據類型,而POST沒有限制

    八、HTTP1.0和HTTP1.1的區別

    主要有五個方面的區別:長連接、HOST域、寬帶優化、消息傳遞、緩存

    1、長連接

    長連接指的是傳輸完成了保持TCP連接不斷開,等待在同域名下繼續用這個通道傳輸數據,反之就是短連接。

    HTTP1.1支持長連接和請求的流水線處理,并且默認使用長連接,只有介入“connection:close”才斷開連接。

    HTTP1.0默認使用短連接,規定客戶端和服務器只能保持短暫的連接,客戶端的每次請求都需要與到服務端建立一個TCP連接,服務器完成請求處理后立即斷開TCP連接,服務器不跟蹤客戶端也不記錄之前的請求。

    如果HTTP1.0想要建立長連接,可以在請求消息中包含Connection:Keep-Alive頭域,如果服務器想要維持這條連接,在相應的消息中也會包含一個Connection:Keep-Alive的頭域。

    2、HOST域

    HTTP1.1在Request消息頭里多了一個Host域,并且是必須傳的,HTTP1.0中沒有這個域。

    在HTTP1.0中認為每個服務器都綁定唯一一個IP地址,所以在請求消息中的URL并沒有傳遞主機名。但隨著虛擬主機技術的發展,在一臺物理服務器上可以存在多個虛擬主機,并且他們共享一個IP地址。

    HTTP1.1的請求小和響應消息都應支持Host頭域,且請求消息中如果沒有Host頭域匯報稿一個錯誤(狀態碼400),并且服務器應該接受一絕對路徑標記的資源請求。

    3、帶寬優化

    HTTP1.0中,存在一些浪費寬帶的現象 ,比如說客戶端只是需要某個對象的一部分資源,但是服務器將整個對象都傳送過來了。又比如下載大文件時不支持斷點續傳功能,所以在發生斷鏈后不得不重新下載完整的包。

    HTTP1.1中在請求消息中引入了range頭域,它支持之請求資源的某個部分。他在響應消息中Content-Range頭域聲明了返回的這部分對象的偏移值和長度。如果服務器相應地反悔了對象所請求范圍的內容,則響應狀態碼為206,它可以防止Cache將響應誤以為是完整的一個對象。

    HTTP支持只發送header信息,如果服務器認為客戶端有權限請求服務器,則返回100,否則返回401,只有客戶端接收到了100,才開始把請求body發送到服務器。如果接收到401,客戶端就可以不用發送請求body了,這樣可以節約帶寬。

    4、消息傳遞

    HTTP消息中可以包含任意長度的尸體,通常他們使用Content-Length來給出消息結束的標志。但是對于很多動態產生的響應,只能通過緩沖完整的消息來判斷消息的大小,但這樣會加大延遲。如果不適用長連接,還可以通過連接關閉的信號來判定一個消息的結束。

    HTTP1.1zongoing引入了Chunke來解決上面的這個問題,發送方將消息分割成若干個任意大小的數據塊,每個數據塊在發送時都會附上塊的長度,最右用一個零長度的塊作為消息的結束標志。這種方法允許發送方只緩沖消息的一個片段,還可以避免緩沖整個消息帶來的過載。

    在HTTP1.0中,有一個Content-MD5的頭域,要計算這個頭域需要發送方緩沖完整個消息后才能進行,這在HTTP1.1中,采用chunked分塊傳遞的消息在最后一個塊結束后會再傳遞一個拖尾,它包含一個或多個頭域,這些頭域是發送方在傳遞完所有塊之后在計算出的值。

    5、緩存

    HTTP1.0中使用Expire頭域來判斷資源是否是新資源,并使用條件請求來判斷資源是否仍然有效。

    HTTP1.1在1.0的基礎上加入了一些新特性,當緩存對象的生命超過了周期編程stale的時候,不需要直接拋棄stale對象,而是與源服務器進行重新激活。

    九、HTTP流水線技術

    HTTP流水線技術指的是在一個TCP鏈接內,多個HTTP請求可以并行,客戶端不用等待上一次請求的結果返回,就可以發出下一次的請求,但是服務器必須要按照接收到客戶端的請求的順序依次會送響應的結果,以保證客戶端可以區分出每次請求響應的內容。使用HTTP流水線技術必須要求客戶端和服務端都要支持,目前有部分瀏覽器完全支持,而服務端的支持僅需要按照HTTP請求順序正確返回,也就是先來先服務模式,只要服務器能夠正確處理使用流水線技術的客戶端的請求,那么服務器就算是支持了HTTP流水線技術。

    GET 用于獲取信息,是無副作用的,是冪等的,且可緩存 POST 用于修改服務器上的數據,有副作用,非冪等,不可緩存

    十、HTTP無狀態問題

    http是無狀態協議,無狀態也就是對于十五處理沒有記憶能力。缺少狀態意味著如果后續處理需要之前的信息。使用cookie和session來解決無狀態問題。

    十一、cookie和session的區別

    cookie

    是客戶端技術,程序把每個用戶的數據以cookie的形式寫給用戶各自的瀏覽器。當用戶使用瀏覽器再去訪問服務器的時候,就會帶著各自的數據過去,這樣就服務器就知道當前是哪個用戶,并處理對應的數據。會話cookie保存在瀏覽器的內存中,瀏覽器關閉后cookie就消失了,持久化cookie是保存在本地的磁盤上,一般會有一定的過期時間。

    session

    是服務器技術,利用這個技術,服務器可以為每個用戶的瀏覽器創建一個獨立的session對象,并且每個session都有自己唯一對應的ID,可以把各自的數據保存在自己的session中,這樣當用戶再去訪問服務器的時候,服務器就可以用當前用戶的session中取出數據為之服務。session都有過期時間,如果一段時間沒有更新數據庫,就會消失。

    區別

    1、cookie和session都是會話技術,cookie數據存放在客戶端,session的數據存放在服務器。

    2、cookie不是很安全,可以對本地存放的cookie進行cookie欺騙,考慮到安全應該使用session。

    3、session會在一定的時間內保存在服務器中,當訪問增多,會比較占用服務器性能,所有考慮到減輕服務器的負載,應該使用cookie

    4、登陸注冊等重要信息要存放在session中,其他的信息可以保存在cookie中

    5、Cookie 只能保存ASCII字符串,如果是Unicode字符或者二進制數據需要先進行編碼。Session能夠存取很多類型的數據,包括String、Integer、List、Map等。

    十九、怎么讓客戶端和服務端一直保持連接

    通情況下tcp的連接是一直保持很長時間,但是有可能這個鏈接會一直空閑著,這樣會造成資源浪費,所以TCP就提供了保活機制和心跳報文

    保活機制

    也就是keepalive,在客戶端和服務端都可以設置,通常是服務端設置的。保活時長一般是2小時,如果這個tcp連接空閑了兩個小時,這個保活計時器超時,服務端會向客戶端發送一個探測報文,這時客戶端通常處于這么幾個狀態。

    1、客戶端處于正常狀態,收到探測報文后發送了響應報文,服務端知道客戶端是正常的,將保活計時器復位。

    2、客戶端已經崩潰,正在關機或者重啟,沒有響應服務端的探測報文, 服務端由于沒有收到回復,會每個75秒發送一個探測報文,如果連續發送10探測報文都沒有響應,就會認為客戶端掛了,會關閉TCP連接。

    3、客戶端已經重啟,并且正常運行了,但是在重啟之前關閉了tcp,這個客戶端收到探測報文,回應一個RST,服務端收到這個報文將TCP連接關閉。

    4、客戶端正常運行,但是接收不到服務端的報文,可以說是和情況2是一樣的,服務端并不能去放是網絡問題還是客戶端的問題,所以同樣會將連接關閉。

    心跳報文

    每隔一段時間向對端發送一個較小的數據包,通知對方自己在線,并傳送一些可能必要的數據包,并且定時檢測對端返回的數據,如果連續幾次沒有在規定的時間中收到回復,則判斷對端已經掉線,然后做進一步處理,這個方法適合客戶端,在應用層開一個線程發送心跳包,檢測對端情況。

    總結

    以上是生活随笔為你收集整理的C++校招常见面试题(2019年校招总结)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。