【C++后台开发面经】面试总结第八波:整个知识的查漏补缺
前言
? ?面試總結第八波,面試了騰訊、百度、阿里、虎牙直播等幾個公司,然后總結了這一波面經,主要針對前面總結的那些,在面試時,有些被問到了,所以進行的一個查漏補缺總結。
1、C++
unique_ptr的用途
特性總結:1)默認情況下,占用內存大小和raw指針一樣(除非指定了用戶自定義deleter);2)運行過程中unique_ptr消耗資源和raw指針一樣;3)unique指針只可以進行轉移操作,不能拷貝、賦值。所以unique指針作為函數入參數類型的時候,函數的調用方必須使用轉移語義;4)允許在定義unique指針的時候,指定用戶自定義的指針銷毀函數(在指針析構的時候會回溯);5)從一個unique指針轉換成shared指針很容易
使用場景:1)用作工廠函數的返回類型
unique_ptr無拷貝構造函數,僅能通過move進行轉移;只有在計數歸零的時候,shared_ptr才會真正釋放所占用的堆內存空間;weak_ptr可以指向shared_ptr指針的對象內存,卻并不擁有該內存,weak_ptr成員函數lock(),則可以返回指向內存的一個shared_ptr對象,且在所指對象內存已經無效的時候,返回指針空值(nullptr)。
智能指針和普通指針混合使用
當將一個shared_ptr綁定到一個普通指針時,我們就將內存的管理責任交給了這個shared_ptr。一旦這樣做了,我們就不應該再使用內置指針來訪問shared_ptr所指向的內存了。
如果傳入形參在前面沒定義,那函數結束后就會銷毀不能再用,如果先定義了這個數據再傳入函數,函數結束了也不影響這個數據。
1)普通指針轉化成智能指針:shared_ptr<int> p(iPtr);
2)智能指針轉化成普通指針:int *iPtr=p.get();
注意:
1)普通指針轉智能指針
void f(shared_ptr<int> ptr){ //值傳遞,增加引用計數//do something }//銷毀ptr,減少引用計數一直使用智能指針可解決問題。
2)智能指針轉普通指針
auto p=make_shared<int>(42); int *iPtr=p.get(); {shared_ptr<int>(iPtr); } int value=*p; //Error! 內存釋放p與iPtr指向了相同的內存,然而通過get方法后,將內存管理權轉移給普通指針。iPtr傳遞給里面程序塊的臨時智能指針后,引用計數為1,隨后出了作用域,減少為0,釋放內存。
使用shared_ptr<int>(智能指針)時需要格外注意,因為這個不會增加智能指針的計數器,但是在離開作用域后會減少計數器。
vector
1)當容量不足時,即finish==end_of_storage,調用vector的成員函數insert_aux(end(), x)函數進行擴容,復制。
2)vector的效率低原因:當vector預留空間不足時,要重新分配內存,并且拷貝當前已有的所有元素到新的內存區域。
3)解決效率低方法:可以預先估計元素個數,用reserve函數進行預留空間的分配。
map/set區別
都是關聯式容器
set:以紅黑樹作為底層容器;所得元素只有key沒有value,value就是key;不允許出現鍵值重復;所有的元素都會被自動排序;不能通過迭代器來改變set的值,因為set的值就是鍵。
map:以紅黑樹作為底層容器;所有元素都是以鍵+值存在;不允許鍵重復;所有元素是通過鍵自動排序的;map的鍵是不能修改的,但是其鍵對應的值是可以修改的。
inline
1)使用了inline關鍵字的函數只是用戶希望它成為內聯函數,但編譯器有權忽略這個請求;
2)關鍵字inline必須與函數定義體放在一起才能使函數稱為內聯,僅將inline放在函數聲明前面不起任何作用;
3)inline函數可以定義在源文件中,但多個源文件中同名inline函數的實現必須相同,一般把inline函數定義放在頭文件中更加合適;
4)類中的成員函數,默認都是inline的。
內聯函數就是將很簡單的函數“內嵌”到調用他的程序代碼中,為了避免原本函數調用時的時空開銷(保護現場,恢復現場)。
常量表達式:允許一些計算發生在編譯時,即發生在代碼編譯而不是運行的時候,優化:假如有些事情可以在編譯時做,它將只做一次,而不是每次程序運行時都計算。
constexpr函數限制:1)函數中只有一個return語句;2)函數必須返回值;3)在使用前必須已有定義;4)return返回語句表達式不能使用非常量表達式的函數、全局數據,且必須是一個常量表達式。
常量表達式的構造函數:1)函數體必須為空;2)初始化列表只能由常量表達式來賦值?
多線程
?
原子類型和原子操作
原子操作:多個線程訪問同一個資源時,有且僅有一個線程對資源進行操作。可以通過加鎖保證。
C++新增了原子類型:atomic_llong、atomic_int等,它們都是用atomic<T>模板定義的,例如std::atomic_llong就是用std::atomic<long long>來定義的。C++11中將原子操作定義為atmoic模板類的成員函數,包括大多數類型的操作,比如讀寫、交換等。對于內置類型,主要通過重載全局操作符來實現。?operator+=()函數會產生一條特殊的以lock為前綴的x86_64指令,用于控制總線及實現x86_64平臺上的原子性加法。
atomic_flag一種簡單的原子布爾類型,只支持兩種操作:test_and_set和clear
使用ATOMIC_FLAG_INIT宏初始化,可以保證該對象創建處于clear狀態。
CPU指令是多線程不可再分的最小單位,如果我們有辦法將代碼語句和指令對應起來,就不需要引入互斥鎖從而提高性能?而這個對應關系就是所謂的原子操作,C++11中的atomic中有兩種做法:1)模擬,對于一個atomic<T>類型,可以給他附帶一個mutex,操作時lock/unlock一下,這種在多線程下進行訪問,必然會導致線程阻塞;2)有相應的CPU層級的對應,這就是一個標準的lock-free類型。
例如:在執行自增操作的時候,在xaddl指令前多了一個lock前綴,而CPU對這個lock指令的支持就是所謂的底層硬件支持,增加了這個前綴后,保證了load-add-store步驟的不可分割性。
lock指令的實現
CPU在執行任務的時候并不是直接從內存中加載數據,而是會先把數據加載到L1和L2的cache中(典型的是兩層緩存,甚至更多),然后再從cache中讀取數據進行運算。
現在計算機通常都是多核處理器,每個內核都對應一個獨立的L1層緩存,多核之間的緩存數據同步是CPU框架設計的重要部分,MESI是比較常用的多核緩存同步方案。當我們在單線程內執行atomic++操作,自然不會發生多核之間數據不同步的問題,但是我們在多線程多核的情況下,cpu是如何保證lock特性呢?
以intel x86架構的cpu為例:lock前綴實現原子性的兩種方式:1)鎖bus:性能消耗大,在intel 486處理器上用此種方式實現;2)鎖cache:在現代處理器上使用此種方式,但是無法鎖定cache的時候(如果鎖駐留在不可緩存的內存種,或者鎖超出了劃分cache line的cache body),任然會去鎖定總線。
extern
1)聲明外部變量
各個文件定義的全局變量是互相透明的,在鏈接時,要將各個文件的內容合為一體,因此某些文件中定義的全局變量名相同的話,在這個時候就會出現錯誤,也就是會出現重定義的錯誤。extern的原理很簡單,就是告訴編譯器,現在編譯的文件中,這個標識符雖然沒有在本文中定義,但是在別的文件中定義的全局變量。
2)在C++文件中調用C定義的變量
因為C++中新增了諸如重載新特性,所以全局變量和函數名編譯后的命名方式有很大區別,使用extern "C"{ int iRI;}告訴編譯器,iRI是使用C方式編譯的。
3)C++調用C定義的function?
static:1)函數內;2)模塊內;3)類中變量;4)類中函數
const:1)阻止一個變量被改變;2)聲明常量指針和指針常量;3)修飾形參,表明該輸入參數在函數內部不能改變其值;4)修飾類的成員函數,是指不能修改類的成員變量;5)對于類的成員函數,有時候必須指定其返回值為const類型,以使得其返回值不為“左值”。
STL的map和unordered_map
1)構造函數:?unordered_map需要hash函數,比如取模函數;map只需要比較函數
2)存儲結構:unordered_map采用hash表存儲;map一般采用紅黑樹實現
3)查找時間復雜度:unordered_map是O(1);map是log(n)
4)插入時間復雜度:unordered_map取決于哈希函數,平均大概O(c);map是log(n)
5)是否有序:unordered_map無序;map按照key有序
使用場景:
unordered_map查找速度比map塊,而且查找速度基本和數據量大小,屬于常數級別,但內存消耗大。
?vector中的emplace_back為何優于push_back
如果一個對象有顯示的構造函數,則使用emplace_back會省去調用本類的構造函數;emplace_back("yyqx"),push_back(CText("yyqx"));省去了調用CText進行構造。
添加一個元素到結束容器,該元件是構成在就地,即沒有復制或移動操作進行。push_back會先構造一個臨時對象,再使用拷貝構造函數進行拷貝。
emplace_back(vec.back()):會出現問題,因為emplace_back可能會造成迭代器失效。
string
string s1="asdf"; //使用帶參的構造函數 string s2=s1; //使用拷貝構造函數 "asdf"在內存中有一份 string s3=string("sdfgh"); //使用帶參的構造函數 string s4; //執行了無參的構造函數 s4=s1; //執行了=賦值操作引用和指針的區別
本質:引用是別名,指針是地址
1)從現象上看,指針在運行時可改變其所指向的值,而引用一旦和某個對象綁定后就不再改變。即指針可以被重新賦值以指向另一個不同的對象,但是引用總指向在初始化時被指定的對象,以后不能改變,但是指定對象內容可以改變;
2)從內存上分配看,程序為指針變量分配內存空間,而不用為引用分配內存區域,引用聲明時必須初始化,從而指向一個已經存在的對象,引用不能指向空值;
3)sizeof(引用類型)的結果是被引用對象的大小,而不是引用本身的大小。
左值引用:從一個變量取得地址,然后賦值給引用變量
右值引用:會對變量進行一個拷貝在臨時變量中,這個臨時變量沒有名字而已,它的生命周期和函數棧幀是一致的,也可以說是臨時變量和它的引用具有相同的生命周期。
const int &i=10,內部和右值引用沒有什么區別。
能將右值引用賦值給左值引用。
?把局部變量改變為靜態變量后改變了它的存儲方式即改變了它的生存期,static局部變量只被初始化一次;把全局變量改為靜態變量后是改變了它的作用域,限制了它使用的范圍。
?hello.c——預編譯器(hello.i)——編譯器(hello.s)——匯編器(hello.o)——鏈接器(可執行文件)
符號表
存在于系統中,如常數表、變量名表、數組名表、過程名表、標號表等,統稱為符號表。
符號表屬性:符號名、符號類型、符號存儲類別、符號的作用域及可視性、符號變量的存儲分配信息
拷貝構造函數為什么使用引用類型?
因為如果不使用引用傳參,那么在傳參時,會進行調用拷貝構造函數,那么又會觸發拷貝構造函數,就這下永遠的遞歸下去。所以拷貝構造函數使用引用類型不是為了減少一次內存拷貝,而是避免拷貝構造函數無限遞歸下去。導致棧溢出。
賦值運算符重載函數需要避免自賦值
因為可能導致內存泄漏,使用懸掛指針。僅僅內容相同的賦值不是自賦值。?
對于移動構造函數,拋出異常比較危險,因為可能移動語義還沒有完成,就拋出了異常,從而導致一些指針稱為懸掛指針。使用noexcept關鍵字即可,這樣當拋出異常時,程序就會被std::terminate()終止。?
強類型枚舉
enum class M_Type::char{value1,value2};??
1)枚舉常量不會暴露在外層作用域中;2)枚舉值不會被隱式轉換成整數,無法和整數值比較;3)枚舉類型所使用的類型默認為int類型,也可以指定其他類型?
虛函數表和vptr指針
虛表指針的初始化,在構造子類對象時,要先調用父類的構造函數,此時編譯器只“看到了”父類,并不知道后面是否還有繼承者,它初始化父類的虛表指針,該虛表指針指向父類的虛表,當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。
C++編譯器在編譯的時候,發現這個函數是虛函數,這個時候C++就會采用晚綁定技術,也就是編譯時并不確定具體調用的函數,而是在運行時,依據對象的類型來確定調用的哪一個函數。
?虛表是和類對應的,虛表指針是和對象對應的。
虛函數表是由編譯器自動生成與維護的,存儲類成員函數指針的數據結構。虛函數表屬于類,類的所有對象共享這個類的虛函數表。虛函數表由編譯器在編譯時生成,保存在可執行文件的.rdata只讀數據段。
每個對象都有一個指向虛函數表的指針,vptr指針,C++編譯器不需要區分子類或者父類對象,只需要在base指針中,找到vptr指針即可。vptr一般作為類對象的第一個成員。
多重繼承,會有多個虛函數表。
override關鍵字:確保該函數為虛函數并覆蓋來自基類的虛函數,如果基類無此函數,或基類中函數并不是虛函數,編譯器會給出相關錯誤信息。
靜態聯編:在編譯的時候就確定了函數的地址,然后call就調用了;
動態聯編:首先需要取到對象的首地址,然后解引用取到虛函數表的首地址,再加上偏移量才能找到要調的虛函數,然后call調用。
子類和父類的析構函數名字不一樣,但作為虛函數,也可以構成重寫,因為編譯器對析構函數的名字做了特殊處理,在內部函數名是一樣的。
RTTI機制
RTTI:提供了運行時確定對象類型的方法。typeid函數返回的是一個結構體或者類,然后,再調用這個返回結構體或類的name成員函數。
dynamic_cast主要用于在多態的時候,它允許在運行時刻進行類型轉換,從而使程序能夠在一個類層次結構中安全地轉換類型,把基類指針(引用)轉換為派生類指針(引用)。
構造函數不能是虛函數:虛函數對應一個虛函數表,虛函數的調用是通過虛函數指針指向虛函數表進行調用,該指針存放在對象的內存空間中,但對象還沒實例化,還沒有內存空間,即沒有虛函數指針。
類的非靜態成員函數調用時,編譯器會傳入一個“隱藏”的參數,這個參數就是通常我們說的“this”指針,它的值就是對象的地址。后來把虛表的地址存在對象的起始地址,即對象的第一個數據成員,也就是它的虛表指針。
構造函數調用過程細分:1)進入到構造函數體之前,在這個階段如果存在虛函數的話,虛表指針被初始化,如果存在構造函數的初始化列表的話,初始化列表也會被執行;2)進入到構造函數體內,這一階段是我們通常意義上說的構造函數。?
結構體聲明只聲明一個結構體“看起來是什么樣子的”,所以不會在內存中創建成員變量。只有通過定義該結構體類型的變量來實例化結構體,才有地方存儲初始值。
棧上的變量是在程序運行時分配內存的,但分配的大小多少是確定的,而這個“大小多少”是在編譯時確定的,不是在運行時。堆是應用程序在運行的時候請求操作系統分配給自己內存,是由操作系統管理的內存分配,編譯器并不知道要從堆里分配多少內存空間。
靜態存儲:分配要求在編譯時就能知道所有變量的存儲要求;棧式存儲:分配要求在運行時必須知道所有的存儲要求;堆式存儲:分配要求在編譯時或運行時都無法確定存儲要求的數據結構。?
C++中內存分區
內存分為5個區:堆、棧、自由存儲區、全局/靜態存儲區、常量存儲區
棧:由編譯器在需要的時候分配,在不需要的時候自動清除的變量存儲區。里面的變量通常是局部變量、函數參數等;
自由存儲區:由new分配的內存塊,釋放編譯器不管,由我們的應用程序去控制。一般一個new就要對應一個delete。如果程序員沒有釋放,那么在程序結束后,操作系統會自動釋放。(無論你是怎么分配的,也無論你是分配在堆還是棧上面,很明顯,它都是屬于進程的,當程序退出時候,進程就不存在了,進程所占用的所有資源,操作系統都會收回的);
堆:由malloc等分配內存塊,由free來釋放;
全局/靜態存儲區:由編譯器在編譯階段就分配好了內存,程序結束時,由操作系統回收;
常量存儲區:比較特殊的存儲區,存放的是常量。
堆和棧
int *p=new int[5];? 在程序中先確定在堆中分配內存的大小,然后調用operator new分配內存,然后返回這塊內存的首地址,放入棧中。
靜態數據成員:在程序一開始就必須存在,因為函數在程序運行中被調用,所以靜態數據不能在任何函數內分配空間和初始化。類聲明只是聲明一個類的“尺寸和規格”,并不進行實際的內存分配,所以在類聲明中寫成定義是錯的,它也不能在頭文件中類聲明的外部定義,因為那會造成在多個使用該類的源文件中,對其重復定義。
全局變量、文件域的靜態變量和類的成員變量是在main執行之前的靜態初始化過程中分配內存并初始化的;局部靜態變量是在第一次使用時分配內存并初始化。
STL allocator將兩階段操作區分開來
內存配置由alloc::allocate 負責,對象構造由alloc::construct()負責;內存釋放由alloc::deallocate()負責,對象析構操作由::destroy()負責。
針對內存碎片問題,SGI設計了兩層的配置器,第一級配置器和第二級配置器,SGI版STL提供了一層更高級的封裝,定義了一個simple_alloc類,無論是用哪一級
第一級配置器:直接調用malloc和free來配置釋放內存
第二級配置器:根據情況來判定,如果配置區塊大于128bytes,說明足夠大,調用第一級配置器,而小于128bytes,則采用復雜內存池來管理。(維護16個自由鏈表,負責16種小型區塊的配置能力,內存池以malloc配置而得,如果內存不夠,轉調用第一級配置器)。如果自由鏈表有,則直接取走,不然則需要裝填自由鏈表。釋放操作:大于128,直接調用第一級空間配置器收回,小于等于128,則有自由鏈表收回。
自由鏈表:指針數組,類似hash表,它的數組大小為16,每個數組元素代表所掛的區塊大小。同時我們還有一個被稱為內存池地方,以start_free和end_free記錄其大小,用于保存未被掛在自由鏈表的區塊,它和自由鏈表構成了伙伴系統。
如果自由鏈表對應的位置沒有所需的內存塊,使用Refill函數實現:默認獲取20的新節點,然后返回一塊給調用者。其他掛在自由鏈表中。
系統會自動將n字節擴展到8的倍數,用戶需要n字節,且自由鏈表中沒有,因此系統會向內存池申請nobjs*n大小的內存塊,默認nobjs=20。如果內存池大于nobjs*n,那么直接從內存池中取出;如果內存池小于nobjs*n,但是比一塊大小n要大,那么此時將內存最大可分配的塊數給自由鏈表,并且更新nobjs為最大分配塊數x;如果內存池連一個區塊的大小n都無法提供,那么首先將內存池殘余的零頭給掛在自由鏈表上,然后向系統heap申請空間,申請成功則返回,申請失敗則到自己的自由中看看還有沒有可用區塊,如果連自由鏈表都沒了最后會調用一級配置器。
優點:1)避免頻繁調用malloc,free開辟釋放小塊內存帶來的性能效率的低下;2)內存碎片問題,導致不連續內存不可用的浪費。
缺點:1)內存碎片的問題,自由鏈表所掛區塊都是8的整數倍,因此當我們需要非8倍數的區塊,往往會導致浪費,以空間換時間。2)似乎沒有釋放自由鏈表所掛區塊的函數,由于配置器的所有方法,成員都是靜態的,那么他們就是存放在靜態區,釋放時機就是程序結束,這樣子會導致自由鏈表一直占用內存,自己進程可以用,其他進程卻用不了。
內存泄漏
1)內存泄漏指是在程序里動態申請的內存在使用完后,沒有進行釋放,導致這部分內存沒有被系統回收,久而久之,可能導致程序內存不斷增大,系統內存不足。。。引發一系列災難性后果。
2)檢測內存泄漏:VS下使用CRT,在程序前加上:#define CRTDBG_MAP_ALLOC,在程序最后加上:_CrtDumpMemoryLeaks(); 程序運行后在下面的窗口中可以看到內存泄漏的信息,{65}代表了第65次內存分配操作發生了泄漏,所以根據這個信息,可以定位到內存泄漏的位置,可以添加如下代碼:_CrtSetBreakAlloc(65)。
linux系統下內存泄漏的檢測方法:valgrind? ?編譯:g++ -g -o test test.cpp;使用:valgrind --tool=memcheck ./test??
指針函數和函數指針
指針函數:本質是一個函數,函數返回類型是某一類型的指針
函數指針:指向函數的指針變量,即本質是一個指針變量。int (*fun)(int x);可以通過它來調用函數。
后面倆字是本質。?
traits特性萃取技術
在STL中,算法和容器是分開的,所以算法的實現并不知道自己被傳進來什么,萃取器相當于在接口的實現之間加一層封裝,來隱藏一些細節并寫主調用合適的方法。
traits一方面,在面對不同的輸入類時,能找到合適的返回類型;原始指針無法定義型別,需通過類模板的偏特化得到型別,而迭代器能定義自己的型別。常用迭代器型別:迭代器所指對象的型別、兩個迭代器之間距離,即容量、用來指向迭代器所指之物、解引用時獲取左值。?
一個空類默認產生哪些類成員函數?
默認產生:構造函數、拷貝構造函數、析構函數、賦值運算符、取地址運算符、常量取地址運算符。
C++11:默認移動構造函數A(A&&)、移動賦值運算符A& operator=(const A&&)。?
C++類型轉換
1)static_cast:靜態類型轉換,即在編譯期間即可確定的類型轉換,能代替C風格的類型轉換
2)dynamic_cast:可以在執行期決定真正的類型。主要用于類層次間的上行轉換和下行轉換,還可以用于類之間的交叉轉換。提供了類型安全檢查。被轉換對象T1必須是多態類型,即T1必須公有繼承其他類,或者T1擁有虛函數(繼承或自定義)。對指針進行轉換,失敗返回NULL,成功返回正常cast后的對象指針;對引用進行轉換,失敗拋出異常,成功返回正常cast后的對象引用。
A *a=new B; a->aa(); B *b=dynamic_cast<B*>(a); //父類轉換成子類 b->aa();/* B *b=new B; b->aa(); A *a=dynamic_cast<A*>(b); //子類轉換成父類 a->aa(); */3)const_cast:去掉const屬性轉換,const_cast<目標類型>,目標類型只能是指針或者引用
4)reinterpret_cast:重新解釋類型轉換,相當于強制類型轉換
hash表中當某一個鏈表太長時,會把鏈表改成紅黑樹結構。?
指針和數組的區別
指針:是一個變量,存放的是其他變量在內存中的地址;同類型指針變量可以相互賦值;
賦值、存儲方式、sizeof、初始化?
32位數中1的個數
查表法利用空間換事件,將32位拆成4個8位數字,分別進行4次查表操作,然后將結果相加。??
STL容器是否是線程安全的?
1)多個讀者是安全的;2)對不同容器的多個寫入者也是安全的。
需要鎖定:1)每次調用容器的成員函數的期間需要鎖定;2)每個容器返回迭代器的生存期需要鎖定;3)每個容器在調用算法的執行期需要鎖定。?
處理Hash沖突
(1)再散列法:1)線性探測再散列;2)二次探測再散列。(2)拉鏈法?
在跨平臺進行指針傳遞時,應該用int類型還是long類型
結論:應該用long類型
int類型的長度為4個字節,而long類型是不定的,和操作系統位數保持一致(32位操作系統的long長度為32bit,4個字節,64位操作系統的long長度為64bit,8個字節)。如果用int類型傳遞64位操作系統的指針,會把高四位地址截斷,導致錯誤。而long型,由于其字節長度和操作系統位數保持一致,因此不會產生地址截斷的問題。?
2、操作系統
在同一臺主機上使用socket通信會不會經過網卡?
結論:不走網卡,不走物理設備,但走虛擬設備,loopback device環回。
本機的報文的路徑:應用層——>socket接口——>傳輸層——>網絡層——>back to傳輸層——>back to socket接口——>傳回應用程序。
測試:可以在不用網絡的情況下,進行測試。
在網絡層,會在路由表查詢路由,路由表初始化時會保存主機路由,查詢后發現不用轉發就不用走中斷,不用發送給鏈路層,不用發送給網絡設備(網卡)。像網卡發送接收報文一樣,走相同的接收流程,只不過net device是loopback device,最后發送回應用程序。?
epoll
1)支持一個進程打開大數目的socket描述符:1GB內存大約是10萬作用,和系統內存關系很大;
2)IO效率不隨FD數目增加而線性下降
3)使用mmap加速內核與用戶空間的消息傳遞?
poll
int poll(struct pollfd *fds,nfds_t nfds,int timeout)
fds:指向一個結構體數組的第0個元素的指針,每個數組元素都是一個struct pollfd{fd 文件描述符、events等待事件讀寫、revents實際發生事件}
nfds:指定第一個參數數組元素個數
mmap應用
1)malloc分配內存;2)epoll模型中用于內核態和用戶態通信。?
CPU上下文切換
上下文切換是指CPU從一個進程或線程切換到另一個進程或線程。上下文是指某一個時間點CPU寄存器和程序計數器的內容。寄存器是CPU內部的數量較少但是速度很快的內存。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度。程序計數器?
進程切換與線程切換的代價比較
進程切換分兩步:
1)切換頁目錄以使用新的地址空間;2)切換內核和硬件上下文。
切換的性能消耗:1)線程上下文切換和進程上下文切換一個最主要的區別是線程的切換虛擬內存空間依然是相同的,但是進程切換是不同的。這兩種上下文切換的處理都是通過操作系統內核來完成。內核的這種切換過程伴隨的最顯著性能損耗是將寄存器中的內容切換出。2)另外一個隱藏的損耗是上下文的切換會擾亂處理器的緩存機制。簡單的說,一旦去切換上下文,處理器中所有已經緩存的內存地址一瞬間都作廢了,還有一個顯著的區別是當你改變虛擬內存空間的時候,處理的頁表緩沖,這將導致內存的訪問在一段時間內相當的低效。但是在線程的切換中,不會出現這個問題。?
為什么虛擬地址切換很慢
現在我們已經知道了進程都有自己的虛擬地址空間,把虛擬地址轉換為物理地址需要查找頁表,頁表查找是一個很慢的過程,因此通常使用Cache來緩存常用的地址映射,這樣可以加速頁表查找,這個cache就是TLB,Translation Lookaside Buffer,我們不需要關心這個名字只需要知道TLB本質上就是一個cache,是用來加速頁表查找的。由于每個進程都有自己的虛擬地址空間,那么顯然每個進程都有自己的頁表,那么當進程切換后頁表也要進行切換,頁表切換后TLB就失效了,cache失效導致命中率降低,那么虛擬地址轉換為物理地址就會變慢,表現出來的就是程序運行會變慢,而線程切換則不會導致TLB失效,因為線程線程無需切換地址空間,因此我們通常說線程切換要比較進程切換塊,原因就在這里。
int recv(socket s,char *buf,int len,int flags);
接收端套接字描述符、緩沖區用來存儲recv函數接收到的數據、buf的長度、0
返回值:<0 出錯、=0連接關閉、>0接收到數據大小。若沒有數據,則recv會一直等。
阻塞模式下recv會一直阻塞直到接收到數據,非阻塞模式下如果沒有數據就會返回,不會阻塞著讀,因此需要循環讀取。
int send(socket s,char* buf,int len,int flags)
?返回值:<0 出錯、=0 連接關閉、>0表示發送的字節數
ps -aux 查看進程PID
lsof -p 1430:可以獲取打開的文件信息,類型、大小、節點信息等
ll proc/1430/fd:查看打開的文件描述符
netstat -nap | grep PID
Makefile和Cmake的聯系和區別
CMake:是一種跨平臺編譯工具,比make更高級,使用起來更方便。CMake主要是編寫CMakeList.txt文件,然后用cmake指令將CMakeList.txt文件轉化成make所需要的makefile文件,最后用make命令編譯源代碼生成可執行程序。?
自旋鎖:當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那么該線程將循環等待,然后不斷判斷該鎖是否能被成功獲取,直到獲取到鎖才會退出循環。
優點:自旋鎖不會使線程狀態發生切換,一直處于用戶態,即線程一直都是活躍的,不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快。非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要線程上下文切換。?
3、計算機網絡
流量控制
1)定義:TCP根據接收端緩沖區的大小,來決定發送數據的快慢,這個機制叫做流量控制。
2)原因:任何一個接受端接受的能力都是有限的,如果接受緩沖區滿了,而發送端依然發送數據,就會導致丟包,而引起超時重傳。這對資源也是一種浪費。
3)實現:接收端每次都會將自己的緩沖區大小放入到TCP報頭的“滑動窗口”字段,通過ACK確認應答的來通知發送端。如果發送端得知接收端的緩沖區很小,就會減慢發送的頻率。如果得知緩沖區滿了,發送端就不再發送數據了,但是會定期發送一個零窗口探測數據段,來得知是否可以接受數據了。
?什么時候開始三次握手?
客戶端進程connect(),服務端進行了listen()時,進行三次握手。
accept函數會從已經建立連接的隊列中取出第一個連接,并創建一個新的socket,新的socket的類型和地址參數要和原來那個的指定的socket的地址一樣,并且還要為新的socket分配文件描述符。默認會阻塞進程。
四次揮手只進行了兩次會怎么辦?
四次揮手服務器先關閉,客戶端不關閉,繼續發送數據,因為對方關閉(相當于管道中對方的讀端口關閉寫端口寫滿緩沖區就會觸發SIGPIPE信號,操作系統會強制關閉寫端),客戶端繼續寫的話,會觸發SIGPIPE信號,操作系統會強制關閉客戶端。?
time_wait
產生原因:1)為實現TCP全雙工連接的可靠釋放,確保對方收到最后的ACK,不然會超時重傳FIN;2)為使舊的數據包在網絡因過期而消失,假設當前有一條TCP連接,因某些原因,我們先關閉,接著很快以相同的四元組建立一條新連接,TCP協議棧無法區分前后兩條TCP連接是不同的,前一條TCP連接會被當做當前TCP連接的正常數據接收并向上傳遞至應用層,從而導致數據錯亂進而導致各種無法預知的詭異現象。
避免time_wait:服務器可以設置SO_REUSEADDR套接字選項來通知內核,如果端口忙,但TCP連接位于time_wait狀態時可以重用端口。例如,如果你的服務器程序停止后想立即重啟,而新的套接字依舊希望使用同一端口,此時SO_REUSEADDR選項可以避免time_wait狀態。
編輯內核文件/etc/sysctl.conf,加入以下內容:
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉; net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉; net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。 net.ipv4.tcp_fin_timeout 修改系默認的 TIMEOUT 時間TCP/IP中close_wait狀態和time_wait狀態
close_wait:說明套接字是被動關閉的,且還沒有發FIN給對方,那么可能是在關閉連接之前還有許多數據要發送或者其他事要做,導致沒有發這個FIN,發送完了自然就要通過系統調用發FIN了,這個場景并不是我們提到的持續的close_wait狀態,這個在受控范圍。
默認會至少維持2個小時,消耗大量資源,可通過修改TCP/IP的參數,來縮短這個時間,修改tcp_keepalive_*系列參數有助于解決這個問題。?
粘包問題
只有TCP協議中才會發生粘包問題,TCP粘包是指發送方發送的若干包數據到接收方時粘成一包,從接收緩沖區看,后一包數據的頭緊接著前一包數據的尾。
原因:1)發送方,TCP默認使用Nagle算法,這個算法主要做兩件事:1.只有上一個岑組得到確認,才會發送下一個分組;2.收集多個小分組,在一個確認到達時一起發送。2)接收方,TCP接收到的分組保存至接收緩沖區里,然后應用程序主動從緩存里讀收到的分組,這樣一來,如果TCP接收到分組的速度大于應用程序讀分組的速度,多個包就會被存至緩存,應用程序讀時,就會讀到多個首尾相接粘到一起的包。
解決:通過發送長度或者格式化數據?
物理層:通過光纖、電纜、雙絞線等把兩臺計算機連接起來,然后在計算機之間傳送0,1這樣的電信號;
數據鏈路層:工作在物理層之上,負責給這些0,1制定傳送規則,然后另一方再按照相應的規則來進行解讀。根據以太網協議,把一組電信號構成一個數據包,稱為“幀”。每一個幀由標頭(說明數據,例如發送者或者接收者等信息)長度固定為18個字節和數據(不是固定的,64~1518個字節)兩部分組成。?
廣播與ARP協議:計算機A知道計算機B的MAC地址,可是計算機A無法知道計算機B是分布在哪邊路線上的,實際上,計算機A是通過廣播的方式把數據發送給計算機B,在同一個子網中,計算機A要向計算機B發送一個數據包,這個數據包包含接收者的MAC地址。這個時候同一個子網中的計算機C,D也會收到這個數據包,然后收到這個數據包的計算機,會把數據包的MAC地址取出來,與自身的MAC地址對比,如果兩者相同,則接收這個數據包,否則就丟棄這個數據包。這種發送方式稱為廣播。
網絡層:IP協議幫助我們區分MAC地址是否處于同一個子網中。IP地址由網絡部分+主機部分。把子網掩碼和IP相與,若得到的一樣,則處于同一個子網中。假如兩臺計算機的IP不是處于同一個子網中,這個時候,我們就會把數據包發送給網關,然后網關讓我們進行轉發。
根據MAC地址發送數據和根據IP地址詢問MAC地址都是通過廣播的形式發送,區分:在詢問MAC地址的數據包中,在對方的MAC地址這一欄中,填的是一個特殊的MAC地址,其他計算機看到這個特殊的MAC地址之后,就能知道廣播想干嘛了。
DNS服務器:通過DNS服務器來對域名進行解析,得到對方的IP
傳輸層:雖然已經把數據成功從計算機A傳送到計算機B,可是計算機B里面有各種各樣的應用程序,計算機通過端口進行解決的,傳輸層的功能就是建立端口到端口的通信。
應用層:雖然已經收到了傳輸層傳來的數據,可是這些數據有html格式的,有mp4格式的,各種各樣的,應用層就是指定這些數據的格式規則,收到后才能進行解讀。
伙伴系統:伙伴系統從物理連續的大小固定的段上進行分配,假設內存段的大小最初為256KB,內核請求21KB,最初,這個段分為兩個伙伴,稱為AL和AR,每個的大小都為128KB;這兩個伙伴之一進一步分成兩個64KB的伙伴,即BL和BR,然而,從21KB開始的下一個大的2的冪是32KB,因此BL或BR再次劃分為兩個32KB的伙伴CL和CR,因此,其中一個32KB的段可用滿足21KB請求,當釋放已分配的CL內存時,系統可以將CL和CR合并成64KB的段,然后繼續合并,最終可以得到原來的256KB段。
缺點:由于圓整到下一個2的冪,很可能造成分配段內的碎片。
slab分配:每個slab由一個或多個物理連續的頁面組成,每個cache由一個或多個slab組成,每個內核數據結構都有一個cache。例如,用于表示進程描述符、文件對象、信號量等的數據結構都有各自單獨的cache,每個cache含有內核數據結構的對象實例稱為object。例如,信號量cache有信號量對象。
優點:1)沒有因碎片而引起的內存浪費。每個內核數據結構都有關聯的cache,每個cache都由一個或多個slab組成,而slab按所表示對象的大小來分塊。因此當內核請求對象內存時,slab分配器可以返回剛好表示對象的所需內存;2)可以快速滿足內存請求。由于對象已經預先創建,因此可以從cache中快速分配。再者,當內核用完對象并釋放它時,它被標記為空閑并返回到cache,從而立即可用于后續的內核請求。??
http常見狀態碼
1xx:代表消息,一般告訴客戶端,請求已經收到了,正在處理
2xx:代表請求成功,一般是請求收到,請求已經處理完成等信息
3xx:代表重定向到其他地方,他讓客戶端在發起一個請求,以完成整個處理
304:每個資源請求完成后,通常會被緩存在客戶端,并會記錄資源的有效時間和修改時間。當客戶再次請求該資源,客戶端首先從緩存中查找該資源。如果該資源存在,并且在有效期,則不請求服務器,就不會產生對應的請求數據包。如果不在有效期,客戶端會請求服務器,重新獲取,服務器會判斷修改時間,如果沒有修改過,就會返回狀態碼304,告訴客戶端該資源仍然有效,客戶端會直接使用緩存的資源。客戶端和服務器端只需要傳輸很少的數據量來做文件的校驗,如果文件沒有修改,則不需要返回全局的數據。
4xx:代表處理錯誤,責任在客戶端,如客戶端請求一個不存在的資源、客戶端未被授權、禁止訪問等
5xx:處理發生錯誤,責任在服務端,如服務端拋出異常,路由出錯、HTTP版本不支持等?
Linux系統中,可以用來查找可執行文件是:whereis、locate、which、type、find
進程由執行——>阻塞:I/O請求?
UDP:無連接的,面向消息的,不會使用塊的合并優化算法,由于UDP支持一對多的模式,所以接收端的套接字緩沖區采用鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有消息頭(消息來源地址,端口等信息),這樣對于接收端來說,就容易進行區分處理。即面向消息的通信是有消息保護邊界的。?
4、算法
發紅包
int price=rand()%(money/count*2)+1
money=money-price;
count-=1;?
大數int求平局值,可能爆int?
用到貪心算法:Dijkstra、Kruskal、Prim?
5、高并發服務器
web服務器在web頁面處理上的步驟:
1)web瀏覽器向一個特定的服務器發出web頁面請求;
2)web服務器收到web頁面請求后,尋找所請求的web頁面,并將所有請求的web頁面傳送給web瀏覽器;
3)web瀏覽器接收到所請求的web頁面內容,并將它顯示出來。
影響web頁面訪問的影響因素會有這幾個
1)web服務器磁盤性能:提升服務器磁盤訪問性能,也即通常所說的I/O性能;
2)web服務器與應用服務器交互的性能
3)應用服務器處理動態內容的性能,或者說動態內容應用處理性能:加快動態內容的處理性能;盡可能多地使用靜態內容,這樣web服務器就可以無需請求應用服務器,直接將web內容發給瀏覽器,這里可以入手的方案又有:1.動態內容緩存、2.動態內容靜態化;
4)客戶端與web服務器的連接速度,即網絡傳輸性能:增加寬帶,包括服務器和客戶端兩邊的internet連接寬帶;
5)web瀏覽器解釋和渲染web內容的性能
6)web訪問并發性能:多態服務器負載均衡同時處理大量的并發訪問;
6、數據庫
索引B+樹
1)葉子節點包含全部關鍵字以及指向相應記錄的指針,而且葉節點的關鍵字按大小順序排列,相鄰葉節點用指針鏈接;
2)非葉節點僅存儲其子樹最大(或最小)關鍵字,可以看成是索引。
優點:
1)B+樹更適合外部存儲,由于根節點不存放真正數據(只存放其子樹的最大或最小的關鍵字,作為索引),一個節點可以存儲更多的關鍵字,每個節點能索引的范圍更大更精確,也意味著B+樹單次磁盤IO的信息量大于B樹,I/O的次數相對減少
2)MySql是一種關系型數據庫,區間訪問是常見的一種情況,B+樹葉節點增加的鏈指針,加強了區間訪問性,可使用在區間查詢的場景;?
事務四大特性:原子性、一致性、隔離性、持久性
事務隔離級別:讀未提交、讀已提交、可重復讀
索引:1)MyISAM使用B+樹作為索引結構,葉節點的data域存放的是數據記錄的地址,即MyISAM索引文件和數據文件是分離的,MyISAM的索引文件僅僅保存記錄的地址。2)InnoDB索引使用B+樹作為索引,葉節點的data域存放的就是數據記錄。這個索引的key是數據表的主鍵。
MySQL數據庫的四類索引:普通索引、唯一索引(允許有空值)、主鍵索引(不允許空值)、組合索引
索引生效條件:最左前綴匹配原則
數據庫三大范式:1)字段具有原子性,不可再分解;2)非主鍵字段不能出現部分依賴主鍵;3)非主鍵字段不能出現傳遞依賴。
1、MySQL的主從復制
三個線程:主binlog線程、從IO線程、從SQL執行線程
日志:主bin-log日志、從relay log日志?
2、MyISAM和InnoDB區別
MyISAM:不支持事務、支持表級鎖、不支持MVCC、不支持外鍵、支持全文索引。MyISAM內部維護一個計算器,可以直調取。MyISAM的索引和數據是分開的,并且索引是有壓縮的,內存使用率就對應提高不少。能加載更多索引,而InnoDB是索引和數據是緊密捆綁,沒有使用壓縮從而造成InnoDB比MyISAM體積龐大不少。
InnoDB:支持事務、支持行級別鎖、支持外鍵、不支持全文索引。
select count(*):MyISAM更快,因為MyISAM內部維護了一個計數器,存儲了表的總行數,每次新增一行,這個計數器就加1,可以直接調取
select:InnoDB在select時,要緩存數據庫和索引塊,而MyISAM只緩存索引塊,這中間還有換進換出的減少;InnoDB尋址要映射到塊,再到行,而MyISAM記錄的直接是文件的OFFSET,定位比InnoDB快;InnoDB還需要維護MVCC一致。
堆表:數據插入時存儲位置是隨機的,主要是數據庫內部塊的空閑情況決定,獲取數據是按照命中率計算,全表掃描時不見得先插入的數據先查到;
索引組織表:數據存儲是把表按照索引的方式存儲的,數據是有序的,數據的位置是預先定好的,與插入的順序沒有關系。
索引表的查詢效率比堆表高(相當于查詢索引效率),插入數據的速度比堆表慢。
3、事務4種隔離級別
讀未提交:臟讀意味著在事務A中,事務B雖然沒有提交,但它任何一條數據變化,在事務A中都可以看到
讀已提交:不可重復讀意味著在同一個事務中執行完全相同的select語句時可能看到不一樣的結果。
可重復讀:是MySQL的默認事務隔離級別,可能出現幻讀,即一個事務在進行插入數據后,另一個事務先select后,并未發現這條數據,然后第一個事務進行了提交,然后第二個事務進行插入同樣的事務,發現插入不成功,即出現幻覺。
可串行化:它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。簡而言之,它是再每個讀的數據行加上共享鎖。
4、對于MyISAM表會把自增主鍵的最大ID記錄到數據文件里,重啟MySQL自增主鍵最大ID也不會丟失;如果是InnoDB,只是把自增主鍵最大ID記錄到內存中,所以重啟數據庫或者是對表進行OPTIMIZE操作,都會導致最大ID丟失。?
?5、MySQL為什么用自增列作為主鍵
如果表使用自增索引,那么每次插入新的記錄,記錄就會順序添加到當前索引節點的后續位置,當一頁寫滿,就會自動開辟一個新的頁。
如果使用非自增主鍵,由于每次插入主鍵的值近似于隨機,因此每次新的記錄都要被插入到現有索引頁的中間某個位置,此時MySQL不得不為了將新紀錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁盤上而從緩存中清掉,此時又要從磁盤上都回來,這增加了很多開銷,同時頻繁的移動、分頁操作造成了大量的碎片,得到了不夠緊湊的索引結構,后續不得不通過OPTIMIZE TABLE來重建表并優化填充頁面。
6、為什么使用數據索引能提高效率
1)數據索引的存儲是有序的;2)在有序的情況下,通過索引查詢一個數據是無需遍歷索引記錄的;3)極端情況下,數據索引的查詢效率為二分法查詢效率,趨近于log2(N)?
7、什么情況下應不建或少建索引
1)表記錄太少;2)經常插入、刪除、修改的表;3)數據重復且分布平均的表字段;4)經常和主字段一塊查詢但主字段索引值比較多的表字段?
表分區:根據一定規則,將數據庫中的一張表分解成多個更小的,容易管理的部分。從邏輯上看,只有一張表,但是底層卻是由多個物理分區組成。
分表:指的是通過一定規則,將一張表分解成多張不同的表。
分表與分區的區別:分區從邏輯上來講只有一張表,而分表則是將一張表分解成多張表。
表分區好處:1)高效利用多個硬件設備;2)可以存儲更多數據;3)分區表更容易維護,例如批量刪除大量數據可以清楚整個分區;
分區表的限制因素:1)一個表最多只能有1024個分區;2)分區字段要么不包含主鍵或者索引列,要么包含全部主鍵和索引列;3)分區表無法使用外鍵約束;4)MySQL的分區適用于一個表所有數據和索引,不能只對表數據分區而不對索引分區。?
行級鎖定的優點:1)當在許多線程中訪問不同的行時減少鎖定沖突;2)回滾時只有少量的更改;3)可以長時間鎖定單一的行。
行級鎖定的缺點:1)比頁級或表級鎖定占用更多的內存;2)當在表的大部分中使用時,比頁級或表級鎖定速度慢,因為必須獲取更多的鎖;3)如果在大部分數據上經常進行GROUP BY操作或者必須經常掃描整個表,比其他鎖定明顯慢很多;
加表鎖:Lock tables db1 read local;
MySQL優化
1)開啟查詢緩存,優化查詢;修改配置文件,vi /etc/my.cnf,在[mysqld]中添加:query_cache_size=20M,query_cache_type=ON
2)explain你的select語句,可以幫忙分析查詢語句或是表結構的性能瓶頸。還會顯示索引主鍵被如何利用的,你的數據表是如何被搜索和排序的;?
3)當只要一條語句時使用limit 1,MySQL數據庫引擎會在找到一條數據后停止搜索;
4)為搜索字段建索引;
5)當知道這些字段是有限而且固定的,那么應該使用ENUM,而不是VARCHAR;
6)選擇正確的存儲引擎。
key:是數據庫的物理結構,包含兩層意義和作用,一是約束(偏重于約束和規范數據庫的結構完整性),二是索引(輔助查詢用的),包括primary key,unique key
index:是數據庫物理結構,它只是輔助查詢的,它創建時會在另外的表空間(mysql的innodb表空間)以一個類似目錄的結構存儲。索引要分類的話,分為前綴索引、全文本索引等。?
show processlist:Id user host db command time state info
iotop:查看當前系統進程的磁盤讀寫情況
top:cpu和內存占用資源情況
netstat -lnp:根據網絡連接情況,最后一欄顯示PID/Program name
netstat -an:打印網絡連接狀況?
幻讀:一個事務進行了插入數據,還沒提交,另一個事務在查詢時,沒有查到該記錄,然后進行插入同樣的數據,但在這之前,前一個事務進行了提交,然后后面這個事務就不能插入成功了,出現了幻覺。?
MySQL主要的兩種搜索引擎有MyISAM和InnoDB,前者大多索引的結構為B-tree,而后者只有主鍵索引B+tree,非主鍵索引也用B-tree,所以應該默認的索引結構是B-tree。
在MySQL中只有Memory引擎顯式支持哈希索引。
哈希索引只包含索引值和行指針,不存儲字段值。
MyISAM支持空間索引。
空間索引還是用where,全文索引查詢用的是match agatinst
索引一定要用順序I/O?
web開發常見問題(SQL注入、XSS攻擊、CSRF攻擊)
SQL注入:發生在應用程序之數據庫層的安全漏洞。在輸入的字符串之中注入SQL指令,在設計不良的程序當中忽略了檢查,那么這些注入進去的指令就會被數據庫服務器誤認為是正常的SQL指令而運行,因此遭到破壞或是入侵。
XSS攻擊:類似于SQL注入,通過插入惡意腳本,實現對用戶?
總結
以上是生活随笔為你收集整理的【C++后台开发面经】面试总结第八波:整个知识的查漏补缺的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 强大的视频播放器 KMPlayer 简单
- 下一篇: s3c2440移植MQTT