那些不能遺忘的知識點回顧——C/C++系列(筆試面試高頻題)
有那麼一些零碎的小知識點,偶爾很迷惑,偶爾被忽略,偶然卻發現它們很重要,這段時間正好在溫習這些,就整理在這裡,一起學習一起提高!後面還會繼續補充。
——前言
1.面向物件的特性
封裝、繼承、多型。
封裝:把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。(優點:可以隱藏實現細節,使得程式碼模組化)
繼承:可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴充套件。(優點:可以擴充套件已存在的程式碼模組(類))
多型:一個類例項的相同方法在不同情形有不同表現形式。多型機制使具有不同內部結構的物件可以共享相同的外部介面。雖然針對不同物件的具體操作不同,但通過一個公共的類,這些操作可以通過相同的方式被呼叫。
多型實現的兩種方式:父類指標指向子類物件 或 將一個基類的引用型別賦值為它的派生類例項。(重要:虛擬函式 + 指標或引用)
建構函式、複製建構函式、解構函式、賦值運算子不能被繼承。
2.堆和棧
從記憶體角度來說:棧區(stack)由編譯器自動分配釋放,存放函式的引數值,區域性變變數的值等,其操作方式類似於資料結構中的棧,可靜態亦可動態分配。
堆區(heap)一般由程式設計師分配釋放,若程式設計師不釋放,可能造成記憶體洩漏,程式結束時可能由OS回收。只可動態分配,分配方式類似於連結串列。
從資料結構角度來說:堆可以被看成是一棵樹,如:堆排序。
而棧是一種先進後出的資料結構。
3.malloc和new
1.malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子。但它們都可用於申請動態記憶體和釋放記憶體。
2.對於非內部資料型別的物件而言,用maloc/free無法滿足動態物件的要求。物件在建立的同時要自動執行建構函式,物件在消亡之前要自動執行解構函式。由malloc/free是庫函式而不是運算子,不在編譯器控制權限之內,不能夠把執行建構函式和解構函式的任務強加於malloc/free,因此C++語言需要一個能完成動態記憶體分配和初始化工作的運算子new,和一個能完成清理與釋放記憶體工作的運算子delete。
3.new可以認為是malloc加建構函式的執行。new出來的指標是直接帶型別資訊的。而malloc返回的都是void*指標。new delete在實現上其實呼叫了malloc,free函式。
4.new 建立的是一個物件;malloc分配的是一塊記憶體。
4.虛擬函式實現機制,虛繼承在sizeof中有沒有影響,建構函式能否為虛擬函式,與純虛擬函式
虛擬函式表:類的虛擬函式表是一塊連續的記憶體,每個記憶體單元中記錄一個JMP指令的地址。
編譯器會為每個有虛擬函式的類建立一個虛擬函式表,該虛擬函式表將被該類的所有物件共享。類的每個虛擬函式佔據虛擬函式表中的一塊。如果類中有N個虛擬函式,那麼其虛擬函式表將有N*4位元組的大小。
在有虛擬函式的類的例項中分配了指向這個表的指標的記憶體,所以,當用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得尤為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。
編譯器應該是保證虛擬函式表的指標存在於物件例項中最前面的位置(這是為了保證取到虛擬函式表的有最高的效能——如果有多層繼承或是多重繼承的情況下)。 這意味著可以通過物件例項的地址得到這張虛擬函式表,然後就可以遍歷其中函式指標,並呼叫相應的函式。
->有虛擬函式或虛繼承的類例項化後的物件大小至少為4位元組(確切的說是一個指標的位元組數;說至少是因為還要加上其他非靜態資料成員,還要考慮對齊問題);沒有虛擬函式和虛繼承的類例項化後的物件大小至少為1位元組(沒有非靜態資料成員的情況下也要有1個位元組來記錄它的地址)。
有純虛擬函式的類為抽象類,不能定義抽象類的物件,它的子類要麼實現它所有的純虛擬函式變為一個普通類,要麼還是一個抽象類。
特別的:
(1)當存在類繼承並且解構函式中有必須要進行的操作時(如需要釋放某些資源,或執行特定的函式)解構函式需要是虛擬函式,否則若使用父類指標指向子類物件,在delete時只會呼叫父類的解構函式,而不能呼叫子類的解構函式,從而造成記憶體洩露或達不到預期結果;
(2)行內函數不能為虛擬函式:行內函數需要在編譯階段展開,而虛擬函式是執行時動態繫結的,編譯時無法展開;
(3)建構函式不能為虛擬函式:建構函式在進行呼叫時還不存在父類和子類的概念,父類只會呼叫父類的建構函式,子類呼叫子類的,因此不存在動態繫結的概念;但是建構函式中可以呼叫虛擬函式,不過並沒有動態效果,只會呼叫本類中的對應函式;
(4)靜態成員函式不能為虛擬函式:靜態成員函式是以類為單位的函式,與具體物件無關,虛擬函式是與物件動態繫結的。
5.面向物件的多型、多型的實現機制,多型的例子
見知識點4
6.對一個類求sizeof需要考慮的內容
見知識點4。同時,對於一個結構體和一個類執行sizeof()運算時情況比較複雜,詳細分析請移步另一篇博文struct/class等記憶體位元組對齊問題詳解
7.過載和重寫(覆蓋)
方法的重寫Overriding和過載Overloading是多型性的不同表現。
重寫Overriding是父類與子類之間多型性的一種表現,過載Overloading是一個類中多型性的一種表現。
如果在子類中定義某方法與其父類有相同的名稱和引數,我們說該方法被重寫 (Overriding)。子類的物件使用這個方法時,將呼叫子類中的定義,對它而言,父類中的定義如同被“遮蔽”了,而且如果子類的方法名和引數型別和個數都和父類相同,那麼子類的返回值型別必須和父類的相同;如果在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別,則稱為方法的過載(Overloading)。Overloading的方法是可以改變返回值的型別。也就是說,過載的返回值型別可以相同也可以不同。
8.“引用”與多型的關係?
引用是除指標外另一個實現多型的方式。這意味著,一個基類的引用可以指向它的派生類例項。例:
Class A; Class B : Class A{…};
B b; A& ref = b;
9.計算機載入程式包括哪幾個區?
一個由C/C++編譯的程式佔用的記憶體分為以下幾個部分:
(1)棧區(stack):—由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。可靜態也可動態分配。其操作方式類似於資料結構中的棧。
(2)堆區(heap):一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由OS回收。動態分配。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。
(3)全域性區(靜態區):—程式結束後由系統釋放,全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域;未初始化的全域性變數和靜態變數在相鄰的另一塊區域(BSS,Block Started by Symbol),在程式執行之前BSS段會自動清0。
(4)文字常量區:—程式結束後由系統釋放,常量字串就是放在這裡的。
(5)程式程式碼區:—存放函式體的二進位制程式碼。
10.派生類中建構函式與解構函式,呼叫順序
建構函式的呼叫順序總是如下:
1.基類建構函式。如果有多個基類,則建構函式的呼叫順序是某類在類派生表中出現的順序,而不是它們在成員初始化表中的順序。
2.成員類物件建構函式。如果有多個成員類物件則建構函式的呼叫順序是物件在類中被宣告的順序,而不是它們出現在成員初始化表中的順序。如果有的成員不是類物件,而是基本型別,則初始化順序按照宣告的順序來確定,而不是在初始化列表中的順序。
3.派生類建構函式。
解構函式正好和建構函式相反。
11.extern “C”的作用
extern “C”實現C++與C及其它語言的混合程式設計,是用在C和C++之間的橋樑。之所以需要這個橋樑是因為C編譯器編譯函式時不帶函式的型別資訊,只包含函式符號名字;而C++編譯器為了實現函式過載,編譯時會帶上函式的型別資訊,如他把上面的a函式可能編譯成_a_float這樣的符號為了實現過載。
extern “C”的慣用法:
在C++中引用C語言中的函式和變數,在包含C語言標頭檔案(假設為cExample.h)時,需進行下列處理:
extern “C”{
#include “cExample.h”
}
而在C語言的標頭檔案中,對其外部函式只能指定為extern型別,C語言中不支援extern “C”宣告,在.c檔案中包含了extern “C”時會出現編譯語法錯誤。
extern本身作為關鍵字修飾變數(函式)時宣告該變數(函式)是外部變數(函式),通常全域性變數在標頭檔案中用這種方式宣告,在對應原始檔中定義,來防止重定義的錯誤。
12.解構函式、建構函式能不能被繼承
見知識點1
13.C++為什麼用模板類,為什麼用泛型
通過泛型可以定義型別安全的資料結構(型別安全),而無須使用實際的資料型別(可擴充套件)。這能夠顯著提高效能並得到更高質量的程式碼(高效能),因為您可以重用資料處理演算法,而無須複製型別特定的程式碼(可重用)。
14.結構體記憶體對齊,與什麼有關(CPU)
在系統預設的對齊方式下:每個成員相對於這個結構體變數地址的偏移量正好是該成員型別所佔位元組的整數倍,且最終佔用位元組數為成員型別中最大佔用位元組數的整數倍。
為什麼要對齊?當CPU訪問正確對齊的資料時,它的執行效率最高,當資料大小的資料模數的記憶體地址是0時,資料是對齊的。例如:WORD值應該是總是從被2除盡的地址開始,而DWORD值應該總是從被4除盡的地址開始,資料對齊不是記憶體結構的一部分,而是CPU結構的一部分。當CPU試圖讀取的數值沒有正確的對齊時,CPU可以執行兩種操作之一:產生一個異常條件;執行多次對齊的記憶體訪問,以便讀取完整的未對齊資料,若多次執行記憶體訪問,應用程式的執行速度就會慢。
15.指標和引用
1.指標是一個變數,只不過這個變數儲存的是一個地址,指向記憶體的一個儲存單元;而引用跟原來的變數實質上是同一個東西,只不過是原變數的一個別名而已。
2.指標可以有多級,但是引用只能是一級;
3.指標的值可以為空,也可能指向一個不確定的記憶體空間,但是引用的值不能為空,並且引用在定義的時候必須初始化為特定物件;(因此引用更安全)
4.指標的值在初始化後可以改變,即指向其它的儲存單元,而引用在進行初始化後就不會再改變引用物件了;
5.sizeof引用得到的是所指向的變數(物件)的大小,而sizeof指標得到的是指標本身的大小;
6.指標和引用的自增(++)運算意義不一樣;
16.static關鍵字作用
在C語言中,關鍵字static有三個明顯的作用:
1)在函式體內,一個被宣告為靜態的變數在這一函式被呼叫過程中維持上一次的值不變,即只初始化一次(該變數存放在靜態變數區,而不是棧區)。
2)在模組內(但在函式體外),一個被宣告為靜態的變數可以被模組內所用函式訪問,但不能被模組外訪問。(注:模組可以理解為檔案)
3)在模組內,一個被宣告為靜態的函式只可被這一模組內的其它函式呼叫。那就是,這個函式被限制在宣告它的模組的本地範圍內使用。
【補充】《C和指標》中說static有兩層含義:指明儲存屬性;改變連結屬性。
具體解釋:(1)全域性變數(包括函式)加上static關鍵字後,連結屬性變為internal,也就是將他們限定在了本作用域內;(2)區域性變數加上static關鍵字後,儲存屬性變為靜態儲存,不儲存在棧區,下一次將保持上一次的尾值。
除此之外,C++中還有新用法:
4)在類中的static成員變數意味著它為該類的所有例項所共享,也就是說當某個類的例項修改了該靜態成員變數,其修改值為該類的其它所有例項所見;
5)在類中的static成員函式屬於整個類所擁有,這個函式不接收this指標,因而只能訪問類的static成員變數(當然,可以通過傳遞一個物件來訪問其成員)。
17.虛表,基類的虛表是什麼樣的,派生類虛表
(1)單繼承情況
(2)多重繼承(無虛擬函式覆蓋)
(3)多重繼承(有虛擬函式覆蓋)
18.volatile
volatile關鍵字是一種型別修飾符,用它宣告的型別變量表示可以被某些編譯器未知的因素更改,比如:作業系統、硬體或者其它執行緒等。遇到這個關鍵字宣告的變數,編譯器對訪問該變數的程式碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。
當要求使用volatile 宣告的變數的值的時候,系統總是重新從它所在的記憶體讀取資料,即使它前面的指令剛剛從該處讀取過資料。而且讀取的資料立刻被儲存。
volatile 指出 i是隨時可能發生變化的,每次使用它的時候必須從i的地址中讀取,因而編譯器生成的彙編程式碼會重新從i的地址讀取資料放在b中。而優化做法是,由於編譯器發現兩次從i讀資料的程式碼之間的程式碼沒有對i進行過操作,它會自動把上次讀的資料放在b中。而不是重新從i裡面讀。這樣一來,如果i是一個暫存器變數或者表示一個埠資料就容易出錯,所以說volatile可以保證對特殊地址的穩定訪問。
19.#define與const的區別
• define不會做型別檢查(容易出錯),const擁有型別,會執行相應的型別檢查
• define僅僅是巨集替換,不佔用記憶體,而const會佔用記憶體
• const記憶體效率更高,編譯器可能將const變數儲存在符號表中,而不會分配儲存空間,這使得它成 為一個編譯期間的常量,沒有儲存和讀取的操作
當使用#define定義一個簡單的函式時,強烈建議使用行內函數替換!
20.C++中的強制型別轉換
• reinterpret_cast: 轉換一個指標為其它型別的指標。它也允許從一個指標轉換為整數型別,反之亦 然. 這個操作符能夠在非相關的型別之間轉換. 操作結果只是簡單的從一個指標到別的指標的值的 二進位制拷貝. 在型別之間指向的內容不做任何型別的檢查和轉換?
class A{};
class B{};
A* a = new A;
B* b = reinterpret_cast(a);
• static_cast: 允許執行任意的隱式轉換和相反轉換動作(即使它是不允許隱式的),例如:應用到類 的指標上, 意思是說它允許子類型別的指標轉換為父類型別的指標(這是一個有效的隱式轉換), 同 時, 也能夠執行相反動作: 轉換父類為它的子類
class Base {};
class Derive:public Base{};
Base* a = new Base;
Derive *b = static_cast(a);
• dynamic_cast: 只用於物件的指標和引用. 當用於多型型別時,它允許任意的隱式型別轉換以及相 反過程. 不過,與static_cast不同,在後一種情況裡(注:即隱式轉換的相反過程),dynamic_cast 會檢查操作是否有效. 也就是說, 它會檢查轉換是否會返回一個被請求的有效的完整物件。檢測在 執行時進行. 如果被轉換的指標不是一個被請求的有效完整的物件指標,返回值為NULL. 對於引用 型別,會丟擲bad_cast異常。
• const_cast: 這個轉換型別操縱傳遞物件的const屬性,或者是設定或者是移除,例如:
class C{};
const C* a = new C;
C *b = const_cast(a);
21.解構函式中丟擲異常時概括性總結
(1) C++中解構函式的執行不應該丟擲異常;
(2) 假如解構函式中丟擲了異常,那麼系統將變得非常危險,也許很長時間什麼錯誤也不會發生;但也許系統有時就會莫名奇妙地崩潰而退出了,而且什麼跡象也沒有;
(3) 當在某一個解構函式中會有一些可能(哪怕是一點點可能)發生異常時,那麼就必須要把這種可能發生的異常完全封裝在解構函式內部,決不能讓它丟擲函式之外,即在解構函式內部寫出完整的throw…catch()塊。
22.C++11新特性
Lambda、變參模板、auto、decltype、constexpr、智慧指標、列表初始化、正則表示式、執行緒庫、靜態斷言、委託構造。
weak_ptr被設計為與shared_ptr共同工作,可以從一個shared_ptr或者另一個weak_ptr物件構造,獲得資源的觀測權。但weak_ptr沒有共享資源,它的構造不會引起指標引用計數的增加。
使用weak_ptr的成員函式use_count()可以觀測資源的引用計數,另一個成員函式expired()的功能等價於use_count()==0,但更快,表示被觀測的資源(也就是shared_ptr的管理的資源)已經不復存在。
weak_ptr可以使用一個非常重要的成員函式lock()從被觀測的shared_ptr獲得一個可用的shared_ptr物件, 從而操作資源。但當expired()==true的時候,lock()函式將返回一個儲存空指標的shared_ptr.