C++ 八股文(一)
多態(tài)
什么是多態(tài),有什么用
C++ 多態(tài)有兩種:靜態(tài)多態(tài)(早綁定)、動態(tài)多態(tài)(晚綁定)。靜態(tài)多態(tài)是通過函數(shù)重載實現(xiàn)的;動態(tài)多態(tài)是通過虛函數(shù)實現(xiàn)的。
定義:“一個接口,多種方法”,程序在運行時才決定要調(diào)用的函數(shù)。 實現(xiàn):C++ 多態(tài)性主要是通過虛函數(shù)實現(xiàn)的,虛函數(shù)允許子類重寫 override(注意和 overload 的區(qū)別,overload 是重載,是允許同名函數(shù)的表現(xiàn),這些函數(shù)參數(shù)列表/類型不同)。
注:多態(tài)與非多態(tài)的實質(zhì)區(qū)別就是函數(shù)地址是靜態(tài)綁定還是動態(tài)綁定。如果函數(shù)的調(diào)用在編譯器編譯期間就可以確定函數(shù)的調(diào)用地址,并產(chǎn)生代碼,說明地址是靜態(tài)綁定的;如果函數(shù)調(diào)用的地址是需要在運行期間才確定,屬于動態(tài)綁定。
目的:接口重用。封裝可以使得代碼模塊化,繼承可以擴展已存在的代碼,他們的目的都是為了代碼重用。而多態(tài)的目的則是為了接口重用。 用法:聲明基類的指針,利用該指針指向任意一個子類對象,調(diào)用相應(yīng)的虛函數(shù),可以根據(jù)指向的子類的不同而實現(xiàn)不同的方法。
用一句話概括:在基類的函數(shù)前加上 virtual 關(guān)鍵字,在派生類中重寫該函數(shù),運行時將會根據(jù)對象的實際類型來調(diào)用相應(yīng)的函數(shù)。如果對象類型是派生類,就調(diào)用派生類的函數(shù);如果對象類型是基類,就調(diào)用基類的函數(shù)。
重寫、重載與隱藏的區(qū)別
Overload 重載
在 C++ 程序中,可以將語義、功能相似的幾個函數(shù)用同一個名字表示,但參數(shù)或返回值不同(包括類型、順序不同),即函數(shù)重載。
相同的范圍(在同一個類中); 函數(shù)名字相同; 參數(shù)不同; virtual 關(guān)鍵字可有可無;
Override(覆蓋或重寫)
是指派生類函數(shù)覆蓋基類函數(shù),特征是:
不同的范圍(分別位于派生類與基類); 函數(shù)名字相同;參數(shù)相同; 基類函數(shù)必須有 virtual 關(guān)鍵字。
注:重寫基類虛函數(shù)的時候,會自動轉(zhuǎn)換這個函數(shù)為 virtual 函數(shù),不管有沒有加 virtual,因此重寫的時候不加 virtual 也是可以的,不過為了易讀性,還是加上比較好。
Overwrite(重寫)隱藏,
是指派生類的函數(shù)屏蔽了與其同名的基類函數(shù),規(guī)則如下:
如果派生類的函數(shù)與基類的函數(shù)同名,但是參數(shù)不同。此時,不論有無 virtual 關(guān)鍵字,基類的函數(shù)將被隱藏(注意別與重載混淆)。 如果派生類的函數(shù)與基類的函數(shù)同名,并且參數(shù)也相同,但是基類函數(shù)沒有 virtual 關(guān)鍵字。此時,基類的函數(shù)被隱藏(注意別與覆蓋混淆)。
虛函數(shù)和純虛函數(shù)
虛函數(shù):為了實現(xiàn)動態(tài)綁定。使用基類的引用或指針調(diào)用虛函數(shù)的時候會發(fā)生動態(tài)綁定。 純虛函數(shù):抽象類 構(gòu)造函數(shù)可以重載,但不能是虛函數(shù),析構(gòu)函數(shù)可以是虛函數(shù)。
基類為什么需要虛析構(gòu)函數(shù)?
防止內(nèi)存泄漏。想去借助父類指針去銷毀子類對象的時候,不能去銷毀子類對象。假如沒有虛析構(gòu)函數(shù),釋放一個由基類指針指向的派生類對象時,不會觸發(fā)動態(tài)綁定,則只會調(diào)用基類的析構(gòu)函數(shù),不會調(diào)用派生類的。派生類中申請的空間則得不到釋放導(dǎo)致內(nèi)存泄漏。
構(gòu)造/析構(gòu)函數(shù)調(diào)用虛函數(shù)
派生類對象構(gòu)造期間進入基類的構(gòu)造函數(shù)時,對象類型變成了基類類型,而不是派生類類型。
同樣,進入基類析構(gòu)函數(shù)時,對象也是基類類型。
所以,虛函數(shù)始終僅僅調(diào)用基類的虛函數(shù)(如果是基類調(diào)用虛函數(shù)),不能達到多態(tài)的效果。
虛函數(shù)表
產(chǎn)生時間:編譯期 存儲位置:只讀數(shù)據(jù)段 .rodata 虛指針:類的每一個對象都包含一個虛指針(指向虛表),存在對象實例的最前面四個字節(jié) 虛指針創(chuàng)建時間:構(gòu)造函數(shù)
注:虛表中的指針會指向其繼承的最近的一個類的虛函數(shù)
const 相關(guān)
如何初始化 const 和 static 數(shù)據(jù)成員?
通常在類外申明 static 成員,但是 static const 的整型( bool,char,int,long )可以在類中聲明且初始化,static const 的其他類型必須在類外初始化(包括整型數(shù)組)。
static 和 const 分別怎么用,類里面 static 和 const 可以同時修飾成員函數(shù)嗎?
static 的作用:對 static 的三條作用做一句話總結(jié)。首先 static 的最主要功能是隱藏,其次因為 static 變量存放在靜態(tài)存儲區(qū),所以它具備持久性和默認值 0。
對變量
局部變量
在局部變量之前加上關(guān)鍵字 static,局部變量就被定義成為一個局部靜態(tài)變量。
內(nèi)存中的位置:靜態(tài)存儲區(qū) 初始化:未經(jīng)初始化的全局靜態(tài)變量會被程序自動初始化為0(自動對象的值是任意的,除非他被顯示初始化) 作用域:作用域仍為局部作用域,當定義它的函數(shù)或者語句塊結(jié)束的時候,作用域隨之結(jié)束。
注:當 static 用來修飾局部變量的時候,它就改變了局部變量的存儲位置(從原來的棧中存放改為靜態(tài)存儲區(qū))及其生命周期(局部靜態(tài)變量在離開作用域之后,并沒有被銷毀,而是仍然駐留在內(nèi)存當中,直到程序結(jié)束,只不過我們不能再對他進行訪問),但未改變其作用域。
全局變量
在全局變量之前加上關(guān)鍵字 static,全局變量就被定義成為一個全局靜態(tài)變量。
內(nèi)存中的位置:靜態(tài)存儲區(qū)(靜態(tài)存儲區(qū)在整個程序運行期間都存在) 初始化:未經(jīng)初始化的全局靜態(tài)變量會被程序自動初始化為 0(自動對象的值是任意的,除非他被顯示初始化) 作用域:全局靜態(tài)變量在聲明他的文件之外是不可見的。準確地講從定義之處開始到文件結(jié)尾。
注:static 修飾全局變量,并未改變其存儲位置及生命周期,而是改變了其作用域,使當前文件外的源文件無法訪問該變量,好處如下:
不會被其他文件所訪問,修改 其他文件中可以使用相同名字的變量,不會發(fā)生沖突。對全局函數(shù)也是有隱藏作用。而普通全局變量只要定義了,任何地方都能使用,使用前需要聲明所有的 .c 文件,只能定義一次普通全局變量,但是可以聲明多次(外部鏈接)。
注意:全局變量的作用域是全局范圍,但是在某個文件中使用時,必須先聲明。
對類
成員變量
用 static 修飾類的數(shù)據(jù)成員實際使其成為類的全局變量,會被類的所有對象共享,包括派生類的對象。因此,static 成員必須在類外進行初始化(初始化格式:int base::var=10;),而不能在構(gòu)造函數(shù)內(nèi)進行初始化,不過也可以用 const 修飾 static 數(shù)據(jù)成員在類內(nèi)初始化 。因為靜態(tài)成員屬于整個類,而不屬于某個對象,如果在類內(nèi)初始化,會導(dǎo)致每個對象都包含該靜態(tài)成員,這是矛盾的。
特點:
不要試圖在頭文件中定義(初始化)靜態(tài)數(shù)據(jù)成員。在大多數(shù)的情況下,這樣做會引起重復(fù)定義這樣的錯誤。即使加上 #ifndef#define#endif或者#pragma once也不行。靜態(tài)數(shù)據(jù)成員可以成為成員函數(shù)的可選參數(shù),而普通數(shù)據(jù)成員則不可以。 靜態(tài)數(shù)據(jù)成員的類型可以是所屬類的類型,而普通數(shù)據(jù)成員則不可以。普通數(shù)據(jù)成員的只能聲明為 所屬類類型的指針或引用。
成員函數(shù)
用 static 修飾成員函數(shù),使這個類只存在這一份函數(shù),所有對象共享該函數(shù),不含 this 指針。 靜態(tài)成員是可以獨立訪問的,也就是說,無須創(chuàng)建任何對象實例就可以訪問。 base::func(5,3);當 static 成員函數(shù)在類外定義時不需要加 static 修飾符。在靜態(tài)成員函數(shù)的實現(xiàn)中不能直接引用類中說明的非靜態(tài)成員,可以引用類中說明的靜態(tài)成員。因為靜態(tài)成員函數(shù)不含this指針。
不可以同時用 const 和 static 修飾成員函數(shù)。
C++ 編譯器在實現(xiàn) const 的成員函數(shù)的時候為了確保該函數(shù)不能修改類的實例的狀態(tài),會在函數(shù)中添加一個隱式的參數(shù) const this*。但當一個成員為 static 的時候,該函數(shù)是沒有 this 指針的。也就是說此時 const 的用法和 static 是沖突的。
我們也可以這樣理解:兩者的語意是矛盾的。static 的作用是表示該函數(shù)只作用在類型的靜態(tài)變量上,與類的實例沒有關(guān)系;而 const 的作用是確保函數(shù)不能修改類的實例的狀態(tài),與類型的靜態(tài)變量沒有關(guān)系。因此不能同時用它們。
const的作用:
限定變量為不可修改。 限定成員函數(shù)不可以修改任何數(shù)據(jù)成員。 const 與指針:
const char *p 常量指針,可以換方向,不可以改內(nèi)容
char * const p,指針常量,不可以換方向,可以改內(nèi)容
構(gòu)造函數(shù)
構(gòu)造函數(shù)調(diào)用順序
虛基類構(gòu)造函數(shù)(被繼承的順序) 非虛基類構(gòu)造函數(shù)(被繼承的順序) 成員對象構(gòu)造函數(shù)(聲明順序) 自己的構(gòu)造函數(shù)
自身構(gòu)造函數(shù)順序
虛表指針(防止初始化列表里面調(diào)用虛函數(shù),否則調(diào)用的是父類的虛函數(shù)) 初始化列表(const、引用、沒有定義默認構(gòu)造函數(shù)的類型) 花括號里的 (初始化列表直接初始化,這個先初始化后賦值)
this 指針
創(chuàng)建時間:成員函數(shù)調(diào)用前生成,調(diào)用后清除
如何傳遞給成員函數(shù):通過函數(shù)參數(shù)的首參數(shù)來傳遞
extern 關(guān)鍵字
置于變量或者函數(shù)前,以標示變量或者函數(shù)的定義在別的文件中,提示編譯器遇到此變量和函數(shù)時在其他模塊中尋找其定義 extern “C” void fun(); 告訴編譯器按C的規(guī)則去翻譯
以下關(guān)鍵字的作用?使用場景?
inline:在 c/c++ 中,為了解決一些頻繁調(diào)用的小函數(shù)大量消耗??臻g(棧內(nèi)存)的問題,特別的引入了 inline 修飾符,表示為內(nèi)聯(lián)函數(shù)。 decltype:從表達式中推斷出要定義變量的類型,但卻不想用表達式的值去初始化變量。還有可能是函數(shù)的返回類型為某表達式的的值類型。 volatile:volatile 關(guān)鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統(tǒng)、硬件或者其它線程等。遇到這個關(guān)鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優(yōu)化,從而可以提供對特殊地址的穩(wěn)定訪問。
淺拷貝與深拷貝
什么時候用到拷貝函數(shù)?
一個對象以值傳遞的方式傳入函數(shù)體(參數(shù)); 一個對象以值傳遞的方式從函數(shù)返回(返回值); 一個對象需要通過另外一個對象進行初始化(初始化)。
如果在類中沒有顯式地聲明一個拷貝構(gòu)造函數(shù),那么,編譯器將會自動生成一個默認的拷貝構(gòu)造函數(shù),該構(gòu)造函數(shù)完成對象之間的位拷貝。位拷貝又稱淺拷貝
默認拷貝構(gòu)造函數(shù)是淺拷貝。如果一個類擁有資源,當這個類的對象發(fā)生復(fù)制過程的時候,資源重新分配,這個過程就是深拷貝,反之,沒有重新分配資源,就是淺拷貝。
如果實行位拷貝,也就是把對象里的值完全復(fù)制給另一個對象,如 A=B。這時,如果 B 中有一個成員變量指針已經(jīng)申請了內(nèi)存,那 A 中的那個成員變量也指向同一塊內(nèi)存。這就出現(xiàn)了問題:當 B 把內(nèi)存釋放了(如:析構(gòu)),這時A內(nèi)的指針就是野指針了,出現(xiàn)運行錯誤。
C++類中成員初始化順序
成員變量在使用初始化列表初始化時,與構(gòu)造函數(shù)中初始化成員列表的順序無關(guān),只與定義成員變量的順序有關(guān)。
類中 const 成員常量必須在構(gòu)造函數(shù)初始化列表中初始化。類中 static 成員變量,只能在類外初始化(同一類的所有實例共享靜態(tài)成員變量)。
構(gòu)造過程
分配內(nèi)存 進行父類的構(gòu)造,按照父類的聲明順序(遞歸過程) 構(gòu)造虛表指針,對虛表指針賦值 根據(jù)初始化列表中的值初始化變量 執(zhí)行構(gòu)造函數(shù){}內(nèi)的
構(gòu)造函數(shù)初始化列表
const 或引用類型的成員。因為 const 對象或引用類型只能初始化,不能對他們賦值。
與對數(shù)據(jù)成員賦值的區(qū)別:
內(nèi)置數(shù)據(jù)類型,復(fù)合類型(指針,引用):結(jié)果和性能上相同。 用戶定義類型(類類型):結(jié)果上相同,但是性能上存在很大的差別。
vector 中 size() 和 capacity() 的區(qū)別
size() 指容器當前擁有的元素個數(shù)(對應(yīng)的resize(size_type)會在容器尾添加或刪除一些元素,來調(diào)整容器中實際的內(nèi)容,使容器達到指定的大小。);capacity()指容器在必須分配存儲空間之前可以存儲的元素總數(shù)。
size 表示的這個 vector 里容納了多少個元素,capacity 表示 vector 能夠容納多少元素,它們的不同是在于 vector 的 size 是 2 倍增長的。如果 vector 的大小不夠了,比如現(xiàn)在的 capacity 是 4,插入到第五個元素的時候,發(fā)現(xiàn)不夠了,此時會給他重新分配 8 個空間,把原來的數(shù)據(jù)及新的數(shù)據(jù)復(fù)制到這個新分配的空間里。(會有迭代器失效的問題)
定義一個空類編譯器做了哪些操作
如果你只是聲明一個空類,不做任何事情的話,編譯器會自動為你生成一個默認構(gòu)造函數(shù)、一個拷貝默認構(gòu)造函數(shù)、一個默認拷貝賦值操作符和一個默認析構(gòu)函數(shù)。這些函數(shù)只有在第一次被調(diào)用時,才會被編譯器創(chuàng)建。所有這些函數(shù)都是 inline 和 public 的。
強制類型轉(zhuǎn)換
static_cast
用法:static_cast < type-id > ( expression )
q1. 為什么需要 static_cast 強制轉(zhuǎn)換?
void指針->其他類型指針 (不安全) 改變通常的標準轉(zhuǎn)換 用于類層次結(jié)構(gòu)中基類和子類之間指針或引用的轉(zhuǎn)換。進行上行轉(zhuǎn)換(把子類的指針或引用轉(zhuǎn)換成基類表示)是安全的;進行下行轉(zhuǎn)換(把基類指針或引用轉(zhuǎn)換成子類指針或引用)時,由于沒有動態(tài)類型檢查,所以是不安全的。
dynamic_cast
用法:dynamic_cast < type-id > ( expression )
dynamic_cast 主要用于類層次間的上行轉(zhuǎn)換和下行轉(zhuǎn)換,還可以用于類之間的交叉轉(zhuǎn)換(同一基類的兩個同級派生類)。
在類層次間進行上行轉(zhuǎn)換時,dynamic_cast和static_cast的效果是一樣的;在進行下行轉(zhuǎn)換時,dynamic_cast具有類型檢查的功能,比static_cast更安全。
reinpreter_cast
它可以把一個指針轉(zhuǎn)換成一個整數(shù),也可以把一個整數(shù)轉(zhuǎn)換成一個指針(先把一個指針轉(zhuǎn)換成一個整數(shù),在把該整數(shù)轉(zhuǎn)換成原類型的指針,還可以得到原先的指針值)。
const_cast該運算符用來修改類型的 const 或 volatile 屬性。除了 const 或 volatile 修飾之外, type_id 和 expression 的類型是一樣的。
常量指針被轉(zhuǎn)化成非常量指針,并且仍然指向原來的對象;常量引用被轉(zhuǎn)換成非常量引用,并且仍然指向原來的對象;常量對象被轉(zhuǎn)換成非常量對象。
volatile 關(guān)鍵字
使用方法: int volatile x;作用:編譯器不再優(yōu)化。讓編譯器每次操作該變量時一定要從內(nèi)存中真正取出,而不是使用已經(jīng)存在寄存器中的值 。
內(nèi)存管理
C 內(nèi)存分配
malloc:在內(nèi)存的動態(tài)分配區(qū)域中分配一個長度為 size 的連續(xù)空間,如果分配成功,則返回所分配內(nèi)存空間的首地址,否則返回 NULL,申請的內(nèi)存不會初始化。 calloc:分配一個 num * size 連續(xù)的空間,會自動初始化為0。 realloc:動態(tài)分配一個長度為 size 的內(nèi)存空間,并把內(nèi)存空間的首地址賦值給 ptr,把 ptr 內(nèi)存空間調(diào)整為 size。
C++ 內(nèi)存分配:
-棧區(qū)(stack):主要存放函數(shù)參數(shù)以及局部變量,由系統(tǒng)自動分配釋放。
堆區(qū)(heap):由用戶通過 malloc/new 手動申請,手動釋放。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事,分配方式倒是類似于鏈表。 全局/靜態(tài)區(qū):存放全局變量、靜態(tài)變量;程序結(jié)束后由系統(tǒng)釋放。- - 字符串常量區(qū):字符串常量就放在這里,程序結(jié)束后由系統(tǒng)釋放。 代碼區(qū):存放程序的二進制代碼。
結(jié)構(gòu)體字節(jié)對齊問題?結(jié)構(gòu)體/類大小的計算?
默認字節(jié)對齊
各成員變量存放的起始地址相對于結(jié)構(gòu)的起始地址的偏移量必須是該變量的類型所占用的字節(jié)數(shù)的倍數(shù),結(jié)構(gòu)的大小為結(jié)構(gòu)的字節(jié)邊界數(shù)(即該結(jié)構(gòu)中占用最大空間的類型所占用的字節(jié)數(shù))的倍數(shù) n 字節(jié)對齊。
pragma pack(n)
如果 n 大于等于該變量所占用的字節(jié)數(shù),那么偏移量必須滿足默認的對齊方式; 如果 n 小于該變量的類型所占用的字節(jié)數(shù),那么偏移量為 n 的倍數(shù),不用滿足默認的對齊方式; 如果 n 大于所有成員變量類型所占用的字節(jié)數(shù),那么結(jié)構(gòu)體的總大小必須為占用空間最大的變量占用的空間數(shù)的倍數(shù);否則必須為n的倍數(shù)(兩者相比,取?。?;
虛函數(shù)的大小計算
假設(shè)經(jīng)過成員對齊后的類的大小為 size 個字節(jié)。那么類的 sizeof 大小可以這么計算:size + 4*(虛函數(shù)指針的個數(shù) n)。
聯(lián)合體的大小計算
聯(lián)合體所占的空間不僅取決于最寬成員,還跟所有成員有關(guān)系,即其大小必須滿足兩個條件:
大小足夠容納最寬的成員; 大小能被其包含的所有基本數(shù)據(jù)類型的大小所整除。
常見例子:
class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位機器)/8(64位機器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;
指針和引用
區(qū)別
定義:指針是一個對象,引用本身不是對象,只是另一個對象的別名; 指針是“指向”另外一種類型的復(fù)合類型; 引用本身不是一個對象,所以不能定義引用的引用; 引用只能綁定到對象上,它只是一個對象的別名,因此引用必須初始化,且不能更換引用對象。
指針
可以有 const 指針,但是沒有 const 引用(const 引用可讀不可改,與綁定對象是否為 const 無關(guān))
注:引用可以指向常量,也可以指向變量。例如int &a=b,使引用 a 指向變量 b。而為了讓引用指向常量,必須使用常量引用,如const int &a=1; 它代表的是引用 a 指向一個const int型,這個 int 型的值不能被改變,而不是引用 a 的指向不能被改變,因為引用的指向本來就是不可變的,無需加 const 聲明。即指針存在常量指針int const *p和指針常量int *const p,而引用只存在常量引用int const &a,不存在引用常量int& const a。
指針可以有多級,但是引用只能是一級(int **p;合法 而 int &&a 是不合法的) 指針的值可以為空,但是引用的值不能為 NULL,并且引用在定義的時候必須初始化; 指針的值在初始化后可以改變,即指向其它的存儲單元,而引用在進行初始化后就不會再改變了。 "sizeof 引用"得到的是所指向的變量(對象)的大小,而" sizeof 指針"得到的是指針本身的大?。?/section> 指針和引用的自增(++)運算意義不一樣; 指針使用時需要解引用(*),引用則不需要;
指針的注意點
1、指針指向常量存儲區(qū)對象
char *p="abc";此時 p 指向的是一個字符串常量,不能對 *p 的內(nèi)容進行寫操作,如 srtcpy(p,s) 是錯誤的,因為 p 的內(nèi)容為 “abc” 字符串常量,該數(shù)據(jù)存儲在常量存儲區(qū),但可以對指針 p 進行操作,讓其指向其他的內(nèi)存空間。
2、資源泄漏
char *p=new char[3]; //分配三個字符空間,p指向該內(nèi)存空間
p="ab"; //此時p指向常量“ab”,而不再是new char分配的內(nèi)存空間了,從而造成了資源泄漏
delete []p; //釋放時報錯
3、內(nèi)存越界
char *p=new char[3]; //分配三個字符空間,p指向該內(nèi)存空間
strcpy(p,"abcd"); //將abcd存處在分配的內(nèi)存空間中,由于strlen("abcd")=4>3,越界
delete []p; //釋放時出錯
new和malloc的區(qū)別
new 是運算符,malloc() 是一個庫函數(shù); new 會調(diào)用構(gòu)造函數(shù),malloc 不會; new 返回指定類型指針,malloc 返回 void* 指針,需要強制類型轉(zhuǎn)換; new 會自動計算需分配的空間,malloc 不行; new 可以被重載,malloc 不能。
懸空指針與野指針
懸空指針:當所指向的對象被釋放或者收回,但是沒有讓指針指向NULL; 野指針:那些未初始化的指針;
空指針能調(diào)用類成員函數(shù)嗎
可以調(diào)用成員函數(shù)。當調(diào)用p->func1(); 這句話時,其實就是調(diào)用 A::func1(this) ,而成員函數(shù)的地址在編譯時就已經(jīng)確定, 所以空指針也是可以調(diào)用普通成員函數(shù),只不過此時的 this 指針指向空而已,但函數(shù) fun1 函數(shù)體內(nèi)并沒有用到 this 指針,所以不會出現(xiàn)問題。
不可以調(diào)用虛函數(shù)。如果一個類中包含虛函數(shù),那么它所實例化處的對象的前四個字節(jié)是一個虛表指針,這個虛表指針指向的是虛函數(shù)表。當然,虛函數(shù)的地址也是在編譯時就已經(jīng)確定了,這些虛函數(shù)地址存放在虛函數(shù)表里面,而虛函數(shù)表就在程序地址空間的數(shù)據(jù)段(靜態(tài)區(qū)),也就是說虛表的建立是在編譯階段就完成的;當調(diào)用構(gòu)造函數(shù)的時候才會初始化虛函數(shù)表指針,即把虛表指針存放在對象前四個字節(jié)(32 位下)。試想一下,假如用空指針調(diào)用虛函數(shù),這個指針根本就找不到對應(yīng)的對象的地址,因此他也不知道虛表的地址,沒有虛表的地址,怎么能調(diào)用虛函數(shù)呢
智能指針
unique_ptr
摒棄 auto_ptr 的原因:避免潛在的內(nèi)存崩潰問題。如下代碼用 auto_ptr 的話不會出現(xiàn)問題,但 p3 是無法訪問的。
unique_ptr<string> p3 (new string ("auto");
unique_ptr<string> p4;
p4 = p3; // 編譯器認為非法
只允許基礎(chǔ)指針的一個所有者。unique_ptr小巧高效;大小等同于一個指針且支持右值引用,從而可實現(xiàn)快速插入和對STL集合的檢索。
注意:當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這么做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這么做。
shared_ptr
采用引用計數(shù)的智能指針,主要用于要將一個原始指針分配給多個所有者(例如,從容器返回了指針副本又想保留原始指針時)的情況。當所有的 shared_ptr 所有者超出了范圍或放棄所有權(quán),才會刪除原始指針。大小為兩個指針;一個用于對象,另一個用于包含引用計數(shù)的共享控制塊。
最安全的分配和使用動態(tài)內(nèi)存的方法是調(diào)用 make_shared 標準庫函數(shù),此函數(shù)在動態(tài)分配內(nèi)存中分配一個對象并初始化它,返回對象的 shared_ptr。
堆和棧
棧 :只要棧的剩余空間大于所申請的空間,系統(tǒng)將為程序提供內(nèi)存,否則將報異常提示棧溢出。 堆:首先應(yīng)該知道操作系統(tǒng)有一個記錄空閑內(nèi)存地址的鏈表,當系統(tǒng)受到程序的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆。
編譯與優(yōu)化
靜態(tài)鏈接與動態(tài)鏈接
靜態(tài)鏈接:
定義:在生成可執(zhí)行文件的時候(鏈接階段),把所有需要的函數(shù)的二進制代碼都包含到可執(zhí)行文件中去。 特點:鏈接器需要知道參與鏈接的目標文件需要哪些函數(shù),同時也要知道每個目標文件都能提供什么函數(shù),這樣鏈接器才能知道是不是每個目標文件所需要的函數(shù)都能正確地鏈接。如果某個目標文件需要的函數(shù)在參與鏈接的目標文件中找不到的話,鏈接器就報錯了。目標文件中有兩個重要的接口來提供這些信息:一個是符號表,另外一個是重定位表。 缺點:1. 程序體積會變大;2. 靜態(tài)庫有更新的話,所有可執(zhí)行文件都需要重新鏈接
動態(tài)鏈接:
定義:在編譯的時候不直接拷貝可執(zhí)行代碼,而是通過記錄一系列符號和參數(shù),在程序運行或加載時將這些信息傳遞給操作系統(tǒng),操作系統(tǒng)負責(zé)將需要的動態(tài)庫加載到內(nèi)存中,然后程序在運行到指定的代碼時,去共享執(zhí)行內(nèi)存中已經(jīng)加載的動態(tài)庫可執(zhí)行代碼,最終達到運行時連接的目的。
缺點:1. 運行時加載,影響性能
靜態(tài)鏈接過程
操作系統(tǒng)會讀取可執(zhí)行文件的頭部,檢查文件的合法性,然后從頭部中的 “Program Header”中讀取每個“Segment”的虛擬地址、文件地址和屬性,并將它們映射到進程虛擬空間的相應(yīng)位置;操作系統(tǒng)就會把控制權(quán)交給可執(zhí)行文件的入口地址,然后程序開始執(zhí)行。
動態(tài)鏈接過程
操作系統(tǒng)會讀取可執(zhí)行文件的頭部,檢查文件的合法性,然后從頭部中的 “Program Header”中讀取每個“Segment”的虛擬地址、文件地址和屬性,并將它們映射到進程虛擬空間的相應(yīng)位置;操作系統(tǒng)啟動一個動態(tài)鏈接器——ld.so,它其實是個共享對象,操作系統(tǒng)同樣通過映射的方式將它加在到進程的地址空間中,加載完動態(tài)鏈接器之后,將控制權(quán)交給動態(tài)鏈接器的入口地址; 動態(tài)鏈接器開始執(zhí)行一系列自身的初始化操作,然后根據(jù)當前的環(huán)境參數(shù),開始對可執(zhí)行文件進行動態(tài)鏈接工作; 所有動態(tài)鏈接工作完成后,動態(tài)鏈接器就會將控制權(quán)交給可執(zhí)行文件的入口地址,程序開始正式執(zhí)行。
程序加載的內(nèi)存分布
在多任務(wù)操作系統(tǒng)中,每個進程都運行在一個屬于自己的虛擬內(nèi)存中,而虛擬內(nèi)存被分為許多頁,并映射到物理內(nèi)存中,被加載到物理內(nèi)存中的文件才能夠被執(zhí)行。
代碼段(.text):用來存放可執(zhí)行文件的機器指令。存放在只讀區(qū)域,以防止被修改。 只讀數(shù)據(jù)段(.rodata):用來存放常量存放在只讀區(qū)域,如字符串常量、全局const變量等。 可讀寫數(shù)據(jù)段(.data):用來存放可執(zhí)行文件中已初始化的全局變量和局部靜態(tài)變量。 BSS 段(.bss):未初始化的全局變量和局部靜態(tài)變量以及初始化為 0 的全局變量一般放在 .bss 的段里,以節(jié)省內(nèi)存空間。 static int a=0;(初始化為 0 的全局變量(靜態(tài)變量)放在 .bss)。堆:用來容納應(yīng)用程序動態(tài)分配的內(nèi)存區(qū)域。當程序使用 malloc 或 new 分配內(nèi)存時,得到的內(nèi)存來自堆。堆通常位于棧的下方。向上生長 棧:用于維護函數(shù)調(diào)用的上下文。棧通常分配在用戶空間的最高地址處分配。向下生長 動態(tài)鏈接庫映射區(qū):如果程序調(diào)用了動態(tài)鏈接庫,則會有這一部分。該區(qū)域是用于映射裝載的動態(tài)鏈接庫。 保留區(qū):內(nèi)存中受到保護而禁止訪問的內(nèi)存區(qū)域。
溢出,越界,泄漏
溢出
1、棧溢出:棧的大小通常是 1M-2M,所以棧溢出包含兩種情況,一是分配的的大小超過棧的最大值,二是分配的大小沒有超過最大值,但是接收的 buff 比新 buff 小 ,具體情況如下。
char a[10] = {0};
strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");
注意:調(diào)試時棧溢出的異常要在函數(shù)調(diào)用結(jié)束后才會檢測到,因為棧是在函數(shù)結(jié)束時才會開始進行出棧操作。
2、內(nèi)存溢出:使用 malloc 和 new 分配的內(nèi)存,在拷貝時接收 buff 小于新 buff 時造成的現(xiàn)象。
越界
通常指數(shù)組越界
泄露
指堆內(nèi)存泄漏,是指使用 malloc 和 new 分配的內(nèi)存沒有釋放造成的
文章鏈接:https://www.zhihu.com/question/400543720
