C++面試寶典--基礎知識

(一)基本語言

1. 說一下static關鍵字的作用

  1. 全局靜態變量
    在全局變量前加上關鍵字static,全局變量就定義成一個全局靜態變量.
    內存中位置:靜態存儲區,在整個程序運行期間一直存在。
    初始化:未經初始化的全局靜態變量會被自動初始化爲0(自動對象的值是任意的,除非他被顯式初始化);
    作用域:全局靜態變量在聲明他的文件之外是不可見的,準確地說是從定義之處開始,到文件結尾。
  2. 局部靜態變量
    在局部變量之前加上關鍵字static,局部變量就成爲一個局部靜態變量。
    內存中的位置:靜態存儲區
    初始化:未經初始化的局部靜態變量會被自動初始化爲0(自動對象的值是任意的,除非他被顯式初始化);
    作用域:作用域仍爲局部作用域,當定義它的函數或者語句塊結束的時候,作用域結束。但是當局部靜態變量離開作用域後,並沒有銷燬,而是仍然駐留在內存當中,只不過我們不能再對它進行訪問,直到該函數再次被調用,並且值不變;
  3. 靜態函數
    在函數返回類型前加static,函數就定義爲靜態函數。函數的定義和聲明在默認情況下都是extern的,但靜態函數只是在聲明他的文件當中可見,不能被其他文件所用。
    函數的實現使用static修飾,那麼這個函數只可在本cpp內使用,不會同其他cpp中的同名函數引起衝突;
    warning:不要再頭文件中聲明static的全局函數,不要在cpp內聲明非static的全局函數,如果你要在多個cpp中複用該函數,就把它的聲明提到頭文件裏去,否則cpp內部聲明需加上static修飾;
  4. 類的靜態成員
    在類中,靜態成員可以實現多個對象之間的數據共享,並且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此,靜態成員是類的所有對象中共享的成員,而不是某個對象的成員。對多個對象來說,靜態數據成員只存儲一處,供所有對象共用
  5. 類的靜態函數
    靜態成員函數和靜態數據成員一樣,它們都屬於類的靜態成員,它們都不是對象成員。因此,對靜態成員的引用不需要用對象名。
    在靜態成員函數的實現中不能直接引用類中說明的非靜態成員,可以引用類中說明的靜態成員(這點非常重要)。如果靜態成員函數中要引用非靜態成員時,可通過對象來引用。從中可看出,調用靜態成員函數使用如下格式:<類名>::<靜態成員函數名>(<參數表>);

2. 說一下C++和C的區別

設計思想上:
C++是面向對象的語言,而C是面向過程的結構化編程語言
語法上:
C++具有封裝、繼承和多態三種特性
C++相比C,增加多許多類型安全的功能,比如強制類型轉換、
C++支持範式編程,比如模板類、函數模板等

3. 說一說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的強制轉換表面上看起來功能強大什麼都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。

4. 請說一下C/C++ 中指針和引用的區別?

1.指針有自己的一塊空間,而引用只是一個別名;
2.使用sizeof看一個指針的大小是4,而引用則是被引用對象的大小;
3.指針可以被初始化爲NULL,而引用必須被初始化且必須是一個已有對象 的引用;
4.作爲參數傳遞時,指針需要被解引用纔可以對對象進行操作,而直接對引 用的修改都會改變引用所指向的對象;
5.可以有const指針,但是沒有const引用;
6.指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能 被改變;
7.指針可以有多級指針(**p),而引用至於一級;
8.指針和引用使用++運算符的意義不一樣;
9.如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露。

5. 給定三角形ABC和一點P(x,y,z),判斷點P是否在ABC內,給出思路並手寫代碼

根據面積法,如果P在三角形ABC內,那麼三角形ABP的面積+三角形BCP的面積+三角形ACP的面積應該等於三角形ABC的面積。
計算三角形面積使用海倫公式:S = sqrt(p * (p-a) * (p-b) * (p-c));
其中 p = (a + b + c) / 2;

6. 請你說一下你理解的c++中的smart pointer四個智能指針: shared_ptr,unique_ptr,weak_ptr,auto_ptr

爲什麼要使用智能指針:
智能指針的作用是管理一個指針,因爲存在以下這種情況:申請的空間在函數結束時忘記釋放,造成內存泄漏。使用智能指針可以很大程度上的避免這個問題,因爲智能指針就是一個類,當超出了類的作用域是,類會自動調用析構函數,析構函數會自動釋放資源。所以智能指針的作用原理就是在函數結束時自動釋放內存空間,不需要手動釋放內存空間。

  1. auto_ptr(c++98的方案,cpp11已經拋棄)
    採用所有權模式。
    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
    auto_ptr< string> p2;
    p2 = p1; //auto_ptr不會報錯.
    此時不會報錯,p2剝奪了p1的所有權,但是當程序運行時訪問p1將會報錯。所以auto_ptr的缺點是:存在潛在的內存崩潰問題!
  2. unique_ptr(替換auto_ptr)
    unique_ptr實現獨佔式擁有或嚴格擁有概念,保證同一時間內只有一個智能指針可以指向該對象。它對於避免資源泄露(例如“以new創建對象後因爲發生異常而忘記調用delete”)特別有用。
    採用所有權模式,還是上面那個例子
    unique_ptr< string> p3 (new string (“auto”)); //#4
    unique_ptr< string> p4; //#5
    p4 = p3; //此時會報錯!!
    編譯器認爲p4=p3非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全。
    另外unique_ptr還有更聰明的地方:當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做,比如:
    unique_ptr< string> pu1(new string (“hello world”));
    unique_ptr< string> pu2;
    pu2 = pu1; // #1 not allowed
    unique_ptr< string> pu3;
    pu3 = unique_ptr< string>(new string (“You”)); // #2 allowed
    其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因爲它調用 unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 pu3 後就會被銷燬。這種隨情況而已的行爲表明,unique_ptr 優於允許兩種賦值的auto_ptr 。
    注:如果確實想執行類似與#1的操作,要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能夠將一個unique_ptr賦給另一個。例如:
    unique_ptr< string> ps1, ps2;
    ps1 = demo(“hello”);
    ps2 = move(ps1);
    ps1 = demo(“alexia”);
    cout << *ps2 << *ps1 << endl;
  3. shared_ptr
    shared_ptr實現共享式擁有概念。多個智能指針可以指向相同對象,該對象和其相關資源會在“最後一個引用被銷燬”時候釋放。從名字share就可以看出了資源可以被多個指針共享,它使用計數機制來表明資源被幾個指針共享。可以通過成員函數use_count()來查看資源的所有者個數。除了可以通過new來構造,還可以通過傳入auto_ptr, unique_ptr,weak_ptr來構造。當我們調用release()時,當前指針會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。
    shared_ptr 是爲了解決 auto_ptr 在對象所有權上的侷限性(auto_ptr 是獨佔的), 在使用引用計數的機制上提供了可以共享所有權的智能指針。
    成員函數:
    use_count 返回引用計數的個數
    unique 返回是否是獨佔所有權( use_count 爲 1)
    swap 交換兩個 shared_ptr 對象(即交換所擁有的對象)
    reset 放棄內部對象的所有權或擁有對象的變更, 會引起原有對象的引用計數的減少
    get 返回內部對象(指針), 由於已經重載了()方法, 因此和直接使用對象是一樣的.如 shared_ptr< int> sp(new int(1)); sp 與 sp.get()是等價的
  4. weak_ptr
    weak_ptr 是一種不控制對象生命週期的智能指針, 它指向一個 shared_ptr 管理的對象. 進行該對象的內存管理的是那個強引用的 shared_ptr. weak_ptr只是提供了對管理對象的一個訪問手段。weak_ptr 設計的目的是爲配合 shared_ptr 而引入的一種智能指針來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 對象構造, 它的構造和析構不會引起引用記數的增加或減少。weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指針的引用計數永遠不可能下降爲0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr。
    class B;
    class A
    {
    public:
    shared_ptr< B> pb_;
    ~A()
    {
    cout<<“A delete\n”;
    }
    };
    class B
    {
    public:
    shared_ptr< A> pa_;
    ~B()
    {
    cout<<“B delete\n”;
    }
    };
    void fun()
    {
    shared_ptr< B> pb(new B());
    shared_ptr< A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
    }
    int main()
    {
    fun();
    return 0;
    }
    可以看到fun函數中pa ,pb之間互相引用,兩個資源的引用計數爲2,當要跳出函數時,智能指針pa,pb析構時兩個資源引用計數會減一,但是兩者引用計數還是爲1,導致跳出函數時資源沒有被釋放(A B的析構函數沒有被調用),如果把其中一個改爲weak_ptr就可以了,我們把類A裏面的shared_ptr pb_; 改爲weak_ptr pb_; 運行結果如下,這樣的話,資源B的引用開始就只有1,當pb析構時,B的計數變爲0,B得到釋放,B釋放的同時也會使A的計數減一,同時pa析構時使A的計數減一,那麼A的計數爲0,A得到釋放。
    注意的是我們不能通過weak_ptr直接訪問對象的方法,比如B對象中有一個方法print(),我們不能這樣訪問,pa->pb_->print(); 英文pb_是一個weak_ptr,應該先把它轉化爲shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

7. 請回答一下數組和指針的區別

指針 數組
保存數據的地址 保存數據
間接訪問數據,首先獲得指針的內容,然後將其作爲地址,從該地址中提取數據 直接訪問數據
通常用於動態的數據結構 通常用於固定數目且數據類型相同的元素
通過Malloc分配內存,free釋放內存 隱式的分配和刪除
通常指向匿名數據,操作匿名函數 自身即爲數據名

8. 請你回答一下野指針是什麼?

野指針就是指向一個已刪除的對象或者未申請訪問受限內存區域的指針

9. 請你回答一下爲什麼析構函數必須是虛函數?爲什麼C++默認的析構函數不是虛函數 考點:虛函數 析構函數

將可能會被繼承的父類的析構函數設置爲虛函數,可以保證當我們new一個子類,然後使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。
C++默認的析構函數不是虛函數是因爲虛函數需要額外的虛函數表和虛表指針,佔用額外的內存。而對於不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。因此C++默認的析構函數不是虛函數,而是隻有當需要當作父類時,設置爲虛函數。

10. 請你來說一下函數指針

1、定義
函數指針是指向函數的指針變量。
函數指針本身首先是一個指針變量,該指針變量指向一個具體的函數。這正如用指針變量可指向整型變量、字符型、數組一樣,這裏是指向函數。
C在編譯時,每一個函數都有一個入口地址,該入口地址就是函數指針所指向的地址。有了指向函數的指針變量後,可用該指針變量調用函數,就如同用指針變量可引用其他類型變量一樣,在這些概念上是大體一致的。
2、用途:
調用函數和做函數的參數,比如回調函數。
3、示例:
char * fun(char * p) {…} // 函數fun
char * (*pf)(char * p); // 函數指針pf
pf = fun; // 函數指針pf指向函數fun
pf§; // 通過函數指針pf調用函數fun

11. 請你來說一下fork函數

Fork:創建一個和當前進程映像一樣的進程可以通過fork( )系統調用:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
成功調用fork( )會創建一個新的進程,它幾乎與調用fork( )的進程一模一樣,這兩個進程都會繼續運行。在子進程中,成功的fork( )調用會返回0。在父進程中fork( )返回子進程的pid。如果出現錯誤,fork( )返回一個負值。
最常見的fork( )用法是創建一個新的進程,然後使用exec( )載入二進制映像,替換當前進程的映像。這種情況下,派生(fork)了新的進程,而這個子進程會執行一個新的二進制可執行文件的映像。這種“派生加執行”的方式是很常見的。
在早期的Unix系統中,創建進程比較原始。當調用fork時,內核會把所有的內部數據結構複製一份,複製進程的頁表項,然後把父進程的地址空間中的內容逐頁的複製到子進程的地址空間中。但從內核角度來說,逐頁的複製方式是十分耗時的。現代的Unix系統採取了更多的優化,例如Linux,採用了寫時複製的方法,而不是對父進程空間進程整體複製。

12. 請你來說一下C++中析構函數的作用

析構函數與構造函數對應,當對象結束其生命週期,如對象所在的函數已調用完畢時,系統會自動執行析構函數。
析構函數名也應與類名相同,只是在函數名前面加一個位取反符~,例如~stud( ),以區別於構造函數。它不能帶任何參數,也沒有返回值(包括void類型)。只能有一個析構函數,不能重載。
如果用戶沒有編寫析構函數,編譯系統會自動生成一個缺省的析構函數(即使自定義了析構函數,編譯器也總是會爲我們合成一個析構函數,並且如果自定義了析構函數,編譯器在執行時會先調用自定義的析構函數再調用合成的析構函數),它也不進行任何操作。所以許多簡單的類中沒有用顯式的析構函數。
如果一個類中有指針,且在使用的過程中動態的申請了內存,那麼最好顯示構造析構函數在銷燬類之前,釋放掉申請的內存空間,避免內存泄漏。
類析構順序:1)派生類本身的析構函數;2)對象成員析構函數;3)基類析構函數。

13. 請你來說一下靜態函數和虛函數的區別

靜態函數在編譯的時候就已經確定運行時機,虛函數在運行的時候動態綁定。虛函數因爲用了虛函數表機制,調用的時候會增加一次內存開銷。

14. 請你來說一說重載和覆蓋

重載:兩個函數名相同,但是參數列表不同(個數,類型),返回值類型沒有要求,在同一作用域中。
重寫:子類繼承了父類,父類中的函數是虛函數,在子類中重新定義了這個虛函數,這種情況是重寫

15. 請你說一說strcpy和strlen

strcpy是字符串拷貝函數,原型:
char strcpy(char dest, const char *src);
從src逐字節拷貝到dest,直到遇到’\0’結束,因爲沒有指定長度,可能會導致拷貝越界,造成緩衝區溢出漏洞,安全版本是strncpy函數。
strlen函數是計算字符串長度的函數,返回從開始到’\0’之間的字符個數。

16. 請你說一說你理解的虛函數和多態

多態的實現主要分爲靜態多態和動態多態,靜態多態主要是重載,在編譯的時候就已經確定;動態多態是用虛函數機制實現的,在運行期間動態綁定。舉個例子:一個父類類型的指針指向一個子類對象時候,使用父類的指針去調用子類中重寫了的父類中的虛函數的時候,會調用子類重寫過後的函數,在父類中聲明爲加了virtual關鍵字的函數,在子類中重寫時候不需要加virtual也是虛函數。
虛函數的實現:在有虛函數的類中,類的最開始部分是一個虛函數表的指針,這個指針指向一個虛函數表,表中放了虛函數的地址,實際的虛函數在代碼段(.text)中。當子類繼承了父類的時候也會繼承其虛函數表,當子類重寫父類中虛函數時候,會將其繼承到的虛函數表中的地址替換爲重新寫的函數地址。使用了虛函數,會增加訪問內存開銷,降低效率。

17. 請你來說一說++i和i++的實現

  1. ++i 實現:
    int& int::operator++()
    {
    *this +=1;
    return *this;
    }
  2. i++ 實現:
    const int int::operator(int)
    {
    int oldValue = *this;
    ++(*this);
    return oldValue;
    }

18. 請你來寫個函數在main函數執行前先運行

class BeforeMain{
public:
BeforeMain();
};
BeforeMain::BeforeMain() {
cout << “Before main” << endl;
}
BeforeMain bM; // 利用全局變量和構造函數的特性,通過全局變量的構造函數執行

19. 有段代碼寫成了下邊這樣,如果在只修改一個字符的前提下,使代碼輸出20個hello?

修改前:
for(int i = 0; i < 20; i–)
cout << “hello” << endl;
修改後:
for(int i = 0; i + 20; i–)
cout << “hello” << endl;

20. 以下四行代碼的區別是什麼?

代碼:
const char * arr = “123”;
char * brr = “123”;
const char crr[] = “123”;
char drr[] = “123”;
回答:
const char * arr = “123”;
//字符串123保存在常量區,const本來是修飾arr指向的值不能通過arr去修改,但是字符串“123”在常量區,本來就不能改變,所以加不加const效果都一樣
char * brr = “123”;
//字符串123保存在常量區,這個arr指針指向的是同一個位置,同樣不能通過brr去修改"123"的值
const char crr[] = “123”;
//這裏123本來是在棧上的,但是編譯器可能會做某些優化,將其放到常量區
char drr[] = “123”;
//字符串123保存在棧區,可以通過drr去修改

21. 請你來說一下C++裏是怎麼定義常量的?常量存放在內存的哪個位置?

常量在C++裏的定義就是一個top-level const加上對象類型,常量定義必須初始化。對於局部對象,常量存放在棧區,對於全局對象,常量存放在全局/靜態存儲區。對於字面值常量,常量存放在常量存儲區。

22. 請你來回答一下const修飾成員函數的目的是什麼?

const修飾的成員函數表明函數調用不會對對象做出任何更改,事實上,如果確認不會對對象做更改,就應該爲函數加上const限定,這樣無論const對象還是普通對象都可以調用該函數。

23. 如果同時定義了兩個函數,一個帶const,一個不帶,會有問題嗎?

不會,這相當於函數的重載。

24. 請你來說一說隱式類型轉換

首先,對於內置類型,低精度的變量給高精度變量賦值會發生隱式類型轉換,其次,對於只存在單個參數的構造函數的對象構造來說,函數調用可以直接使用該參數傳入,編譯器會自動調用其構造函數生成臨時對象。

25. 請你來說一說C++函數棧空間的最大值

默認是1M,不過可以調整

26. 請你來說一說extern“C”

C++調用C函數需要extern C,因爲C語言沒有函數重載。

27. 請你回答一下new/delete與malloc/free的區別是什麼

首先,new/delete是C++的關鍵字,而malloc/free是C語言的庫函數,後者使用必須指明申請內存空間的大小,對於類類型的對象,後者不會調用構造函數和析構函數
malloc需要給定申請內存的大小,返回的指針需要強轉。
new會調用構造函數,不用指定內存大小,返回的指針不用強轉。

28. 請你說說你瞭解的RTTI

運行時類型檢查,在C++層面主要體現在dynamic_cast和typeid, VS中虛函數表的-1位置存放了指向type_info的指針。對於存在虛函數的類型,typeid和dynamic_cast都會去查詢type_info。

29. 請你說說虛函數表具體是怎樣實現運行時多態的?

子類若重寫父類虛函數,虛函數表中,該函數的地址會被替換,對於存在虛函數的類的對象,在VS中,對象的對象模型的頭部存放指向虛函數表的指針,通過該機制實現多態。

30. 請你說說C語言是怎麼進行函數調用的?

每一個函數調用都會分配函數棧,在棧內進行函數執行過程。調用前,先把返回地址壓棧,然後把當前函數的esp指針壓棧。

31. 請你說說C語言參數壓棧順序?

從右到左

32. 請你說說C++如何處理返回值?

生成一個臨時變量,把它的引用作爲函數參數傳入函數內。

33. 請你回答一下C++中拷貝賦值函數的形參能否進行值傳遞?

不能。如果是這種情況下,調用拷貝構造函數的時候,首先要將實參傳遞給形參,這個傳遞的時候又要調用拷貝構造函數。如此循環,無法完成拷貝,棧也會滿。

34. 請你說一說select

select在使用前,先將需要監控的描述符對應的bit位置1,然後將其傳給select,當有任何一個事件發生時,select將會返回所有的描述符,需要在應用程序自己遍歷去檢查哪個描述符上有事件發生,效率很低,並且其不斷在內核態和用戶態進行描述符的拷貝,開銷很大。

35. 請你說說fork,wait,exec函數

父進程產生子進程使用fork拷貝出來一個父進程的副本,此時只拷貝了父進程的頁表,兩個進程都讀同一塊內存,當有進程寫的時候使用寫實拷貝機制分配內存,exec函數可以加載一個elf文件去替換父進程,從此父進程和子進程就可以運行不同的程序了。fork從父進程返回子進程的pid,從子進程返回0。調用了wait的父進程將會發生阻塞,直到有子進程狀態改變,執行成功返回0,錯誤返回-1。exec執行成功則子進程從新的程序開始運行,無返回值,執行失敗返回-1。

(二)容器和算法

1. 請你來說一下map和set有什麼區別,分別又是怎麼實現的?

map和set都是C++的關聯容器,其底層實現都是紅黑樹(RB-Tree)。由於 map 和set所開放的各種操作接口,RB-tree 也都提供了,所以幾乎所有的 map 和set的操作行爲,都只是轉調 RB-tree 的操作行爲。
map和set區別在於:
(1)map中的元素是key-value(關鍵字—值)對:關鍵字起到索引的作用,值則表示與索引相關聯的數據;Set與之相對就是關鍵字的簡單集合,set中每個元素只包含一個關鍵字。
(2)set的迭代器是const的,不允許修改元素的值;map允許修改value,但不允許修改key。其原因是因爲map和set是根據關鍵字排序來保證其有序性的,如果允許修改key的話,那麼首先需要刪除該鍵,然後調節平衡,再插入修改後的鍵值,調節平衡,如此一來,嚴重破壞了map和set的結構,導致iterator失效,不知道應該指向改變前的位置,還是指向改變後的位置。所以STL中將set的迭代器設置成const,不允許修改迭代器的值;而map的迭代器則不允許修改key值,允許修改value值。
(3)map支持下標操作,set不支持下標操作。map可以用key做下標,map的下標運算符[ ]將關鍵碼作爲下標去執行查找,如果關鍵碼不存在,則插入一個具有該關鍵碼和mapped_type類型默認值的元素至map中,因此下標運算符[ ]在map應用中需要慎用,const_map不能用,只希望確定某一個關鍵值是否存在而不希望插入元素時也不應該使用,mapped_type類型沒有默認值也不應該使用。如果find能解決需要,儘可能用find。

2. 請你來介紹一下STL的allocaotr

STL的分配器用於封裝STL容器在內存管理上的底層細節。在C++中,其內存配置和釋放如下:
new運算分兩個階段:(1)調用::operator new配置內存;(2)調用對象構造函數構造對象內容
delete運算分兩個階段:(1)調用對象希構函數;(2)掉員工::operator delete釋放內存
爲了精密分工,STL allocator將兩個階段操作區分開來:內存配置有alloc::allocate()負責,內存釋放由alloc::deallocate()負責;對象構造由::construct()負責,對象析構由::destroy()負責。
同時爲了提升內存管理的效率,減少申請小內存造成的內存碎片問題,SGI STL採用了兩級配置器,當分配的空間大小超過128B時,會使用第一級空間配置器;當分配的空間大小小於128B時,將使用第二級空間配置器。第一級空間配置器直接使用malloc()、realloc()、free()函數進行內存空間的分配和釋放,而第二級空間配置器採用了內存池技術,通過空閒鏈表來管理內存。

3. 請你來說一說STL迭代器刪除元素

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

4. 請你說一說STL中MAP數據存放形式

紅黑樹。unordered map底層結構是哈希表

5. 請你講講STL有什麼基本組成

STL主要由:以下幾部分組成:
容器、迭代器、仿函數、算法、分配器、配接器
他們之間的關係:分配器給容器分配存儲空間,算法通過迭代器獲取容器中的內容,仿函數可以協助算法完成各種操作,配接器用來套接適配仿函數

6. 請你說說STL中map與unordered_map

  1. Map映射,map 的所有元素都是 pair,同時擁有實值(value)和鍵值(key)。pair 的第一元素被視爲鍵值,第二元素被視爲實值。所有元素都會根據元素的鍵值自動被排序。不允許鍵值重複。
    底層實現:紅黑樹
    適用場景:有序鍵值對不重複映射
    2、Multimap
    多重映射。multimap 的所有元素都是 pair,同時擁有實值(value)和鍵值(key)。pair 的第一元素被視爲鍵值,第二元素被視爲實值。所有元素都會根據元素的鍵值自動被排序。允許鍵值重複。
    底層實現:哈希表
    適用場景:有序鍵值對可重複映射

7. 請你說一說vector和list的區別,應用,越詳細越好

  1. 概念:
    1)Vector
    連續存儲的容器,動態數組,在堆上分配空間
    底層實現:數組
    兩倍容量增長:
    vector 增加(插入)新元素時,如果未超過當時的容量,則還有剩餘空間,那麼直接添加到最後(插入指定位置),然後調整迭代器。
    如果沒有剩餘空間了,則會重新配置原有元素個數的兩倍空間,然後將原空間元素通過複製的方式初始化新空間,再向新空間增加元素,最後析構並釋放原空間,之前的迭代器會失效。
    性能:
    訪問:O(1)
    插入:在最後插入(空間夠):很快
    在最後插入(空間不夠):需要內存申請和釋放,以及對之前數據進行拷貝。
    在中間插入(空間夠):內存拷貝
    在中間插入(空間不夠):需要內存申請和釋放,以及對之前數據進行拷貝。
    刪除:在最後刪除:很快
    在中間刪除:內存拷貝
    適用場景:經常隨機訪問,且不經常對非尾節點進行插入刪除。
    2)List
    動態鏈表,在堆上分配空間,每插入一個元數都會分配空間,每刪除一個元素都會釋放空間。
    底層:雙向鏈表
    性能:
    訪問:隨機訪問性能很差,只能快速訪問頭尾節點。
    插入:很快,一般是常數開銷
    刪除:很快,一般是常數開銷
    適用場景:經常插入刪除大量數據
    2、區別:
    1)vector底層實現是數組;list是雙向鏈表。
    2)vector支持隨機訪問,list不支持。
    3)vector是順序內存,list不是。
    4)vector在中間節點進行插入刪除會導致內存拷貝,list不會。
    5)vector一次性分配好內存,不夠時才進行2倍擴容;list每次插入新節點都會進行內存申請。
    6)vector隨機訪問性能好,插入刪除性能差;list隨機訪問性能差,插入刪除性能好。
    3、應用
    vector擁有一段連續的內存空間,因此支持隨機訪問,如果需要高效的隨即訪問,而不在乎插入和刪除的效率,使用vector。
    list擁有一段不連續的內存空間,如果需要高效的插入和刪除,而不關心隨機訪問,則應使用list。

8. 請你來說一下STL中迭代器的作用,有指針爲何還要迭代器

  1. 迭代器
    Iterator(迭代器)模式又稱Cursor(遊標)模式,用於提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示。或者這樣說可能更容易理解:Iterator模式是運用於聚合對象的一種模式,通過運用該模式,使得我們可以在不知道對象內部表示的情況下,按照一定順序(由iterator提供的方法)訪問聚合對象中的各個元素。
    由於Iterator模式的以上特性:與聚合對象耦合,在一定程度上限制了它的廣泛運用,一般僅用於底層聚合支持類,如STL的list、vector、stack等容器類及ostream_iterator等擴展iterator。
    2、迭代器和指針的區別
    迭代器不是指針,是類模板,表現的像指針。他只是模擬了指針的一些功能,通過重載了指針的一些操作符,->、*、++、–等。迭代器封裝了指針,是一個“可遍歷STL( Standard Template Library)容器內全部或部分元素”的對象, 本質是封裝了原生指針,是指針概念的一種提升(lift),提供了比指針更高級的行爲,相當於一種智能指針,他可以根據不同類型的數據結構來實現不同的++,–等操作。
    迭代器返回的是對象引用而不是對象的值,所以cout只能輸出迭代器使用*取值後的值而不能直接輸出其自身。
    3、迭代器產生原因
    Iterator類的訪問方式就是把不同集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。

9. 請你說一說epoll原理

調用順序:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
首先創建一個epoll對象,然後使用epoll_ctl對這個對象進行操作,把需要監控的描述添加進去,這些描述如將會以epoll_event結構體的形式組成一顆紅黑樹,接着阻塞在epoll_wait,進入大循環,當某個fd上有事件發生時,內核將會把其對應的結構體放入到一個鏈表中,返回有事件發生的鏈表。

10. n個整數的無序數組,找到每個元素後面比它大的第一個數,要求時間複雜度爲O(N)

vector< int> findMax(vector< int>num)
{
if(num.size()==0)
return num;
vector< int>res(num.size());
int i=0;
stack< int>s;
while(i<num.size())
{
if(s.empty() || num[s.top()] >= num[i])
{
s.push(i++);
}
else
{
res[s.top()]=num[i];
s.pop();
}
}
while(!s.empty())
{
res[s.top()]=INT_MAX;
s.pop();
}
for(int i=0; i<res.size(); i++)
cout<<res[i]<<endl;
return res;
}

11. 請你回答一下STL裏resize和reserve的區別

resize():改變當前容器內含有元素的數量(size()),eg: vector< int>v; v.resize(len); v的size變爲len, 如果原來v的size小於len,那麼容器新增(len-size)個元素,元素的值爲默認爲0。當v.push_back(3); 之後,則是3是放在了v的末尾,即下標爲len,此時容器是size爲len+1;
reserve():改變當前容器的最大容量(capacity),它不會生成元素,只是確定這個容器允許放入多少對象,如果reserve(len)的值大於當前的capacity(),那麼會重新分配一塊能存len個對象的空間,然後把之前v.size()個對象通過copy construtor複製過來,銷燬之前的內存;

(三)類和數據抽象

1. 請你來說一下C++中類成員的訪問權限

C++通過 public、protected、private 三個關鍵字來控制成員變量和成員函數的訪問權限,它們分別表示公有的、受保護的、私有的,被稱爲成員訪問限定符。在類的內部(定義類的代碼內部),無論成員被聲明爲 public、protected 還是 private,都是可以互相訪問的,沒有訪問權限的限制。在類的外部(定義類的代碼之外),只能通過對象訪問成員,並且通過對象只能訪問 public 屬性的成員,不能訪問 private、protected 屬性的成員。

2. 請你來說一下C++中struct和class的區別

在C++中,可以用struct和class定義類,都可以繼承。區別在於:struct的默認繼承權限和默認訪問權限是public,而class的默認繼承權限和默認訪問權限是private。
另外,class還可以定義模板類形參,比如template <class T, int i>。

3. 請你回答一下C++類內可以定義引用數據成員嗎?

可以,必須通過成員函數初始化列表初始化。

(四)面向對象與泛型編程

1. 請你回答一下什麼是右值引用,跟左值又有什麼區別?

右值引用是C++11中引入的新特性 , 它實現了轉移語義和精確傳遞。它的主要目的有兩個方面:

  1. 消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率。
  2. 能夠更簡潔明確地定義泛型函數。
    左值和右值的概念:
    左值:能對錶達式取地址、或具名對象/變量。一般指表達式結束後依然存在的持久對象。
    右值:不能對錶達式取地址,或匿名對象。一般指表達式結束就不再存在的臨時對象。
    右值引用和左值引用的區別:
  3. 左值可以尋址,而右值不可以。
  4. 左值可以被賦值,右值不可以被賦值,可以用來給左值賦值。
  5. 左值可變,右值不可變(僅對基礎類型適用,用戶自定義類型右值引用可以通過成員函數改變)。

(五)編譯與底層

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

對於C++源文件,從文本到可執行文件一般需要四個過程:
預處理階段:對源代碼文件中文件包含關係(頭文件)、預編譯語句(宏定義)進行分析和替換,生成預編譯文件。
編譯階段:將經過預處理後的預編譯文件轉換成特定彙編代碼,生成彙編文件
彙編階段:將編譯階段生成的彙編文件轉化成機器碼,生成可重定位目標文件
鏈接階段:將多個目標文件及所需要的庫連接成最終的可執行目標文件

2. 請你來回答一下include頭文件的順序以及雙引號””和尖括號<>的區別?

Include頭文件的順序:對於include的頭文件來說,如果在文件a.h中聲明一個在文件b.h中定義的變量,而不引用b.h。那麼要在a.c文件中引用b.h文件,並且要先引用b.h,後引用a.h,否則彙報變量類型未聲明錯誤。
雙引號和尖括號的區別:編譯器預處理階段查找頭文件的路徑不一樣。
1)對於使用雙引號包含的頭文件,查找頭文件路徑的順序爲:
當前頭文件目錄
編譯器設置的頭文件路徑(編譯器可使用-I顯式指定搜索路徑)
系統變量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的頭文件路徑
2)對於使用尖括號包含的頭文件,查找頭文件的路徑順序爲:
編譯器設置的頭文件路徑(編譯器可使用-I顯式指定搜索路徑)
系統變量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的頭文件路徑

3. 請你回答一下malloc的原理,另外brk系統調用和mmap系統調用的作用分別是什麼?

Malloc函數用於動態分配內存。爲了減少內存碎片和系統調用的開銷,malloc其採用內存池的方式,先申請大塊內存作爲堆區,然後將堆區分爲多個內存塊,以塊作爲內存管理的基本單位。當用戶申請內存時,直接從堆區分配一塊合適的空閒塊。Malloc採用隱式鏈表結構將堆區分成連續的、大小不一的塊,包含已分配塊和未分配塊;同時malloc採用顯示鏈表結構來管理所有的空閒塊,即使用一個雙向鏈表將空閒塊連接起來,每一個空閒塊記錄了一個連續的、未分配的地址。
當進行內存分配時,Malloc會通過隱式鏈表遍歷所有的空閒塊,選擇滿足要求的塊進行分配;當進行內存合併時,malloc採用邊界標記法,根據每個塊的前後塊是否已經分配來決定是否進行塊合併。
Malloc在申請內存時,一般會通過brk或者mmap系統調用進行申請。其中當申請內存小於128K時,會使用系統函數brk在堆區中分配;而當申請內存大於128K時,會使用系統函數mmap在映射區分配

4. 請你說一說C++的內存管理是怎樣的?

在C++中,虛擬內存分爲代碼段、數據段、BSS段、堆區、文件映射區以及棧區六部分。
代碼段:包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。
數據段:存儲程序中已初始化的全局變量和靜態變量
bss 段:存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化爲0的全局變量和靜態變量。
堆區:調用new/malloc函數時在堆區動態分配內存,同時需要調用delete/free來手動釋放申請的內存。
映射區:存儲動態鏈接庫以及調用mmap函數進行的文件映射
棧:使用棧空間存儲函數的返回地址、參數、局部變量、返回值

5. 請你來說一下C++/C的內存分配

32bitCPU可尋址4G線性空間,每個進程都有各自獨立的4G邏輯地址,其中0~3G是用戶態空間,3~4G是內核空間,不同進程相同的邏輯地址會映射到不同的物理地址中。其邏輯地址其劃分如下:
各個段說明如下:
3G用戶空間和1G內核空間
靜態區域:
text segment(代碼段):包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。
data segment(數據段):存儲程序中已初始化的全局變量和靜態變量
bss segment:存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化爲0的全局變量和靜態變量,對於未初始化的全局變量和靜態變量,程序運行main之前時會統一清零。即未初始化的全局變量編譯器會初始化爲0
動態區域:
heap(堆): 當進程未調用malloc時是沒有堆段的,只有調用malloc時採用分配一個堆,並且在程序運行過程中可以動態增加堆大小(移動break指針),從低地址向高地址增長。分配小內存時使用該區域。 堆的起始地址由mm_struct 結構體中的start_brk標識,結束地址由brk標識。
memory mapping segment(映射區):存儲動態鏈接庫等文件映射、申請大內存(malloc時調用mmap函數)
stack(棧):使用棧空間存儲函數的返回地址、參數、局部變量、返回值,從高地址向低地址增長。在創建進程時會有一個最大棧大小,Linux可以通過ulimit命令指定。

6. 請你回答一下如何判斷內存泄漏?

內存泄漏通常是由於調用了malloc/new等內存申請的操作,但是缺少了對應的free/delete。爲了判斷內存是否泄露,我們一方面可以使用linux環境下的內存泄漏檢查工具Valgrind,另一方面我們在寫代碼時可以添加內存申請和釋放的統計功能,統計當前申請和釋放的內存是否一致,以此來判斷內存是否泄露。

7. 請你來說一下什麼時候會發生段錯誤

段錯誤通常發生在訪問非法內存地址的時候,具體來說分爲以下幾種情況:
使用野指針
試圖修改字符串常量的內容

8. 請你來回答一下什麼是memory leak,也就是內存泄漏

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

  1. 堆內存泄漏 (Heap leak)。對內存指的是程序運行中根據需要分配通過malloc,realloc new等從堆中分配的一塊內存,再是完成後必須通過調用對應的 free或者delete 刪掉。如果程序的設計的錯誤導致這部分內存沒有被釋放,那麼此後這塊內存將不會被使用,就會產生Heap Leak.
  2. 系統資源泄露(Resource Leak)。主要指程序使用系統分配的資源比如 Bitmap,handle ,SOCKET等沒有使用相應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能降低,系統運行不穩定。
  3. 沒有將基類的析構函數定義爲虛函數。當基類指針指向子類對象時,如果基類的析構函數不是virtual,那麼子類的析構函數將不會被調用,子類的資源沒有正確是釋放,因此造成內存泄露。

9. 請你來回答一下new和malloc的區別

  1. new分配內存按照數據類型進行分配,malloc分配內存按照指定的大小分配;
  2. new返回的是指定對象的指針,而malloc返回的是void*,因此malloc的返回值一般都需要進行類型轉化。
  3. new不僅分配一段內存,而且會調用構造函數,malloc不會。
  4. new分配的內存要用delete銷燬,malloc要用free來銷燬;delete銷燬的時候會調用對象的析構函數,而free則不會。
  5. new是一個操作符可以重載,malloc是一個庫函數。
  6. malloc分配的內存不夠的時候,可以用realloc擴容。new沒用這樣操作。
  7. new如果分配失敗了會拋出bad_malloc的異常,而malloc失敗了會返回NULL。
  8. 申請數組時: new[]一次分配所有內存,多次調用構造函數,搭配使用delete[],delete[]多次調用析構函數,銷燬數組中的每個對象。而malloc則只能sizeof(int) * n。

10. 請你來說一下共享內存相關api

Linux允許不同進程訪問同一個邏輯內存,提供了一組API,頭文件在sys/shm.h中。
1)新建共享內存shmget
int shmget(key_t key,size_t size,int shmflg);
key:共享內存鍵值,可以理解爲共享內存的唯一性標記。
size:共享內存大小
shmflag:創建進程和其他進程的讀寫權限標識。
返回值:相應的共享內存標識符,失敗返回-1
2)連接共享內存到當前進程的地址空間shmat
void *shmat(int shm_id,const void *shm_addr,int shmflg);
shm_id:共享內存標識符
shm_addr:指定共享內存連接到當前進程的地址,通常爲0,表示由系統來選擇。
shmflg:標誌位
返回值:指向共享內存第一個字節的指針,失敗返回-1
3)當前進程分離共享內存shmdt
int shmdt(const void *shmaddr);
4)控制共享內存shmctl
和信號量的semctl函數類似,控制共享內存
int shmctl(int shm_id,int command,struct shmid_ds *buf);
shm_id:共享內存標識符
command: 有三個值
IPC_STAT:獲取共享內存的狀態,把共享內存的shmid_ds結構複製到buf中。
IPC_SET:設置共享內存的狀態,把buf複製到共享內存的shmid_ds結構。
IPC_RMID:刪除共享內存
buf:共享內存管理結構體。

11. 請你來說一下reactor模型組成

reactor模型要求主線程只負責監聽文件描述上是否有事件發生,有的話就立即將該事件通知工作線程,除此之外,主線程不做任何其他實質性的工作,讀寫數據、接受新的連接以及處理客戶請求均在工作線程中完成。其模型組成如下:
1)Handle:即操作系統中的句柄,是對資源在操作系統層面上的一種抽象,它可以是打開的文件、一個連接(Socket)、Timer等。由於Reactor模式一般使用在網絡編程中,因而這裏一般指Socket Handle,即一個網絡連接。
2)Synchronous Event Demultiplexer(同步事件複用器):阻塞等待一系列的Handle中的事件到來,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的執行返回的事件類型。這個模塊一般使用操作系統的select來實現。
3)Initiation Dispatcher:用於管理Event Handler,即EventHandler的容器,用以註冊、移除EventHandler等;另外,它還作爲Reactor模式的入口調用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的Event Handler處理,即回調EventHandler中的handle_event()方法。
4)Event Handler:定義事件處理方法:handle_event(),以供InitiationDispatcher回調使用。
5)Concrete Event Handler:事件EventHandler接口,實現特定事件處理邏輯。

12. 請自己設計一下如何採用單線程的方式處理高併發

在單線程模型中,可以採用I/O複用來提高單線程處理多個請求的能力,然後再採用事件驅動模型,基於異步回調來處理事件。

13. 請你說一說C++ STL 的內存優化

1)二級配置器結構
STL內存管理使用二級內存配置器。

  1. 第一級配置器
    第一級配置器以malloc(),free(),realloc()等C函數執行實際的內存配置、釋放、重新配置等操作,並且能在內存需求不被滿足的時候,調用一個指定的函數。
    一級空間配置器分配的是大於128字節的空間
    如果分配不成功,調用句柄釋放一部分內存
    如果還不能分配成功,拋出異常
  2. 第二級配置器
    在STL的第二級配置器中多了一些機制,避免太多小區塊造成的內存碎片,小額區塊帶來的不僅是內存碎片,配置時還有額外的負擔。區塊越小,額外負擔所佔比例就越大。
  3. 分配原則
    如果要分配的區塊大於128bytes,則移交給第一級配置器處理。
    如果要分配的區塊小於128bytes,則以內存池管理(memory pool),又稱之次層配置(sub-allocation):每次配置一大塊內存,並維護對應的16個空閒鏈表(free-list)。下次若有相同大小的內存需求,則直接從free-list中取。如果有小額區塊被釋放,則由配置器回收到free-list中。
    當用戶申請的空間小於128字節時,將字節數擴展到8的倍數,然後在自由鏈表中查找對應大小的子鏈表。
    如果在自由鏈表查找不到或者塊數不夠,則向內存池進行申請,一般一次申請20塊
    如果內存池空間足夠,則取出內存。
    如果不夠分配20塊,則分配最多的塊數給自由鏈表,並且更新每次申請的塊數。
    如果一塊都無法提供,則把剩餘的內存掛到自由鏈表,然後向系統heap申請空間,如果申請失敗,則看看自由鏈表還有沒有可用的塊,如果也沒有,則最後調用一級空間配置器。

2)二級內存池
二級內存池採用了16個空閒鏈表,這裏的16個空閒鏈表分別管理大小爲8、16、24…120、128的數據塊。這裏空閒鏈表節點的設計十分巧妙,這裏用了一個聯合體既可以表示下一個空閒數據塊(存在於空閒鏈表中)的地址,也可以表示已經被用戶使用的數據塊(不存在空閒鏈表中)的地址。

  1. 空間配置函數allocate
    首先先要檢查申請空間的大小,如果大於128字節就調用第一級配置器,小於128字節就檢查對應的空閒鏈表,如果該空閒鏈表中有可用數據塊,則直接拿來用(拿取空閒鏈表中的第一個可用數據塊,然後把該空閒鏈表的地址設置爲該數據塊指向的下一個地址),如果沒有可用數據塊,則調用refill重新填充空間。
  2. 空間釋放函數deallocate
    首先先要檢查釋放數據塊的大小,如果大於128字節就調用第一級配置器,小於128字節則根據數據塊的大小來判斷回收後的空間會被插入到哪個空閒鏈表。
  3. 重新填充空閒鏈表refill
    在用allocate配置空間時,如果空閒鏈表中沒有可用數據塊,就會調用refill來重新填充空間,新的空間取自內存池。缺省取20個數據塊,如果內存池空間不足,那麼能取多少個節點就取多少個。
    從內存池取空間給空閒鏈表用是chunk_alloc的工作,首先根據end_free-start_free來判斷內存池中的剩餘空間是否足以調出nobjs個大小爲size的數據塊出去,如果內存連一個數據塊的空間都無法供應,需要用malloc取堆中申請內存。
    假如山窮水盡,整個系統的堆空間都不夠用了,malloc失敗,那麼chunk_alloc會從空閒鏈表中找是否有大的數據塊,然後將該數據塊的空間分給內存池(這個數據塊會從鏈表中去除)。

3)總結:

  1. 使用allocate向內存池請求size大小的內存空間,如果需要請求的內存大小大於128bytes,直接使用malloc。
  2. 如果需要的內存大小小於128bytes,allocate根據size找到最適合的自由鏈表。
    a. 如果鏈表不爲空,返回第一個node,鏈表頭改爲第二個node。
    b. 如果鏈表爲空,使用blockAlloc請求分配node。
    x. 如果內存池中有大於一個node的空間,分配竟可能多的node(但是最多20個),將一個node返回,其他的node添加到鏈表中。
    y. 如果內存池只有一個node的空間,直接返回給用戶。
    z. 若果如果連一個node都沒有,再次向操作系統請求分配內存。
    ①分配成功,再次進行b過程。
    ②分配失敗,循環各個自由鏈表,尋找空間。
    I. 找到空間,再次進行過程b。
    II. 找不到空間,拋出異常。
  3. 用戶調用deallocate釋放內存空間,如果要求釋放的內存空間大於128bytes,直接調用free。
  4. 否則按照其大小找到合適的自由鏈表,並將其插入。

14. 請你說說select,epoll的區別,原理,性能,限制都說一說

1)IO多路複用
IO複用模型在阻塞IO模型上多了一個select函數,select函數有一個參數是文件描述符集合,意思就是對這些的文件描述符進行循環監聽,當某個文件描述符就緒的時候,就對這個文件描述符進行處理。
這種IO模型是屬於阻塞的IO。但是由於它可以對多個文件描述符進行阻塞監聽,所以它的效率比阻塞IO模型高效。
IO多路複用就是我們說的select,poll,epollselect/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。
當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
所以,I/O 多路複用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回。
I/O多路複用和阻塞I/O其實並沒有太大的不同,事實上,還更差一些。因爲這裏需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。
所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)
在IO multiplexing Model中,實際中,對於每一個socket,一般都設置成爲non-blocking,但是整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
2) select
select:是最初解決IO阻塞問題的方法。用結構體fd_set來告訴內核監聽多個文件描述符,該結構體被稱爲描述符集。由數組來維持哪些描述符被置位了。對結構體的操作封裝在三個宏定義中。通過輪尋來查找是否有描述符要被處理。
存在的問題:

  1. 內置數組的形式使得select的最大文件數受限與FD_SIZE;
  2. 每次調用select前都要重新初始化描述符集,將fd從用戶態拷貝到內核態,每次調用select後,都需要將fd從內核態拷貝到用戶態;
  3. 輪尋排查當文件描述符個數很多時,效率很低;

3) poll
poll:通過一個可變長度的數組解決了select文件描述符受限的問題。數組中元素是結構體,該結構體保存描述符的信息,每增加一個文件描述符就向數組中加入一個結構體,結構體只需要拷貝一次到內核態。poll解決了select重複初始化的問題。輪尋排查的問題未解決。

4)epoll
epoll:輪尋排查所有文件描述符的效率不高,使服務器併發能力受限。因此,epoll採用只返回狀態發生變化的文件描述符,便解決了輪尋的瓶頸。
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式

  1. LT模式
    LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的。
  2. ET模式
    ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)
    ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
  3. LT模式與ET模式的區別如下:
    LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
    ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

15. 請你說說C++如何處理內存泄漏?

使用varglind,mtrace檢測

(六)C++11新特性

1. 請問C++11有哪些新特性?

C++11 最常用的新特性如下:
(1)auto關鍵字:編譯器可以根據初始值自動推導出類型。但是不能用於函數傳參以及數組類型的推導
(2)nullptr關鍵字:nullptr是一種特殊類型的字面值,它可以被轉換成任意其它的指針類型;而NULL一般被宏定義爲0,在遇到重載時可能會出現問題。
(3)智能指針:C++11新增了std::shared_ptr、std::weak_ptr等類型的智能指針,用於解決內存管理的問題。
(4)初始化列表:使用初始化列表來對類進行初始化
(5)右值引用:基於右值引用可以實現移動語義和完美轉發,消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率
(6)atomic原子操作用於多線程資源互斥操作
(7)新增STL容器array以及tuple

2. 請你詳細介紹一下C++11中的可變參數模板、右值引用和lambda這幾個新特性。

(1)可變參數模板:
C++11的可變參數模板,對參數進行了高度泛化,可以表示任意數目、任意類型的參數,其語法爲:在class或typename後面帶上省略號”。
例如:
Template<class … T>
void func(T … args)
{
cout<<”num is”<<sizeof …(args)<<endl;
}
func();//args不含任何參數
func(1);//args包含一個int類型的實參
func(1,2.0)//args包含一個int一個double類型的實參
其中T叫做模板參數包,args叫做函數參數包
省略號作用如下:
1)聲明一個包含0到任意個模板參數的參數包
2)在模板定義得右邊,可以將參數包展成一個個獨立的參數
C++11可以使用遞歸函數的方式展開參數包,獲得可變參數的每個值。通過遞歸函數展開參數包,需要提供一個參數包展開的函數和一個遞歸終止函數。例如:
#include using namespace std;
// 最終遞歸函數
void print()
{
cout << “empty” << endl;
}
// 展開函數
template void print(T head, Args… args)
{
cout << head << “,”; print(args…);
}
int main()
{
print(1, 2, 3, 4); return 0;
}
參數包Args …在展開的過程中遞歸調用自己,沒調用一次參數包中的參數就會少一個,直到所有參數都展開爲止。當沒有參數時就會調用非模板函數printf終止遞歸過程。

(2)右值引用:
C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,沒有名字的就是右值。而在指C++11中,右值是由兩個概念構成,將亡值和純右值純右值是用於識別臨時變量和一些不跟對象關聯的值,比如1+3產生的臨時變量值,2、true等,而將亡值通常是指具有轉移語義的對象,比如返回右值引用T&&的函數返回值等。
C++11中,右值引用就是對一個右值進行引用的類型。由於右值通常不具有名字,所以我們一般只能通過右值表達式獲得其引用,比如:
T && a=ReturnRvale();
假設ReturnRvalue()函數返回一個右值,那麼上述語句聲明瞭一個名爲a的右值引用,其值等於ReturnRvalue函數返回的臨時變量的值。
基於右值引用可以實現轉移語義和完美轉發新特性。

移動語義:
對於一個包含指針成員變量的類,由於編譯器默認的拷貝構造函數都是淺拷貝,所有我們一般需要通過實現深拷貝的拷貝構造函數,爲指針成員分配新的內存並進行內容拷貝,從而避免懸掛指針的問題。
完美轉發:
完美轉發是指在函數模板中,完全依照模板的參數的類型,將參數傳遞給函數模板中調用的另一個函數,即傳入轉發函數的是左值對象,目標函數就能獲得左值對象,轉發函數是右值對象,目標函數就能獲得右值對象,而不產生額外的開銷。
因此轉發函數和目標函數參數一般採用引用類型,從而避免拷貝的開銷。其次,由於目標函數可能需要能夠既接受左值引用,又接受右值引用,所以考慮轉發也需要兼容這兩種類型。
C++11採用引用摺疊的規則,結合新的模板推導規則實現完美轉發。其引用摺疊規則如下:
因此,我們將轉發函數和目標函數的參數都設置爲右值引用類型,
當傳入一個X類型的左值引用時,轉發函數將被實例爲:
經過引用摺疊,變爲:
當傳入一個X類型的右值引用時,轉發函數將被實例爲:
經過引用摺疊,變爲:
除此之外,還可以使用forward()函數來完成左值引用到右值引用的轉換:

(3)Lambda表達式:
Lambda表達式定義一個匿名函數,並且可以捕獲一定範圍內的變量,其定義如下:
[capture] (params)mutable->return-type{statement}
其中,
[capture]:捕獲列表,捕獲上下文變量以供lambda使用。同時[]是lambda寅初復,編譯器根據該符號來判斷接下來代碼是否是lambda函數。
(Params):參數列表,與普通函數的參數列表一致,如果不需要傳遞參數,則可以連通括號一起省略。
mutable是修飾符,默認情況下lambda函數總是一個const函數,Mutable可以取消其常量性。在使用該修飾符時,參數列表不可省略。
->return-type:返回類型是返回值類型
{statement}:函數體,內容與普通函數一樣,除了可以使用參數之外,還可以使用所捕獲的變量。
Lambda表達式與普通函數最大的區別就是其可以通過捕獲列表訪問一些上下文中的數據。其形式如下:
Lambda的類型被定義爲“閉包”的類,其通常用於STL庫中,在某些場景下可用於簡化仿函數的使用,同時Lambda作爲局部函數,也會提高複雜代碼的開發加速,輕鬆在函數內重用代碼,無須費心設計接口。

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