1. 程式人生 > >【C++】面試基礎準備(00)

【C++】面試基礎準備(00)

1、extern關鍵字

extern可以置於變數或者函式前,以標示變數或者函式的定義在別的檔案中,提示編譯器遇到此變數和函式時在其他模組中尋找其定義。此外extern也可用來進行連結指定。

也就是說,extern有兩個作用:

當它與"C"一起連用時,如:extern "C" void fun(int a, int b);,則告訴編譯器在編譯fun這個函式名時按著C的規則去翻譯相應的函式名而不是C++的,C++的規則在翻譯這個函式名時會把fun這個名字變得面目全非,可能是[email protected]_int_int#%$也可能是別的,這要看編譯器的"脾氣"了(不同的編譯器採用的方法不一樣)。為什麼這麼做呢,因為C++支援函式的過載;

當extern不與"C"在一起修飾變數或函式時,如在標頭檔案中:extern int g_Int; ,它的作用就是宣告函式或全域性變數的作用範圍的關鍵字,其宣告的函式和變數可以在本模組和其他模組中使用,記住它是一個宣告不是定義!也就是說B模組要是引用模組A中定義的全域性變數或函式時,它只要包含A模組的標頭檔案即可,在編譯階段,模組B雖然找不到該函式或變數,但它不會報錯,它會在連線時從模組A生成的目的碼中找到此函式。

在一個檔案中定義的全域性變數預設為外部的,即關鍵字extern可以省略,但如果在其他的檔案中使用這個檔案定義的全域性變數,則必須在使用前用extern進行外部宣告。

也就是說,用extern修飾只是表示宣告變數,不表示定義變數。如果不用extern修飾的全域性變數,就預設宣告加定義了。

2、static關鍵字

static關鍵字的作用有:

  • static修飾區域性變數:被修飾的變數成為靜態變數,儲存在靜態區。儲存在靜態區的資料生命週期與程式相同,在main函式之前初始化,在程式退出時銷燬。區域性靜態變數使得該變數在退出函式後,不會被銷燬,因此再次呼叫該函式時,該變數的值與上次退出函式時值相同;
  • static修飾全域性變數:全域性變數本來就儲存在靜態區,因此static並不能改變其儲存位置。但是,static限定該全域性變數在本編譯單元內,一個編譯單元就是指一個cpp和它包含的標頭檔案;
  • static修飾普通函式:限定該函式只能在本編譯單元內被呼叫;
  • static修飾成員變數:所有的物件都只維持同一個例項,可以採用static可以實現不同物件之間資料共享;
  • static修飾成員函式:類中的static成員函式屬於整個類所擁有,這個函式不接收this指標,所以只能訪問類的static成員變數。

static與extern是一對“水火不容”的傢伙,也就是說extern和static不能同時修飾一個變數;其次,static修飾的全域性變數宣告與定義同時進行,也就是說當你在標頭檔案中使用static聲明瞭全域性變數後,它也同時被定義了;最後,static修飾全域性變數的作用域 只能是本身的編譯單元,也就是說它的“全域性”只對本編譯單元有效,其他編譯單元則看不到它。

3、volatile關鍵字

一個定義為volatile的變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。精確地說就是,優化器在用到這個變數時必須每次都小心地重新讀取這個變數的值,而不是使用儲存在暫存器裡的備份。下面是volatile變數的幾個例子:

  • 儲存器對映的硬體暫存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義;
  • 在中斷函式中的互動變數,一定要加上volatile關鍵字修飾,這樣每次讀取非自動儲存型別的值(全域性變數,靜態變數)都是在其記憶體地址中讀取的,確保是我們想要的資料;
  • 多工環境下各任務間共享的標誌應該加volatile。

一個引數既可以是const還可以是volatile嗎?

可以的,例如只讀的狀態暫存器。它是volatile因為它可能被意想不到地改變。它是const因為程式不應該試圖去修改它。軟體不能改變,並不意味著我硬體不能改變你的值,這就是微控制器中的應用。

4、const關鍵字

簡單地說,const意味著常數。

  • const定義的變數,它的值不能被改變,在整個作用域中都保持固定;
  • 同巨集定義一樣,可以避免意義模糊的數字出現,同樣可以很方便地進行引數的調整和修改;
  • 可以保護被修飾的東西,防止意外的修改,增強程式的健壯性。const是通過編譯器在編譯的時候執行檢查來確保實現的。

const與指標:修飾指標、修飾指標指向的物件;

const與函式:

  • 修飾函式形參,如果形參是一個指標,為了防止在函式內部修改指標指向的資料,就可以用 const 來限制;
  • 修飾返回值,表明函式的返回值是一個常量;
  • 修飾類成員函式(放在函式的引數表之後,函式體之前),表示該函式的this指標是一個常量,不能修改該物件的資料成員。

5、new與malloc區別

  • new分配記憶體按照資料型別進行分配(不需要指定大小),返回的是指定物件的指標。malloc分配記憶體按照大小分配,返回的是void*;
  • new不僅分配一段記憶體,而且會呼叫建構函式,但是malloc則不會;
  • new是一個操作符可以過載,malloc是一個庫函式;
  • new分配的記憶體要用delete銷燬,malloc要用free來銷燬;delete銷燬的時候會呼叫物件的解構函式,而free則不會;
  • new如果分配失敗了會丟擲bad_alloc的異常,而malloc失敗了會返回NULL;
  • new[]與delete[]來專門處理陣列型別,而malloc,它並知道你在這塊記憶體上要放的陣列還是啥別的東西,反正它就給你一塊原始的記憶體,在給你個記憶體的地址就完事;
  • new和delete可以被過載,但是malloc/free並不允許過載;
  • 使用malloc分配的記憶體後,如果在使用過程中發現記憶體不足,可以使用realloc函式進行記憶體重新分配實現記憶體的擴充,new沒有這樣直觀的配套設施來擴充記憶體。

注:realloc先判斷當前的指標所指記憶體是否有足夠的連續空間,如果有,原地擴大可分配的記憶體地址,並且返回原來的地址指標;如果空間不夠,先按照新指定的大小分配空間,將原有資料從頭到尾拷貝到新分配的記憶體區域,而後釋放原來的記憶體區域。

接下來講解一下,new/delete的內部實現原理:

new 運算子的內部實現分為兩步:

  • 記憶體分配:呼叫相應的 operator new(size_t) 函式,動態分配記憶體。如果 operator new(size_t) 不能成功獲得記憶體,則呼叫 new_handler() 函式用於處理new失敗問題。如果沒有設定 new_handler() 函式或者 new_handler() 未能分配足夠記憶體,則丟擲 std::bad_alloc 異常。“new運算子”所呼叫的 operator new(size_t) 函式,按照C++的名字查詢規則,首先做依賴於實參的名字查詢(即ADL規則),在要申請記憶體的資料型別T的內部(成員函式)、資料型別T定義處的名稱空間查詢;如果沒有查詢到,則直接呼叫全域性的 ::operator new(size_t) 函式;
  • 建構函式:在分配到的動態記憶體塊上 初始化 相應型別的物件(建構函式)並返回其首地址。如果呼叫建構函式初始化物件時丟擲異常,則自動呼叫 operator delete(void*, void*) 函式釋放已經分配到的記憶體。

delete 運算子的內部實現分為兩步:

  • 解構函式:呼叫相應型別的解構函式,處理類內部可能涉及的資源釋放;
  • 記憶體釋放:呼叫相應的 operator delete(void *) 函式。呼叫順序參考上述 operator new(size_t) 函式(ADL規則)。

6、運算子過載

運算子過載的規則:

  • 不能過載的運算子有:::(作用域運算子)、.(成員運算子)、.*(指標成員運算子)、?:(三目運算子)、sizeof()。
  • 只能使用成員函式過載的運算子有:=、()、[]、->、new、delete;(型別轉換函式也是)
  • 單目運算子最好過載為成員函式;
  • 對於複合的賦值運算子如+=、-=、*=、/=、&=、!=、~=、%=、>>=、<<=建議過載為成員函式;
  • 對於其它運算子,建議過載為友元函式;
  • 型別轉換函式只能定義為一個類的成員函式而不能定義為類的友元函式。 

7、C++多型性與虛擬函式表

多型可分為靜態多型和動態多型,具體的分類情況如下:

靜態多型和動態多型的區別其實只是在什麼時候將函式實現和函式呼叫關聯起來,是在編譯時期還是執行時期,即函式地址是早繫結還是晚繫結的? 

  • 靜態多型是指在編譯期間就可以確定函式的呼叫地址,並生產程式碼,這就是靜態的,也就是說地址是早早繫結的,靜態多型也往往被叫做靜態聯編。 靜態多型往往通過函式過載和模版來實現;
  • 動態多型則是指函式呼叫的地址不能在編譯器期間確定,必須需要在執行時才確定,這就屬於晚繫結,動態多型也往往被叫做動態聯編。動態多型通過虛擬函式和繼承關係來實現。

補充一下過載、重寫(覆蓋)、重定義(隱藏)的區別:

動態多型實現有幾個條件: 

  • 虛擬函式; 
  • 一個基類的指標或引用指向派生類的物件。

基類指標在呼叫成員函式(虛擬函式)時,就會去查詢該物件的虛擬函式表。虛擬函式表的地址在每個物件的首地址。查詢該虛擬函式表中該函式的指標進行呼叫。 

每個物件中儲存的只是一個虛擬函式表的指標,C++內部為每一個類維持一個虛擬函式表,該類的物件的都指向這同一個虛擬函式表。也就是說,編譯器為每一個類維護一個虛擬函式表,每個物件的首地址儲存著該虛擬函式表的指標,同一個類的不同物件實際上指向同一張虛擬函式表。 

虛擬函式的作用,用專業術語來解釋就是實現多型性(Polymorphism),多型性是將介面與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略。

虛擬函式是通過一張虛擬函式表(Virtual Table)來實現的。在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函式。在類繼承的時候,虛擬函式表直接從基類也繼承過來;如果派生類覆蓋了其中的某個虛擬函式,那麼虛擬函式表的指標就會被替換。在執行時,動態繫結的呼叫過程是這樣的,首先,基類指標被賦值為派生類物件的地址,那麼就可以找到指向這個類的虛擬函式的隱含指標,然後通過該虛擬函式的名字就可以在這個虛擬函式表中找到對應的虛擬函式的地址。然後進行呼叫就可以了。

8、建構函式、解構函式

建構函式能定義成虛擬函式麼?

不能,虛擬函式的作用在於通過父類的指標或引用來呼叫子類的那個成員函式,而建構函式是在建立物件時自己主動呼叫的,不可能說呼叫子類的建構函式來建立自己(沒有意義)。另外,虛擬函式相應一個指向vtable虛擬函式表的指標,但是這個指向vtable的指標事實上是儲存在物件的記憶體空間的。假設建構函式是虛的,就須要通過 vtable來呼叫,但是物件還沒有例項化,也就是記憶體空間還沒有,怎麼找vtable呢?所以建構函式不能是虛擬函式。

基類解構函式要定義成虛擬函式麼?

要,為了實現多型進行動態繫結,將派生類物件指標繫結到基類指標上,物件銷燬時,如果解構函式沒有定義為虛擬函式,則會呼叫基類的解構函式,顯然只能銷燬部分資料。如果要呼叫物件的解構函式,就需要將該物件的解構函式定義為虛擬函式,銷燬時通過虛擬函式表找到對應的解構函式。

建構函式中能呼叫虛擬函式麼?

不能,建構函式是先構造父親類再構造子類,如果父類建構函式有虛擬函式,會導致呼叫子類還沒構造的內容。

解構函式中能呼叫虛擬函式麼?

不能,解構函式是先析構子類再析構父類,如果父類解構函式有虛擬函式,會導致呼叫子類的已經析構的內容。

建構函式能丟擲異常嗎?

不能, 建構函式中丟擲異常,會導致解構函式不能被呼叫,但物件本身已申請到的記憶體資源會被系統釋放(已申請到資源的內部成員變數會被系統依次逆序呼叫其解構函式)。

解構函式能丟擲異常嗎?

不能,C++標準指明解構函式不能、也不應該丟擲異常。C++異常處理模型最大的特點和優勢就是對C++中的面向物件提供了最強大的無縫支援。那麼如果物件在執行期間出現了異常,C++異常處理模型有責任清除那些由於出現異常所導致的已經失效了的物件(也即物件超出了它原來的作用域),並釋放物件原來所分配的資源, 這就是呼叫這些物件的解構函式來完成釋放資源的任務,所以從這個意義上說,解構函式已經變成了異常處理的一部分。也就是說:

如果解構函式丟擲異常,則異常點之後的程式不會執行,如果解構函式在異常點之後執行了某些必要的動作比如釋放某些資源,則這些動作不會執行,會造成諸如資源洩漏的問題;

通常異常發生時,c++的機制會呼叫已經構造物件的解構函式來釋放資源,此時若解構函式本身也丟擲異常,則前一個異常尚未處理,又有新的異常,會造成程式崩潰的問題。

9、指標和引用的區別

  • 指標儲存的是所指物件的地址,引用是所指物件的別名,指標需要通過解引用間接訪問,而引用是直接訪問;
  • 指標是一個實體,需要分配記憶體空間,引用只是變數的別名,不需要分配記憶體空間;
  • 指標可以改變地址,從而改變所指的物件,而引用必須從一而終;
  • 引用在定義的時候必須初始化,而指標則不需要;
  • 有多級指標,但是沒有多級引用,只能有一級引用;
  • 指標有指向常量的指標和指標常量,而引用沒有常量引用;
  • 指標和引用的自增運算結果不一樣(指標是指向下一個空間,引用時引用的變數值加1);
  • sizeof 引用得到的是所指向的變數(物件)的大小,而sizeof 指標得到的是指標本身的大小;
  • 指標更靈活,用的好威力無比,用的不好處處是坑,而引用用起來則安全多了,但是比較死板。

關於常量引用,這是不存在的:

int x = 100;

int& _x = x;

這裡還有一個問題,就是int& x = 100;如果寫成這樣,編譯器會報錯,對常數的引用必須加const修飾,const int& x = 100(這個const表示的是,引用x是常量100的引用,而不是表示引用x是常量引用)。

引用的底層:

  • 定義一個引用就是定義一個指標,這個指標儲存引用物件的地址,且指標型別為const,不可以再指向其他物件; 
  • 每次對引用變數的使用,實際都伴隨著解引用,知識我們看不到符號*;
  • 從彙編後的程式碼看,引用和指標並沒有區別,引用也是佔用記憶體空間的。

10、指標與陣列千絲萬縷的聯絡

一個一維int陣列的陣列名實際上是一個int* 型別;

一個二維int陣列的陣列名實際上是一個int (* p)[n](陣列指標,行指標);

陣列名做引數會退化為指標,除了sizeof()。

11、智慧指標

使用智慧指標的原因至少有以下三點:

  • 智慧指標能夠幫助我們處理資源洩露問題;
  • 它也能夠幫我們處理空懸指標的問題;
  • 它還能夠幫我們處理比較隱晦的由異常造成的資源洩露。

自C++11起,C++標準提供兩大型別的智慧指標:

  • Class shared_ptr實現共享式擁有(shared ownership)概念。多個智慧指標可以指向相同物件,該物件和其相關資源會在“最後一個引用(reference)被銷燬”時候釋放。為了在結構複雜的情境中執行上述工作,標準庫提供了weak_ptr、bad_weak_ptr和enable_shared_from_this等輔助類;
  • Class unique_ptr實現獨佔式擁有(exclusive ownership)或嚴格擁有(strict ownership)概念,保證同一時間內只有一個智慧指標可以指向該物件。它對於避免資源洩露(resourece leak)——例如“以new建立物件後因為發生異常而忘記呼叫delete”——特別有用。

注:C++98中的Class auto_ptr在C++11中已不再建議使用。

幾乎每一個有分量的程式都需要“在相同時間的多處地點處理或使用物件”的能力。為此,我們必須在程式的多個地點指向(refer to)同一物件。但往往必須確保當“指向物件”的最末一個引用被刪除時該物件本身也被刪除,畢竟物件被刪除時解構函式可以要求某些操作,例如釋放記憶體或歸還資源等等。Class shared_ptr提供了“當物件再也不被使用時就被清理”共享式擁有語義。它使用計數機制來表明資源被幾個指標共享。也就是說,多個shared_ptr可以共享(或說擁有)同一物件。物件的最末一個擁有者有責任銷燬物件,並清理與該物件相關的所有資源(淺拷貝)。release()時,當前指標會釋放資源所有權,計數減一。

unique_ptr是C++標準庫自C++11起開始提供的型別。它是一種在異常發生時可幫助避免資源洩露的智慧指標。一般而言,這個智慧指標實現了獨佔式擁有概念,意味著它可確保一個物件和其相應資源同一時間只被一個指標擁有。一旦擁有者被銷燬或變成空,或開始擁有另一個物件,先前擁有的那個物件就會被銷燬,其任何相應資源也會被釋放。

weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指標的引用計數永遠不可能下降為0,資源永遠不會釋放。它是對物件的一種弱引用,不會增加物件的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過呼叫lock函式來獲得shared_ptr。

12、shared_ptr什麼時候修改引用計數?

shared_ptr修改引用指標的時機:

  • 建構函式中計數初始化為1;
  • 拷貝建構函式中計數值加1;
  • 賦值運算子中,左邊的物件引用計數減一,右邊的物件引用計數加一;
  • 解構函式中引用計數減一;
  • 在賦值運算子和解構函式中,如果減一後為0,則呼叫delete釋放物件。

13、explicit關鍵字

C++提供了關鍵字explicit,可以阻止不應該允許的經過轉換建構函式進行的隱式轉換的發生。宣告為explicit的建構函式不能在隱式轉換中使用。也就是說,explicit修飾的建構函式必須顯示使用。

14、C++四種類型轉換

C++四種類型轉換:static_cast、dynamic_cast、const_cast、reinterpret_cast。

為什麼不使用C的強制轉換?

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

static_cast:可以實現C++中內建基本資料型別之間的相互轉換。它主要有如下幾種用法:

用於類層次結構中基類和派生類之間指標或引用的轉換,進行上行轉換(把派生類的指標或引用轉換成基類表示)是安全的,進行下行轉換(把基類的指標或引用轉換為派生類表示),由於沒有動態型別檢查,所以是不安全的;

  • 用於基本資料型別之間的轉換,如把int轉換成char。這種轉換的安全也要開發人員來保證;
  • 把空指標轉換成目標型別的空指標;
  • 把任何型別的表示式轉換為void型別。

注意:static_cast不能轉換掉expression的const、volitale或者__unaligned屬性。

如果涉及到類的話,static_cast只能在有相互聯絡的型別中進行相互轉換,不一定包含虛擬函式。

const_cast:可以強制去掉const這種不能被修改的常數特性,但需要特別注意的是const_cast不是用於去除變數的常量性,而是去除指向常數物件的指標或引用的常量性,其去除常量性的物件必須為指標或引用。通常用於去除const型別返回值的常量性。

為什麼要設定為去除指向常數物件的指標或引用呢?

因為,如果可以將變數的常量性去除,那麼就和const的常量意思相悖了,這是很可怕的。而如果是指向該常量的指標,儘管該指標指向的是常量,但是去除它的常量性,也是可以理解的。但是,這樣就出現了可能指向同一塊記憶體地址的指標和這塊記憶體地址的常量值不同的情況(解釋可以看下面的參考文獻)。

reinterpret_cast:主要有三種強制轉換用途,改變指標或引用的型別、將指標或引用轉換為一個足夠長度的整形、將整型轉換為指標或引用型別。也就是說,它可以把一個指標轉換成一個整數,也可以把一個整數轉換成一個指標。在使用reinterpret_cast強制轉換過程僅僅只是位元位的拷貝,因此在使用過程中需要特別謹慎!

dynamic_cast:其他三種都是編譯時完成的,dynamic_cast是執行時處理的,執行時要進行型別檢查。

  • 不能用於內建的基本資料型別的強制轉換;
  • dynamic_cast轉換如果成功的話返回的是指向類的指標或引用,轉換失敗的話則會返回NULL;
  • 使用dynamic_cast進行轉換的,基類中一定要有虛擬函式,否則編譯不通過;
  • 在類的轉換時,在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的。在進行下行轉換時,dynamic_cast具有型別檢查的功能,比static_cast更安全。

向上轉換,即為子類指標指向父類指標(一般不會出問題);

向下轉換,即將父類指標轉化子類指標。向下轉換的成功與否還與將要轉換的型別有關,即要轉換的指標指向的物件的實際型別與轉換以後的物件型別一定要相同,否則轉換失敗。

為什麼dynamic_cast基類中必須要有虛擬函式?

dynamic_cast依賴於RTTI資訊。RTTI(Run Time Type Identification)即通過執行時型別識別,程式能夠使用基類的指標或引用來檢查著這些指標或引用所指的物件的實際派生型別。許多編譯器都是通過vtable找到物件的RTTI資訊的,這也就意味著,如果基類沒有虛方法,也就無法判斷一個基類指標變數所指物件的真實型別。

dynamic_cast 主要用於執行“安全的向下轉型(safe downcasting)”,要實現dynamic_cast,編譯器會在每個含有虛擬函式的類的虛擬函式表的前四個位元組存放一個指向_RTTICompleteObjectLocator結構的指標,當然還要額外空間存放_RTTICompleteObjectLocator及其相關結構的資料。

15、虛擬函式和純虛擬函式的區別:

虛擬函式和純虛擬函式的區別:

  • 虛擬函式和純虛擬函式可以定義在同一個類中,含有純虛擬函式的類被稱為抽象類,抽象類不能直接例項化,否則會出現抽象類不能例項化物件的錯誤,只有被繼承並且重寫後才能使用。而只含有虛擬函式的類不能被稱為抽象類;
  • 虛擬函式可以被直接使用,也可以被子類過載以後以多型的形式呼叫,而純虛擬函式必須在子類中實現該函式才可以使用,因為純虛擬函式在基類只有宣告而沒有定義;
  • 虛擬函式和純虛擬函式都可以在子類中被重寫,以多型的形式被呼叫;
  • 在虛擬函式和純虛擬函式的定義中不能有static識別符號,原因很簡單,被static修飾的函式在編譯時候要求前期靜態繫結,然而虛擬函式卻是動態繫結,而且被兩者修飾的函式生命週期也不一樣;
  • 虛擬函式必須實現,如果不實現,編譯器將報錯;
  • 對於虛擬函式來說,父類和子類都有各自的版本。由多型方式呼叫的時候動態繫結。實現了純虛擬函式的子類,該純虛擬函式在子類中就變成了虛擬函式,子類的子類即孫子類可以覆蓋;
  • 虛擬函式主要強調繼承。繼承了基類的介面,並且繼承了基類的一些定義實現,當然也可以自己去定義,增加類的多型性。而純函式主要強調的是介面的統一性和規範性,主要用於通訊協議中,具體的實現由子類完成。

16、過載、覆蓋與隱藏

過載、覆蓋與隱藏 的區別:

  • 過載(overload):函式名相同 、函式引數不同、 必須位於同一個域(類)中;
  • 覆蓋(override):函式名相同 、函式引數相同、 分別位於派生類和基類中、virtual(虛擬函式);
  • 隱藏(hide):函式名相同、 函式引數相同、 分別位於派生類和基類中、非virtual(即跟覆蓋的區別是基類中函式是否為虛擬函式);函式名相同、 函式引數不同、 分別位於派生類和基類中(即與過載的區別是兩個函式是否在同一個域(類)中)。

覆蓋達到的效果:

  • 在子類中重寫了父類的虛擬函式,那麼子類物件呼叫該重寫函式,呼叫到的是子類內部重寫的虛擬函式,而並不是從父類繼承下來的虛擬函式;
  • 在子類中重寫了父類的虛擬函式,如果用一個父類的指標(或引用)指向(或引用)子類物件,那麼這個父類的指標或用引用呼叫該重寫的虛擬函式,呼叫的是子類的虛擬函式;相反,如果用一個父類的指標(或引用)指向(或引用)父類的物件,那麼這個父類的指標或用引用呼叫該重寫的虛擬函式,呼叫的是父類的虛擬函式。

隱藏,即:派生類中函式隱藏(遮蔽)了基類中的同名函式。

關於隱藏的理解,在呼叫一個類的成員函式時,編譯器會沿著類的繼承鏈逐級向上查詢函式的定義,如果找到了則停止查詢;所以如果一個派生類和一個基類都有一個同名函式(不論函式引數是否相同),而編譯器最終選擇了在派生類中的函式,那麼就說這個派生類的成員函式“隱藏”了基類的同名函式,即它阻止了編譯器繼續向上查詢函式的定義。

17、define和const

define和const的區別:

  • const 定義的常數是變數,也帶型別, #define 定義的只是個常數,不帶型別;
  • define是在編譯的預處理階段起作用,而const是在 編譯、執行的時候起作用;
  • define只是簡單的字串替換,沒有型別檢查。而const有對應的資料型別,是要進行判斷的,可以避免一些低階的錯誤;
  • const變數存放在記憶體的靜態區域中,在程式執行過程中const變數只有一個拷貝,而#define 所定義的巨集變數卻有多個拷貝,所消耗的記憶體要比const變數的大得多;
  • 用define可以定義一些簡單的函式,const是不可以定義函式的;
  • define可以用來防止標頭檔案重複引用,而const不能;
  • const不足的地方,是與生俱來的,const不能重定義,而#define可以通過#undef取消某個符號的定義,再重新定義;
  • 在編譯時, 編譯器通常不為const常量分配儲存空間,而是將它們儲存在符號表中,這使得它成為一個編譯期間的常量,沒有了儲存與讀記憶體的操作,使得它的效率也很高。 

18、記憶體對齊

記憶體對齊的原則:

  • 從0位置開始儲存;
  • 變數儲存的起始位置是該變數大小的整數倍;
  • 結構體總的大小是其最大元素的整數倍,不足的後面要補齊;
  • 結構體中包含結構體,從結構體中最大元素的整數倍開始存;
  • 如果加入pragma pack(n) ,取n和變數自身大小較小的一個。

19、行內函數

行內函數有什麼優點?

Inline這個名稱就可以反映出它的工作方式,函式會在它所呼叫的位置上展開。這麼做可以消除函式呼叫和返回所帶來的開銷(暫存器儲存和恢復),而且,由於編譯器會把呼叫函式的程式碼和函式本身放在一起優化,所以也有進一步優化程式碼的可能。不過這麼做是有代價的,程式碼會變長,這就意味著佔用更多的記憶體空間或者佔用更多的指令快取。

核心開發者通常把那些對時間要求比較高,而本身長度又比較短的函式定義成行內函數。如果你把一個大塊頭的程式做成了行內函數,卻不需要爭分奪秒,反而反覆呼叫它,這麼做就失去了內聯的意義了。

行內函數與巨集定義的區別?

  • 巨集不是函式,inline函式是函式;
  • 巨集定義在預編譯的時候就會進行巨集替換;
  • 行內函數在編譯階段,由編譯器決定是否內聯,在呼叫行內函數的地方進行替換,減少了函式的呼叫過程,但是使得編譯檔案變大。因此,行內函數適合簡單函式,對於複雜函式,即使定義了內聯編譯器可能也不會按照內聯的方式進行編譯;
  • 行內函數相比巨集定義更安全,行內函數可以檢查引數,而巨集定義只是簡單的文字替換。因此推薦使用行內函數,而不是巨集定義;
  • 巨集定義小心處理巨集引數(一般引數要括號起來),否則易出現二義性,而內聯定義不會出現。

20、C++記憶體管理

在C++中,記憶體分成5個區,他們分別是堆、棧、自由儲存區、全域性/靜態儲存區和常量儲存區。

堆:堆是作業系統中的術語,是作業系統所維護的一塊特殊記憶體,用於程式的記憶體動態分配,C語言使用malloc從堆上分配記憶體,使用free釋放已分配的對應記憶體;

棧:在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限;

自由儲存區:自由儲存區是C++基於new操作符的一個抽象概念,凡是通過new操作符進行記憶體申請,該記憶體即為自由儲存區;

全域性/靜態儲存區:這塊記憶體是在程式編譯的時候就已經分配好的,在程式整個執行期間都存在。例如全域性變數,靜態變數;

常量儲存區:這是一塊比較特殊的儲存區,他們裡面存放的是常量(const),不允許修改。

堆和棧的區別:

  • 管理方式:對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程式設計師控制,容易產生memory leak;
  • 空間大小:一般來講在32位系統下,堆記憶體可以達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,預設的棧空間大小是1M;
  • 碎片問題:對於堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個問題,因為棧是先進後出的佇列,它們是如此的一一對應;
  • 生長方向:對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長;
  • 分配效率:棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函式庫提供的,它的機制是很複雜的,例如為了分配一塊記憶體,庫函式會按照一定的演算法在堆記憶體中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就有機會分到足夠大小的記憶體,然後進行返回。