C++ 常面知識點

1.new/delete 與malloc/free 的區別

參考:https://www.cnblogs.com/QG-whz/p/5140930.html

共同點:

(1)都可用於申請動態內存和釋放內存

不同點:

(1)malloc與free是C++/C 語言的標準庫函數new/delete 是C++的運算符。對於非內部數據類的對象而言,光用maloc/free 無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數, 對象消亡之前要自動執行析構函數。由於malloc/free 是庫函數而不是運算符不在編譯器控制權限之內不能夠把執行構造函數和析構函數的任務強加malloc/free。(本質區別)。由於內部數據類型的“對象”沒有構造與析構的過程對它們而言malloc/free和new/delete是等價的。

(2)用法上也有所不同

malloc與free:void * malloc(size_t size); 用malloc 申請一塊長度爲length 的整數類型的內存;malloc 返回值的類型是void *,所以在調用malloc 時要顯式地進行類型轉換,將void * 轉換成所需要的指針類型int *p = (int *) malloc(sizeof(int) * length); malloc 函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數。void free( void * memblock ) ,這是因爲指針p 的類型以及它所指的內存的容量事先都是知道的,語句free(p)能正確地釋放內存。如果p 是NULL 指針,那麼free對p 無論操作多少次都不會出問題如果p 不是NULL 指針,那麼free 對p連續操作兩次就會導致程序運行錯誤。

new/delete :運算符new 使用起來要比函數malloc 簡單得多,int *p2 = new int[length];new 內置了sizeof、類型轉換和類型安全檢查功能。對於非內部數據類型的對象而言,new 在創建動態對象的同時完成了初始化工作。如果類有多個構造函數那麼new 的語句也可以有多種形式。如果用new 創建對象數組,那麼只能使用類的無參數構造函數,在用delete 釋放對象數組時,留意不要丟了符號‘[]’。new對數組的支持體現在它會分別調用構造函數函數初始化每一個數組元素,釋放對象時爲每個對象調用析構函數注意delete[]要與new[]配套使用,不然會找出數組對象部分釋放的現象,造成內存泄漏。

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

(3)既然new/delete的功能完全覆蓋了malloc/free,爲什麼C++還保留malloc/free呢?

因爲C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存。如果用free釋放“new創建的動態對象”,那麼該對象因無法執行析構函數而可能導致程序出錯。如果用delete釋放“malloc申請的動態內存”,理論上講程序不會出錯,但是該程序的可讀性很差所以new/delete、malloc/free必須配對使用。

(4)new操作符從自由存儲區(free store)上爲對象動態分配內存空間,而malloc函數從堆上動態分配內存。自由存儲區是C++基於new操作符的一個抽象概念,凡是通過new操作符進行內存申請,該內存即爲自由存儲區。而堆是操作系統中的術語,是操作系統所維護的一塊特殊內存,用於程序的內存動態分配,C語言使用malloc從堆上分配內存,使用free釋放已分配的對應內存。那麼自由存儲區是否能夠是堆(問題等價於new是否能在堆上動態分配內存),這取決於operator new 的實現細節。自由存儲區不僅可以是堆,還可以是靜態存儲區,這都看operator new在哪裏爲對象分配內存。

(5)new操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符;而malloc內存分配成功則是返回void * ,需要通過強制類型轉換將void*指針轉換成我們需要的類型。類型安全很大程度上可以等價於內存安全。

(6)new內存分配失敗時會拋出bac_alloc異常,它不會返回NULLmalloc分配內存失敗時返回NULL。在使用C語言時,我們習慣在malloc分配內存後判斷分配是否成功;

(7)new操作符來分配對象的內存時:第一步:調用operator new 函數(對於數組是operator new[])分配一塊足夠大的,原始的,未命名的內存空間以便存儲特定類型的對象。第二步:編譯器運行相應的構造函數以構造對象,併爲其傳入初值。第三步:對象構造完成後,返回一個指向該對象的指針。delete運算符釋放對象時會經歷的步驟:第一步:調用對象的析構函數。第二步:編譯器調用operator delete(或operator delete[])函數釋放內存空間。malloc/free不會調用構造函數和析構函數,來處理C++的自定義類型不合適,其實不止自定義類型,標準庫中凡是需要構造/析構的類型通通不合適

(8)operator new /operator delete的實現可以基於malloc而malloc的實現不可以去調用new

(9)opeartor new /operator delete可以被重載。標準庫是定義了operator new函數和operator delete函數的8個重載版本。而malloc/free並不允許重載

(10)使用malloc分配的內存後,如果在使用過程中發現內存不足,可以使用realloc函數進行內存重新分配實現內存的擴充。realloc先判斷當前的指針所指內存是否有足夠的連續空間,如果有,原地擴大可分配的內存地址,並且返回原來的地址指針;如果空間不夠,先按照新指定的大小分配空間,將原有數據從頭到尾拷貝到新分配的內存區域,而後釋放原來的內存區域。new沒有這樣直觀的配套設施來擴充內存。

(11)在operator new拋出異常以反映一個未獲得滿足的需求之前,它會先調用一個用戶指定的錯誤處理函數,這就是new-handler。對於malloc,客戶並不能夠去編程決定內存不足以分配時要幹什麼事,只能看着malloc返回NULL。

2.c++多態:覆蓋和虛函數

C++多態(polymorphism)是通過虛函數來實現的虛函數允許子類重新定義成員函數,而子類重新定義父類的做法稱爲覆蓋(override)。最常見的用法就是聲明基類的指針利用該指針指向任意一個子類對象,調用相應的虛函數動態綁定。由於編寫代碼的時候並不能確定被調用的是基類的函數還是哪個派生類的函數,所以被成爲“虛”函數。如果沒有使用虛函數的話,即沒有利用C++多態性,則利用基類指針調用相應的函數的時候,將總被限制在基類函數本身,而無法調用到子類中被重寫過的函數。

純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型後加“=0” 。包含純虛函數的類稱爲抽象類,由於抽象類包含了沒有定義的純虛函數,所以不能定義抽象類的對象

虛函數是C++中用於實現多態的機制。核心理念就是通過基類訪問派生類定義的函數如果父類或者祖先類中函數func()爲虛函數,則子類及後代類中,函數func()是否加virtual關鍵字,都將是虛函數。爲了提高程序的可讀性,建議後代中虛函數都加上virtual關鍵字。

3. override (覆蓋),overload(重載)關鍵字,overwrite(重寫)關鍵字

override (覆蓋)關鍵字:僅在成員函數聲明之後使用時纔是區分上下文的且具有特殊含義;否則,它不是保留的關鍵字使用 override 有助於防止代碼中出現意外的繼承行爲。發生在父類和基類之間。

overload(重載)關鍵字:將語義相近的幾個函數用同一個名字表示,但是函數的參數或者返回值不同。發生在同一個類中,可有virtual 關鍵字

overwrite(重寫)關鍵字:派生類中屏蔽同名的基類函數不同範圍(派生類和基類),參數不同或者相同,無virtual 關鍵字

4. 指針和引用的區別

(1)指針是一個新的變量存儲了另外一個變量的地址,可以通過存儲的這個地址修改指針指向的變量;而引用是變量的標籤(別名)還是變量本身,任何作用於引用的操作都是作用於變量本身;

(2)指針可以多級指向,也引用只有一級。

(3)指針的傳參本質上還是值傳遞,需要通過解引用對指向對象進行操作,而引用傳遞傳進來是變量本身,可以直接對變量本身進行操作

3.虛函數表、虛函數指針大小

虛函數的實現機制:每個含有虛函數的類,至少有一個虛函數表,存放着該類所有虛函數的指針,派生類會生成一個兼容基類的虛函數表

4.gcc編譯過程,c++ 動態聯編

預處理:預處理指令;編譯:編譯成彙編代碼;彙編:把彙編代碼編譯成機器碼;鏈接:鏈接目標代碼生成可執行程序。

5.C++ 11新特性
6. const作用:const int func(const int& A) const , volatile關鍵字的作用

7. 虛繼承的實現原理

C++ 利用虛表和虛指針來實現虛函數;每個類用了一個虛表,每個類的對象用了一個虛指針;

推薦:https://blog.csdn.net/jiangnanyouzi/article/details/3720807


8.四種類型轉換

const_cast , static_cast , dynamic_cast , reinterpret_cast;

c語言風格的類型轉換:TypeName b = (TypeName)a;C++也是支持C風格的強制轉換,但是C風格的強制轉換可能帶來一些隱患

(1)const_cast:常量指針被轉化成非常量的指針, 並且仍然指向原來的對象;常量引用被轉換成非常量的引用,並且仍然指向原來的對象;const_cast一般用於修改指針。如const char *p形式

(2)static_cast :作用和C語言風格強制轉換的效果基本一樣,由於沒有運行時類型檢查來保證轉換的安全性,所以這類型的強制轉換和C語言風格的強制轉換都有安全隱患用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換。注意:進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性需要開發者來維護static_cast不能轉換掉原有類型的const、volatile、或者 __unaligned屬性;在c++ primer 中說道:c++ 的任何的隱式轉換都是使用 static_cast 來實現。

(3)dynamic_cast:使用 dynami_cast 轉換時檢查的運行期類型,dynamic_cast強制轉換,應該是這四種中最特殊的一個,因爲他涉及到面向對象的多態性和程序運行時的狀態,也與編譯器的屬性設置有關.

(4)reinterpret_cast:用來處理無關類型轉換的,通常爲操作數的位模式提供較低層次的重新解釋!他僅僅是重新解釋了給出的對象的比特模型,並沒有進行二進制的轉換!常常使用的地方:指針轉向足夠大的整數類型; 從整型或者enum枚舉類型轉換爲指針;從指向函數的指針轉向另一個不同類型的指向函數的指針; 從一個指向對象的指針轉向另一個不同類型的指向對象的指針; 從一個指向成員的指針轉向另一個指向類成員的指針!或者是類型,如果類型的成員和函數都是函數類型或者對象類型。

9.智能指針及其原理

c++裏面的四個智能指針: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中後三個是c++11支持,並且第一個已經被c++11棄用。

智能指針本質是存放在棧的模板對象,只是在棧內部包了一層指針。而棧在其生命週期結束時,其中的指針指向的堆內存也自然被釋放了。因而實現了智能管理的效果,不需要考慮內存問題了。

C++ 11之前auto_ptr 在c++11之後逐漸將它棄用,而加入了 shared_ptr, weak_ptr, unique_ptr ;包含的頭文件#include <memory>

auto_ptr:控制權可以隨便轉換(把賦值智能指針的內存交給被賦值智能指針),但是隻有一個在用,用起來會受到諸多限制,所以有了下面的智能指針。

auto_ptr <Base1> base1(new Base1);base1.get():返回當前指針對象;base1.release():清空當前智能指針對象,並返回類型指針。所以假如我們要正常刪除,那麼需要這樣:Base1*base2 = base1.release();delete base2;base1.reset():從圖中可看出,是重置智能指針,即把內存刪除,且智能指針指向空,但類型不變,所以可以這樣安全便捷地刪除

unique_ptr:中的拷貝構造和賦值操作符delete了,所以也就意味着,他和auto_ptr有區別,控制權唯一,不能隨意轉換。它倆用法都差不多;但是如果想切換控制權的話也不是沒有辦法,我們可以看到還有個這樣的函數:move函數;

shared_ptr:前兩者控制權唯一,切換的時候把前面的清除。而shared_ptr不會,可以共享多個使用;有個地方需要注意,當刪除一個智能指針時,並不影響其它兩個智能指針的繼續使用。因爲該片內存添加了一個引用計數,每shared_ptr一次,引用計數+1每次調用析構函數,引用計數減一。直到最後一個智能指針刪除,纔會釋放內存。其實就是和unique_ptr一樣可以通過move來切換控制權,這個時候是切換,不是共享了。auto_ptr和unique_ptr都可以通過move函數轉換成shared_ptr類型,當然,一樣是切換控制權的形式,即舊的置空。

weak_ptr:(1)他不像其餘三種,可以通過構造函數直接分配對象內存;他必須通過shared_ptr來共享內存。(2)沒有重載opreator*和->操作符,沒法使用該對象 (3)不主動參與引用計數,即,share_ptr釋放了,那麼weak_ptr所存的對象也釋放了。(4)使用成員函數use_count()可以查看當前引用計數expired()判斷引用計數是否爲空。(5)lock()函數,返回一個shared_ptr智能指針;

weak_ptr是爲了配合shared_ptr而引入的一種智能指針,它更像是shared_ptr的一個助手而不是智能指針,最大作用在於協助shared_ptr工作,像旁觀者那樣觀測資源的使用情況

shared_ptr不是線程安全的:

參考博客:https://blog.csdn.net/zy19940906/article/details/50470087

11.c++初始化列表, export

C++ 類構造函數初始化列表的異常機制 function-try block,https://blog.csdn.net/hikaliv/article/details/4457090

初始化對象成員不會再調用默認構造函數,再調用複製構造函數,而是直接調用複製構造函數https://blog.csdn.net/hikaliv/article/details/4456862

 

12.C++類的析構函數不用虛函數,對繼承有什麼影響?

13.C++內存管理

堆,棧,自由存儲存儲(有的說法是代碼區),靜態存儲區,常量區

在函數中,函數的局部變量的存儲單元在棧上函數執行結束後棧區自動釋放。棧內存分配效率高,由操作系統和編譯器自動分配,但存儲空間有限。

堆區:堆區中的內存由程序員自己創建並維護,每個new(malloc)都應該對應於一個delete(free),如果程序員忘記釋放堆內存則在程序最後結束後會由操作系統完成釋放,但在程序運行過程中可能會造成堆區越來越大,從而造成內存溢出。

靜態(全局)存儲區:這部分內存區存儲程序的全局變量和靜態變量

常量區:常量區內存空間存儲常量(包括字符串,等內容)

代碼區:存放函數體的二進制代碼。

堆棧的區別:(1)管理方式不同(2)空間大小不同;棧的內存空間是連續的空間大小通常是系統預先規定好的,即棧頂地址和最大空間是確定的;而堆得內存空間是不連續的,由一個記錄空間的鏈表負責管理因此內存空間幾乎沒有限制,在32位系統下,內存空間大小可達到4G(3)能否產生碎片不同:由於管理方式的不同,所以堆更容易產生碎片。這是由於頻繁的調用new/delete而造成內存空間不連續。而對於棧,其由操作系統管理,每次彈出的內存塊意味着它上面的內存塊也已經彈出,所以幾乎不會產生碎片。(4)生長方式不同堆的生長方向是向上的,也就是向着內存地址增加的方向而對於棧,其的生長放下是向下的,向着內存地址減小的方向生長。(5)分配方式不同:堆是動態分配的;而棧其實具有兩種分配方式,在棧的靜態分配中是由編譯器完成的,如局部變量的分配;動態分配的棧是由函數 alloca完成的,雖然是動態分配的棧,但是我們也無需對其進行手工釋放,也是由操作系統完成的。(6)分配效率不同: 棧是及其系統提供的數據結構,計算機對其底層提供支持有專門的寄存器存放棧的地址,並有專門的指令執行push/pop等操作。而堆是由庫函數提供的,其機制很複雜。

14.explicit, export, mutable關鍵字的含義

mutable 可變的,易變,類成員加上它,const function 可以修改它

15.深拷貝和淺拷貝的區別

c++ 默認的拷貝是欠拷貝,對於指針類型而言如果是淺拷貝,一個被析構(delete),另外一個也就完蛋了,所以要用深拷貝

16.右值,右值構造函數,move,右值指針

c++引入右值引用和移動語義可以避免無謂的複製,提高程序的性能;c++中所有的值必然屬於左值或者右值中的一種

 左值指的是既能夠出現在等號左邊也能出現在等號右邊的變量(或表達式),右值指的則是隻能出現在等號右邊的變量(或表達式)

左值是指表達式結束後依然存在的持久對象,而右值是指表達式結束時就不再存在的臨時對象T& 指向的是 lvalue,而 const T& 指向的,卻可能是 lvalue 或 rvalue,左值引用&與右值引用&&(右值引用是c++11加上的)判斷左值還是右值的放法:看能不能對錶達式取地址,如果能,則爲左值,否則爲右值

move函數可以是用於構造函數,也可以用於賦值函數,但都需要手動顯示添加。其實move函數用直白點的話來說就是省去拷貝構造和賦值時中間的臨時對象,將資源的內存從一個對象移動到(共享也可以)另一個對象。官話是:c++11 中的 move() 是這樣一個函數,它接受一個參數,然後返回一個該參數對應的右值引用。

std::forward<T>(u) 有兩個參數:T 與 u。當T爲左值引用類型時,u將被轉換爲T類型的左值,否則u將被轉換爲T類型右值。如此定義std::forward是爲了在使用右值引用參數的函數模板中解決參數的完美轉發問題。

(1)將亡值則是c++11新增的和右值引用相關的表達式,這樣的表達式通常時將要移動的對象T&&函數返回值、std::move()函數的返回值等,c++11增加了右值引用,常說的引用指的是左值引用,右值引用相當於給右值取了別名,延長了右值的生命週期;左值引用只能綁定左值,右值引用只能綁定右值如果綁定的不對,編譯就會失敗。右值引用其實是個左值,已經可以取地址了,擁有了地址。

(2)常量左值引用卻是個奇葩,它可以算是一個“萬能”的引用類型,它可以綁定非常量左值、常量左值、右值,而且在綁定右值的時候,常量左值引用還可以像右值引用一樣將右值的生命期延長缺點是,只能讀不能改

(3)總結一下,其中T是一個具體類型:

  1. 左值引用, 使用 T&, 只能綁定左值
  2. 右值引用, 使用 T&&, 只能綁定右值
  3. 常量左值, 使用 const T&, 既可以綁定左值又可以綁定右值
  4. 已命名的右值引用,編譯器會認爲是個左值
  5. 編譯器有返回值優化,但不要過於依賴

(4)要實現移動語義就必須增加兩個函數:移動構造函數和移動賦值構造函數。

move 避免了沒有意義的資源申請和釋放操作,以及內存間的拷貝操作;對於左值才調用拷貝構造函數,而對於右值,調用移動構造函數c++11使用std::move將左值(比如可以轉換一些局部變量,延長局部變量的生命週期)轉換爲右值,從而方便應用移動構造函數和移動複製構造函數,它其實就是告訴編譯器,雖然我是一個左值但是不要對我用拷貝構造函數,而是用移動構造函數吧。。。

(5)如果編譯器找不到移動構造函數,,就會對右值調用拷貝構造函數,而且把變量變成右值,變量不會立刻失效,會在變量對應的作用域之後才失效,所以變量和構造的對象會共享內存空間,產生意想不到的錯誤;C++11的所有容器都實現了move語義;move只是轉移了資源的控制權,本質上是將左值強制轉化爲右值使用,以用於移動拷貝或賦值,避免對含有資源的對象發生無謂的拷貝​​​​​和資源申請;move對於擁有如內存、文件句柄等資源的成員的對象有效,如果是一些基本類型,如int和char[10]數組等,如果使用move,仍會發生拷貝(因爲沒有對應的移動構造函數),所以說move對含有資源的對象說更有意義。

(6)當右值引用和模板結合的時候,就複雜了。T&&並不一定表示右值引用,它可能是個左值引用又可能是個右值引用:

template<typename T>
void f( T&& param){
    
}
f(10);  //10是右值
int x = 10; //
f(x); //x是左值

這裏的&&是一個未定義的引用類型,稱爲universal references它必須被初始化它是左值引用還是右值引用卻決於它的初始化,如果它被一個左值初始化,它就是一個左值引用;如果被一個右值初始化,它就是一個右值引用。

(7)引用疊加規則:

  1. 所有的右值引用疊加到右值引用上仍然使一個右值引用。
  2. 所有的其他引用類型之間的疊加都將變成左值引用。
template<typename T>
void f( T&& param); //這裏T的類型需要推導,所以&&是一個 universal references

傳遞左值進去,就是左值引用,傳遞右值進去,就是右值引用。如它的名字,這種類型確實很"通用",下面要講的完美轉發,就利用了這個特性。

(8)完美轉發

所謂轉發,就是通過一個函數將參數繼續轉交給另一個函數進行處理,原參數可能是右值可能是左值,如果還能繼續保持參數的原有特徵,那麼它就是完美的。c++中提供了一個std::forward()模板函數解決這個問題;在universal referencesstd::forward的合作下,能夠完美的轉發。

(9)emplace_back減少內存拷貝和移動

emplace_back()可以直接通過構造函數的參數構造對象,但前提是要有對應的構造函數(使用emplace_back()替換push_back()

(10) 高效的交換

template <typename T>
void swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

對容器類而言

參考博客:https://www.jianshu.com/p/d19fc8447eaa

17.static 加全局變量,理解局部變量, 全局變量,局部靜態變量,全局靜態變量的區別

c++ 變量根據作用的位置有着不同的生命週期,具有不同作用域作用域可分爲6種:全局作用域,局部作用域,語句作用域,類作用域,命名空間作用域和文件作用域。

從作用域上看:

局部變量也只有局部作用域,它是自動對象(auto),它在程序運行期間不是一直存在,而是只在函數執行期間存在,函數的一次調用執行結束後,變量被撤銷,其所佔用的內存也被收回

全局變量具有全局作用域。全局變量只需在一個源文件中定義,就可以作用於所有的源文件。當然,其他不包含全局變量定義的源文件需要用extern 關鍵字再次聲明這個全局變量。

靜態局部變量具有局部作用域它只被初始化一次自從第一次被初始化直到程序運行結束都一直存在,它和全局變量的區別在於全局變量對所有的函數都是可見的,而靜態局部變量只對定義自己的函數體始終可見

靜態全局變量也具有全局作用域,它與全局變量的區別在於如果程序包含多個文件的話它作用於定義它的文件裏不能作用到其它文件裏,即被static關鍵字修飾過的變量具有文件作用域。這樣即使兩個不同的源文件都定義了相同名字的靜態全局變量,它們也是不同的變量。

全局變量本身就是靜態存儲方式靜態全局變量當然也是靜態存儲方式這兩者在存儲方式上並無不同。這兩者的區別雖在於非靜態全局變量的作用域是整個源程序,當一個源程序由多個源文件組成時,非靜態的全局變量在各個源文件中都是有效的。 而靜態全局變量則限制了其作用域, 即只在定義該變量的源文件內有效,在同一源程序的其它源文件中不能使用它。由於靜態全局變量的作用域侷限於一個源文件內,只能爲該源文件內的函數公用,因此可以避免在其它源文件中引起錯誤

從分配內存空間看
全局變量,靜態局部變量,靜態全局變量都在靜態存儲區分配空間,而局部變量在棧裏分配空間。

1)、靜態變量會被放在程序的靜態數據存儲區(數據段)(全局可見)中,這樣可以在下一次調用的時候還可以保持原來的賦值。這一點是它與堆棧變量和堆變量的區別。
2)、變量用static告知編譯器,自己僅僅在變量的作用範圍內可見。這一點是它與全局變量的區別。

把局部變量改變爲靜態變量後是改變了它的存儲方式即改變了它的生存期。把全局變量改變爲靜態變量後是改變了它的作用域,限制了它的使用範圍。因此static 這個說明符在不同的地方所起的作用是不同的

函數中必須要使用static變量情況:比如當某函數的返回值爲指針類型時,則必須是static的局部變量的地址作爲返回值,若爲auto類型,則返回爲錯指針。

static 全局變量:改變作用範圍,不改變存儲位置

static 局部變量:改變存儲位置,不改變作用範圍

靜態函數 :在函數的返回類型前加上static關鍵字,函數即被定義爲靜態函數。靜態函數與普通函數不同,它只能在聲明它的文件當中可見,不能被其它文件使用。

 如果在一個源文件中定義的函數,只能被本文件中的函數調用,而不能被同一程序其它文件中的函數調用,這種函數也稱爲內部函數。定義一個內部函數,只需在函數類型前再加一個“static”關鍵字即可。

18.組合和繼承的區別以及採用哪一個更好

C++三大特性:封裝, 繼承,多態

繼承是爲了實現代碼的複用, 如果在邏輯上B是A的一種,即B類的man也是A類的 Hunman的一種我們就可以讓B類去繼承A類。

組合也是類的一種複用技術, 它遵循的就是如果A類是B類的一部分,則不要讓B類去繼承A類而是採用組合的形式;

組合的優點:(1)不會破環封裝性,父類的任何變化不會引起子類的變化;(2)組合運用複雜的設計,他們的關係是在程序運行的時候才確定,可以支持動態的組合;(3)整體類可以對局部類的接口進行封裝,提供新的接口。

組合的缺點:(1)整體類不能自動獲得和局部類同樣的接口,只有通過創建局部的對象去調用它;(2)創建整體類的時候需要創建局部類的對象。

繼承的優點:(1)子類繼承了父類能自動獲得父類的接口;(2)創建子類對象的時候不用創建父類對象

繼承的缺點:(1)破壞了封裝父類的改變必定引起子類的改變子類缺乏獨立性;(2)支持功能上的擴展,但多重繼承往往增加了系統結構的複雜度;​​​​(3) 繼承是在靜態編譯的時候就已經確定了關係不支持動態繼承

因此優先考慮組合而不是繼承

19.STL map 和vector的實現機制

STL map的原理:內部實現是二叉平衡樹(紅黑樹), 

20.紅黑樹(近似平衡的二叉搜素樹)的算法原理

平衡二叉樹最大的作用就是查找,AVL樹的查找、插入和刪除在平均和最壞情況下都是O(logn)。

(1)AVL樹的時間複雜度雖然優於紅黑樹,但是對於現在的計算機,cpu太快,可以忽略性能差異 ;

(2)紅黑樹的插入刪除比AVL樹更便於控制操作

(3)紅黑樹整體性能略優於AVL樹(紅黑樹旋轉情況少於AVL樹)

紅黑樹是一棵二叉搜索樹,它在每個節點增加了一個存儲位記錄節點的顏色,可以是RED,也可以是BLACK;通過任意一條從根到葉子簡單路徑上顏色的約束,紅黑樹保證最長路徑不超過最短路徑的二倍,因而近似平衡。

性質:

(1)每個節點顏色不是黑色,就是紅色

(2)根節點是黑色的

(3)如果一個節點是紅色,那麼它的兩個子節點就是黑色的(沒有連續的紅節點)

(4)對於每個節點,從該節點到其後代葉節點的簡單路徑上,均包含相同數目的黑色節點

紅黑樹的插入過程:

(1)紅黑樹是二叉搜索樹,所以按照二叉搜索樹的方法對其進行節點插入

(2)RBTree有顏色約束性質,因此我們在插入新節點之後要進行顏色調整

a)根節點爲NULL,直接插入新節點並將其顏色置爲黑色

b)根節點不爲NULL,找到要插入新節點的位置

c)插入新節點

d)判斷新插入節點對全樹顏色的影響,更新調整顏色

一共分爲三種情況對紅黑樹進行調整

(1)cur爲紅,parent爲紅,pParent爲黑,uncle存在且爲紅 
則將parent,uncle改爲黑,pParent改爲紅,然後把pParent當成cur,繼續向上調整。

參考:https://blog.csdn.net/tanrui519521/article/details/80980135

21.平衡二叉樹的構建過程(AVL)

平衡二叉樹是二叉搜索樹的一種:

它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。

平衡二叉樹中引入了一個概念:平衡二叉樹節點的平衡因子,它指的是該節點的兩個子樹,即左子樹和右子樹的高度差即用左子樹的高度減去右子樹的高度如果該節點的某個子樹不存在,則該子樹的高度爲0,如果高度差的絕對值超過1就要根據情況進行調整。

參考博客:https://blog.csdn.net/u014634338/article/details/42465089

22. C++原子操作,atomic

原子(atomic)本意是”不能被進一步分割的最小粒子”,而原子操作(atomic operation)意爲”不可被中斷的一個或一系列操作”.

(1)處理器使用總線鎖保證原子性,CPU提供了在指令執行期間對總線加鎖的手段,這樣同一總線上別的CPU就暫時不能通過總線訪問內存了,保證了這條指令在多處理器環境中的

(2)使用緩衝鎖保證原子性,在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷(代價)比較大。如果緩存在處理器緩存行中內存區域在LOCK操作期間被鎖定;修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時會起緩存行無效。

23.extern 的作用,export的作用

extern C C++的編譯器會對程序中的符號進行修飾,這個過程在編譯器中叫符號修飾(Name Decoration)或者符號改編(Name Mangling),C++是能夠兼容C的,如果我們有了一個C語言的頭文件和其對於的庫,在C++中如何使用它呢?在include該頭文件的時候當然要加入extern "C"否則按照C++的符號進行符號修飾,那麼在庫中就會找不到該符號了。

爲了訪問其他編譯單元(如另一代碼文件)中的變量或對象,對普通類型(包括基本數據類、結構和類)可以利用關鍵字extern,來使用這些變量或對象時;但是對模板類型,則必須在定義這些模板類對象和模板函數時,使用標準C++新增加的關鍵字export導出/出口/輸出)。

24.vector<int>返回值類型的函數,在返回時會出現什麼情況?一定會是拷貝構造嗎?

常規的是會的:調用相應的構造函數構造function()返回的匿名對象 -> 析構function()中的對象 -> function()結束 -> 主調函數繼續執行

考慮編譯器的優化,類似右值作爲參數,他是不會重複申請內存,而是直接延續右值變量。

參考:https://blog.csdn.net/u010029439/article/details/80808425

25.C++並行與併發,多線程編程

要實現併發有兩種方法:多進程和多線程。

使用多進程併發是將一個應用程序劃分爲多個獨立的進程(每個進程只有一個線程)這些獨立的進程間可以互相通信共同完成任務。由於操作系統對進程提供了大量的保護機制以避免一個進程修改了另一個進程的數據,使用多進程比多線程更容易寫出安全的代碼。但這也造就了多進程併發的兩個缺點:

在進程件的通信,無論是使用信號、套接字,還是文件、管道等方式,其使用要麼比較複雜,要麼就是速度較慢或者兩者兼而有之

運行多個進程的開銷很大操作系統要分配很多的資源來對這些進程進行管理

所以C++選用多線程併發:在同一個進程中執行多個線程。有操作系統相關知識的應該知道,線程是輕量級的進程每個線程可以獨立的運行不同的指令序列,但是線程不獨立的擁有資源依賴於創建它的進程而存在。也就是說,同一進程中的多個線程共享相同的地址空間,可以訪問進程中的大部分數據,指針和引用可以在線程間進行傳遞。這樣,同一進程內的多個線程能夠很方便的進行數據共享以及通信,也就比進程更適用於併發操作。由於缺少操作系統提供的保護機制,在多線程共享數據及通信時,就需要程序員做更多的工作以保證對共享數據段的操作是以預想的操作順序進行的,並且要極力的避免死鎖(deadlock)。

++11的標準庫中提供了多線程庫,使用時需要#include <thread>頭文件,該頭文件主要包含了對線程的管理類std::thread以及其他管理線程相關的類。

CPU有4核,可以同時執行4個線程這是沒有問題了,但是控制檯卻只有一個,同時只能有一個線程擁有這個唯一的控制檯,將數字輸出;

共享數據的管理以及線程間的通信,是多線程編程的兩大核心。每個應用程序至少有一個進程,而每個進程至少有一個主線程除了主線程外,在一個進程中還可以創建多個線程每個線程都需要一個入口函數入口函數返回退出該線程也會退出主線程就是以main函數作爲入口函數的線程。在C++ 11的線程庫中,將線程的管理在了類std::thread中,使用std::thread可以創建、啓動一個線程,並可以將線程掛起、結束等操作。

線程間通信的三種方式:共享內存、管道通信(Linux)、future通信機制
(1)共享內存:多線程會共享全局變量區,所以可以多個線程去option 這個臨界區的XXX;共享內存會引發不安全的結果  ==》所以就有了一些保護機制:互斥鎖mutex條件變量cv原子操作和線程局部存儲等。

(2)管道通信(Linux):與進程間通信的不同,進程間通信時,子進程會copy父進程的fd,故兩端要各關閉一個讀寫。

(3)future通信機制

26.C++ 優先隊列priority_queue 的實現原理以及使用方法

實現原理利用堆實現的

27.hash_map的底層實現,hash的散列方法

新版c++的hash_map都是unordered_map;map和hash_map在運行效率方面:unordered_map最高而map效率較低但 提供了穩定效率和有序的序列。佔用內存方面:map內存佔用略低unordered_map內存佔用略高,而且是線性成比例的。需要無序容器快速查找刪除,不擔心略高的內存時用unordered_map;有序容器穩定查找刪除效率,內存很在意時候用map

hash_map內部是一個hash_table一般是由一個大vector, vector元素節點可掛接鏈表來解決衝突,來實現.

hash_map其插入過程是:得到key,通過hash函數得到hash值,得到桶號(一般都爲hash值對桶數求模),存放key和value在桶內。

其取值過程是:得到key,通過hash函數得到hash值;得到桶號(一般都爲hash值對桶數求模);比較桶的內部元素是否與key相等;若都不相等,則沒有找到;取出相等的記錄的value。
 

28.c++ 常用的設計模式

參考:https://blog.csdn.net/hechao3225/article/details/71366058

(1)單例模式

作用:保證一個類只有一個實例,並提供一個訪問它的全局訪問點,使得系統中只有唯一的一個對象實例

應用:常用於管理資源,如日誌、線程池

實現要點:在類中,要構造一個實例,就必須調用類的構造函數,並且爲了保證全局只有一個實例,需要提供一個全局訪問點,就需要在類中定義一個static函數,返回在類內部唯一構造的實例需防止在外部調用類的構造函數而構造實例需要將構造函數的訪問權限標記爲private。

同時阻止拷貝創建對象時賦值時拷貝對象,因此也將它們聲明並權限標記爲private

(2)工廠模式:簡單工廠模式,工廠方法模式,抽像工廠模式;工廠模式的主要作用是封裝對象的創建,分離對象的創建和操作過程,用於批量管理對象的創建過程,便於程序的維護和擴展。​​​​​​​

簡單工廠模式:工廠模式最簡單的一種實現,對於不同產品的創建定義一個工廠類,將產品的類型作爲參數傳入到工廠的創建函數,根據類型分支選擇不同的產品構造函數。

工廠方法模式:工廠方法模式在簡單工廠模式的基礎上增加對工廠的基類抽象不同的產品創建採用不同的工廠創建(從工廠的抽象基類派生),這樣創建不同的產品過程就由不同的工廠分工解決:FactoryA專心負責生產ProductA,FactoryB專心負責生產ProductB,FactoryA和FactoryB之間沒有關係;如果到了後期,如果需要生產ProductC時,我們則可以創建一個FactoryC工廠類,該類專心負責生產ProductC類產品。該模式相對於簡單工廠模式的優勢在於:便於後期產品種類的擴展。

抽像工廠模式:抽象工廠模式對工廠方法模式進行了更加一般化的描述工廠方法模式適用於產品種類結構單一的場合,爲一類產品提供創建的接口;而抽象工廠方法適用於產品種類結構多的場合,就是當具有多個抽象產品類型時,抽象工廠便可以派上用場。抽象工廠模式更適合實際情況,受生產線所限,讓低端工廠生產不同種類的低端產品,高端工廠生產不同種類的高端產品。

29.c++ public,protected, private 以及對應的繼承方式

(1)訪問範圍

private: 只能由該類中的函數、其友元函數訪問,不能被任何其他訪問,該類的對象也不能訪問

protected: 可以被該類中的函數子類的函數、以及其友元函數訪問,但不能被該類的對象訪問。

public: 可以被該類中的函數、子類的函數、其友元函數訪問,也可以由該類的對象訪問 。

(2)訪問權限

public:可以被任意實體訪問

protected:只允許子類及本類的成員函數訪問

private:只允許本類的成員函數訪問

(3)三種繼承方法

基類中 繼承方式 子類中

public & public繼承 => public

public & protected繼承 => protected

public & private繼承 = > private
protected & public繼承 => protected

protected & protected繼承 => protected

protected & private繼承 = > private
private & public繼承 => 子類無權訪問

private & protected繼承 => 子類無權訪問

private & private繼承 = > 子類無權訪問

public繼承不改變基類成員的訪問權限,private繼承使得基類所有成員在子類中的訪問權限變爲private,protected繼承將基類中public成員變爲子類的protected成員,其它成員的訪問 權限不變。基類中的private成員不受繼承方式的影響,子類永遠無權訪問。

30. C++內聯函數(行函數)

內聯函數是C++中的一種特殊函數,它可以像普通函數一樣被調用,但是在調用時並不通過函數調用的機制而是通過將函數體直接插入調用處來實現的這樣可以大大減少由函數調用帶來的開銷,從而提高程序的運行效率。一般來說inline用於定義類的成員函數。inline適用的函數有兩種,一種是在類內定義的成員函數,另一種是在類內聲明,類外定義的成員函數;

(1)類內定義成員函數

這種情況下,我們可以不用在函數頭部加inline關鍵字,因爲編譯器會自動將類內定義的函數聲明爲內聯函數,編譯器會自動將類內定義的函數(構造函數、析構函數、普通成員函數等)設置爲內聯,具有內聯函數調用的性質

(2)類內聲明函數,在類外定義函數

根據C++編譯器的規則,這種情況下如果想將該函數設置爲內聯函數,則可以在類內聲明時不加inline關鍵字,而在類外定義函數時加上inline關鍵字;另外,我們可以在聲明函數和定義函數的同時寫inline,也可以只在函數聲明時加inline,而定義函數時不加inline。只要在調用該函數之前把inline的信息告知編譯系統,編譯系統就會在處理函數調用時按內聯函數處理。

內聯函數的優缺點:

優點:(1)inline 定義的類的內聯函數,函數的代碼被放入符號表中,在使用時直接進行替換,(像宏一樣展開);沒有了調用的開銷,效率也很高。(2)2.很明顯,類的內聯函數也是一個真正的函數,編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。然後進行一系列的相關檢查,就像對待任何一個真正的函數一樣。這樣就消除了它的隱患和侷限性。(宏替換不會檢查參數類型,安全隱患較大)(3)inline函數可以作爲一個類的成員函數,與類的普通成員函數作用相同,可以訪問一個類的私有成員和保護成員。內聯函數可以用於替代一般的宏定義,最重要的應用在於類的存取函數的定義上面。

缺點:(1)內聯函數具有一定的侷限性,內聯函數的函數體一般來說不能太大,如果內聯函數的函數體過大,一般的編譯器會放棄內聯方式,而採用普通的方式調用函數。(換句話說就是,你使用內聯函數,只不過是向編譯器提出一個申請,編譯器可以拒絕你的申請)這樣,內聯函數就和普通函數執行效率一樣了。因此並不是說把一個函數定義爲inline函數就一定會被編譯器識別爲內聯函數具體取決於編譯器的實現和函數體的大小。內聯函數不能包括複雜的控制語句,如循環語句和switch語句;​​​​​​​只將規模很小(一般5個語句一下)而使用頻繁的函數聲明爲內聯函數。在函數規模很小的情況下,函數調用的時間開銷可能相當於甚至超過執行函數本身的時間,把它定義爲內聯函數,可大大減少程序運行時間。

內聯函數和宏定義的區別

內聯函數和宏的區別在於,宏是由預處理器對宏進行替代,而內聯函數是通過編譯器控制來實現的而且內聯函數是真正的函數,只是在需要用到的時候,內聯函數像宏一樣的展開,所以取消了函數的參數壓棧,減少了調用的開銷。你可以象調用函數一樣來調用內聯函數,而不必擔心會產生於處理宏的一些問題。內聯函數與帶參數的宏定義進行下比較,它們的代碼效率是一樣,但是內聯歡函數要優於宏定義,因爲內聯函數遵循的類型和作用域規則,它與一般函數更相近,在一些編譯器中,一旦關聯上內聯擴展,將與一般函數一樣進行調用,比較方便。 

       另外,宏定義在使用時只是簡單的文本替換並沒有做嚴格的參數檢查也就不能享受C++編譯器嚴格類型檢查的好處另外它的返回值也不能被強制轉換爲可轉換的合適的類型,這樣,它的使用就存在着一系列的隱患和侷限性。

       C++的inline的提出就是爲了完全取代宏定義,因爲inline函數取消了宏定義的缺點,又很好地繼承了宏定義的優點,《Effective C++》中就提到了儘量使用Inline替代宏定義的條款,足以說明inline的作用之大。

31.B樹/B+樹的特點及其實現原理

動態查找樹主要包括:二叉搜索樹,平衡二叉樹,紅黑樹,B樹,B-樹時間複雜度O(log2N),通過對樹高度的降低可以提升查找效率​​​​​​​

B樹:就是爲了存儲設備或者磁盤設計的一種平衡查找樹

B樹的節點可以有很多孩子節點紅黑樹是一種近似平衡的二叉搜索樹即每個節點只有兩個孩子,一顆含有N個節點的B樹和紅黑樹的高度是一樣的O(lgn)

m階B樹的定義:(1)每個節點至多有m顆子樹;(2)除了根結點和葉子結點其他結點至少有[ceil(m / 2)(代表是取上限的函數)]個孩子;(3)若根結點不是葉子結點時,則至少有兩個孩子(除了沒有孩子的根結點)(4)所有的葉子結點都出現在同一層中葉子結點不包含任何關鍵字信息;

B樹的插入:

1)若B樹中已存在需要插入的鍵值時,用新的鍵值替換舊值;

(2)若B樹中不存在這個值,則在葉子節點進行插入操作;

對於高度爲h的m階B樹,新節點一般插在第h層。    
1)若該節點中關鍵碼個數小於m-1,則直接插入
2)若該節點中關鍵碼個數等於m-1,則節點分裂。以中間的關鍵碼爲界,
    將節點一分爲二,產生一個新的節點,並將中間關鍵碼插入到父節點中。
重複上述過程,最壞情況一直分裂高根節點,則B樹就會增加一層。
B樹刪除:

首先要查找該值是否在B樹中存在,如果存在,判斷該元素是否存在左右孩子結點,如果有,則上移孩子結點中的相近結點(左孩子最右邊的結點或者有孩子最左邊的結點)到父結點中,然後根據移動之後的情況;如果沒有,進行直接刪除;如果不存在對應的值,則刪除失敗。

1)如果當前要刪除的值位於非葉子結點,則用後繼值覆蓋要刪除的值,再用後繼值所在的分支刪除該後繼值。(該後繼值必須位於葉子結點上)

2)該結點值個數不小於Math.ceil(m/2)-1(取上線函數),結束刪除操作,否則下一步

3)如果兄弟結點值個數大於Math.ceil(m/2)-1,則父結點中下移到該結點,兄弟的一個值上移,刪除操作結束。

將父結點的key下移與當前的結點和他的兄弟姐妹結點key合併,形成一個新的結點,

有些結點可能有左兄弟,也有右兄弟,我們可以任意選擇一個兄弟結點即可。

B+樹:B+樹用於數據庫和文件系統中,NTFS等都使用B+樹作爲數據索引

是B樹的一種變形,它把數據都存儲在葉結點,而內部結點只存關鍵字孩子指針;因此簡化了內部結點的分支因子B+樹遍歷也更高效,其中B+樹只需所有葉子節點串成鏈表這樣就可以從頭到尾遍歷,其中內部結點是並不存儲信息,而是存儲葉子結點的最小值作爲索引,下面將講述到。

(1)有n棵子樹的結點含有n個關鍵字每個關鍵字都不會保存數據,只會用來索引,並且所有數據都會保存在葉子結點;(2)所有的葉子結點包含所有關鍵字信息以及指向關鍵字記錄的指針關鍵字自小到大順序連接;

B+樹的插入:

1)若爲空樹,直接插入,此時也就是根結點

2)對於葉子結點:根據key找葉子結點,對葉子結點進行插入操作。插入後,如果當前結點key的個數不大於m-1,則插入就結束。反之將這個葉子結點分成左右兩個葉子結點進行操作,左葉子結點包含了前m/2個記錄,右結點包含剩下的記錄key,將第m/2+1個記錄的key進位到父結點中(父結點必須是索引類型結點),進位到父結點中的key左孩子指針向左結點,右孩子指針向右結點。

3)針對索引結點:如果當前結點key的個數小於等於m-1,插入結束。反之將這個索引類型結點分成兩個索引結點,左索引結點包含前(m-1)/2個數據,右結點包含m-(m-1)/2個數據,然後將第m/2個key父結點中,進位到父結點的key左孩子指向左結點, 父結點的key右孩子指向右結點。

B+樹的刪除:

爲什麼說B+樹比B樹更適合做操作系統的數據庫索引和文件索引?

(1)B+樹的磁盤讀寫的代價更低

B+樹內部結點沒有指向關鍵字具體信息的指針,這樣內部結點相對B樹更小。B+通過最後一層可以對所有數據進行訪問

(2)B+樹的查詢更加的穩定

因爲非終端結點並不是最終指向文件內容的結點,僅僅是作爲葉子結點中關鍵字的索引。這樣所有的關鍵字的查找都會走一條從根結點到葉子結點的路徑。所有的關鍵字查詢長度都是相同的,查詢效率相當。

參考鏈接:

https://www.cnblogs.com/guohai-stronger/p/9225057.html

https://blog.csdn.net/zhuanzhe117/article/details/78039692

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