目錄
Chapter 1 | Chapter 2 | Chapter 3 | Chapter 4 |
---|---|---|---|
編程基礎 | C++11 | 面向對象基礎 | 標準模板庫 |
內容
編程基礎
- 變量聲明和定義區別?
- 聲明僅僅是把變量的聲明的位置及類型提供給編譯器,並不分配內存空間;定義要在定義的地方爲其分配存儲空間。
- 相同變量可以再多處聲明(外部變量extern),但只能在一處定義。
- extern關鍵字
- extern可以置於變量或者函數前,以標示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。
- 當它與”C”一起連用時,如: extern “C” void fun(int a, int b);主要是解決在C++代碼中調用C代碼。告訴編譯器,被extern “C”修飾的變量和函數,按照C語言方式編譯和鏈接。
- C++ 中函數 void print(int i),編譯爲 _print_int。C中直接編譯爲_print
- 函數傳參有幾種方式?傳引用和指針有什麼區別呢?
- 傳值:形參是實參的拷貝,改變形參的值並不會影響外部實參的值。從被調用函數的角度來說,值傳遞是單向的(實參->形參),參數的值只能傳入,不能傳出。
- 傳指針:形參爲指向實參地址的指針,當對形參的指向操作時,就相當於對實參本身進行的操作
- 傳引用:形參相當於是實參的“別名”,對形參的操作其實就是對實參的操作,在引用傳遞過程中,被調函數的形式參數雖然也作爲局部變量在棧中開闢了內存空間,但是這時存放的是由主調函數放進來的實參變量的地址。被調函數對形參的任何操作都被處理成間接尋址,即通過棧中存放的地址訪問主調函數中的實參變量。
- 傳指針和傳引用區別:主要是指針和引用的區別。首先語法就不一樣,指針要取值需要能*ptr,引用可以直接取; 另外傳進來的指針可以指向其他位置,但是引用只能綁定在傳進來的固定的值上。
- “零值比較”?
- bool類型:if(flag)
- int類型:if(flag == 0)
- 指針類型:if(flag == nullptr)
- float類型:if((flag >= -0.000001) && (flag <= 0. 000001))
- strlen和sizeof區別?
- sizeof是運算符,並不是函數,結果在編譯時得到而非運行中獲得;strlen是字符處理的庫函數。
- sizeof參數可以是任何數據的類型或者數據(sizeof參數不退化);strlen的參數只能是字符指針且結尾是’\0’的字符串。
- 因爲sizeof值在編譯時確定,所以不能用來得到動態分配(運行時分配)存儲空間的大小。
- 同一類的不同對象可以互相賦值嗎?
- 可以,但含有指針成員時需要注意。
- 對比類的對象賦值時深拷貝和淺拷貝。
- 爲什麼要對齊?結構體內存對齊問題?
- 如果不按照平臺要求對數據存放進行對齊,會帶來存取效率上的損失。比如32位的Intel處理器通過總線訪問(包括讀和寫)內存數據。每個總線週期從偶地址開始訪問32位內存數據,內存數據以字節爲單位存放。如果一個32位的數據沒有存放在4字節整除的內存地址處,那麼處理器就需要2個總線週期對其進行訪問,顯然訪問效率下降很多。
- 結構體內成員按照聲明順序存儲,第一個成員地址和整個結構體地址相同。
- 結構體變量中成員的偏移量必須是成員大小的整數倍(0被認爲是任何數的整數倍)。
- 結構體大小必須是所有成員大小的整數倍。
- 如果不按照平臺要求對數據存放進行對齊,會帶來存取效率上的損失。比如32位的Intel處理器通過總線訪問(包括讀和寫)內存數據。每個總線週期從偶地址開始訪問32位內存數據,內存數據以字節爲單位存放。如果一個32位的數據沒有存放在4字節整除的內存地址處,那麼處理器就需要2個總線週期對其進行訪問,顯然訪問效率下降很多。
- static作用是什麼?在C和C++中有何區別?
- static可以修飾局部變量(靜態局部變量)、全局變量(靜態全局變量)和函數,被修飾的變量存儲位置在靜態區。對於靜態局部變量,相對於一般局部變量其生命週期延長,直到程序運行結束而非函數調用結束,且只在第一次被調用時定義;對於靜態全局變量,相對於全局變量其可見範圍被縮小,只能在本文件中可見;修飾函數時作用和修飾全局變量相同,都是爲了限定訪問域。
- C++的static除了上述兩種用途,還可以修飾類成員(靜態成員變量和靜態成員函數),靜態成員變量和靜態成員函數不屬於任何一個對象,是所有類實例所共有。
- static的數據記憶性可以滿足函數在不同調用期的通信,也可以滿足同一個類的多個實例間的通信。
- 未初始化時,static變量默認值爲0。
- const的作用
- 修飾普通變量: 該變量值不可更改。頂層/底層const
- 修飾函數參數: 函數形參聲明加const保護某些值在操作過程中不會改變
- 修飾返回值:表明返回的數據是不可修改的
- 修飾成員函數: 類的成員函數加上const限定可以聲明此函數不會更改類對象的內容
結構體和類的區別?
結構體的默認限定符是public;類是private。
結構體不可以繼承,類可以。C++中結構體也可以繼承。
malloc和new的區別?
- malloc和free是標準庫函數,支持覆蓋;new和delete是運算符,並且支持重載。
- malloc僅僅分配內存空間,free僅僅回收空間,不具備調用構造函數和析構函數功能,用malloc分配空間存儲類的對象存在風險;new和delete除了分配回收功能外,還會調用構造函數和析構函數。
- 分配成功時:malloc返回的是void類型指針(必須進行類型轉換),new返回的是具體類型指針。
- 分配失敗時:malloc返回NULL,new默認拋出異常。
指針和引用區別?
- 引用只是別名,不佔用具體存儲空間,只有聲明沒有定義;指針是具體變量,需要佔用存儲空間。
- 引用在聲明時必須初始化爲另一變量,一旦出現必須爲typename refname = &varname形式;指針聲明和定義可以分開,可以先只聲明指針變量而不初始化,等用到時再指向具體變量。
- 引用一旦初始化之後就不可以再改變(變量可以被引用爲多次,但引用只能作爲一個變量引用);指針變量可以重新指向別的變量。
- 不存在指向空值的引用,必須有具體實體;但是存在指向空值的指針。
宏定義和函數有何區別?
- 宏在編譯時完成替換,之後被替換的文本參與編譯,相當於直接插入了代碼,運行時不存在函數調用,執行起來更快;函數調用在運行時需要跳轉到具體調用函數。
- 宏定義沒有返回值;函數調用具有返回值。
- 宏定義參數沒有類型,不進行類型檢查;函數參數具有類型,需要檢查類型。
宏定義和內聯函數(inline)區別?
- 在使用時,宏只做簡單字符串替換(編譯前)。而內聯函數可以進行參數類型檢查(編譯時),且具有返回值。
- 內聯函數本身是函數,強調函數特性,具有重載等功能。
- 內聯函數可以作爲某個類的成員函數,這樣可以使用類的保護成員和私有成員。而當一個表達式涉及到類保護成員或私有成員時,宏就不能實現了。
宏定義和const全局變量的區別?
- 宏替換髮生在預編譯階段,屬於文本插入替換;const作用發生於編譯過程中。
- 宏不檢查類型;const會檢查數據類型。
- 宏定義的數據沒有分配內存空間,只是插入替換掉;const變量分配內存空間。
- 宏不是語句,不在最後加分號;
宏定義和typedef區別?
- 宏主要用於定義常量及書寫複雜的內容;typedef主要用於定義類型別名。
- 宏替換髮生在編譯階段之前,屬於文本插入替換;typedef是編譯的一部分。
- 宏不檢查類型;typedef會檢查數據類型。
- 宏不是語句,不在最後加分號;typedef是語句,要加分號標識結束。
- 注意對指針的操作,
typedef (char*) p_char
和#define p_char char*
在使用起來區別巨大。
條件編譯#ifdef, #else, #endif作用?
- 可以通過加#define,並通過#ifdef來判斷,將某些具體模塊包括進要編譯的內容。
- 用於子程序前加#define DEBUG用於程序調試。
- 應對硬件的設置(機器類型等)。
- 條件編譯功能if也可實現,但條件編譯可以減少被編譯語句,從而減少目標程序大小。
區別以下幾種變量?
const int a; int const a; const int *a; int *const a;
- int const a和const int a均表示定義常量類型a。
const int *a
,其中a爲指向int型變量的指針,const在 * 左側,表示a指向不可變常量。(看成const (*a)
,對a指向的對象加const,底層const)- int *const a,依舊是指針類型,表示a爲指向整型數據的常指針。(看成const(a),對指針const,頂層const)
volatile有什麼作用?
- volatile定義變量的值是易變的,每次用到這個變量的值的時候都要去重新讀取這個變量的值,而不是讀寄存器內的備份。
- 多線程中被幾個任務共享的變量需要定義爲volatile類型。
什麼是常引用?
- 常引用可以理解爲常量指針,形式爲const typename & refname = varname。
- 常引用下,原變量值不會被別名所修改。
- 原變量的值可以通過原名修改。
- 常引用通常用作只讀變量別名或是形參傳遞。
區別以下指針類型?
int *p[10] int (*p)[10] int *p(int) int (*p)(int)
- int *p[10]表示指針數組,強調數組概念,是一個數組變量,數組大小爲10,數組內每個元素都是指向int類型的指針變量。
- int (*p)[10]表示數組指針,強調是指針,只有一個變量,是指針類型,不過指向的是一個int類型的數組,這個數組大小是10。
- int p(int)是函數聲明,函數名是p,參數是int類型的,返回值是int 類型的。
- int (*p)(int)是函數指針,強調是指針,該指針指向的函數具有int類型參數,並且返回值是int類型的。
常量指針和指針常量區別?
- 常量指針是一個指針,讀成常量的指針,指向一個只讀變量。如int const *p或const int *p。
- 指針常量是一個不能給改變指向的指針。如int *const p。
a和&a有什麼區別?
假設數組int a[10]; int (*p)[10] = &a;
- a是數組名,是數組首元素地址,+1表示地址值加上一個int類型的大小,如果a的值是0x00000001,加1操作後變爲0x00000005。*(a + 1) = a[1]。
- &a是數組的指針,其類型爲int (*)[10](就是前面提到的數組指針),其加1時,系統會認爲是數組首地址加上整個數組的偏移(10個int型變量),值爲數組a尾元素後一個元素的地址。
- 若(int )p ,此時輸出 *p時,其值爲a[0]的值,因爲被轉爲int 類型,解引用時按照int類型大小來讀取。
數組名和指針(這裏爲指向數組首元素的指針)區別?
- 二者均可通過增減偏移量來訪問數組中的元素。
- 數組名不是真正意義上的指針,可以理解爲常指針,所以數組名沒有自增、自減等操作。
- 當數組名當做形參傳遞給調用函數後,就失去了原有特性,退化成一般指針,多了自增、自減操作,但sizeof運算符不能再得到原數組的大小了。
野指針是什麼?
- 也叫空懸指針,不是指向null的指針,是指向垃圾內存的指針。
- 產生原因及解決辦法:
- 指針變量未及時初始化 => 定義指針變量及時初始化,要麼置空。
- 指針free或delete之後沒有及時置空 => 釋放操作後立即置空。
堆和棧的區別?
- 申請方式不同。
- 棧由系統自動分配。
- 堆由程序員手動分配。
- 申請大小限制不同。
- 棧頂和棧底是之前預設好的,大小固定,可以通過ulimit -a查看,由ulimit -s修改。
- 堆向高地址擴展,是不連續的內存區域,大小可以靈活調整。
- 申請效率不同。
- 棧由系統分配,速度快,不會有碎片。
- 堆由程序員分配,速度慢,且會有碎片。
- 申請方式不同。
delete和delete[]區別?
- delete只會調用一次析構函數。
- delete[]會調用數組中每個元素的析構函數。
char *p = new char[32];
- 如果用delete p,會發生什麼?
- 因爲char是基本數據類型沒有析構函數,而系統也記憶了內存大小。所以用delete不會造成內存泄露。但是這種方法在C++標準裏是未定義行爲,要避免使用。
- 如果用malloc和free怎麼申請和釋放32個字節的buffer。
- char ptr = (char)malloc(32);free(ptr);ptr = nullptr;
- 如果用free(p) 可以嗎?
- 這是未定義行爲。但是對於基本數據類型是可以使用的。
- 爲什麼delete []就可以刪除整個數組。
- 因爲系統分配額外空間記錄數組大小。具體來說是在p指針附近(下個位置)存儲了指針指向物體的大小的信息
- 如果用delete p,會發生什麼?
模板的優點和缺點
- 優點:
- 代碼複用。
- 模板類更加的安全,因其參數類型在編譯時都是已知的。
- 缺點:
- 模板必須在頭文件中,這樣一旦有所變更需要重編譯所有相關工程;同時也沒有信息隱藏。
- 一些編譯器對template支持不好。
- 模板對每種類型生成額外的代碼,可能導致代碼膨脹。
- 優點:
模板的特化與偏特化
- 全特化就是限定死模板實現的具體類型,偏特化就是如果這個模板有多個類型,那麼只限定其中的一部分。
c++中內存分配
-虛函數表儲存在全局常量區
- ps:new分配的自由存儲區,這是一個C++抽象的概念,不是具體虛擬內存的位置。
C++11 新特性
瞭解C++11的哪些新特性?
- 易用性:nullptr / auto / 範圍for循環 / 初始化列表 / override和final / lambda表達式
- 右值引用和移動語義
- 智能指針
- 標準庫擴充,新增array/forward_list/兩個unordered/tuple新容器,語言級線程支持,thread/mutex/unique_lock等
C++四種類型轉換:static_cast, dynamic_cast, const_cast, reinterpret_cast
- const_cast用於將const變量轉爲非const
- static_cast用的最多,對於各種隱式轉換,非const轉const,void*轉指針等, static_cast能用於多態向上轉化,如果向下轉能成功但是不安全,結果未知;
- dynamic_cast用於動態類型轉換。只能用於含有虛函數的類,用於類層次間的向上和向下轉化。只能轉指針或引用。向下轉化時,如果是非法的對於指針返回NULL,對於引用拋異常。要深入瞭解內部轉換的原理。
- reinterpret_cast,將數據的二進制形式重新解釋,但是不改變其值。幾乎什麼都可以轉,比如將int轉指針,可能會出問題,儘量少用;
dynamic_cast如何實現
- 編譯器會在每個含有虛函數的類的虛函數表的前四個字節存放一個指向_RTTICompleteObjectLocator結構的指針,當然還要額外空間存放_RTTICompleteObjectLocator及其相關結構的數據。裏面存放了vptr相對this指針的偏移,構造函數偏移(針對虛擬繼承),type_info指針,以及類層次結構中其它類的相關信息。如果是多重繼承,這些信息更加複雜。
四種智能指針
- shared_ptr, 共享所有權,允許多個指針指向同一個對象,其內部有一個關聯的引用計數,用來記錄有多少個其他的shared_ptr指向相同的對象,當引用計數爲0時將調用析構函數釋放對應空間。
- unique_ptr遵循獨佔語義,在任何時間點,資源只能唯一地被一個unique_ptr佔有,當其離開作用域,所包含的資源被釋放。
- weak_ptr解決shared_ptr循環引用時的bug。
- auto_ptr常常會導致野指針,不能指向一組對象,不支持標準容器。所以在C++11被廢棄了。
函數對象
- 如果類重載了函數調用運算符,則我們可以像使用函數一樣使用該類的對象。因爲這樣的類同時也能儲存狀態,所以與普通函數相比,它們更加靈活。
cpp int operator() (int val) const
- 如果類重載了函數調用運算符,則我們可以像使用函數一樣使用該類的對象。因爲這樣的類同時也能儲存狀態,所以與普通函數相比,它們更加靈活。
lambda
- lambda表達式是匿名函數,可以認爲是一個可執行體functor,語法規則如下:
\\[捕獲區](參數區){代碼區}; auto add = [](int a, int b) {return a + b};
- 捕獲的意思即爲獲得一些局部變量,使其爲lambda內部可見,具體方式有如下幾種:
- [] 不捕獲,大部分情況下不捕獲就可以了;
- [a,&b] 其中 a 以複製捕獲而 b 以引用捕獲;
- [this] 以引用捕獲當前對象( *this );
- [&] 以引用捕獲所有在 lambda 體內使用的外部變量,並以引用捕獲當前對象,若存在。
- [=] 以複製捕獲所有用於 lambda 體內的自動變量,並以引用捕獲當前對象,若存在。
右值引用:以&&符號表示
可以把右值簡單理解爲臨時變量。std::move :將左值引用變成一個右值引用。
移動語義:移動構造函數 和 移動賦值操作符。MyString類的移動構造函數
- 移後源對象最終會被銷燬,所以在移動之後要使其進入對析構函數安全的狀態。
MyString(MyString&& str) { std::cout << "Move Ctor source from " << str._data << endl; _len = str._len; _data = str._data; //令str進入對析構函數安全的狀態 str._len = 0; str._data = NULL; }
完美轉發
- 在泛型函數中:需要將一組參數原封不動地傳遞給另一個函數。原封不動不僅僅是參數的值不變,在C++中還有以下的兩組屬性:左值/右值,const/非const。通過定義一個右值引用參數的函數版本,就能解決所有屬性問題。
- C++11對T&&的類型推導:接受參數類型 + 傳入參數 => 推導結果
1. T& + & => T& 2. T&& + & => T& 3. T& + && => T& 4. T&& + && => T&&
-
override
- 明確地表示一個函數是對基類中一個虛函數的override。編譯器也知道它是一個override,它會檢查基類虛函數和派生類中重載函數的簽名不匹配問題。如果簽名不匹配,編譯器會發出錯誤信息。
final
- final有兩個用途。第一,它阻止了從類繼承;第二,阻止一個虛函數的override。
面向對象基礎
能夠準確理解下面這些問題是從C程序員向C++程序員進階的基礎。當然了,這只是一部分。
面向對象三大特性?
- 封裝性:數據和代碼捆綁在一起,避免外界干擾和不確定性訪問。
- 繼承性:讓某種類型對象獲得另一個類型對象的屬性和方法。
- 多態性:同一事物表現出不同事物的能力,即向不同對象發送同一消息,不同的對象在接收時會產生不同的行爲(重載實現編譯時多態,虛函數實現運行時多態)。
public/protected/private的區別?
- public的變量和函數在類的內部外部都可以訪問。
- protected的變量和函數只能在類的內部和其派生類中訪問。
- private修飾的元素只能在類內訪問。
對象存儲空間?
- 非靜態成員的數據類型大小之和。
- 編譯器加入的額外成員變量(如指向虛函數的指針)。
- 爲了邊緣對齊優化加入的padding。
C++空類有哪些成員函數?
- 首先,空類對象大小爲1字節。
- 默認函數有:
- 構造函數 A();
- 析構函數 ~A(void);
- 拷貝構造函數 A(const A &a);
- 賦值運算符 A& operate =(const A &a);
構造函數能否爲虛函數,析構函數呢?
- 析構函數:
- 析構函數可以爲虛函數,並且一般情況下基類析構函數要定義爲虛函數。
- 只有在基類析構函數定義爲虛函數時,調用操作符delete銷燬指向對象的基類指針時,才能準確調用派生類的析構函數(從該級向上按序調用虛函數),才能準確銷燬數據。
- 析構函數可以是純虛函數,含有純虛函數的類是抽象類,此時不能被實例化。但派生類中可以根據自身需求重新改寫基類中的純虛函數。
- 構造函數:
- 構造函數不能定義爲虛函數。虛函數對應一個vtable,可是這個vtable其實是存儲在對象的內存空間的。問題出來了,如果構造函數是虛的,就需要通過 vtable來調用,可是對象還沒有實例化,也就是內存空間還沒有,怎麼找vtable呢?所以構造函數不能是虛函數。
- 析構函數:
構造函數和析構函數能否調用虛函數?
- 從語法上講,調用完全沒有問題。但是從效果上看,往往不能達到需要的目的。
- 假設一個基類A的構造函數中調用了一個虛函數。派生類B繼承自A 。當用構造函數創建一個B類對象時,先調用基類A的構造函數,而此時編譯器認爲正在創建的對象的類型是A,所以虛函數是A類的虛函數。析構時同理,派生類成員先被析構了,當進入基類的析構函數時,就認爲對象是基類對象調用基類的虛函數。
構造函數調用順序,析構函數呢?
- 第1 基類的構造函數:如果有多個基類,先調用縱向上最上層基類構造函數,如果橫向繼承了多個類,調用順序爲派生表從左到右順序。
- 第2 成員類對象的構造函數:如果類的變量中包含其他類(類的組合),需要在調用本類構造函數前先調用成員類對象的構造函數,調用順序遵照在類中被聲明的順序。
- 第3 派生類的構造函數。
- 第4 析構函數與之相反。
拷貝構造函數中深拷貝和淺拷貝區別?
- 深拷貝時,當被拷貝對象存在動態分配的存儲空間時,需要先動態申請一塊存儲空間,然後逐字節拷貝內容。
- 淺拷貝僅僅是拷貝指針字面值。
- 當使用淺拷貝時,如果原來的對象調用析構函數釋放掉指針所指向的數據,則會產生空懸指針。因爲所指向的內存空間已經被釋放了。
拷貝構造函數和賦值運算符重載的區別?
拷貝構造函數是函數,賦值運算符是運算符重載。
拷貝構造函數會生成新的類對象,賦值運算符不能。
拷貝構造函數是直接構造一個新的類對象,所以在初始化對象前不需要檢查源對象和新建對象是否相同;賦值運算符需要上述操作並提供兩套不同的複製策略,另外賦值運算符中如果被賦值對象有內存分配則需要先把內存釋放掉。
形參傳遞是調用拷貝構造函數(調用的被賦值對象的拷貝構造函數),但並不是所有出現”=”的地方都是使用賦值運算符,如下:
Student s; Student s1 = 2; // 調用拷貝構造函數 Student s2; s2 = s; // 賦值運算符操作
注:類中有指針變量指向動態分配的內存資源時,要重寫析構函數、拷貝構造函數和賦值運算符
拷貝構造函數在什麼時候會被調用?
- 假設Person是一個類。
- Person p(q) //使用拷貝構造函數來創建實例p;
- Person p = q; //使用拷貝構造函數來定義實例p時初始化p
- f(p) //p參數進行值傳遞時,會調用複製構造函數創建一個局部對象
- 假設Person是一個類。
虛函數和純虛函數區別?
- 虛函數是爲了實現動態編聯產生的,目的是通過基類類型的指針指向不同對象時,自動調用相應的、和基類同名的函數(使用同一種調用形式,既能調用派生類又能調用基類的同名函數)。虛函數需要在基類中加上virtual修飾符修飾,因爲virtual會被隱式繼承,所以子類中相同函數都是虛函數。當一個成員函數被聲明爲虛函數之後,其派生類中同名函數自動成爲虛函數,在派生類中重新定義此函數時要求函數名、返回值類型、參數個數和類型全部與基類函數相同。
- 純虛函數只是相當於一個接口名,含有純虛函數的類不能夠實例化。
虛函數機制帶來的開銷有哪些?
- 主要是虛表的存儲開銷、函數通過指針使用帶來的時間開銷。
覆蓋、重載和隱藏的區別?
- override 覆蓋(也叫重寫)是派生類中重新定義父類的虛函數,其函數名、參數列表(個數、類型和順序)、返回值類型和父類完全相同,只有函數體有區別。派生類雖然繼承了基類的同名函數,但用派生類對象調用該函數時會根據對象類型調用相應的函數。覆蓋只能發生在類的成員函數中。
- overwrite 隱藏是指派生類函數屏蔽了與其同名的函數,這裏僅要求基類和派生類函數同名即可。其他狀態同覆蓋。可以說隱藏比覆蓋涵蓋的範圍更寬泛,畢竟參數不加限定。
- overload 重載是具有相同函數名但參數列表不同(個數、類型或順序)的兩個函數(不關心返回值),當調用函數時根據傳遞的參數列表來確定具體調用哪個函數。重載可以是同一個類的成員函數也可以是類外函數。
在main執行之前執行的代碼可能是什麼?
- 全局對象的構造函數。
哪幾種情況必須用到初始化成員列表?
- 類中有const成員。
- 類中有reference成員。
- 調用一個基類的構造函數,而該函數有一組參數。
- 調用一個數據成員對象的構造函數,而該函數有一組參數。
什麼是虛指針?
- 虛指針或虛函數指針是虛函數的實現細節。
- 虛指針指向虛表結構。
重載和函數模板的區別?
- 重載需要多個函數,這些函數彼此之間函數名相同,但參數列表中參數數量和類型不同。在區分各個重載函數時我們並不關心函數體。
- 模板函數是一個通用函數,函數的類型和形參不直接指定而用虛擬類型來代表。但只適用於參數個數相同而類型不同的函數。
this指針是什麼?
- this指針是類的指針,指向對象的首地址。
- this 實際上是成員函數的一個形參,在調用成員函數時將對象的地址作爲實參傳遞給this。所以this指針只能在成員函數中使用。在靜態成員函數中不能用this。
- this指針只有在成員函數中才有定義,且存儲位置會因編譯器不同有不同存儲位置。
類模板是什麼?
- 用於解決多個功能相同、數據類型不同的類需要重複定義的問題。
- 在建立類時候使用template及任意類型標識符T,之後在建立類對象時,會指定實際的類型,這樣纔會是一個實際的對象。
- 類模板是對一批僅數據成員類型不同的類的抽象,只要爲這一批類創建一個類模板,即給出一套程序代碼,就可以用來生成具體的類。
構造函數和析構函數調用時機?
- 全局範圍中的對象:構造函數在所有函數調用之前執行,在主函數執行完調用析構函數。
- 局部自動對象:建立對象時調用構造函數,函數結束時調用析構函數。
- 動態分配的對象:建立對象時調用構造函數,調用釋放時調用析構函數。
- 靜態局部變量對象:建立時調用一次構造函數,主函數結束時調用析構函數。
delete this
類的成員函數中能不能調用delete this?
可以。假設一個成員函數release,調用了delete this。那麼這個對象在調用release方法後,還可以進行其他操作,比如調用其他方法。前提是:被調用的方法不涉及這個對象的數據成員和虛函數,否則會出現不可預期的問題。
爲什麼是不可預期的問題?
這涉及到操作系統的內存管理策略。delete this釋放了類對象的內存空間,但是內存空間卻並不是馬上被回收到系統中,可能是緩衝或者其他什麼原因,導致這段內存空間暫時並沒有被系統收回。但是其中的值是不確定的。
類的析構函數中調用delete this,會發生什麼?
導致棧溢出。delete的本質是爲將被釋放的內存調用一個或多個析構函數,然後,釋放內存。顯然,delete this會去調用本對象的析構函數,而析構函數中又調用delete this,形成無限遞歸,造成堆棧溢出,系統崩潰。
當把一個派生類對象指針賦值給其基類指針時會發生什麼樣的行爲
- 當使用基類的指針指向一個派生類的對象時,編譯器會安插相應的代碼,調整指針的指向,使基類的指針指向派生類對象中其對應的基類子對象的起始處。
在類的構造函數裏面直接使用 memset(this,0,sizeof(*this)) 來初始化整個類裏會發生什麼?
- 將所有非靜態成員變量置0。當有虛函數的時候,虛函數表指針vptr會被置成空。
多繼承有什麼問題?
- 多繼承比單繼承複雜,引入了歧義的問題( 如果基類的成員函數名稱相同,匹配度相同, 則會造成歧義)
- 菱形的多繼承,導致虛繼承的必要性;但虛繼承在大小、速度、初始化/賦值的複雜性上有不小的代價,當虛基類中沒有數據時還是比較合適的。
析構函數能拋出異常嗎?
- 不能,也不應該拋出。
- 如果析構函數拋出異常,則異常點之後的程序不會執行,如果析構函數在異常點之後執行了某些必要的動作比如釋放某些資源,則這些動作不會執行,會造成諸如資源泄漏的問題。
- 通常異常發生時,c++的機制會調用已經構造對象的析構函數來釋放資源,此時若析構函數本身也拋出異常,則前一個異常尚未處理,又有新的異常,會造成程序崩潰的問題。
爲什麼內聯函數,構造函數,靜態成員函數不能爲virtual函數
- 內聯函數
內聯函數是在編譯時期展開,而虛函數的特性是運行時才動態聯編,所以兩者矛盾,不能定義內聯函數爲虛函數。
- 構造函數
構造函數用來創建一個新的對象,而虛函數的運行是建立在對象的基礎上,在構造函數執行時,對象尚未形成,所以不能將構造函數定義爲虛函數
- 靜態成員函數
靜態成員函數屬於一個類而非某一對象,沒有this指針,它無法進行對象的判別。
- 友元函數
C++不支持友元函數的繼承,對於沒有繼承性的函數沒有虛函數
如何定義一個只能在堆上生成對象的類?
- 只能在堆上,析構函數設爲protected。編譯器在爲類對象分配棧空間時,會先檢查類的析構函數的訪問性,其實不光是析構函數,只要是非靜態的函數,編譯器都會進行檢查。如果類的析構函數是私有的,則編譯器不會在棧空間上爲類對象分配內存。
- 類中必須提供一個destroy函數,調用delete this,來進行內存空間的釋放。類對象使用完成後,必須調用destroy函數。
- 用new建立對象,destroy銷燬對象很奇怪。可以用一個靜態成員函數將new封裝起來。同時將構造函數設爲protected。
class A{ protected: A(){}; ~A(){}; public: static A* create(){ return new A(); } void destroy(){ delete this; } };
如何定義一個只能在棧上生成對象的類?
- 只有使用new運算符,對象纔會建立在堆上,因此,只要禁用new運算符就可以實現類對象只能建立在棧上。將operator new()設爲私有即可。
class A{ public: A(){}; ~A(){}; private: void * operator new ( size_t t){} // 注意函數的第一個參數和返回值都是固定的 void operator delete ( void * ptr){} // 重載了new就需要重載delete };
標準模板庫
vector
用法:
定義:
vector<T> vec;
插入元素:
vec.push_back(element);
vec.insert(iterator, element);
刪除元素:
vec.pop_back();
vec.erase(iterator);
修改元素:
vec[position] = element;
遍歷容器:
for(auto it = vec.begin(); it != vec.end(); ++it) {......}
其他:
vec.empty(); //判斷是否空
vec.size(); // 實際元素
vec.capacity(); // 容器容量
vec.begin(); // 獲得首迭代器
vec.end(); // 獲得尾迭代器
vec.clear(); // 清空
實現:
- 線性表,數組實現。
- 支持隨機訪問。
- 插入刪除操作需要大量移動數據。
- 需要連續的物理存儲空間。
- 每當大小不夠時,重新分配內存(*2),並複製原內容。
錯誤避免:
- 插入元素
- 尾後插入:size < capacity時,首迭代器不失效尾迭代失效(未重新分配空間),size == capacity時,所有迭代器均失效(需要重新分配空間)。
- 中間插入:size < capacity時,首迭代器不失效但插入元素之後所有迭代器失效,size == capacity時,所有迭代器均失效。
- 刪除元素
- 尾後刪除:只有尾迭代失效。
- 中間刪除:刪除位置之後所有迭代失效。
map
用法:
定義:
mao<T_key, T_value> map;
插入元素:
map.insert(pair<T_key, T_value>(key, value)); // 同key不插入
map.insert(map<T_key, T_value>::value_type(key, value)); // 同key不插入
map[key] = value; // 同key覆蓋
刪除元素:
map.erase(key); // 按值刪
map.erase(iterator); // 按迭代器刪
修改元素:
map[key] = new_value;
遍歷容器:
for(auto it = vec.begin(); it != vec.end(); ++it) {......}
實現:
- 樹狀結構,RBTree實現。
- 插入刪除不需要數據複製。
- 操作複雜度僅跟樹高有關。
- RBTree本身也是二叉排序樹的一種,key值有序,且唯一。
- 必須保證key可排序。
基於紅黑樹實現的map結構(實際上是map, set, multimap,multiset底層均是紅黑樹),不僅增刪數據時不需要移動數據,其所有操作都可以在O(logn)時間範圍內完成。另外,基於紅黑樹的map在通過迭代器遍歷時,得到的是key按序排列後的結果,這點特性在很多操作中非常方便。
面試時候現場寫紅黑樹代碼的概率幾乎爲0,但是紅黑樹一些基本概念還是需要掌握的。
它是二叉排序樹(繼承二叉排序樹特顯):
- 若左子樹不空,則左子樹上所有結點的值均小於或等於它的根結點的值。
- 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值。
- 左、右子樹也分別爲二叉排序樹。
它滿足如下幾點要求:
- 樹中所有節點非紅即黑。
- 根節點必爲黑節點。
- 紅節點的子節點必爲黑(黑節點子節點可爲黑)。
- 從根到NULL的任何路徑上黑結點數相同。
查找時間一定可以控制在O(logn)。
紅黑樹的節點定義如下:
enum Color { RED = 0, BLACK = 1 }; struct RBTreeNode { struct RBTreeNode*left, *right, *parent; int key; int data; Color color; };
所以對紅黑樹的操作需要滿足兩點:1.滿足二叉排序樹的要求;2.滿足紅黑樹自身要求。通常在找到節點通過和根節點比較找到插入位置之後,還需要結合紅黑樹自身限制條件對子樹進行左旋和右旋。
相比於AVL樹,紅黑樹平衡性要稍微差一些,不過創建紅黑樹時所需的旋轉操作也會少很多。相比於最簡單的BST,BST最差情況下查找的時間複雜度會上升至O(n),而紅黑樹最壞情況下查找效率依舊是O(logn)。所以說紅黑樹之所以能夠在STL及Linux內核中被廣泛應用就是因爲其折中了兩種方案,既減少了樹高,又減少了建樹時旋轉的次數。
從紅黑樹的定義來看,紅黑樹從根到NULL的每條路徑擁有相同的黑節點數(假設爲n),所以最短的路徑長度爲n(全爲黑節點情況)。因爲紅節點不能連續出現,所以路徑最長的情況就是插入最多的紅色節點,在黑節點數一致的情況下,最可觀的情況就是黑紅黑紅排列……最長路徑不會大於2n,這裏路徑長就是樹高。
Reference:
- 編程語言C++
- C++總結筆記
- C++後臺開發面試常見問題彙總
- 《C++ primer》
- 《effective C++》
- 《深度探索C++對象模型》
- 《STL源碼解析》