C/C++知識點詳解(持續更新...)

2020-08-12 16:19:24

一、C/C++基礎部分

C++和C的區別

設計思想上:
C++是物件導向的語言,而C是程序導向的結構化程式語言。
語法上:
C++具有封裝、繼承和多型三種特性;
C++相比C,增加多許多型別安全的功能,比如強制型別轉換、
C++支援範式程式設計,比如模板類、函數模板等。

物件導向與程序導向的區別

物件導向與程序導向語言的區別

static關鍵字的作用

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

簡潔回答:(推薦)
① 加了 static 關鍵字的全域性變數只能在本檔案中使用。
例如在 a.c 中定義了 static int a=10;那麼在 b.c 中用extern int a 是拿不到 a 的值得,a 的作用域只在 a.c 中。
② static 定義的靜態區域性變數分配在數據段上,普通的區域性變數分配在棧上,會因爲函數棧幀的釋放而被釋放掉。
③ 對一個類中成員變數和成員函數來說,加了 static 關鍵字,則此變數/函數就沒有了 this 指針了,必須通過類名才能 纔能存取

const關鍵字的作用

在这里插入图片描述

extern關鍵字的作用

  1. extern修飾變數的宣告。舉例來說,如果檔案a.c需要參照b.c中變數int v,就可以在a.c中宣告extern int v,然後就可以參照變數v。這裏需要注意的是,被參照的變數v的鏈接屬性必須是外鏈接(external)的,也就是說a.c要參照到v,不只是取決於在a.c中宣告extern int v,還取決於變數v本身是能夠被參照到的。這涉及到c語言的另外一個話題--變數的作用域。能夠被其他模組以extern修飾符參照到的變數通常是全域性變數。還有很重要的一點是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函數fun定義的開頭處宣告extern int v,然後就可以參照到變數v了,只不過這樣只能在函數fun作用域中參照v罷了,這還是變數作用域的問題。對於這一點來說,很多人使用的時候都心存顧慮。好像extern宣告只能用於檔案作用域似的。
  2. extern修飾函數宣告。從本質上來講,變數和函數沒有區別。函數名是指向函數二進制塊開頭處的指針。如果檔案a.c需要參照b.c中的函數,比如在b.c中原型是int fun(int mu),那麼就可以在a.c中宣告extern int fun(int mu),然後就能使用fun來做任何事情。就像變數的宣告一樣,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的檔案作用域的範圍中。對其他模組中函數的參照,最常用的方法是包含這些函數宣告的標頭檔案。
  3. 此外,extern修飾符可用於指示C或者C++函數的呼叫規範。比如在C++中呼叫C庫函數,就需要在C++程式中用extern 「C」宣告要參照的函數。這是給鏈接器用的,告訴鏈接器在鏈接的時候用C函數規範來鏈接。主要原因是C++和C程式編譯完成後在目的碼中命名規則不同。

C/C++中extern的用法

volatile關鍵字的作用

C/C++中volatile關鍵字詳解

C++中volatile關鍵字的作用

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轉指針,可能會出問題,儘量少用;

注意:爲什麼不使用C的強制轉換?
C的強制轉換表面上看起來功能強大什麼都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。

struct和class的區別

struct和class的區別

sizeof和strlen的區別

strlen 與 sizeof 的區別

c++中的四個智慧指針

智慧指針主要用於管理在堆上分配的記憶體,它將普通的指針封裝爲一個棧物件。當棧物件的生存週期結束後,會在解構函式中釋放掉申請的記憶體,從而防止記憶體漏失。C++ 11中最常用的智慧指針型別爲shared_ptr,它採用參照計數的方法,記錄當前記憶體資源被多少個智慧指針參照。該參照計數的記憶體在堆上分配。當新增一個時參照計數加1,當過期時參照計數減一。只有參照計數爲0時,智慧指針纔會自動釋放參照的記憶體資源。對shared_ptr進行初始化時不能將一個普通指針直接賦值給智慧指針,因爲一個是指針,一個是類。可以通過make_shared函數或者通過建構函式傳入普通指針。並可以通過get函數獲得普通指針。

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

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

  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的缺點是:存在潛在的記憶體崩潰問題!

  1. 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;
  1. 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 sp(new int(1)); sp 與 sp.get()是等價的

  2. 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();

記憶體漏失怎麼處理的

記憶體泄露:
什麼是記憶體泄露?
這個問題,在部落格中好像有一個文章,雖然是轉載的但是對記憶體泄露做了一些說明,就是當我們用new或者malloc申請了記憶體,但是沒有用delete或者ree及時的釋放了記憶體,結果導致一直佔據該記憶體。記憶體漏失形象的比喻是「操作系統可提供給所有進程的儲存空間被某個進程榨乾」,最終結果是程式執行時間越長,佔用儲存空間越來越多,最終用盡全部儲存空間,整個系統崩潰。
程式退出以後,能不能回收記憶體?
程式結束後,會釋放 其申請的所有記憶體,這樣是可以解決問題。但是你的程式還是有問題的,就如你寫了一個函數,申請了一塊記憶體,但是沒有釋放,每呼叫一次你的函數就會白白浪費一些記憶體。如果你的程式不停地在執行,就會有很多的記憶體被浪費,最後可能你的程式會因爲用掉記憶體太多而***作系統殺死。
智慧指針:Effective C++ 建議我們將物件放到智慧指針裡,可以有效的避免記憶體泄露。

什麼是智慧指針?
一種類似指針的數據型別,將物件儲存在智慧指針中,可以不需要處理記憶體泄露的問題,它會幫你呼叫物件的解構函式自動復原物件(主要是智慧指針自己的解構函式用了delete ptr,delete會自動呼叫指針物件的解構函式,前提該記憶體是在堆上的,如果是在棧上就會出錯),釋放記憶體。因此,你要做的就是在解構函式中釋放掉數據成員的資源。

爲了解決回圈參照導致的記憶體漏失,引入了weak_ptr弱指針,weak_ptr的建構函式不會修改參照計數的值,從而不會對物件的記憶體進行管理,其類似一個普通指針,但不指向參照計數的共用記憶體,但是其可以檢測到所管理的物件是否已經被釋放,從而避免非法存取。

野指針

野指針就是指向一個已刪除的物件或者未申請存取受限記憶體區域的指針。

陣列和指針的區別

在这里插入图片描述

c/c++中的參照和指針

定義

1. 參照:
C++是C語言的繼承,它可進行過程化程式設計,又可以進行以抽象數據型別爲特點的基於物件的程式設計,還可以進行以繼承和多型爲特點的物件導向的程式設計。參照就是C++對C語言的重要擴充。參照就是某一變數的一個別名,對參照的操作與對變數直接操作完全一樣。參照的宣告方法:型別識別符號 &參照名=目標變數名;參照引入了物件的一個同義詞。定義參照的表示方法與定義指針相似,只是用&代替了*。
參照注意:

  • 參照必須被初始化;
  • 參照不能改變系結的物件;
  • 參照只有一級;

2. 指針:
指針利用地址,它的值直接指向存在電腦記憶體中另一個地方的值。由於通過地址能找到所需的變數單元,可以說,地址指向該變數單元。因此,將地址形象化的稱爲「指針」。意思是通過它能找到以它爲地址的記憶體單元。

指針使用注意事項:

  • 初始化時要置空;
  • 使用時要考慮指向物件邊界問題;
  • 不能對未初始化的指針取值或賦值;
  • 釋放時要置空;
  • 如果返回動態分配記憶體或物件,必須使用指針;

區別

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

虛擬函式和解構函式

C++中解構函式的作用

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

靜態函數和虛擬函式的區別

靜態函數在編譯的時候就已經確定執行時機,虛擬函式在執行的時候動態系結。虛擬函式因爲用了虛擬函式表機制 機製,呼叫的時候會增加一次記憶體開銷

爲什麼解構函式必須是虛擬函式?C++預設的解構函式不是虛擬函式?

將可能會被繼承的父類別的解構函式設定爲虛擬函式,可以保證當我們new一個子類,然後使用基礎類別指針指向該子類物件,釋放基礎類別指針時可以釋放掉子類的空間,防止記憶體漏失。

C++預設的解構函式不是虛擬函式是因爲虛擬函式需要額外的虛擬函式表和虛表指針,佔用額外的記憶體。而對於不會被繼承的類來說,其解構函式如果是虛擬函式,就會浪費記憶體。因此C++預設的解構函式不是虛擬函式,而是隻有當需要當作父類別時,設定爲虛擬函式。

深拷貝與淺拷貝

c++深拷貝和淺拷貝

過載和覆蓋

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

虛擬函式和多型

多型的實現主要分爲靜態多型動態多型,靜態多型主要是過載,在編譯的時候就已經確定;動態多型是用虛擬函式機制 機製實現的,在執行期間動態系結。

舉個例子:一個父類別型別的指針指向一個子類物件時候,使用父類別的指針去呼叫子類中重寫了的父類別中的虛擬函式的時候,會呼叫子類重寫過後的函數,在父類別中宣告爲加了virtual關鍵字的函數,在子類中重寫時候不需要加virtual也是虛擬函式。

虛擬函式的實現:在有虛擬函式的類中,類的最開始部分是一個虛擬函式表的指針,這個指針指向一個虛擬函式表,表中放了虛擬函式的地址,實際的虛擬函式在程式碼段(.text)中。當子類繼承了父類別的時候也會繼承其虛擬函式表,當子類重寫父類別中虛擬函式時候,會將其繼承到的虛擬函式表中的地址替換爲重新寫的函數地址。使用了虛擬函式,會增加存取記憶體開銷,降低效率。

函數指針

1、定義
函數指針是指向函數的指針變數。
函數指針本身首先是一個指針變數,該指針變數指向一個具體的函數。這正如用指針變數可指向整型變數、字元型、陣列一樣,這裏是指向函數。

C在編譯時,每一個函數都有一個入口地址,該入口地址就是函數指針所指向的地址。有了指向函數的指針變數後,可用該指針變數呼叫函數,就如同用指針變數可參照其他型別變數一樣,在這些概念上是大體一致的。

2、用途:
呼叫函數和做函數的參數,比如回撥函數。

3、範例:

char * fun(char * p)  {}       // 函數fun
char * (*pf)(char * p);             // 函數指針pf
pf = fun;                        // 函數指針pf指向函數fun
pf(p);                        // 通過函數指針pf呼叫函數fun

fork、wait和exec函數

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,採用了寫時複製的方法,而不是對父進程空間進程整體複製。

父進程產生子進程使用fork拷貝出來一個父進程的副本,此時只拷貝了父進程的頁表,兩個進程都讀同一塊記憶體,當有進程寫的時候使用寫實拷貝機制 機製分配記憶體,exec函數可以載入一個elf檔案去替換父進程,從此父進程和子進程就可以執行不同的程式了。fork從父進程返回子進程的pid,從子進程返回0.呼叫了wait的父進程將會發生阻塞,直到有子進程狀態改變,執行成功返回0,錯誤返

隱式型別轉換

型別轉換

reinterpret_cast:可以用於任意型別的指針之間的轉換,對轉換的結果不做任何保證
dynamic_cast:這種其實也是不被推薦使用的,更多使用static_cast,dynamic本身只能用於存在虛擬函式的父子關係的強制型別轉換,對於指針,轉換失敗則返回nullptr,對於參照,轉換失敗會拋出異常
const_cast:對於未定義const版本的成員函數,我們通常需要使用const_cast來去除const參照物件的const,完成函數呼叫。另外一種使用方式,結合static_cast,可以在非const版本的成員函數內新增const,呼叫完const版本的成員函數後,再使用const_cast去除const限定。
static_cast:完成基礎數據型別;同一個繼承體系中型別的轉換;任意型別與空指針型別void* 之間的轉換。

隱式型別轉換

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

new/delete與malloc/free區別(重點)

  1. new/delete是C++的關鍵字,而malloc/free是C語言的庫函數,後者使用必須指明申請記憶體空間的大小,對於類型別的物件,後者不會呼叫建構函式和解構函式;
  2. malloc需要給定申請記憶體的大小,返回的指針需要強轉。
    new會呼叫建構函式,不用指定記憶體大小,返回的指針不用強轉。

new/delete與malloc/free的區別與聯繫

淺談new/delete和malloc/free的用法與區別

在这里插入图片描述

二、STL容器和演算法

map/unordered_map和set/unordered_set的區別

  1. map/unordered_map的區別:unordered_map可類比於Python中的字典。其實現使用了雜湊表,可以以O(1)的時間複雜度存取到對應元素,但缺點是有較高的額外空間複雜度。與之對應,STL中的map對應的數據結構是紅黑樹,紅黑樹內的數據時有序的,在紅黑樹上查詢的時間複雜度是O(logN),相對於unordered_map的查詢速度有所下降,但額外空間開銷減小。
    結論:如果需要內部元素自動排序,使用map,不需要排序使用unordered_map
  2. set/unordered_set:set作爲一個容器也是用來儲存同一數據型別的數據型別,並且能從一個數據集閤中取出數據,在set中每個元素的值都唯一,而且系統能根據元素的值自動進行排序,set的元素不像map那樣可以同時擁有實值(value)和鍵值(key),set元素的鍵值就是實值。set不允許兩個元素有相同的鍵值。
    在这里插入图片描述

set與map、unordered_map、unordered_set與雜湊表

map 和 set 區別在於:

  1. map 中的元素是 key-value(關鍵字—值)對:關鍵字起到索引的作用,值則表示與索引相關聯的數據;set 與之相對就是關鍵字的簡單集合,set 中每個元素只包含一個關鍵字key。

  2. set 的迭代器是 const 的,不允許修改元素的值;map 允許修改 value,但不允許修改 key。其原因是因爲map 和 set 是根據關鍵字排序來保證其有序性的。
    對於set,如果允許修改 key 的話,那麼首先需要刪除該鍵,然後調節平衡,再插入修改後的鍵值,調節平衡,如此一來,嚴重破壞了 map 和 set 的結構,導致 iterator 失效,不知道應該指向改變前的位置,還是指向改變後的位置。所以 STL 中將 set 的迭代器設定成 const,不允許修改迭代器的值;
    對於map,map 的迭代器則不允許修改 key 值,允許修改 value 值。

  3. map 支援下標操作,set 不支援下標操作。map 可以用 key 做下標,map 的下標運算子[ ]將關鍵碼作爲下標去執行查詢,如果關鍵碼不存在,則插入一個具有該關鍵碼和 mapped_type 型別預設值的元素至 map 中,因此下標運算子[ ]在 map 應用中需要慎用,const_map 不能用,只希望確定某一個關鍵值是否存在而不希望插入元素時也不應該使用,mapped_type 型別沒有預設值也不應該使用。如果 find 能解決需要,儘可能用 find。

STL 基本組成及關係

參考答案:

STL 主要由:以下六部分組成:
①容器 ② 迭代器 ③ 仿函數 ④ 演算法 ⑤ 分配器 ⑥ 配接器

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

vector和list的區別

概念:

1)Vector
連續儲存的容器,動態陣列,在堆上分配空間。
底層實現:陣列

兩倍容量增長:
vector 增加(插入)新元素時,如果未超過當時的容量,則還有剩餘空間,那麼直接新增到最後(插入指定位置),然後調整迭代器。如果沒有剩餘空間了,則會重新設定原有元素個數的兩倍空間,然後將原空間元素通過複製的方式初始化新空間,再向新空間增加元素,最後解構並釋放原空間,之前的迭代器會失效。

效能

存取:O(1)
插入:在最後插入(空間夠):很快
在最後插入(空間不夠):需要記憶體申請和釋放,以及對之前數據進行拷貝。
在中間插入(空間夠):記憶體拷貝
在中間插入(空間不夠):需要記憶體申請和釋放,以及對之前數據進行拷貝。
刪除:在最後刪除:很快
在中間刪除:記憶體拷貝

適用場景:經常隨機存取,且不經常對非尾節點進行插入刪除。

2)List
動態鏈表,在堆上分配空間,每插入一個元數都會分配空間,每刪除一個元素都會釋放空間。
底層:雙向鏈表

效能
存取:隨機存取效能很差,只能快速存取頭尾節點。
插入:很快,一般是常數開銷
刪除:很快,一般是常數開銷

適用場景:經常插入刪除大量數據

區別:

1)vector 底層實現是陣列;list 是雙向 鏈表。
2)vector 支援隨機存取,list 不支援。
3)vector 是順序記憶體,list 不是。
4)vector 在中間節點進行插入刪除會導致記憶體拷貝,list 不會。
5)vector 一次性分配好記憶體,不夠時才進行 2 倍擴容;list 每次插入新節點都會進行記憶體申請。
6)vector 隨機存取效能好,插入刪除效能差;list 隨機存取效能差,插入刪除效能好。

應用

vector 擁有一段連續的記憶體空間,因此支援隨機存取,如果需要高效的隨即存取,而不在乎插入和刪除的效率,使用 vector。

list 擁有一段不連續的記憶體空間,如果需要高效的插入和刪除,而不關心隨機存取,則應使用 list。

三、設計模式

單例模式:單例模式主要解決一個全域性使用的類頻繁的建立和銷燬的問題。單例模式下可以確保某一個類只有一個範例,而且自行範例化並向整個系統提供這個範例。單例模式有三個要素:一是某個類只能有一個範例;二是它必須自行建立這個範例;三是它必須自行向整個系統提供這個範例。

工廠模式:工廠模式主要解決介面選擇的問題。該模式下定義一個建立物件的介面,讓其子類自己決定範例化哪一個工廠類,使其建立過程延遲到子類進行。
觀察者模式:定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。

裝飾器模式:對已經存在的某些類進行裝飾,以此來擴充套件一些功能,從而動態的爲一個物件增加新的功能。裝飾器模式是一種用於代替繼承的技術,無需通過繼承增加子類就能擴充套件物件的新功能。使用物件的關聯關係代替繼承關係,更加靈活,同時避免型別體系的快速膨脹。

十種常用的設計模式

未完待續…

========================================================

參考鏈接

牛客網面經1

牛客網面經2

C/C++面試題總結