設計思想上:
C++是物件導向的語言,而C是程序導向的結構化程式語言。
語法上:
C++具有封裝、繼承和多型三種特性;
C++相比C,增加多許多型別安全的功能,比如強制型別轉換、
C++支援範式程式設計,比如模板類、函數模板等。
簡潔回答:(推薦)
① 加了 static 關鍵字的全域性變數只能在本檔案中使用。
例如在 a.c 中定義了 static int a=10;那麼在 b.c 中用extern int a 是拿不到 a 的值得,a 的作用域只在 a.c 中。
② static 定義的靜態區域性變數分配在數據段上,普通的區域性變數分配在棧上,會因爲函數棧幀的釋放而被釋放掉。
③ 對一個類中成員變數和成員函數來說,加了 static 關鍵字,則此變數/函數就沒有了 this 指針了,必須通過類名才能 纔能存取
C++中四種類型轉換是:static_cast, dynamic_cast, const_cast, reinterpret_cast
注意:爲什麼不使用C的強制轉換?
C的強制轉換表面上看起來功能強大什麼都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。
智慧指針主要用於管理在堆上分配的記憶體,它將普通的指針封裝爲一個棧物件。當棧物件的生存週期結束後,會在解構函式中釋放掉申請的記憶體,從而防止記憶體漏失。C++ 11中最常用的智慧指針型別爲shared_ptr,它採用參照計數的方法,記錄當前記憶體資源被多少個智慧指針參照。該參照計數的記憶體在堆上分配。當新增一個時參照計數加1,當過期時參照計數減一。只有參照計數爲0時,智慧指針纔會自動釋放參照的記憶體資源。對shared_ptr進行初始化時不能將一個普通指針直接賦值給智慧指針,因爲一個是指針,一個是類。可以通過make_shared函數或者通過建構函式傳入普通指針。並可以通過get函數獲得普通指針。
C++裏面的四個智慧指針: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中後三個是c++11支援,並且第一個已經被11棄用。
爲什麼要使用智慧指針?
智慧指針的作用是管理一個指針,因爲存在以下這種情況:申請的空間在函數結束時忘記釋放,造成記憶體漏失。使用智慧指針可以很大程度上的避免這個問題,因爲智慧指針就是一個類,當超出了類的作用域是,類會自動呼叫解構函式,解構函式會自動釋放資源。所以智慧指針的作用原理就是在函數結束時自動釋放記憶體空間,不需要手動釋放記憶體空間。
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的缺點是:存在潛在的記憶體崩潰問題!
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;
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()是等價的
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的建構函式不會修改參照計數的值,從而不會對物件的記憶體進行管理,其類似一個普通指針,但不指向參照計數的共用記憶體,但是其可以檢測到所管理的物件是否已經被釋放,從而避免非法存取。
野指針就是指向一個已刪除的物件或者未申請存取受限記憶體區域的指針。
1. 參照:
C++是C語言的繼承,它可進行過程化程式設計,又可以進行以抽象數據型別爲特點的基於物件的程式設計,還可以進行以繼承和多型爲特點的物件導向的程式設計。參照就是C++對C語言的重要擴充。參照就是某一變數的一個別名,對參照的操作與對變數直接操作完全一樣。參照的宣告方法:型別識別符號 &參照名=目標變數名;參照引入了物件的一個同義詞。定義參照的表示方法與定義指針相似,只是用&代替了*。
參照注意:
2. 指針:
指針利用地址,它的值直接指向存在電腦記憶體中另一個地方的值。由於通過地址能找到所需的變數單元,可以說,地址指向該變數單元。因此,將地址形象化的稱爲「指針」。意思是通過它能找到以它爲地址的記憶體單元。
指針使用注意事項:
解構函式與建構函式對應,當物件結束其生命週期,如物件所在的函數已呼叫完畢時,系統會自動執行解構函式。
解構函式名也應與類名相同,只是在函數名前面加一個位取反符,例如stud( ),以區別於建構函式。它不能帶任何參數,也沒有返回值(包括void型別)。只能有一個解構函式,不能過載。
如果使用者沒有編寫解構函式,編譯系統會自動生成一個預設的解構函式(即使自定義了解構函式,編譯器也總是會爲我們合成一個解構函式,並且如果自定義了解構函式,編譯器在執行時會先呼叫自定義的解構函式再呼叫合成的解構函式),它也不進行任何操作。所以許多簡單的類中沒有用顯式的解構函式。
如果一個類中有指針,且在使用的過程中動態的申請了記憶體,那麼最好顯示構造解構函式在銷燬類之前,釋放掉申請的記憶體空間,避免記憶體漏失。
類解構順序:1)派生類本身的解構函式;2)物件成員解構函式;3)基礎類別解構函式。
靜態函數在編譯的時候就已經確定執行時機,虛擬函式在執行的時候動態系結。虛擬函式因爲用了虛擬函式表機制 機製,呼叫的時候會增加一次記憶體開銷
將可能會被繼承的父類別的解構函式設定爲虛擬函式,可以保證當我們new一個子類,然後使用基礎類別指針指向該子類物件,釋放基礎類別指針時可以釋放掉子類的空間,防止記憶體漏失。
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:建立一個和當前進程映像一樣的進程可以通過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的用法與區別
set與map、unordered_map、unordered_set與雜湊表
map 和 set 區別在於:
map 中的元素是 key-value(關鍵字—值)對:關鍵字起到索引的作用,值則表示與索引相關聯的數據;set 與之相對就是關鍵字的簡單集合,set 中每個元素只包含一個關鍵字key。
set 的迭代器是 const 的,不允許修改元素的值;map 允許修改 value,但不允許修改 key。其原因是因爲map 和 set 是根據關鍵字排序來保證其有序性的。
對於set,如果允許修改 key 的話,那麼首先需要刪除該鍵,然後調節平衡,再插入修改後的鍵值,調節平衡,如此一來,嚴重破壞了 map 和 set 的結構,導致 iterator 失效,不知道應該指向改變前的位置,還是指向改變後的位置。所以 STL 中將 set 的迭代器設定成 const,不允許修改迭代器的值;
對於map,map 的迭代器則不允許修改 key 值,允許修改 value 值。
map 支援下標操作,set 不支援下標操作。map 可以用 key 做下標,map 的下標運算子[ ]將關鍵碼作爲下標去執行查詢,如果關鍵碼不存在,則插入一個具有該關鍵碼和 mapped_type 型別預設值的元素至 map 中,因此下標運算子[ ]在 map 應用中需要慎用,const_map 不能用,只希望確定某一個關鍵值是否存在而不希望插入元素時也不應該使用,mapped_type 型別沒有預設值也不應該使用。如果 find 能解決需要,儘可能用 find。
參考答案:
STL 主要由:以下六部分組成:
①容器 ② 迭代器 ③ 仿函數 ④ 演算法 ⑤ 分配器 ⑥ 配接器
他們之間的關係:
分配器:給容器分配儲存空間。
演算法:通過迭代器獲取容器中的內容。
仿函數:可以協助演算法完成各種操作。
配接器:用來套接適配仿函數。
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。
單例模式:
單例模式主要解決一個全域性使用的類頻繁的建立和銷燬的問題。單例模式下可以確保某一個類只有一個範例,而且自行範例化並向整個系統提供這個範例。單例模式有三個要素:一是某個類只能有一個範例;二是它必須自行建立這個範例;三是它必須自行向整個系統提供這個範例。
工廠模式:
工廠模式主要解決介面選擇的問題。該模式下定義一個建立物件的介面,讓其子類自己決定範例化哪一個工廠類,使其建立過程延遲到子類進行。
觀察者模式:定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。
裝飾器模式:
對已經存在的某些類進行裝飾,以此來擴充套件一些功能,從而動態的爲一個物件增加新的功能。裝飾器模式是一種用於代替繼承的技術,無需通過繼承增加子類就能擴充套件物件的新功能。使用物件的關聯關係代替繼承關係,更加靈活,同時避免型別體系的快速膨脹。
========================================================