C++ 複習要點

本文總結一下C++面試時常遇到的問題。C++面試中,主要涉及的考點有

  • 關鍵字極其用法,常考的關鍵字有const, sizeof, typedef, inline, static, extern, new, delete等等
  • 語法問題
  • 類型轉換
  • 指針以及指針和引用的區別
  • 面向對象的相關問題,如虛函數機制等
  • 泛型編程的相關問題,如模板和函數的區別等
  • 內存管理,如字節對齊(內存對齊)、動態內存管理、內存泄漏等
  • 編譯和鏈接
  • 實現函數和類
本文不涉及STL的內容,有關STL的內容,會另有一篇文章專門總結。

零、序章

0.1 C++與C的對比

  1. C++有三種編程方式:過程性,面向對象,泛型編程。
  2. C++函數符號由 函數名+參數類型 組成,C只有函數名。所以,C沒有函數重載的概念。
  3. C++ 在 C的基礎上增加了封裝、繼承、多態的概念
  4. C++增加了泛型編程
  5. C++增加了異常處理,C沒有異常處理
  6. C++增加了bool型
  7. C++允許無名的函數形參(如果這個形參沒有被用到的話)
  8. C允許main函數調用自己
  9. C++支持默認參數,C不支持
  10. C語言中,局部變量必須在函數開頭定義,不允許類似for(int a = 0; ;;)這種定義方法。
  11. C++增加了引用
  12. C允許變長數組,C++不允許
  13. C中函數原型可選,C++中在調用之前必須聲明函數原型
  14. C++增加了STL標準模板庫來支持數據結構和算法

一、重要的關鍵字極其用法

1.1 const 

主要用法

C++ 的const關鍵字的作用有很多,幾乎無處不在,面試中往往會問“說一說const有哪些用法”。下面是一些常見的const用法的總結:

 const 變量

const int a;

不能修改值,必須初始化

 const 類對象

const MyClass a;

不能修改成員變量的值,不能調用非 const 函數

 指向 const 變量的指針

const int * a;

指向內容不可變,指向可變

 const 指針

int * const a;

指向內容可變,指向不可變

 指向 const 變量的 const 指針

const int * const a;

指向內容不可變,指向也不可變

const 引用

 const 變量作爲函數參數

void myfun(const int a);

函數內部不能改變此參數

指向 const 變量的指針做參數,允許上層用一般指針調用。(反之不可)

 const 返回值

const string& myfun(void);

用於返回const引用

上層不能使用返回的引用來修改對象

 const 成員變量

const int a;

static const int a;

必須在初始化列表初始化,之後不能改變

static const 成員變量需要單獨定義和初始化

const 成員函數

void myfun(void) const;

this指針爲指向const對象的const指針

不能修改 非mutable 的成員變量

除此以外,const的用法還有:
  • const引用可以引用右值,如const int& a = 1; 
注:
  1. const 成員方法本質上是使得this指針是指向const對象的指針,所以在const方法內,
  2. const 成員函數可以被非const和const對象調用,而const對象只能調用const 成員函數。原因得從C++底層找,C++方法調用時,會傳一個隱形的this參數(本質上是對象的地址,形參名爲this)進去,所有成員方法的第一個參數是this隱形指針。const成員函數的this指針是指向const對象的const指針,當非const對象調用const方法時,實參this指針的類型是非const對象的const指針,賦給const對象的const指針沒有問題;但是如果const對象調用非const方法,此時實參this指針是指向const對象的const指針,無法賦給非const對象的const指針,所以無法調用。注意this實參是放在ecx寄存器中,而不是壓入棧中,這是this的特殊之處。在類的非成員函數中如果要用到類的成員變量,就可以通過訪問ecx寄存器來得到指向對象的this指針,然後再通過this指針加上成員變量的偏移量來找到相應的成員變量。http://blog.csdn.net/starlee/article/details/2062586/
  3. const 指針、指向const的指針和指向const的const指針,涉及到const的特性“const左效、最左右效”
  4. const 全局變量有內部鏈接性,即不同的文件可以定義不同的同名const全局變量,使用extern定義可以消除內部鏈接性,稱爲類似全局變量,如extern const int a = 10.另一個文件使用extern const int a; 來引用。而且編譯器會在編譯時,將const變量替換爲它的值,類似define那樣。

const 常量和define 的區別

  1. const常量有數據類型,而宏定義沒有數據類型。編譯器可以對前者進行類型安全檢查,而對後者只進行字符替換,沒有類型安全檢查,並且在字符替換中可能會產生意想不到的錯誤(邊際效應)。
  2. 有些集成化的調試工具可以對const常量進行調試,但是不能對宏定義進行調試。
  3. 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
  4. 內存空間的分配上。define進行宏定義的時候,不會分配內存空間,編譯時會在main函數裏進行替換,只是單純的替換,不會進行任何檢查,比如類型,語句結構等,即宏定義常量只是純粹的置放關係,如#define null 0;編譯器在遇到null時總是用0代替null它沒有數據類型.而const定義的常量具有數據類型,定義數據類型的常量便於編譯器進行數據檢查,使程序可能出現錯誤進行排查,所以const與define之間的區別在於const定義常量排除了程序之間的不安全性.
  5. const常量存在於程序的數據段,#define常量存在於程序的代碼段
  6. const常量存在“常量摺疊”,在編譯器進行語法分析的時候,將常量表達式計算求值,並用求得的值來替換表達式,放入常量表,可以算作一種編譯優化。因爲編譯器在優化的過程中,會把碰見的const全部以內容替換掉,類似宏。

1.2 sizeof

  1. sizeof關鍵字不會計算表達式的值,而只會根據類型推斷大小。
  2. sizeof() 的括號可以省略, 如 sizeof a ; 
  3. 類A的大小是 所有非靜態成員變量大小之和+虛函數指針大小

1.3 static 

static的用法有:
(1)聲明靜態全局變量,如static int a; 靜態全局變量的特點:
  • 該變量在全局數據區分配內存; 
  • 未經初始化的靜態全局變量會被程序自動初始化爲0(自動變量的值是隨機的,除非它被顯式初始化); 
  • 靜態全局變量在聲明它的整個文件都是可見的,而在文件之外是不可見的; 
(2)聲明靜態局部變量,即在函數內部聲明的,靜態局部變量的特點:
  • 該變量在全局數據區分配內存; 
  • 靜態局部變量在程序執行到該對象的聲明處時被首次初始化,即以後的函數調用不再進行初始化; 
  • 靜態局部變量一般在聲明處初始化,如果沒有顯式初始化,會被程序自動初始化爲0; 
  • 它始終駐留在全局數據區,直到程序運行結束。但其作用域爲局部作用域,當定義它的函數或語句塊結束時,其作用域隨之結束;
(3)聲明靜態函數,限定函數的局部訪問性,僅在文件內部可見
(4)類的靜態數據成員,與全局變量相比,靜態數據成員的好處有:
  • 靜態數據成員沒有進入程序的全局名字空間,因此不存在與程序中其它全局名字衝突的可能性; 
  • 可以實現信息隱藏。靜態數據成員可以是private成員,而全局變量不能;
(5)類的靜態方法

1.4 typedef 

typedef 用來定義新的類型,類似的還有#define 和 using (C++11) (應該儘可能用using ,比如 using AAA = int64_t; )

與宏定義的對比

  1. #define 在預處理階段進行簡單替換,不做類型檢查; typedef在編譯階段處理,在作用域內給類型一個別名。
  2. typedef 是一個語句,結尾有分號;#define是一個宏指令,結尾沒有分號
  3. typedef int* pInt; 和 #define pInt int* 不等價,前者定義 pInt a, b;會定義兩個指針,後者是一個指針,一個int。

不能聲明爲inline的函數

  1. 包含了遞歸、循環等結構的函數一般不會被內聯。
  2. 虛擬函數一般不會內聯,但是如果編譯器能在編譯時確定具體的調用函數,那麼仍然會就地展開該函數。
  3. 如果通過函數指針調用內聯函數,那麼該函數將不會內聯而是通過call進行調用。
  4. 構造和析構函數一般會生成大量代碼,因此一般也不適合內聯。
  5. 如果內聯函數調用了其他函數也不會被內聯。

1.5 inline

inline用來向編譯器請求聲明爲內聯函數,編譯器有權拒絕。

與宏函數的對比

  1. 內聯函數在運行時可調試,而宏定義不可以;
  2. 編譯器會對內聯函數的參數類型做安全檢查或自動類型轉換(同普通函數),而宏定義則不會;
  3. 內聯函數可以訪問類的成員變量,宏定義則不能;
  4. 在類中聲明同時定義的成員函數,自動轉化爲內聯函數
  5. 宏只是預定義的函數,在編譯階段不進行類型安全性檢查,在編譯的時候將對應函數用宏命令替換。對程序性能無影響

1.6 static const \ const \ static 

1. static const 
static const 數據成員可以在類內初始化 也可以在類外,不能在構造函數中初始化,也不能在構造函數的初始化列表中初始化
2. static
static數據成員只能在類外,即類的實現文件中初始化,也不能在構造函數中初始化,不能在構造函數的初始化列表中初始化;
3. const
const數據成員只能在構造函數的初始化列表中初始化;

1.7 explicit 

explicit禁止了隱式轉換類型,用來修飾構造函數。原則上應該在所有的構造函數前加explicit關鍵字,當你有心利用隱式轉換的時候再去解除explicit,這樣可以大大減少錯誤的發生。如果一個構造函數 Foo(int) ;則下面的語句是合法的:
Foo f; 
f = 12; // 發生了隱式轉換,先調用Foo(int)用12構建了一個臨時對象,然後調用賦值運算符複製到 f 中
如果給構造函數加了explicit,即 explicit Foo(int);就只能進行顯示轉換,無法進行隱式轉換了:
f = 12; // 非法,隱式轉換
f = Foo(12); // 合法,顯示轉換
f = (Foo)12;//合法,顯示轉換,C風格

1.8 extern 

extern可以置於變量或者函數前,以標示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。此外extern也可用來進行鏈接指定。

二、語法問題

2.1 a++ 與 ++a的區別

  1. a++ 返回加之前的值,++a返回加之後的a變量
  2. a++返回的是一個臨時變量,是右值,無法賦值;++a返回的是變量a,是左值

2.2 switch語句

switch語句的表達式必須是整型int , char, short等。

2.3 函數調用過程

執行到函數調用指令時:
  1. +++++++++ 入棧 ++++++++++++
  2. 將實參從右向左壓入棧
  3. 壓入返回地址
  4. 壓入主調函數的基地址
  5. 跳到被調用函數的地址,執行函數代碼,局部變量按聲明順序依次壓入棧
  6. 將返回值放入寄存器eax(累加器)中
  7. +++++++++ 出棧 ++++++++++++
  8. 局部變量全部出棧
  9. 返回地址出棧,找到原執行地址
  10. 形參出棧
  11. 賦值操作將寄存器中的返回值賦給左值(如果有的話)

2.4 左值與右值

判斷左值和右值的標準是是否可以取地址。右值和左值不同,有可能存在於寄存器中,無法取地址,無法被賦值,臨時變量就是右值,存放在寄存器中,被賦給左值後被釋放。

2.5 C語言標識符

關鍵字、預定義標識符、用戶標識符(不能以數字開頭)
a123 // 合法
_a // 合法
_0 // 合法
0asdasd// 非法

2.6 全局變量的優缺點

優點:
(1)可以減少變量的個數
(2)減少由於實際參數和形式參數的數據傳遞帶來的時間消耗。
缺點:
(1)全局變量保存在靜態存貯區,程序開始運行時爲其分配內存,程序結束釋放該內存。與局部變量的動態分配、動態釋放相比,生存期比較長,因此過多的全局變量會佔用較多的內存單元。
(2)全局變量破壞了函數的封裝性能。前面的章節曾經講過,函數象一個黑匣子,一般是通過函數參數和返回值進行輸入輸出,函數內部實現相對獨立。但函數中 如果使用了全局變量,那麼函數體內的語句就可以繞過函數參數和返回值進行存取,這種情況破壞了函數的獨立性,使函數對全局變量產生依賴。同時,也降低了該 函數的可移植性。
(3)全局變量使函數的代碼可讀性降低。由於多個函數都可能使用全局變量,函數執行時全局變量的值可能隨時發生變化,對於程序的查錯和調試都非常不利。

2.7 複合類型有哪些?

6個
class, struct, union, enum, 數組,指針

2.8 運算符優先級和結合性?

結合性有兩種,一種是自左至右,另一種是自右至左,大部分運算符的結合性是自左至右,只有單目運算符、三目運算符的賦值運算符的結合性自右至左
優先級有15種。記憶方法如下:
記住一個最高的:構造類型的元素或成員以及小括號。
記住一個最低的:逗號運算符。
剩餘的是一、二、三、賦值。
意思是單目、雙目、三目和賦值運算符。
在諸多運算符中,又分爲:
算術、關係、邏輯。
兩種位操作運算符中,移位運算符在算術運算符後邊,邏輯位運算符在邏輯運算符的前面。再細分如下:
算術運算符分 *,/,%高於+,-。
關係運算符中,〉,〉=,<,<=高於==,!=。
邏輯運算符中,除了邏輯求反(!)是單目外,邏輯與(&&)高於邏輯或(||)。
邏輯位運算符中,除了邏輯按位求反(~)外,按位與(&)高於按位半加(^),高於按位或(|)。
這樣就將15種優先級都記住了,再將記憶方法總結如下:
去掉一個最高的,去掉一個最低的,剩下的是一、二、三、賦值。雙目運算符中,順序爲 算術、移位、關係(>,<,==)、邏輯位和邏輯(&& ||)。

2.9 using 聲明和using 編譯指令的區別?哪個更好?

using聲明是指類似using std::vector;這種的,using編譯指令是指using namespace std;這種的。區別:
(1)using聲明使特定的標示符可用,using編譯指令使整個名稱空間可用。
(2)假設名稱空間和聲明區域定義了相同的名稱。如果試圖使用using聲明將名稱空間的名稱導入該聲明區域,則這兩個名稱會發生衝突,從而出錯。如果使用using編譯指令將該名稱空間的名稱導入該聲明區域,則局部版本將隱藏名稱空間版本。
(3)一般來說,使用using聲明比使用using編譯指令更安全,這是由於它只導入指定的名稱。如果該名稱與局部名稱發生衝突,編譯器將付出指示。using編譯指令導入所有名稱,包括可能並不需要的名稱。如果與局部名稱發生衝突,則局部名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。

2.10 for循環的效率問題

1. 最長循環放到內部可以提高I cache的效率,降低因爲循環跳轉造成cache的miss以及流水線flush造成的延時
2. 多次相同循環後也能提高跳轉預測的成功率,提高流水線效率
3. 編譯器會自動展開循環提高效率, 這個不一定是必然有效的
但不是絕對正確的,比如: 1 int x[1000][100];
2 for(i=0;i<1000;i++)
3 for(j=0;j<100;j++)
4 {
5 //access x[i][j]
6 }
7
8 int x[1000][100];
9 for(j=0;j<100;j++)
10 for(i=0;i=1000;i++)
11 {
12 //access x[i][j]
13 }
14
這時候第一個的效率就比第二個的高,原因嘛和硬件也有一些關係,CPU對於內存的訪問都是通過數據緩存(cache)來進行的。

三、類型轉換

3.1 四種類型強制轉換

  1. dynamic_cast:該轉換符用於將一個指向派生類的基類指針或引用轉換爲派生類的指針或引用。
  2. const_cast:最常用的用途就是刪除const屬性。
  3. static_cast:static_cast本質上是傳統c語言強制轉換的替代品,比C類型轉換更嚴格, 該操作符用於非多態類型的轉換,任何標準轉換都可以使用他,即static_cast可以把int轉換爲double,但不能把兩個不相關的類對象進行轉換,比如類A不能轉換爲一個不相關的類B類型。static_cast在類對象和基礎類型轉換中,會調用類的構造函數,和類型轉換運算符比如operator int(),來進行顯示轉換。
  4. reinterpret_cast:該操作符用於將一種類型轉換爲另一種不同的類型,比如可以把一個整型轉換爲一個指針,或把一個指針轉換爲一個整型,因此使用該操作符的危險性較高,一般不應使用該操作符。

四、指針

4.1 指針與引用的區別

  1. 指針是一個變量,引用只是別名
  2. 指針需要解引用才能訪問對象,引用不需要
  3. 引用在定義時必須初始化,且以後不可轉移引用的對象,指針可以
  4. 引用沒有const,即int& const a ;沒有;而指針有const指針,即int* const ptr; 
  5. 引用不可以爲空;而指針可以
  6. 指針變量需要分配棧空間;而引用不需要,僅僅是個別名
  7. sizeof(引用)得到對應對象的大小;sizeof(指針)得到指針大小
  8. 指針加法和引用加法不一樣
  9. 引用不需要釋放內存空間,在編譯時就會優化掉

4.2 指針與數組名的區別

  1. 數組名不是指針,對數組名取地址,得到整個數組的地址
  2. 數組名 + 1會跳過整個數組的大小,指針+1只會跳過一個元素的大小
  3. 數組名作爲函數參數傳遞時,退化爲指針
  4. sizeof(數組名)返回整個數組的大小,sizeof(指針)返回指針大小
  5. 數組名無法修改值,是常量
  6. int (*p)[] = &arr; 纔是正確的數組指針寫法

4.3 野指針、空指針的概念

  1. 野指針是指指向無效內存的指針,不能對野指針取內容,delete
  2. 空指針是指置爲0\NULL\nullptr的指針,可以對空指針delete多次

五、面向對象

5.1 面向對象的三大特性

三大特性:封裝,繼承,多態  
  1. 封裝:封裝是實現面向對象程序設計的第一步,封裝就是將數據或函數等集合在一個個的單元中(我們稱之爲類)。封裝的意義在於保護或者防止代碼(數據)被我們無意中破壞。
  2. 繼承:繼承主要實現重用代碼,節省開發時間。子類可以繼承父類的一些東西。
  3. 多態:同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果。分爲編譯時多態和運行時多態。

5.2 函數重載和運算符重載

問:函數重載的依據?

答:
  1. 參數個數
  2. 參數類型
  3. const方法與非const方法構成重載

問:運算符重載的限制?

答:
  1. 被重載的運算符,至少有一個操作數是用戶自定義類型,也就是說不能重載C++語言的標準運算
  2. 重載的運算符的句法規則不可以改變,操作數、結合性和優先級無法更改。以前是幾元現在就是幾元;該是左結合還是左結合;優先級無法更改。
  3. 不能自定義運算符,不能創建新的運算符。
  4. 不能重載的運算符有:
    1. 成員訪問運算符 . 
    2. 成員指針運算符 .* 
    3. 作用域解析運算符 ::
    4. 條件運算符 ?:
    5. sizeof 
    6. typeid
    7. 四個類型轉換運算符
      1. const_cast
      2. static_cast
      3. dynamic_cast
      4. reinterpret_cast
  5. 只能通過成員函數重載,而不能通過友元重載的運算符:
    1. 賦值運算符 = 
    2. 函數調用運算符 () 
    3. 下標運算符 []
    4. 通過指針訪問成員運算符 ->
  6. 只能通過友元重載,不能通過成員函數重載的情況:
    1. 雙目運算符最好用友元重載,單目運算符最好用成員函數重載
    2. 若運算符所需的操作數(尤其是第一個操作數)希望有隱式類型轉換,則只能選用友元函數
    3. 左操作數是不同類的對象或者內部類型,比如ostream, istream, int, float等
    4. 當需要重載運算符具有可交換性時,選擇重載爲友元函數
  7. 對返回類型沒有限制,可以是void或者其他類型
  8. 重載一元運算符需要注意,由於一元運算符沒有參數,前綴和後綴無法區分,所以需要加一個啞元(dummy),啞元永遠用不上,如果有啞元,則是後綴形式,否則,就是前綴。

5.3 哪些成員無法被繼承?

  1. 無法被繼承的有
    1. 構造函數
    2. 析構函數
    3. 賦值運算符
    4. 友元函數
  2. 可以被繼承的有
    1. 靜態成員
    2. 靜態方法
    3. 非靜態成員
    4. 非靜態方法(無論是private\public\protected,只是private的繼承了也無法訪問)
    5. 虛表指針

5.4 定義默認構造函數的兩種方法?

  1. 給已有的構造函數中的一個的所有參數加上默認值
  2. 通過方法重載定義一個無參數構造函數
注意:
  1. 隱式調用默認構造函數不要加括號(), 會被編譯器解釋爲函數聲明。

5.5 調用非默認構造函數的三種方法?

  1. Foo f(...); // 隱式調用
  2. Foo f = Foo(...) ;// 顯式調用
  3. Foo* f = new Foo(); // 顯式調用 

5.6 由編譯器生成的6個成員函數?

注意:對於空類,不會生成任何成員函數,只會生成一個字節的佔位符。
  1. 默認構造函數
  2. 析構函數
  3. 複製構造函數
  4. 賦值運算符
  5. 取地址運算符
  6. 取地址運算符 const版本

5.7 友元的三種實現方式

  1. 友元函數
  2. 友元類
  3. 友元成員函數

5.8 爲什麼基類的析構函數爲什麼要聲明爲虛函數?

爲了能在多態情況下準確調用派生類的析構函數。如果基類的析構函數非虛函數,則用基類指針或引用引用派生類進行析構時,只會調用基類的析構函數;如果是虛析構函數,則會依次調用派生類的析構和基類的析構。(基類的析構是一定會調用的,無論是否爲虛)。

5.9 爲什麼構造函數不可以是虛函數?

  1. 虛函數在運行期決定函數調用,而在構造一個對象時,由於對象還未構造成功,編譯器無法確定對象的實際類型,繼而無法決定調用哪一個構造函數。
  2. 虛函數的執行依賴於虛函數表,而虛函數表在構造函數中進行初始化工作,即初始化 vptr,讓它指向正確的虛函數表,而在構造期間,虛函數表還沒有初始化,所以無法決定調用哪個構造函數。

5.10 析構函數什麼時候聲明爲私有?什麼時候不能聲明爲私有?

  1. 私有析構函數可以使得對象只在堆上構造。在棧上創建的對象要求構造函數和析構函數必須都是公有的,否則編譯器報錯“析構函數不可訪問”;而堆對象由程序員創建和刪除,可以把析構函數聲明爲私有的。由於delete會調用析構函數,而私有的析構無法被訪問,編譯器報錯,此時通過增加一個destroy()方法,在方法內調用析構函數來釋放對象:
    • void destroy() 
    • {
    • delete this; 
    • }
  2. 析構函數不能聲明爲私有的情況:基類的析構函數不能聲明爲私有,因爲要在派生類的析構函數中被隱式調用。

5.11 構造函數什麼時候聲明爲私有?什麼時候不能聲明爲私有?

  1. 單例模式。
  2. 基類的構造函數不能聲明爲私有,因爲要在派生類的構造函數中被隱式調用。如果在派生類的構造函數中沒有顯式調用基類的構造,則會調用基類的默認構造函數。

5.12 不能聲明爲虛函數的成員函數

構造函數:

首先明確一點,在編譯期間編譯器完成了虛表的創建,而虛指針在構造函數期間被初始化。

如果構造函數是虛函數,那必然需要通過虛指針來找到虛構造函數的入口地址,但是這個時候我們還沒有把虛指針初始化。因此,構造函數不能是虛函數。

內聯函數:

編譯期內聯函數在調用處被展開,而虛函數在運行時才能被確定具體調用哪個類的虛函數。內聯函數體現的是編譯期機制,而虛函數體現的是運行期機制。

靜態成員函數:

靜態成員函數和類有關,即使沒有生成一個實例對象,也可以調用類的靜態成員函數。而虛函數的調用和虛指針有關,虛指針存在於一個類的實例對象中,如果靜態成員函數被聲明成虛函數,那麼調用成員靜態函數時又如何訪問虛指針呢。總之可以這麼理解,靜態成員函數與類有關,而虛函數與類的實例對象有關。

非成員函數:

虛函數的目的是爲了實現多態,多態和繼承有關。所以聲明一個非成員函數爲虛函數沒有任何意義。

5.13 虛函數機制以及內存分佈

http://www.cnblogs.com/freeopen/p/5482965.html 重點看多繼承的內存分佈。
虛函數機制涉及的指針和表有:
  • 虛函數表指針 vfptr和虛函數表 vftable
  • 虛繼承下還涉及 虛基類表指針 vbptr和虛基類表 vbtable
虛函數的實現過程:
1.編譯器爲每個含有虛函數的類或者從此類派生的類創建一個虛函數表vftable, 保存此類所有虛函數的地址,並增加一個隱藏成員虛函數表指針vfptr放在所有數據成員之前。在創建類的對象時,在構造函數內部對虛函數表指針進行初始化,指向之前創建的虛函數表。
2. 單繼承情況下,派生類會繼承基類所有的數據成員和虛函數表指針,並由編譯器生成虛函數表,在創建派生類實例時,將虛函數表指針指向新的,屬於派生類的虛函數表。
3. 多重繼承情況下,會有多個虛函數表,幾重繼承,就會有幾個虛函數表。這些表按照派生的順序依次排列,如果派生類改寫了基類的虛函數,那麼就會用派生類自己的虛函數覆蓋虛函數表的相應的位置,如果派生類有新的虛函數,那麼就添加到第一個虛函數表的末尾
4. 虛繼承情況下,會再創建一個虛基類表和一個虛基類表指針,也就是說,編譯器會增加兩個指針,一個是虛基類表指針,指向虛基類表,保存了所有繼承過來的虛基類在內存中的地址(偏移量);另一個是繼承過來的虛函數表指針,保存了虛函數的地址。如果派生類有新的虛函數,那麼就再增加一個虛函數表指針,指向一個新的虛函數表,保存了派生類新的虛函數的地址。
5. 虛基類部分會在C++繼承層次中只有一份。所有由虛基類派生的類都持有一個虛基類表指針,指向一個虛基類表,表裏面保存了所有它繼承的虛基類部分的地址。虛基類部分有一個虛函數表指針,指向虛函數表。

5.14 class 與 struct的區別

  1. class默認的繼承方式爲private, struct 默認繼承方式爲public 
  2. class的成員訪問默認爲private, struct默認爲public 

5.15 重載、重寫(覆蓋)與隱藏(重定義)的關係

重載  override 
重寫(覆蓋)override 
隱藏  hide 
  1. 重載。函數名相同,參數個數、類型不同,或者用const重載。是同一個類中方法之間的關係,是水平關係。
  2. 重寫。派生類重新定義基類中有相同名稱和參數的虛函數,要求參數列表必須相同。方法在基類和派生中的訪問限制可以不同。
  3. 隱藏。派生類重新定義基類中有相同名稱的函數(參數列表可以不同)會把其他基類的同名方法隱藏起來,無法被派生類調用。

5.16 哪些情況下方法可以不寫定義?

  1. 純虛方法
  2. 非虛方法
所以,非純虛的虛方法也就是普通的虛方法必須寫定義,哪怕是空的,因爲要生成虛函數表,沒有方法定義就沒有方法地址。

5.17 派生類可以不實現虛基類的純虛方法,派生類也成了抽象類。

5.18 三種繼承方式(public, private, protected)的區別?

  1. 公有繼承(public): 基類成員對其對象的可見性與一般類及其對象的可見性相同,public成員可見,protected和private成員不可見,基類成員對派生類的可見性對派生類來說,基類的public和protected成員可見:基類的public成員和protected成員作爲派生類的成員時,它們都保持原有狀態;基類的private成員依舊是private,派生類不可訪問基類中的private成員。 基類成員對派生類對象的可見性對派生類對象來說,基類的public成員是可見的,其他成員是不可見的。 所以,在公有繼承時,派生類的對象可以訪問基類中的public成員,派生類的成員方法可以訪問基類中的public成員和protected成員。
  2. 私有繼承(private) 基類成員對其對象的可見性與一般類及其對象的可見性相同,public成員可見,其他成員不可見,基類成員對派生類的可見性對派生類來說,基類的public成員和protected成員是可見的:基類的public成員和protected成員都作爲派生類的private成員,並且不能被這個派生類的子類所訪問;基類的私有成員是不可見的:派生類不可訪問基類中的private成員,基類成員對派生類對象的可見性對派生類對象來說,基類的所有成員都是不可見的,所以在私有繼承時,基類的成員只能由直接派生類訪問,無法再往下繼承。
  3. 保護繼承(protected) 保護繼承與私有繼承相似,基類成員對其對象的可見性與一般類及其對象的可見性相同,public成員可見,其他成員不可見,基類成員對派生類的可見性,對派生類來說,基類的public和protected成員是可見的:基類的public成員和protected成員都作爲派生類的protected成員,並且不能被這個派生類的子類所訪問;基類的private成員是不可見的:派生類不可訪問基類中的private成員。基類成員對派生類對象的可見性對派生類對象來說,基類的所有成員都是不可見的。所以,在保護繼承時,基類的成員也只能由直接派生類訪問,而無法再向下繼承。C++支持多重繼承。多重繼承是一個類從多個基類派生而來的能力。派生類實際上獲取了所有基類的特性。當一個類 是兩個或多個基類的派生類時,派生類的構造函數必須激活所有基類的構造函數,並把相應的參數傳遞給它們 。

5.19 如果賦值構造函數參數不是傳引用而是傳值會有什麼問題?

如果不是傳引用,會造成棧溢出。因爲如果是Foo(Foo f)的形式,實參初始化形參的時候也會調用複製構造函數,造成死循環。所以,複製構造函數一定要傳引用:
Foo(Foo& f); 

5.20 如何實現只能動態分配類對象,不能定義類對象?

即只能將對象創建於堆上,不能創建於棧上。需要把構造函數和析構函數設爲protected,派生類可以訪問,外部無法訪問。同時創建create和destroy函數,在內部調用構造和析構,用於創建和刪除對象。其中create設爲static,使用類名訪問。
 class A{
 protected:
 	A(){};
 	~A(){};
 public:
 	static A* creat(){
 		return new A();
 	}
 	void destroy(){
 		delete this;
 	}
 };
 int main()
 {
 	A* a = A::creat();

 	a->destroy();
 }

5.21 如何實現只能在棧上創建對象?不能在堆上創建對象?

在堆上創建對象的唯一方法是使用new關鍵字,所以,只需要禁用new關鍵字就可以了。將operator new 設爲私有的, 外部不可訪問。
 class A
 {
 private:
 	void* operator new(size_t t){}     // 注意函數的第一個參數和返回值都是固定的
 	void operator delete(void* ptr){} // 重載了new就需要重載delete
 public:
 	A(){}
 	~A(){}
 };

5.22 必須在構造函數初始化式裏進行初始化的數據成員有哪些?

  1. 常量成員,因爲常量只能初始化不能賦值,所以必須放在初始化列表裏面
  2. 引用類型,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裏面
  3. 沒有默認構造函數的類類型,因爲使用初始化列表可以不必調用默認構造函數來初始化,而是直接調用拷貝構造函數初始化

5.23 抽象類和接口的區別?

抽象類是包含純虛函數的類 C++中的接口是指只包含純虛函數的抽象類,不能被實例化。 一個類可以實現多個接口(多重繼承)

5.24 虛基類和虛繼承,虛基指針和虛基表

虛基類是使用virtual繼承的公共基類。虛繼承使得在內存中只有基類成員的一份拷貝。虛繼承消除了歧義,如果B,C,繼承於A,A中有一個公有成員 i,D繼承於B,C,此時D無法訪問 i,因爲會有歧義,不知道是B還是C的,此時使用虛繼承可以解決,讓B,C以虛繼承方式繼承A,這樣就消除了歧義。底層實現原理:底層實現原理與編譯器相關,一般通過虛基類指針實現,即各對象中只保存一份父類的對象,多繼承時通過虛基類指針引用該公共對象,從而避免菱形繼承中的二義性問題。 

虛基類的初始化與一般多繼承的初始化在語法上是一樣的,但構造函數的調用次序不同。派生類構造函數的調用次序有三個原則:
(1)虛基類的構造函數在非虛基類之前調用;
(2)若同一層次中包含多個虛基類,這些虛基類的構造函數按它們說明的次序調用;
(3)若虛基類由非虛基類派生而來,則仍先調用基類構造函數,再調用派生類的構造函數。
虛繼承的派生類會增加一個隱藏成員虛基指針vbPtr指向虛基表vbTable。

5.25 構造函數和析構函數中可以調用調用虛函數嗎?

可以,虛函數底層實現原理(但是最好不要在構造和析構函數中調用) 可以,但是沒有動態綁定的效果,父類構造函數中調用的仍然是父類版本的函數,子類中調用的仍然是子類版本的函數。 effictive c++第九條,絕不在構造和析構過程中調用virtual,因爲構造函數中的base的虛函數不會下降到derived上。而是直接調用base類的虛函數。

5.26 構造函數和析構函數調用順序?

  1. 先調用基類構造函數
  2. 在調用成員類構造函數
  3. 最後調用本身的構造函數
  4. 析構順序相反

5.27 動態綁定如何實現?

C++ 中,通過基類的引用或指針調用虛函數時,發生動態綁定。引用(或指針)既可以指向基類對象也可以指向派生類對象,這一事實是動態綁定的關鍵。用引用(或指針)調用的虛函數在運行時確定,被調用的函數是引用(或指針)所指對象的實際類型所定義的。

5.28 多態性有哪些?

多態指當不同的對象收到相同的消息時,產生不同的動作
  1. 編譯時多態(靜態綁定),函數重載,運算符重載,模板。
  2. 運行時多態(動態綁定),虛函數機制。

5.29 構造函數可不可以拋出異常?析構函數呢?

1. 構造函數中儘量不要拋出異常,能避免的就避免,如果必須,要考慮不要內存泄露!
2. 不要在析構函數中拋出異常!
理論上都可以拋出異常。 但析構函數最好不要拋出異常,將會導致析構不完全,從而有內存泄露。
爲什麼不應該在析構函數中拋出異常?
1)如果析構函數拋出異常,則異常點之後的程序不會執行,如果析構函數在異常點之後執行了某些必要的動作比如釋放某些資源,則這些動作不會執行,會造成諸如內存泄漏的問題。
2)通常異常發生時,c++的機制會調用已經構造對象的析構函數來釋放資源,此時若析構函數本身也拋出異常,則前一個異常尚未處理,又有新的異常,會造成程序崩潰的問題。
3)當在某一個析構函數中會有一些可能(哪怕是一點點可能)發生異常時,那麼就必須要把這種可能發生的異常完全封裝在析構函數內部,決不能讓它拋出函數之外(這招簡直是絕殺!呵呵!

5.30 成員函數調用底層機制?

 例如我們要調用Point的實例 p 的 vec3 normalize() 方法,即 p.normalize();編譯器會做下面的轉變:
1. 改寫函數的原型,增加一個額外的參數 this 指針到參數列表的最前面: 
// 如果成員函數是非const函數,則this指針是指針常量
vec3 Point :: normalize( Point* const this); 
// 如果成員函數是const函數,則this指針是指向常量的指針常量
vec3 Point :: normalize( const Point* const this); 
2. 將函數內部對“非靜態成員”的訪問,改寫爲通過this指針訪問
{
return sqrt(
this->x * this->x + 
this->y * this->y +
this->z * this->z
); 
}
3. 將成員函數重寫寫爲一個外部函數,並修改函數名,避免名稱和其他函數名衝突:
extern normalize__3PointFv(register const Point* const this); 

六、泛型編程

6.1 使用模板的優點和缺點?

優點:
  1. 在一些場景可以避免重複代碼
  2. 有些問題難以使用OO技巧(如繼承和多態)來實現,而使用模版會很方便
  3. template classes更加的類型安全,因其參數類型在編譯時都是已知的。
缺點:
  1. 一些編譯器對template支持不好。
  2. 編譯器給出的有些出錯信息比較晦澀。
  3. 爲每種類型都生成額外的代碼,可能導致生成的exe膨脹。
  4. 使用templates寫的代碼難以調試
  5. templates在頭文件中,這樣一旦有所變更需要重編譯所有相關工程

6.2 模板函數和函數的對比?

  1. 模板函數由函數模板實例化而來,編譯器推斷模板實參,然後實例化出對應的函數定義。模板函數是函數模板的實例。
  2. 普通函數需要程序員手動重載才能實現對於不同類型參數的支持。
  3. 函數模板只能用於函數的參數個數相同而類型不同的情況,如果參數個數不同,則不能使用函數模板,只能使用重載。
  4. 函數模板必須要求所有實參的類型T都相同,無法進行隱式類型轉換。
  5. 進行函數調用時,編譯器優先選擇匹配的非模板函數,如果找不到再試着進行函數模板的實例化,如果還不行,則這個調用違法。這樣做可以減少函數模板實例化次數,提高效率。

6.3 模板的全特化和偏特化?

什麼是特化?
所謂特化,就是將泛型的東東搞得具體化一些,從字面上來解釋,就是爲已有的模板參數進行一些使其特殊化的指定,使得以前不受任何約束的模板參數,或受到特定的修飾(例如const或者搖身一變成爲了指針之類的東東,甚至是經過別的模板類包裝之後的模板類型)或完全被指定了下來。

模板有兩種特化,全特化和偏特化(局部特化) 模板函數只能全特化,沒有偏特化(以後可能有)。 模板類是可以全特化和偏特化的。 全特化,就是模板中模板參數全被指定爲確定的類型。 全特化也就是定義了一個全新的類型,全特化的類中的函數可以與模板類不一樣。 偏特化,就是模板中的模板參數沒有被全部確定,需要編譯器在編譯時進行確定。 在類型上加上const、&、*( cosnt int、int&、int*、等等)並沒有產生新的類型。只是類型被修飾了。模板在編譯時,可以得到這些修飾信息。

模板爲什麼要特化,因爲編譯器認爲,對於特定的類型,如果你能對某一功能更好的實現,那麼就該聽你的。

模板分爲類模板與函數模板,特化分爲全特化與偏特化。全特化就是限定死模板實現的具體類型,偏特化就是如果這個模板有多個類型,那麼只限定其中的一部分。

先看類模板:

[cpp] view plain copy
  1. template<typename T1, typename T2>  
  2. class Test  
  3. {  
  4. public:  
  5.     Test(T1 i,T2 j):a(i),b(j){cout<<"模板類"<<endl;}  
  6. private:  
  7.     T1 a;  
  8.     T2 b;  
  9. };  
  10.   
  11. template<>  
  12. class Test<int , char>  
  13. {  
  14. public:  
  15.     Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}  
  16. private:  
  17.     int a;  
  18.     char b;  
  19. };  
  20.   
  21. template <typename T2>  
  22. class Test<char, T2>  
  23. {  
  24. public:  
  25.     Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}  
  26. private:  
  27.     char a;  
  28.     T2 b;  
  29. };  

那麼下面3句依次調用類模板、全特化與偏特化:

[cpp] view plain copy
  1. Test<double , double> t1(0.1,0.2);  
  2. Test<int , char> t2(1,'A');  
  3. Test<charbool> t3('A',true);  

而對於函數模板,卻只有全特化,不能偏特化:

[cpp] view plain copy
  1. //模板函數  
  2. template<typename T1, typename T2>  
  3. void fun(T1 a , T2 b)  
  4. {  
  5.     cout<<"模板函數"<<endl;  
  6. }  
  7.   
  8. //全特化  
  9. template<>  
  10. void fun<int ,char >(int a, char b)  
  11. {  
  12.     cout<<"全特化"<<endl;  
  13. }  
  14.   
  15. //函數不存在偏特化:下面的代碼是錯誤的  
  16. /* 
  17. template<typename T2> 
  18. void fun<char,T2>(char a, T2 b) 
  19. { 
  20.     cout<<"偏特化"<<endl; 
  21. } 
  22. */  

注意:
  • 至於爲什麼函數不能偏特化,似乎不是因爲語言實現不了,而是因爲偏特化的功能可以通過函數的重載完成。
  • 函數模版的全特化不參與函數重載, 並且優先級低於函數基礎模版參與匹配,也就是說,匹配的順序是:
1. 非模板函數
2. 某個沒有進行全特化的template function
3. 如果這個沒有進行全特化的template function有全特化版本,並且類型也比較匹配,則選擇這個全特化版本

七、內存管理

7.1 new與malloc的區別,delet和free的區別?內部實現?

new 與 malloc的區別:
  1. new 是運算符,malloc是庫函數
  2. new會調用構造函數,malloc只申請內存
  3. new返回指定類型的指針,malloc返回void指針
  4. new自動計算所需的內存大小,malloc需要手動設置空間
  5. new可以被重載
new的內部實現:

delete 和 free 的區別: 
  1. delete 是運算符,free是庫函數
  2. delete會調用析構函數,free是會釋放內存
  3. 使用free之前要檢查指針是否爲空指針,delete不需要,對空指針delete沒有問題
  4. free 和 delete 不能混用,也就是說new 分配的內存空間最好不要使用使用free 來釋放,malloc 分配的空間也不要使用 delete來釋放
delete的內部實現:

7.2 malloc, calloc, realloc, 和 alloca 申請內存的區別?

  1. calloc 是申請N個大小爲S的空間,且會初始化空間值爲0;malloc不會初始化,是隨機的垃圾數據(在VS Debug模式下,會是0xcccccc這種特殊值,爲了調試方便)
  2. malloc 是在堆上申請大小爲S的一個空間,但不會初始化
  3. realloc 是將原本分配的內存擴充到新的大小,要求新的大小必須大於原大小
  4. alloca 是在棧上申請空間,不需要(不能)使用free,運行到作用域以外的時候釋放申請的空間

7.3 內存泄漏(內存溢出)有哪些因素?

  1. 在類的構造函數和析構函數中沒有匹配的調用new和delete函數 兩種情況下會出現這種內存泄露:一是在堆裏創建了對象佔用了內存,但是沒有顯示地釋放對象佔用的內 存;二是在類的構造函數中動態的分配了內存,但是在析構函數中沒有釋放內存或者沒有正確的釋放內存
  2. 沒有正確地清除嵌套的對象指針
  3. 在釋放對象數組時在delete中沒有使用方括號
  4. 指向對象的指針數組不等同於對象數組 對象數組是指:數組中存放的是對象,只需要delete []p,即可調用對象數組中的每個對象的析構函數釋放空間 指向對象的指針數組是指:數組中存放的是指向對象的指針,不僅要釋放每個對象的空間,還要釋放每個指針的空間,delete []p只是釋放了每個指針,但是並沒有釋放對象的空間,正確的做法,是通過一個循環,將每個對象釋放了,然後再把指針釋放了
  5. 缺少拷貝構造函數
  6. 兩次釋放相同的內存是一種錯誤的做法,同時可能會造成堆的奔潰。 按值傳遞會調用(拷貝)構造函數,引用傳遞不會調用。 在C++中,如果沒有定義拷貝構造函數,那麼編譯器就會調用默認的拷貝構造函數,會逐個成員拷貝的方式來複制數據成員,如果是以逐個成員拷貝的方式來複制指針被定義爲將一個變量的地址賦給另一個變量。這種隱式的指針複製結果就是兩個對象擁有指向同一個動態分配的內存空間的指針。當釋放第一個對象的時候,它的析構函數就會釋放與該對象有關的動態分配的內存空間。而釋放第二個對象的時候,它的析構函數會釋放相同的內存,這樣是錯誤的。 所以,如果一個類裏面有指針成員變量,要麼必須顯示的寫拷貝構造函數和重載賦值運算符,要麼禁用拷貝構造函數和重載賦值運算符
  7. 沒有將基類的析構函數定義爲虛函數
  8. 指針的值被篡改,導致喪失了對內存的訪問方式,無法釋放申請的內存

7.4 C++內存模型(堆、棧、靜態區)

C++內存分爲5個區域:
  1. 堆 heap :
    由new分配的內存塊,其釋放編譯器不去管,由我們程序自己控制(一個new對應一個delete)。如果程序員沒有釋放掉,在程序結束時OS會自動回收。涉及的問題:“緩衝區溢出”、“內存泄露”

  2. 棧 stack :
    是那些編譯器在需要時分配,在不需要時自動清除的存儲區。存放局部變量、函數參數。存放在棧中的數據只在當前函數及下一層函數中有效,一旦函數返回了,這些數據也就自動釋放了。函數棧內的變量地址總是連續的,從高地址向低地址生長。

  3. 全局/靜態存儲區 (.bss段和.data段) :
    全局和靜態變量被分配到同一塊內存中。在C語言中,未初始化的靜態變量放在.bss段中,初始化的放在.data段中;在C++裏則不區分了。

  4. 常量存儲區 (.rodata段) :
    存放常量,不允許修改(通過非正當手段也可以修改)

  5. 代碼區 (.text段) :
    存放代碼(如函數),不允許修改(類似常量存儲區),但可以執行(不同於常量存儲區)

根據c/c++對象生命週期不同,c/c++的內存模型有三種不同的內存區域,即

  1. 自由存儲區(棧區):局部非靜態變量的存儲區域,即平常所說的棧
  2. 動態存儲區(堆區): 用operator new ,malloc分配的內存,即平常所說的堆
  3. 靜態存儲區:全局變量 靜態變量 字符串常量存在位置
注意:
  1. 棧區變量要注意析構函數的調用次序,由於是先進後出,則先創建的對象,最後被析構。

7.5 存儲說明符(存儲方案)有哪些?

7個存儲說明符:
  1. auto (C++11去掉),存放在棧區的自動變量
  2. register 存放在寄存器的自動變量
  3. static 存放在靜態區的靜態變量
  4. extern 聲明在外部定義的全局變量
  5. mutable 即使對象聲明爲了const, mutable成員也可以被修改
  6. volatile 聲明不將變量放入寄存器,而是每次訪問都從內存中取值,保證每次的值都是最新的
  7. thread_local 在整個線程週期存在的靜態變量

7.6 堆與棧的區別?

  1. 堆是先進先出,棧是先進後出。
  2. 棧的大小固定,受限於系統中有效的虛擬內存,可能會發生棧溢出;堆可以動態生長
  3. 棧的空間有系統釋放,堆內存由程序員釋放
  4. 堆容易產生碎片
  5. 申請方式上,棧是系統自動分配,堆是由程序員申請

7.7 內存對齊

爲什麼需要內存對齊?
1)平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據,某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常
2)硬件原因:經過內存對齊之後,CPU的內存訪問速度大大提升。

圖一:

我們普通程序員心中的內存印象,由一個個字節組成,但是CPU卻不是這麼看待的

圖二:

cpu把內存當成是一塊一塊的,塊的大小可以是2,4,8,16 個字節,因此CPU在讀取內存的時候是一塊一塊進行讀取的,塊的大小稱爲(memory granularity)內存讀取粒度。

我們再來看看爲什麼內存不對齊會影響讀取速度?

    假設CPU要讀取一個4字節大小的數據到寄存器中(假設內存讀取粒度是4),分兩種情況討論:

           1.數據從0字節開始

        2.數據從1字節開始

解析:當數據從0字節開始的時候,直接將0-3四個字節完全讀取到寄存器,結算完成了。

        當數據從1字節開始的時候,問題很複雜,首先先將前4個字節讀到寄存器,並再次讀取4-7字節的數據進寄存器,接着把0字節,4,6,7字節的數據剔除,最後合併1,2,3,4字節的數據進寄存器,對一個內存未對齊的寄存器進行了這麼多額外操作,大大降低了CPU的性能。

     但是這還屬於樂觀情況,上文提到內存對齊的作用之一是平臺的移植原因,因爲只有部分CPU肯幹,其他部分CPU遇到未對齊邊界就直接罷工了。

內存對齊的三個原則:
  1. 對於結構的各個成員,第一個成員位於偏移爲0的位置,以後的每個數據成員的偏移量必須是 這個數據成員的自身長度(或者可以自己設置)的倍數。
  2. 結構體作爲成員:如果一個結構裏有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲
  3. 結構體的總大小,也就是sizeof的結果,.必須是其內部最大成員的整數倍.不足的要補齊
	typedef struct A{
		int a;//0~4
		double b;//根據規則一,偏移量應該爲sizeof(double)的倍數;8~15
		char c;本來應該16~17但是根據規則三,最後補位16~23
	}A;//所以A的大小應該爲24
	
	struct B{
		int id;0~4
		A a;//規矩規則二,應該爲8~31;
	};
	//所以最後的大小應該爲32

7.8 memcpy 和 memmove的區別

void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
memcpy和memmove()都是C語言中的庫函數,在頭文件string.h中,作用是拷貝一定長度的內存的內容。他們的作用是一樣的,唯一的區別是,當內存發生局部重疊的時候,memmove保證拷貝的結果是正確的,memcpy不保證拷貝的結果的正確。在內存覆蓋情況下, memcpy會報錯。

7.9 動態內存管理

動態內存管理是指管理動態內存,即堆內存。動態內存管理中常見的問題有(發生段錯誤的可能原因):
  • 1. 野指針:一些內存單元已經釋放,但之前指向它的指針還在使用。
  • 2. 重複釋放:程序試圖釋放已經被釋放過的內存單元。
  • 3. 內存泄漏:沒有釋放不再使用的內存單元。
  • 4. 緩衝區溢出:數組越界。
  • 5. 不配對的new[]/delete
針對1~3的問題,C++11提供了只能指針解決。此三種智能指針(unique_ptr、shared_ptr及weak_ptr)使用時,需要包含頭文件:<memory>。

7.10 析構函數會在什麼時候被調用?

1) 變量在離開其作用域時被銷燬
2) 當一個對象被銷燬時,其成員被銷燬
3) 容器被銷燬時,其元素被銷燬
4) 對於動態分配的對象,當對指向它的指針應用delete運算符時被銷燬
5) 對於臨時對象,當創建它的完整表達式結束時被銷燬

7.11 什麼是棧溢出?

棧溢出就是緩衝區溢出的一種。棧溢出就是不顧堆棧中數據塊大小,向該數據塊寫入了過多的數據,導致數據越界,結果覆蓋了老的棧數據。棧是從高地址向低地址方向增漲,堆的方向相反。在一次函數調用中,棧中將被依次壓入:形參,返回地址,EBP(調用地址)。如果函數有局部變量,接下來,就在棧中開闢相應的空間以構造變量。如果這些值的大小超過了函數棧的最大容量(默認的棧大小爲1MB)就會造成棧溢出。因爲棧一般默認爲1-2m,一旦出現死循環或者是大量的遞歸調用,在不斷的壓棧過程中,造成棧容量超過1m而導致溢出。
由於緩衝區溢出而使得有用的存儲單元被改寫,往往會引發不可預料的後果。向這些單元寫入任意的數據,一般只會導致程序崩潰之類的事故,對這種情況我們也至多說這個程序有bug。但如果向這些單元寫入的是精心準備好的數據,就可能使得程序流程被劫持,致使不希望的代碼被執行,落入攻擊者的掌控之中,這就不僅僅是bug,而是漏洞(exploit)了。
解決方案?
(1)用棧將遞歸改寫爲非遞歸
(2)使用靜態變量或者動態變量替代自動變量
(3)增大函數棧的大小
#include <stdio.h>
#include <stdlib.h>
void foo()
{
    printf("foo()\n");
    exit(0);
}
void call()
{
    int buffer[2];
    buffer[3] = (int)foo; // 緩衝區溢出

}
int main(void)
{
    call();
}

八、編譯和鏈接?

8.1 動態鏈接庫和靜態鏈接庫的區別?


用今天的眼光來看,動態鏈接庫節約內存這個優點越來越不重要了,特別是在服務器上:a)、現在PC的內存都很大,指令佔用那一點空間已經微不足道了;b)、由於動態鏈接庫衝突等問題,越來越多的服務器應用更傾向於把所有用到的環境文件用docker打包,這樣一來佔用內存也不少。

相關問題:

8.2 鏈接指示 extern "C"有什麼作用?

extern "C"的主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼。加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。由於C++支持函數重載,因此編譯器編譯函數的過程中會將函數的參數類型也加到編譯後的代碼中,而不僅僅是函數名;而C語言並不支持函數重載,因此編譯C語言代碼的函數時不會帶上函數的參數類型,一般之包括函數名。
這個功能十分有用處,因爲在C++出現以前,很多代碼都是C語言寫的,而且很底層的庫也是C語言寫的,爲了更好的支持原來的C代碼和已經寫好的C語言庫,需要在C++中儘可能的支持C,而extern "C"就是其中的一個策略。
這個功能主要用在下面的情況:
1、C++代碼調用C語言代碼
2、在C++的頭文件中使用
3、在多個人協同開發時,可能有的人比較擅長C語言,而有的人擅長C++,這樣的情況下也會有用到

8.3 現代編譯器的編譯過程?

  1. 預編譯,展開所有的宏定義#define, 處理所有的預編譯指令如#if,遞歸的包含文件#include,刪除所有註釋,添加行號和文件標識,保留所有的編譯指令#pragma。
  2. 編譯
    1. 詞法分析
    2. 語法分析
    3. 語義分析
    4. 優化生成彙編代碼
  3. 彙編,將彙編代碼轉化成機器可以執行的指令,得到目標文件(.o \ .obj),
  4. 連接,鏈接將目標文件進行處理,得到可執行文件。

8.4 pdb文件有什麼用?

Visual Studio 調試需要pdb文件。Native C++ PDB包含了如下的信息:
* public,private 和static函數地址;
* 全局變量的名字和地址;
* 參數和局部變量的名字和在堆棧的偏移量;
* class,structure 和數據的類型定義;
* Frame Pointer Omission 數據,用來在x86上的native堆棧的遍歷;
* 源代碼文件的名字和行數;

九、實現函數和類

9.1 char *strcpy(char *dst, const char *src);

http://blog.csdn.net/yangquanhui1991/article/details/51804600
char *strcpy(char *dst, const char *src);
返回dst的原始值使函數能夠支持鏈式表達式:strlen(strcpy(strA,strB)); 假如考慮dst和src內存重疊的情況,strcpy該怎麼實現 char s[10]="hello"; strcpy(s, s+1); //應返回ello, strcpy(s+1, s);
//應返回hhello,但實際會報錯,因爲dst與src重疊了,把'\0'覆蓋了
//
//C語言標準庫函數strcpy的一種典型的工業級的最簡實現。
 
//返回值:目標串的地址。
 
//對於出現異常的情況ANSI-C99標準並未定義,故由實現者決定返回值,通常爲NULL。
 
//參數:des爲目標字符串,source爲原字符串。
 
 
 
char* strcpy(char* des,const char* source)
 
{
 
 char* r=des;
   
  assert((des != NULL) && (source != NULL));
 
 while((*r++ = *source++)!='\0');
 
 return des;
 
}
//while((*des++=*source++));的解釋:賦值表達式返回左操作數,所以在賦值'\0'後,循環停止。

9.2 string類

主要的數據成員是 char* data和 int size; 
#include <iostream>
#include <cstring>
using namespace std;

class String {
public:
    // 默認構造函數
    String(const char* str = NULL);
    // 複製構造函數
    String(const String &str);
    // 析構函數
    ~String();
    // 字符串連接
    String operator+(const String & str);
    // 字符串賦值
    String & operator=(const String &str);
    // 字符串賦值
    String & operator=(const char* str);
    // 判斷是否字符串相等
    bool operator==(const String &str);
    // 獲取字符串長度
    int length();
    // 求子字符串[start,start+n-1]
    String substr(int start, int n);
    // 重載輸出
    friend ostream & operator<<(ostream &o, const String &str);
private:
    char* data;
    int size;
};
// 構造函數
String::String(const char *str) {
    if (str == NULL) {
        data = new char[1];
        data[0] = '\0';
        size = 0;
    }//if
    else {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }//else
}
// 複製構造函數
String::String(const String &str) {
    size = str.size;
    data = new char[size + 1];
    strcpy(data, str.data);
}
// 析構函數
String::~String() {
    delete[] data;
}
// 字符串連接
String String::operator+(const String &str) {
    String newStr;
    //釋放原有空間
    delete[] newStr.data;
    newStr.size = size + str.size;
    newStr.data = new char[newStr.size + 1];
    strcpy(newStr.data, data);
    strcpy(newStr.data + size, str.data);
    return newStr;
}
// 字符串賦值
String & String::operator=(const String &str) {
    if (data == str.data) { // 注意要先判斷是否是自己給自己賦值
        return *this;
    }//if
    delete[] data;
    size = str.size;
    data = new char[size + 1];
    strcpy(data, str.data);
    return *this;
}
// 字符串賦值
String& String::operator=(const char* str) {
    if (data == str) {
        return *this;
    }//if
    delete[] data;
    size = strlen(str);
    data = new char[size + 1];
    strcpy(data, str);
    return *this;
}
// 判斷是否字符串相等
bool String::operator==(const String &str) {
    return strcmp(data, str.data) == 0;
}
// 獲取字符串長度
int String::length() {
    return size;
}
// 求子字符串[start,start+n-1]
String String::substr(int start, int n) {
    String newStr;
    // 釋放原有內存
    delete[] newStr.data;
    // 重新申請內存
    newStr.data = new char[n + 1];
    for (int i = 0; i < n; ++i) {
        newStr.data[i] = data[start + i];
    }//for
    newStr.data[n] = '\0';
    newStr.size = n;
    return newStr;
}
// 重載輸出
ostream & operator<<(ostream &o, const String &str) {
    o << str.data;
    return o;
}

int main() {
    String str1("hello ");
    String str2 = "world";
    String str3 = str1 + str2;
    cout << "str1->" << str1 << " size->" << str1.length() << endl;
    cout << "str2->" << str2 << " size->" << str2.length() << endl;
    cout << "str3->" << str3 << " size->" << str3.length() << endl;

    String str4("helloworld");
    if (str3 == str4) {
        cout << str3 << " 和 " << str4 << " 是一樣的" << endl;
    }//if
    else {
        cout << str3 << " 和 " << str4 << " 是不一樣的" << endl;
    }

    cout << str3.substr(6, 5) << " size->" << str3.substr(6, 5).length() << endl;
    return 0;
}

9.3 void* memcpy(void* dst, const void* src, size_t n)

memcpy將源移動n個字節到目標,但是源和目的的內存區域不能重疊,不保證內存覆蓋時移動正確
void* my_memcpy(void* dst, const void* src, size_t n)  
{  
    char *tmp = (char*)dst;  
    char *s_src = (char*)src;  
  
    while(n--) {  
        *tmp++ = *s_src++;  
    }  
    return dst;  
}  

9.4 void* memmove(void* dst, const void* src, size_t n)

memmove保證內存覆蓋時移動正確
void* my_memmove(void* dst, const void* src, size_t n)  
{  
    char* s_dst;  
    char* s_src;  
    s_dst = (char*)dst;  
    s_src = (char*)src;  
    if(s_dst>s_src && (s_src+n>s_dst)) {      //-------------------------第二種內存覆蓋的情形。  
        s_dst = s_dst+n-1;  
        s_src = s_src+n-1;  
        while(n--) {  
            *s_dst-- = *s_src--;  
        }  
    }else {  
        while(n--) {  
            *s_dst++ = *s_src++;  
        }  
    }  
    return dst;  
}  



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章