摘錄一些C++面試常考問題,寫一些自己的理解,花了挺長時間的,作圖是真的累,歡迎來摘果子。
static關鍵字
用於聲明靜態對象;
靜態函數只在本文件可見。(默認是extern的)
全局靜態對象:全局靜態對象,存儲在全局/靜態區,作用域整個程序,在程序結束才銷毀;
局部靜態對象:在函數內部加上static聲明的變量,在首次調用時初始化,然后一直駐留在內存,作用域是該函數,可用於函數調用計數(primary有例子),程序結束釋放;
靜態數據成員:歸屬於類,類對象共享,類外初始化,類對象可訪問;
靜態函數成員:歸屬於類,只能訪問靜態數據成員。
const 關鍵字
核心功能:限定只讀
const T var; 聲明常量,存儲在常量區;
const T* p : 不可通過p指針修改對象值;T * const p : 常量指針,指針不可被賦值/地址不可改變。
const T function(const T, const T*, T* const, const T&) const &/&& {…;}
限定返回值為常量、常量形參、指針常量、常量指針、常引用、防止對象屬性被改變(對象只讀)STL源碼大量使用
如何突破const的限制?mutable 在const函數中修改成員變量
View Code1 #include <iostream> 2 using namespace std; 3 4 class Test 5 { 6 public: 7 mutable int a; //突破限制 8 int change(int b) const; 9 }; 10 11 int Test::change(int b) const 12 { 13 a = b; 14 return a; 15 } 16 17 int main() 18 { 19 Test t; 20 t.a = 10; 21 cout << t.change(20) << endl; 22 return 0; 23 }
C/C++區別
C:面向過程語言、編譯型(也可以實現對象化,但沒有C++那樣順暢強大)。
C++:面向對象語言、編譯型、擁有封裝繼承多態三大特性、支持泛型編程、模板。
引用指針區別
指針是對象的地址,而引用是對象的別名,均可改變對象值;
指針可以被重新賦值,引用初始化后不可以改變,就像私家車與共享汽車。
指針可以不知指向對象,引用必須初始化;
在實現層面,指針和引用完全相同,都占空間,匯編代碼看一下就知道;
傳指針和傳引用的區別在於,會否發生拷貝,指針會發生拷貝,會降低效率,使用引用直接訪問該對象,提高效率。
內存泄漏以智能指針,智能指針的內存泄漏,怎么解決?
動態申請的內存,未進行釋放,比如new的空間,沒有適時delete掉。導致內存泄漏,可用智能指針解決。
Auto_ptr可以實現部分shared_ptr的功能,但已經被棄用。
Shared_ptr:多指針共享對象,存在引用計數,賦值給其他指針,計數增加;被賦值,計數減少;當引用計數為0,自帶的銷毀函數調用類析構函數析構對象;當循環引用時,仍然會發生內存泄漏;靠weak_ptr解決。
Weak_ptr:弱引用,被復制shared_ptr對象不會引發引用計數,能很好的解決shared循環引用內存泄露的問題。
Unique_ptr:任意時刻只能由一個指着對象,禁止復制和拷貝,實現形式“=delete”。計數為0,析構對象。
數組和指針,野指針
C++內置數組是一塊連續的內存空間,數組名其實是一個指針,指向數組第一個元素地址。野指針指向已刪除對象地址或是其他未申請地址的指針,訪問野指針很危險。
靜態函數與虛函數(多態),虛析構
靜態函數在編譯時綁定;
虛函數由virtual標識,采用動態綁定方式。派生類覆蓋基類虛函數,基類指針指向派生類對象,實現動態多態。在運行時判斷指針指向的對象類型從而調用該對象的函數版本。
底層的實現是虛表。
基類析構函數必須為虛。否則若有基類指針指向派生類對象進行析構時函數調用基類析構函數,會出現錯誤,發生內存泄漏。
可以不為虛,只要你保證不會發生動態綁定。
函數指針
顧名思義,指向函數的指針。相當於拿到函數的訪問地址,可以方便地將函數作為參數傳入其他函數。。
定義方法:int (*fp)(int, int);
賦值:fp = &max;
直接調用:fp(1, 2)
作為參數使用 int cal(int a, int b, int (*fp)(int, int))
函數重載二義性
二義性是在編譯階段編譯器進行函數匹配(匹配函數名、參數種類和個數)的時候出現的錯誤。多種情景會導致函數重載二義性。
默認參數與無參函數;
隱式類型轉換造成的。比如:double, int, ßlong
類類型的轉換,比如派生類對象指針/引用可以傳給基類指針/引用,當出現派生類指針和基類指針兩個函數時,編譯器無法匹配,出現錯誤。
解決方法:①編程時注意;②使用explicit關鍵字聲明函數,避免隱式類型轉換的出現。
重載和覆蓋和隱藏
重載指的是函數名相同而參數個數/類型不同從而實現調用同名函數,給定不同參數實現靜態多態。
覆蓋多用再類繼承中,派生類繼承基類,然后實現自己的函數版本,如果是虛函數一般在后面加上關鍵字override(可以提示編譯器檢查覆蓋語法是否正確)。
隱藏是指派生類實現了與基類同名的函數,導致基類函數在派生類內不可見的現象。
隱式類型轉換
種類較多。
① Int, double, float, unsigned_int, unsigned_long等內置類型可以相互轉換;
② 整型提升,在計算時位數小於int的類型都會提升成為int進行計算;
③ 在表達式中,先將各個類型轉換成類型中最寬的類型再進行計算,計算完成時將結果(右值)再轉換成為左值變量的類型,然后拋棄將亡右值。
④ 基類與派生類之間存在隱式類型轉換關系。向上(未理解)、向下、橫向轉換。派生類向基類進行轉換,派生類向派生類進行轉換。
四種cast類型轉換
static_cast:用於一般的強制類型轉換
const_cast:專用於除去const屬性
dynamic_cast:RTTI的內容,用於運行時類型檢查,將派生類對象指針/引用轉換成基類指針/引用(upcast);或者將指向派生來對象的指針/引用轉換成派生類對象指針/引用(downcast)。
interpret_cast:強制重新解釋,即顯示聲明該變量/數據由該類來解釋。
New/delete VS malloc/free
new/delete是C++關鍵字,操作的是對象,其實調用的是構造和析構函數。
malloc/free是C庫函數,較為直接的操作內存,需要指定類型和空間大小。
new 有類型檢查,malloc沒有。
new返回內存地址,malloc返回void 指針,需要顯示轉換成我們需要的類型。
malloc函數原型:void* malloc(size_t) 頭文件stdlib
RTTI
Runtime Type Identification,運行時類型識別,能夠在運行時准確判斷對象的類型,引入了type_info對象。通過typeid().name()來查看,頭文件type_info.h。
另外類轉換函數dynamic_cast<T>(expression),可以判斷expression is a T問題。
可以進行upcast和downcast轉換
upcast: 向上轉換,即把派生類對象指針/引用轉換為基類指針/引用;
downcast:向下轉換,把指向派生類的基類指針/引用轉換成為派生類指針/引用。
比較:type_info對象會增加開銷。
虛函數的實現---虛表,怎么實現了多態?
虛表vftable,編譯器為每個擁有虛函數的類都建有一張虛函數表,里面存有虛函數的入口指針(地址)。在類對象的內存布局中,先是一個vfptr虛表指針,指向虛表首地址,而后通過偏移量的形式來訪問虛表中的地址。
發生單繼承時,派生類內存布局,先是復制一份基類內存布局,然后是自己的布局(注意內存對齊)。虛表指針指向自己的虛表,派生類虛函數地址如果自己未覆蓋,那么就是基類的,否則是自己的函數地址。
發生多繼承時:先按照繼承順序,從左到右排布基類的布局包括虛標指針,然后排布自己的指針和數據;派生類虛表排布形式是按照繼承順序,是繼承來的虛函數,如果有覆蓋則換成自己的函數地址;然后是下一個基類,直至基類排布完畢。多張虛表各自獨立。
發生虛繼承時:無論是對象內存排布還是虛表,虛基類的部分都被放到最后排布。
STL map 與 set區別與實現
都是關聯式容器,
① map是映射,存儲的是pair<type, type>(key, value),默認有序,可以根據key修改value;set只存儲key,key就是它的值,保持有序;元素的key都不可以重復,插入調用的都是紅黑樹的insert_unique函數,防止重復。
② map支持下標訪問,set不支持,map下標查找不到對應key,會插入一條,要注意。
二者底層的實現都是紅黑樹,他們的函數只是對紅黑樹做了簡單的封裝。
紅黑樹要點:一種平衡二叉搜索樹
節點紅黑二色;根節點黑;紅色節點子節點必須是黑;任意節點到葉子節點經過的黑色節點數相同;
插入和查找復雜度:O(logn)
維持自身的平衡,需要進行旋轉(最多進行三次旋轉可以平衡)和變色。
AVL樹,另一種平衡二叉搜索樹
維持較為嚴格的平衡條件:左右子樹高度差最大為1;
在高度上會小於紅黑樹,但是在插入和刪除時要進行復雜度旋轉(單雙旋轉),比紅黑樹要復雜。統計性能紅黑樹要優於AVL樹所以STL采用RB-tree。
STL 迭代器 與指針的區別,迭代器怎么刪除元素
迭代器是STL的關鍵所在,STL中心思想是把容器和算法分離開發,獨立設計成泛型(類模板和函數模板),再用一種粘合劑將二者結合起來,迭代器就是該角色。迭代器按照容器內部次序訪問元素,而避免了暴露/考慮容器內不得結構。畢竟容器內部的實現各異,map/set族使用RB-tree,unordered族使用hashtable,vector使用內置數組,通過三個核心指針實現,queue和list使用雙端隊列deque實現。舉個栗子:map的第一個元素是mostleft,最左節點,而vector第一個就是vec[0]使用指針必須要了解各個容器內部的實現,使用迭代器map.begin()/end(), vec.begin()/end()即可。
STL里resize和reserve的區別
resize進行容器大小重定義,如果重定義的size大於原始size,則增大原始size,並對擴充空間調用構造函數初始化;若小,則析構尾部多出的元素。對於某些容器如vector、unordered_map,capacity增長規律可能會不同。
reserve為容器預定空間,即它改變capacity大小。若大於原始容量擴充,若小,不做處理。
vector和list的區別,應用,越詳細越好
vector是一個動態數組,可以動態的擴充容量,擴容規律,每當要超過當前容量,就擴容成為原始兩倍,如果不夠就擴充至需要的大小。(重新申請內存空間,原始數據搬移/拷貝構造,新數據初始化);使用連續的內存地址,支持隨機訪問O(1)、刪除和插入耗時O(n);
list底層實現使用deque,使用非線性地址,不支持隨機訪問,查找耗時O(n),插入刪除快速O(1);
類成員訪問權限
三種public、private、protect
private成員只能在類內訪問,類外不可訪問,派生類內不可見,對象不可訪問;
protect成員類內可訪問,派生類內可訪問,對象不可訪問;
public:類內、派生類內、對象也可訪問。
struct和class區別
struct是從C繼承過來的,完全可以實現class的功能;但是無法實現訪問控制,默認public,class默認private;
另外,class可用於聲明模板,而struct不可以。
聲明一個模板函數和一個模板類
templete<typename T> ret-type func-name(param list)
{ statement-list;}
如:templete <class T> void add(T a, T b)
{ return static_cast<T>(a+b);}
templete<type-name T>
class class-name{
T var1;
int var2;
public:
T add(T &a, T &b);
};
右值與左值,右值引用?用法?
左值和左值引用我們很熟悉了。一般定義的變量/對象都是左值,可以被賦值,直到作用域結束才釋放。左值引用相當於左值的別名,可以通過它修改左值。可以被賦值
右值不同於左值,它是臨時值(比如表達式結果-匯編里是立即數),用完就被拋棄釋放,又稱將亡值,我覺得很是貼切。不可被賦值。
右值引用就是右值的引用,可以實現右值的持久化,可以通過它訪問右值。
主要的用法在於移動語義,,即移動構造和移動賦值函數。構造函數形參設置成右值引用,傳進來的對象成為右值,要初始化的對象將它的內存空間“偷”過來(它的指針地址賦給該對象,再將它的指針置空),這樣一來,就避免了申請新的空間以及釋放右值對象的空間,這一招“借屍還魂”可以有效地提高效率。
include <> 和 “”的區別
<>用於引用編譯器提供的頭文件,在編譯器指定include目錄找不到則報錯;
“”主要用於項目中自己實現的頭文件引用,先在項目目錄尋找,找不到會去編譯器目錄中找,找不到報錯。(至少VS2017是這樣)
C++內存管理/分配
棧區
一些局部變量
堆區
動態申請的內存,如new出來的地址;
全局/靜態區
全局變量、static靜態變量
文字常量區
存儲字符串等常量,const 修飾變量
代碼區
存儲函數內容
堆與棧
① 管理方式:棧由編譯器管理,堆由程序員自己申請和釋放;
② 大小限制:程序棧大小一般固定較小(幾M),堆較大(幾G);
③ 分配方式:堆動態分配,棧一般是靜態分配(如局部變量的定義)也有動態分配(不常用)
④ 生長方向:從上圖可看出,棧向低地址端擴展;堆向高地址端擴展;
⑤ 分配效率:棧由系統提供,甚至有專門的寄存器和指令負責,而堆有C++程序庫實現,有復雜的堆算法,故棧效率要高於堆;
⑥ 此外,堆的分配還會產生內碎片,影響存儲內存的使用,棧先進后出結構,不會產生碎片。
段錯誤
一般由訪問受保護內存、堆棧溢出、數組越界造成。
//數組越界
int a[10] = {0};
cout << a[20] << endl;
//訪問受保護內存
int *p = nullptr;
*p = 10;
解決辦法:檢查是否有上述問題存在;gcc/g++ -g 編譯, 使用gdb調試r
收到信號:SIGSEGV SEGV-segmentation violation 段違例。
STL allocator 的實現
STL空間配置器,SGI的是實現形式是std::alloc,所有的容器都實際使用alloc作為空間配置器,不接受反駁(這里的空間不只是內存還有外存,如磁盤)。alloc並不符合標准,他也實現了一個符合部分標准的std::allocate配置器,但只是對new和delete做了簡單封裝,效率太低,它自己都沒用過。
一般的,我們使用new和delete運算符配置對象。新建對象涉及兩個步驟:①使用::operator new 配置內存,②用構造函數構造對象內容;銷毀對象也涉及兩步驟:①使用析構函數析構函數內容;②使用::operator delete 釋放內存。
SGI的實現則是:內存配置使用alloc:allocate(), 對象構造使用::construct();對象析構使用::destroy(),內存釋放使用alloc:deallocate()。
為什么要實現alloc? 怎么實現的?
二級配置器將128bytes下空間分成16個等級來分配空間,顯著的減少了內部碎片,提高了內存的使用效率。
下圖中第二級忘記畫內存塊的回收了,是存在回收的!
看到這里,足以證明你是一個有耐心的人了,💪!
進階知識
組合還是繼承?
1.拿兩個類來說,如果存在is A關系,意思是如果一個類是另一個類的“一種”,那么使用繼承,而后修改一些屬性,以體現自己的個性。
2.如果一個類需要其他多個類對象的參與來組成,及A has a B & C & D,那么,使用組合。
如果說,還沒明白的話,拿現象級游戲《王者農葯》來說。英雄有不同的身份,那么我們有一個總的“英雄類”,而其他不同的角色,如法師,戰士,射手,都是英雄的一種,所以他們都可繼承自“英雄”。再說組合:只要你氪金,就能擁有閃瞎眼的皮膚和特效,而這些皮膚和特效也是不同的類,一個英雄,有頭、身子、腳、皮膚、技能、特效,都是has a的關系,那么由他們來組成一個英雄。
std::move和std::forward
move:主要用來獲取一個左值的右值引用。(無條件將實參轉換成為右值)。
forward:用於在模板中將一個函數或者函數包,連同實參的類型都傳遞給其他函數。(傳入右值,返回右值,否則返回左值)。
二者都用到了---引用折疊。
當有一系列引用出現時,即 T & &, T && & , T & &&, T&& &&,除了第四種折疊成為右值外,其余都折疊成為左值。
C++的訪問控制
public:類內、類對象、友元內
protected:類內、友元內
private:類內、友元內
發生繼承時:
public:基類private不可訪問,public成為public,protect成為private;
protected:基類private不可訪問,public成為protected,protected還是protected
private:基類private不可訪問,public和protected都成為private
continue...