本文總結一下C++面試時常遇到的問題。C++面試中,主要涉及的考點有
- 關鍵字極其用法,常考的關鍵字有const, sizeof, typedef, inline, static, extern, new, delete等等
- 語法問題
- 類型轉換
- 指針以及指針和引用的區別
- 面向對象的相關問題,如虛函數機制等
- 泛型編程的相關問題,如模板和函數的區別等
- 內存管理,如字節對齊(內存對齊)、動態內存管理、內存泄漏等
- 編譯和鏈接
- 實現函數和類
零、序章
0.1 C++與C的對比
- C++有三種編程方式:過程性,面向對象,泛型編程。
- C++函數符號由 函數名+參數類型 組成,C只有函數名。所以,C沒有函數重載的概念。
- C++ 在 C的基礎上增加了封裝、繼承、多態的概念
- C++增加了泛型編程
- C++增加了異常處理,C沒有異常處理
- C++增加了bool型
- C++允許無名的函數形參(如果這個形參沒有被用到的話)
- C允許main函數調用自己
- C++支持默認參數,C不支持
- C語言中,局部變量必須在函數開頭定義,不允許類似for(int a = 0; ;;)這種定義方法。
- C++增加了引用
- C允許變長數組,C++不允許
- C中函數原型可選,C++中在調用之前必須聲明函數原型
- C++增加了STL標準模板庫來支持數據結構和算法
一、重要的關鍵字極其用法
1.1 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 int& a = 1;
- const 成員方法本質上是使得this指針是指向const對象的指針,所以在const方法內,
- 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/
- const 指針、指向const的指針和指向const的const指針,涉及到const的特性“const左效、最左右效”
- const 全局變量有內部鏈接性,即不同的文件可以定義不同的同名const全局變量,使用extern定義可以消除內部鏈接性,稱爲類似全局變量,如extern const int a = 10.另一個文件使用extern const int a; 來引用。而且編譯器會在編譯時,將const變量替換爲它的值,類似define那樣。
const 常量和define 的區別
- const常量有數據類型,而宏定義沒有數據類型。編譯器可以對前者進行類型安全檢查,而對後者只進行字符替換,沒有類型安全檢查,並且在字符替換中可能會產生意想不到的錯誤(邊際效應)。
- 有些集成化的調試工具可以對const常量進行調試,但是不能對宏定義進行調試。
- 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
- 內存空間的分配上。define進行宏定義的時候,不會分配內存空間,編譯時會在main函數裏進行替換,只是單純的替換,不會進行任何檢查,比如類型,語句結構等,即宏定義常量只是純粹的置放關係,如#define null 0;編譯器在遇到null時總是用0代替null它沒有數據類型.而const定義的常量具有數據類型,定義數據類型的常量便於編譯器進行數據檢查,使程序可能出現錯誤進行排查,所以const與define之間的區別在於const定義常量排除了程序之間的不安全性.
- const常量存在於程序的數據段,#define常量存在於程序的代碼段
- const常量存在“常量摺疊”,在編譯器進行語法分析的時候,將常量表達式計算求值,並用求得的值來替換表達式,放入常量表,可以算作一種編譯優化。因爲編譯器在優化的過程中,會把碰見的const全部以內容替換掉,類似宏。
1.2 sizeof
- sizeof關鍵字不會計算表達式的值,而只會根據類型推斷大小。
- sizeof() 的括號可以省略, 如 sizeof a ;
- 類A的大小是 所有非靜態成員變量大小之和+虛函數指針大小
1.3 static
- 該變量在全局數據區分配內存;
- 未經初始化的靜態全局變量會被程序自動初始化爲0(自動變量的值是隨機的,除非它被顯式初始化);
- 靜態全局變量在聲明它的整個文件都是可見的,而在文件之外是不可見的;
- 該變量在全局數據區分配內存;
- 靜態局部變量在程序執行到該對象的聲明處時被首次初始化,即以後的函數調用不再進行初始化;
- 靜態局部變量一般在聲明處初始化,如果沒有顯式初始化,會被程序自動初始化爲0;
- 它始終駐留在全局數據區,直到程序運行結束。但其作用域爲局部作用域,當定義它的函數或語句塊結束時,其作用域隨之結束;
- 靜態數據成員沒有進入程序的全局名字空間,因此不存在與程序中其它全局名字衝突的可能性;
- 可以實現信息隱藏。靜態數據成員可以是private成員,而全局變量不能;
1.4 typedef
與宏定義的對比
- #define 在預處理階段進行簡單替換,不做類型檢查; typedef在編譯階段處理,在作用域內給類型一個別名。
- typedef 是一個語句,結尾有分號;#define是一個宏指令,結尾沒有分號
- typedef int* pInt; 和 #define pInt int* 不等價,前者定義 pInt a, b;會定義兩個指針,後者是一個指針,一個int。
不能聲明爲inline的函數
- 包含了遞歸、循環等結構的函數一般不會被內聯。
- 虛擬函數一般不會內聯,但是如果編譯器能在編譯時確定具體的調用函數,那麼仍然會就地展開該函數。
- 如果通過函數指針調用內聯函數,那麼該函數將不會內聯而是通過call進行調用。
- 構造和析構函數一般會生成大量代碼,因此一般也不適合內聯。
- 如果內聯函數調用了其他函數也不會被內聯。
1.5 inline
與宏函數的對比
- 內聯函數在運行時可調試,而宏定義不可以;
- 編譯器會對內聯函數的參數類型做安全檢查或自動類型轉換(同普通函數),而宏定義則不會;
- 內聯函數可以訪問類的成員變量,宏定義則不能;
- 在類中聲明同時定義的成員函數,自動轉化爲內聯函數
- 宏只是預定義的函數,在編譯階段不進行類型安全性檢查,在編譯的時候將對應函數用宏命令替換。對程序性能無影響
1.6 static const \ const \ static
static const 數據成員可以在類內初始化 也可以在類外,不能在構造函數中初始化,也不能在構造函數的初始化列表中初始化
2. static
static數據成員只能在類外,即類的實現文件中初始化,也不能在構造函數中初始化,不能在構造函數的初始化列表中初始化;
3. const
const數據成員只能在構造函數的初始化列表中初始化;
1.7 explicit
1.8 extern
二、語法問題
2.1 a++ 與 ++a的區別
- a++ 返回加之前的值,++a返回加之後的a變量
- a++返回的是一個臨時變量,是右值,無法賦值;++a返回的是變量a,是左值
2.2 switch語句
2.3 函數調用過程
- +++++++++ 入棧 ++++++++++++
- 將實參從右向左壓入棧
- 壓入返回地址
- 壓入主調函數的基地址
- 跳到被調用函數的地址,執行函數代碼,局部變量按聲明順序依次壓入棧
- 將返回值放入寄存器eax(累加器)中
- +++++++++ 出棧 ++++++++++++
- 局部變量全部出棧
- 返回地址出棧,找到原執行地址
- 形參出棧
- 賦值操作將寄存器中的返回值賦給左值(如果有的話)
2.4 左值與右值
2.5 C語言標識符
2.6 全局變量的優缺點
(1)可以減少變量的個數
(2)全局變量破壞了函數的封裝性能。前面的章節曾經講過,函數象一個黑匣子,一般是通過函數參數和返回值進行輸入輸出,函數內部實現相對獨立。但函數中 如果使用了全局變量,那麼函數體內的語句就可以繞過函數參數和返回值進行存取,這種情況破壞了函數的獨立性,使函數對全局變量產生依賴。同時,也降低了該 函數的可移植性。
(3)全局變量使函數的代碼可讀性降低。由於多個函數都可能使用全局變量,函數執行時全局變量的值可能隨時發生變化,對於程序的查錯和調試都非常不利。
2.7 複合類型有哪些?
2.8 運算符優先級和結合性?
優先級有15種。記憶方法如下:
記住一個最高的:構造類型的元素或成員以及小括號。
記住一個最低的:逗號運算符。
剩餘的是一、二、三、賦值。
意思是單目、雙目、三目和賦值運算符。
在諸多運算符中,又分爲:
算術、關係、邏輯。
兩種位操作運算符中,移位運算符在算術運算符後邊,邏輯位運算符在邏輯運算符的前面。再細分如下:
算術運算符分 *,/,%高於+,-。
關係運算符中,〉,〉=,<,<=高於==,!=。
邏輯運算符中,除了邏輯求反(!)是單目外,邏輯與(&&)高於邏輯或(||)。
邏輯位運算符中,除了邏輯按位求反(~)外,按位與(&)高於按位半加(^),高於按位或(|)。
這樣就將15種優先級都記住了,再將記憶方法總結如下:
去掉一個最高的,去掉一個最低的,剩下的是一、二、三、賦值。雙目運算符中,順序爲 算術、移位、關係(>,<,==)、邏輯位和邏輯(&& ||)。
2.9 using 聲明和using 編譯指令的區別?哪個更好?
2.10 for循環的效率問題
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 四種類型強制轉換
- dynamic_cast:該轉換符用於將一個指向派生類的基類指針或引用轉換爲派生類的指針或引用。
- const_cast:最常用的用途就是刪除const屬性。
- static_cast:static_cast本質上是傳統c語言強制轉換的替代品,比C類型轉換更嚴格, 該操作符用於非多態類型的轉換,任何標準轉換都可以使用他,即static_cast可以把int轉換爲double,但不能把兩個不相關的類對象進行轉換,比如類A不能轉換爲一個不相關的類B類型。static_cast在類對象和基礎類型轉換中,會調用類的構造函數,和類型轉換運算符比如operator int(),來進行顯示轉換。
- reinterpret_cast:該操作符用於將一種類型轉換爲另一種不同的類型,比如可以把一個整型轉換爲一個指針,或把一個指針轉換爲一個整型,因此使用該操作符的危險性較高,一般不應使用該操作符。
四、指針
4.1 指針與引用的區別
- 指針是一個變量,引用只是別名
- 指針需要解引用才能訪問對象,引用不需要
- 引用在定義時必須初始化,且以後不可轉移引用的對象,指針可以
- 引用沒有const,即int& const a ;沒有;而指針有const指針,即int* const ptr;
- 引用不可以爲空;而指針可以
- 指針變量需要分配棧空間;而引用不需要,僅僅是個別名
- sizeof(引用)得到對應對象的大小;sizeof(指針)得到指針大小
- 指針加法和引用加法不一樣
- 引用不需要釋放內存空間,在編譯時就會優化掉
4.2 指針與數組名的區別
- 數組名不是指針,對數組名取地址,得到整個數組的地址
- 數組名 + 1會跳過整個數組的大小,指針+1只會跳過一個元素的大小
- 數組名作爲函數參數傳遞時,退化爲指針
- sizeof(數組名)返回整個數組的大小,sizeof(指針)返回指針大小
- 數組名無法修改值,是常量
- int (*p)[] = &arr; 纔是正確的數組指針寫法
4.3 野指針、空指針的概念
- 野指針是指指向無效內存的指針,不能對野指針取內容,delete
- 空指針是指置爲0\NULL\nullptr的指針,可以對空指針delete多次
五、面向對象
5.1 面向對象的三大特性
- 封裝:封裝是實現面向對象程序設計的第一步,封裝就是將數據或函數等集合在一個個的單元中(我們稱之爲類)。封裝的意義在於保護或者防止代碼(數據)被我們無意中破壞。
- 繼承:繼承主要實現重用代碼,節省開發時間。子類可以繼承父類的一些東西。
- 多態:同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果。分爲編譯時多態和運行時多態。
5.2 函數重載和運算符重載
問:函數重載的依據?
- 參數個數
- 參數類型
- const方法與非const方法構成重載
問:運算符重載的限制?
- 被重載的運算符,至少有一個操作數是用戶自定義類型,也就是說不能重載C++語言的標準運算
- 重載的運算符的句法規則不可以改變,操作數、結合性和優先級無法更改。以前是幾元現在就是幾元;該是左結合還是左結合;優先級無法更改。
- 不能自定義運算符,不能創建新的運算符。
- 不能重載的運算符有:
- 成員訪問運算符 .
- 成員指針運算符 .*
- 作用域解析運算符 ::
- 條件運算符 ?:
- sizeof
- typeid
- 四個類型轉換運算符
- const_cast
- static_cast
- dynamic_cast
- reinterpret_cast
- 只能通過成員函數重載,而不能通過友元重載的運算符:
- 賦值運算符 =
- 函數調用運算符 ()
- 下標運算符 []
- 通過指針訪問成員運算符 ->
- 只能通過友元重載,不能通過成員函數重載的情況:
- 雙目運算符最好用友元重載,單目運算符最好用成員函數重載
- 若運算符所需的操作數(尤其是第一個操作數)希望有隱式類型轉換,則只能選用友元函數
- 左操作數是不同類的對象或者內部類型,比如ostream, istream, int, float等
- 當需要重載運算符具有可交換性時,選擇重載爲友元函數
- 對返回類型沒有限制,可以是void或者其他類型
- 重載一元運算符需要注意,由於一元運算符沒有參數,前綴和後綴無法區分,所以需要加一個啞元(dummy),啞元永遠用不上,如果有啞元,則是後綴形式,否則,就是前綴。
5.3 哪些成員無法被繼承?
- 無法被繼承的有
- 構造函數
- 析構函數
- 賦值運算符
- 友元函數
- 可以被繼承的有
- 靜態成員
- 靜態方法
- 非靜態成員
- 非靜態方法(無論是private\public\protected,只是private的繼承了也無法訪問)
- 虛表指針
5.4 定義默認構造函數的兩種方法?
- 給已有的構造函數中的一個的所有參數加上默認值
- 通過方法重載定義一個無參數構造函數
- 隱式調用默認構造函數不要加括號(), 會被編譯器解釋爲函數聲明。
5.5 調用非默認構造函數的三種方法?
- Foo f(...); // 隱式調用
- Foo f = Foo(...) ;// 顯式調用
- Foo* f = new Foo(); // 顯式調用
5.6 由編譯器生成的6個成員函數?
- 默認構造函數
- 析構函數
- 複製構造函數
- 賦值運算符
- 取地址運算符
- 取地址運算符 const版本
5.7 友元的三種實現方式
- 友元函數
- 友元類
- 友元成員函數
5.8 爲什麼基類的析構函數爲什麼要聲明爲虛函數?
5.9 爲什麼構造函數不可以是虛函數?
- 虛函數在運行期決定函數調用,而在構造一個對象時,由於對象還未構造成功,編譯器無法確定對象的實際類型,繼而無法決定調用哪一個構造函數。
- 虛函數的執行依賴於虛函數表,而虛函數表在構造函數中進行初始化工作,即初始化 vptr,讓它指向正確的虛函數表,而在構造期間,虛函數表還沒有初始化,所以無法決定調用哪個構造函數。
5.10 析構函數什麼時候聲明爲私有?什麼時候不能聲明爲私有?
- 私有析構函數可以使得對象只在堆上構造。在棧上創建的對象要求構造函數和析構函數必須都是公有的,否則編譯器報錯“析構函數不可訪問”;而堆對象由程序員創建和刪除,可以把析構函數聲明爲私有的。由於delete會調用析構函數,而私有的析構無法被訪問,編譯器報錯,此時通過增加一個destroy()方法,在方法內調用析構函數來釋放對象:
- void destroy()
- {
- delete this;
- }
- 析構函數不能聲明爲私有的情況:基類的析構函數不能聲明爲私有,因爲要在派生類的析構函數中被隱式調用。
5.11 構造函數什麼時候聲明爲私有?什麼時候不能聲明爲私有?
- 單例模式。
- 基類的構造函數不能聲明爲私有,因爲要在派生類的構造函數中被隱式調用。如果在派生類的構造函數中沒有顯式調用基類的構造,則會調用基類的默認構造函數。
5.12 不能聲明爲虛函數的成員函數
構造函數:
首先明確一點,在編譯期間編譯器完成了虛表的創建,而虛指針在構造函數期間被初始化。
如果構造函數是虛函數,那必然需要通過虛指針來找到虛構造函數的入口地址,但是這個時候我們還沒有把虛指針初始化。因此,構造函數不能是虛函數。
內聯函數:
編譯期內聯函數在調用處被展開,而虛函數在運行時才能被確定具體調用哪個類的虛函數。內聯函數體現的是編譯期機制,而虛函數體現的是運行期機制。
靜態成員函數:
靜態成員函數和類有關,即使沒有生成一個實例對象,也可以調用類的靜態成員函數。而虛函數的調用和虛指針有關,虛指針存在於一個類的實例對象中,如果靜態成員函數被聲明成虛函數,那麼調用成員靜態函數時又如何訪問虛指針呢。總之可以這麼理解,靜態成員函數與類有關,而虛函數與類的實例對象有關。
非成員函數:
虛函數的目的是爲了實現多態,多態和繼承有關。所以聲明一個非成員函數爲虛函數沒有任何意義。
5.13 虛函數機制以及內存分佈
- 虛函數表指針 vfptr和虛函數表 vftable
- 虛繼承下還涉及 虛基類表指針 vbptr和虛基類表 vbtable
5.14 class 與 struct的區別
- class默認的繼承方式爲private, struct 默認繼承方式爲public
- class的成員訪問默認爲private, struct默認爲public
5.15 重載、重寫(覆蓋)與隱藏(重定義)的關係
- 重載。函數名相同,參數個數、類型不同,或者用const重載。是同一個類中方法之間的關係,是水平關係。
- 重寫。派生類重新定義基類中有相同名稱和參數的虛函數,要求參數列表必須相同。方法在基類和派生中的訪問限制可以不同。
- 隱藏。派生類重新定義基類中有相同名稱的函數(參數列表可以不同)會把其他基類的同名方法隱藏起來,無法被派生類調用。
5.16 哪些情況下方法可以不寫定義?
- 純虛方法
- 非虛方法
5.17 派生類可以不實現虛基類的純虛方法,派生類也成了抽象類。
5.18 三種繼承方式(public, private, protected)的區別?
- 公有繼承(public): 基類成員對其對象的可見性與一般類及其對象的可見性相同,public成員可見,protected和private成員不可見,基類成員對派生類的可見性對派生類來說,基類的public和protected成員可見:基類的public成員和protected成員作爲派生類的成員時,它們都保持原有狀態;基類的private成員依舊是private,派生類不可訪問基類中的private成員。 基類成員對派生類對象的可見性對派生類對象來說,基類的public成員是可見的,其他成員是不可見的。 所以,在公有繼承時,派生類的對象可以訪問基類中的public成員,派生類的成員方法可以訪問基類中的public成員和protected成員。
- 私有繼承(private) 基類成員對其對象的可見性與一般類及其對象的可見性相同,public成員可見,其他成員不可見,基類成員對派生類的可見性對派生類來說,基類的public成員和protected成員是可見的:基類的public成員和protected成員都作爲派生類的private成員,並且不能被這個派生類的子類所訪問;基類的私有成員是不可見的:派生類不可訪問基類中的private成員,基類成員對派生類對象的可見性對派生類對象來說,基類的所有成員都是不可見的,所以在私有繼承時,基類的成員只能由直接派生類訪問,無法再往下繼承。
- 保護繼承(protected) 保護繼承與私有繼承相似,基類成員對其對象的可見性與一般類及其對象的可見性相同,public成員可見,其他成員不可見,基類成員對派生類的可見性,對派生類來說,基類的public和protected成員是可見的:基類的public成員和protected成員都作爲派生類的protected成員,並且不能被這個派生類的子類所訪問;基類的private成員是不可見的:派生類不可訪問基類中的private成員。基類成員對派生類對象的可見性對派生類對象來說,基類的所有成員都是不可見的。所以,在保護繼承時,基類的成員也只能由直接派生類訪問,而無法再向下繼承。C++支持多重繼承。多重繼承是一個類從多個基類派生而來的能力。派生類實際上獲取了所有基類的特性。當一個類 是兩個或多個基類的派生類時,派生類的構造函數必須激活所有基類的構造函數,並把相應的參數傳遞給它們 。
5.19 如果賦值構造函數參數不是傳引用而是傳值會有什麼問題?
5.20 如何實現只能動態分配類對象,不能定義類對象?
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 如何實現只能在棧上創建對象?不能在堆上創建對象?
class A { private: void* operator new(size_t t){} // 注意函數的第一個參數和返回值都是固定的 void operator delete(void* ptr){} // 重載了new就需要重載delete public: A(){} ~A(){} };
5.22 必須在構造函數初始化式裏進行初始化的數據成員有哪些?
- 常量成員,因爲常量只能初始化不能賦值,所以必須放在初始化列表裏面
- 引用類型,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裏面
- 沒有默認構造函數的類類型,因爲使用初始化列表可以不必調用默認構造函數來初始化,而是直接調用拷貝構造函數初始化
5.23 抽象類和接口的區別?
5.24 虛基類和虛繼承,虛基指針和虛基表
(2)若同一層次中包含多個虛基類,這些虛基類的構造函數按它們說明的次序調用;
(3)若虛基類由非虛基類派生而來,則仍先調用基類構造函數,再調用派生類的構造函數。
5.25 構造函數和析構函數中可以調用調用虛函數嗎?
5.26 構造函數和析構函數調用順序?
- 先調用基類構造函數
- 在調用成員類構造函數
- 最後調用本身的構造函數
- 析構順序相反
5.27 動態綁定如何實現?
5.28 多態性有哪些?
- 編譯時多態(靜態綁定),函數重載,運算符重載,模板。
- 運行時多態(動態綁定),虛函數機制。
5.29 構造函數可不可以拋出異常?析構函數呢?
2. 不要在析構函數中拋出異常!
2)通常異常發生時,c++的機制會調用已經構造對象的析構函數來釋放資源,此時若析構函數本身也拋出異常,則前一個異常尚未處理,又有新的異常,會造成程序崩潰的問題。
5.30 成員函數調用底層機制?
六、泛型編程
6.1 使用模板的優點和缺點?
- 在一些場景可以避免重複代碼
- 有些問題難以使用OO技巧(如繼承和多態)來實現,而使用模版會很方便
- template classes更加的類型安全,因其參數類型在編譯時都是已知的。
- 一些編譯器對template支持不好。
- 編譯器給出的有些出錯信息比較晦澀。
- 爲每種類型都生成額外的代碼,可能導致生成的exe膨脹。
- 使用templates寫的代碼難以調試
- templates在頭文件中,這樣一旦有所變更需要重編譯所有相關工程
6.2 模板函數和函數的對比?
- 模板函數由函數模板實例化而來,編譯器推斷模板實參,然後實例化出對應的函數定義。模板函數是函數模板的實例。
- 普通函數需要程序員手動重載才能實現對於不同類型參數的支持。
- 函數模板只能用於函數的參數個數相同而類型不同的情況,如果參數個數不同,則不能使用函數模板,只能使用重載。
- 函數模板必須要求所有實參的類型T都相同,無法進行隱式類型轉換。
- 進行函數調用時,編譯器優先選擇匹配的非模板函數,如果找不到再試着進行函數模板的實例化,如果還不行,則這個調用違法。這樣做可以減少函數模板實例化次數,提高效率。
6.3 模板的全特化和偏特化?
模板爲什麼要特化,因爲編譯器認爲,對於特定的類型,如果你能對某一功能更好的實現,那麼就該聽你的。
模板分爲類模板與函數模板,特化分爲全特化與偏特化。全特化就是限定死模板實現的具體類型,偏特化就是如果這個模板有多個類型,那麼只限定其中的一部分。
先看類模板:
- template<typename T1, typename T2>
- class Test
- {
- public:
- Test(T1 i,T2 j):a(i),b(j){cout<<"模板類"<<endl;}
- private:
- T1 a;
- T2 b;
- };
- template<>
- class Test<int , char>
- {
- public:
- Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
- private:
- int a;
- char b;
- };
- template <typename T2>
- class Test<char, T2>
- {
- public:
- Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
- private:
- char a;
- T2 b;
- };
那麼下面3句依次調用類模板、全特化與偏特化:
- Test<double , double> t1(0.1,0.2);
- Test<int , char> t2(1,'A');
- Test<char, bool> t3('A',true);
而對於函數模板,卻只有全特化,不能偏特化:
- //模板函數
- template<typename T1, typename T2>
- void fun(T1 a , T2 b)
- {
- cout<<"模板函數"<<endl;
- }
- //全特化
- template<>
- void fun<int ,char >(int a, char b)
- {
- cout<<"全特化"<<endl;
- }
- //函數不存在偏特化:下面的代碼是錯誤的
- /*
- template<typename T2>
- void fun<char,T2>(char a, T2 b)
- {
- cout<<"偏特化"<<endl;
- }
- */
注意:
- 至於爲什麼函數不能偏特化,似乎不是因爲語言實現不了,而是因爲偏特化的功能可以通過函數的重載完成。
- 函數模版的全特化不參與函數重載, 並且優先級低於函數基礎模版參與匹配,也就是說,匹配的順序是:
七、內存管理
7.1 new與malloc的區別,delet和free的區別?內部實現?
- new 是運算符,malloc是庫函數
- new會調用構造函數,malloc只申請內存
- new返回指定類型的指針,malloc返回void指針
- new自動計算所需的內存大小,malloc需要手動設置空間
- new可以被重載
- delete 是運算符,free是庫函數
- delete會調用析構函數,free是會釋放內存
- 使用free之前要檢查指針是否爲空指針,delete不需要,對空指針delete沒有問題
- free 和 delete 不能混用,也就是說new 分配的內存空間最好不要使用使用free 來釋放,malloc 分配的空間也不要使用 delete來釋放
7.2 malloc, calloc, realloc, 和 alloca 申請內存的區別?
- calloc 是申請N個大小爲S的空間,且會初始化空間值爲0;malloc不會初始化,是隨機的垃圾數據(在VS Debug模式下,會是0xcccccc這種特殊值,爲了調試方便)
- malloc 是在堆上申請大小爲S的一個空間,但不會初始化
- realloc 是將原本分配的內存擴充到新的大小,要求新的大小必須大於原大小
- alloca 是在棧上申請空間,不需要(不能)使用free,運行到作用域以外的時候釋放申請的空間
7.3 內存泄漏(內存溢出)有哪些因素?
- 在類的構造函數和析構函數中沒有匹配的調用new和delete函數 兩種情況下會出現這種內存泄露:一是在堆裏創建了對象佔用了內存,但是沒有顯示地釋放對象佔用的內 存;二是在類的構造函數中動態的分配了內存,但是在析構函數中沒有釋放內存或者沒有正確的釋放內存
- 沒有正確地清除嵌套的對象指針
- 在釋放對象數組時在delete中沒有使用方括號
- 指向對象的指針數組不等同於對象數組 對象數組是指:數組中存放的是對象,只需要delete []p,即可調用對象數組中的每個對象的析構函數釋放空間 指向對象的指針數組是指:數組中存放的是指向對象的指針,不僅要釋放每個對象的空間,還要釋放每個指針的空間,delete []p只是釋放了每個指針,但是並沒有釋放對象的空間,正確的做法,是通過一個循環,將每個對象釋放了,然後再把指針釋放了
- 缺少拷貝構造函數
- 兩次釋放相同的內存是一種錯誤的做法,同時可能會造成堆的奔潰。 按值傳遞會調用(拷貝)構造函數,引用傳遞不會調用。 在C++中,如果沒有定義拷貝構造函數,那麼編譯器就會調用默認的拷貝構造函數,會逐個成員拷貝的方式來複制數據成員,如果是以逐個成員拷貝的方式來複制指針被定義爲將一個變量的地址賦給另一個變量。這種隱式的指針複製結果就是兩個對象擁有指向同一個動態分配的內存空間的指針。當釋放第一個對象的時候,它的析構函數就會釋放與該對象有關的動態分配的內存空間。而釋放第二個對象的時候,它的析構函數會釋放相同的內存,這樣是錯誤的。 所以,如果一個類裏面有指針成員變量,要麼必須顯示的寫拷貝構造函數和重載賦值運算符,要麼禁用拷貝構造函數和重載賦值運算符
- 沒有將基類的析構函數定義爲虛函數
- 指針的值被篡改,導致喪失了對內存的訪問方式,無法釋放申請的內存
7.4 C++內存模型(堆、棧、靜態區)
-
堆 heap :
由new分配的內存塊,其釋放編譯器不去管,由我們程序自己控制(一個new對應一個delete)。如果程序員沒有釋放掉,在程序結束時OS會自動回收。涉及的問題:“緩衝區溢出”、“內存泄露” -
棧 stack :
是那些編譯器在需要時分配,在不需要時自動清除的存儲區。存放局部變量、函數參數。存放在棧中的數據只在當前函數及下一層函數中有效,一旦函數返回了,這些數據也就自動釋放了。函數棧內的變量地址總是連續的,從高地址向低地址生長。 -
全局/靜態存儲區 (.bss段和.data段) :
全局和靜態變量被分配到同一塊內存中。在C語言中,未初始化的靜態變量放在.bss段中,初始化的放在.data段中;在C++裏則不區分了。 -
常量存儲區 (.rodata段) :
存放常量,不允許修改(通過非正當手段也可以修改) -
代碼區 (.text段) :
存放代碼(如函數),不允許修改(類似常量存儲區),但可以執行(不同於常量存儲區)
根據c/c++對象生命週期不同,c/c++的內存模型有三種不同的內存區域,即
- 自由存儲區(棧區):局部非靜態變量的存儲區域,即平常所說的棧
- 動態存儲區(堆區): 用operator new ,malloc分配的內存,即平常所說的堆
- 靜態存儲區:全局變量 靜態變量 字符串常量存在位置
- 棧區變量要注意析構函數的調用次序,由於是先進後出,則先創建的對象,最後被析構。
7.5 存儲說明符(存儲方案)有哪些?
- auto (C++11去掉),存放在棧區的自動變量
- register 存放在寄存器的自動變量
- static 存放在靜態區的靜態變量
- extern 聲明在外部定義的全局變量
- mutable 即使對象聲明爲了const, mutable成員也可以被修改
- volatile 聲明不將變量放入寄存器,而是每次訪問都從內存中取值,保證每次的值都是最新的
- thread_local 在整個線程週期存在的靜態變量
7.6 堆與棧的區別?
- 堆是先進先出,棧是先進後出。
- 棧的大小固定,受限於系統中有效的虛擬內存,可能會發生棧溢出;堆可以動態生長
- 棧的空間有系統釋放,堆內存由程序員釋放
- 堆容易產生碎片
- 申請方式上,棧是系統自動分配,堆是由程序員申請
7.7 內存對齊
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遇到未對齊邊界就直接罷工了。
- 對於結構的各個成員,第一個成員位於偏移爲0的位置,以後的每個數據成員的偏移量必須是 這個數據成員的自身長度(或者可以自己設置)的倍數。
- 結構體作爲成員:如果一個結構裏有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲
- 結構體的總大小,也就是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 *memmove(void *dst, const void *src, size_t count);
7.9 動態內存管理
- 1. 野指針:一些內存單元已經釋放,但之前指向它的指針還在使用。
- 2. 重複釋放:程序試圖釋放已經被釋放過的內存單元。
- 3. 內存泄漏:沒有釋放不再使用的內存單元。
- 4. 緩衝區溢出:數組越界。
- 5. 不配對的new[]/delete
7.10 析構函數會在什麼時候被調用?
2) 當一個對象被銷燬時,其成員被銷燬
3) 容器被銷燬時,其元素被銷燬
4) 對於動態分配的對象,當對指向它的指針應用delete運算符時被銷燬
5) 對於臨時對象,當創建它的完整表達式結束時被銷燬
7.11 什麼是棧溢出?
#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 動態鏈接庫和靜態鏈接庫的區別?
8.2 鏈接指示 extern "C"有什麼作用?
這個功能十分有用處,因爲在C++出現以前,很多代碼都是C語言寫的,而且很底層的庫也是C語言寫的,爲了更好的支持原來的C代碼和已經寫好的C語言庫,需要在C++中儘可能的支持C,而extern "C"就是其中的一個策略。
這個功能主要用在下面的情況:
1、C++代碼調用C語言代碼
2、在C++的頭文件中使用
3、在多個人協同開發時,可能有的人比較擅長C語言,而有的人擅長C++,這樣的情況下也會有用到
8.3 現代編譯器的編譯過程?
- 預編譯,展開所有的宏定義#define, 處理所有的預編譯指令如#if,遞歸的包含文件#include,刪除所有註釋,添加行號和文件標識,保留所有的編譯指令#pragma。
- 編譯
- 詞法分析
- 語法分析
- 語義分析
- 優化生成彙編代碼
- 彙編,將彙編代碼轉化成機器可以執行的指令,得到目標文件(.o \ .obj),
- 連接,鏈接將目標文件進行處理,得到可執行文件。
8.4 pdb文件有什麼用?
* 全局變量的名字和地址;
* 參數和局部變量的名字和在堆棧的偏移量;
* class,structure 和數據的類型定義;
* Frame Pointer Omission 數據,用來在x86上的native堆棧的遍歷;
* 源代碼文件的名字和行數;
九、實現函數和類
9.1 char *strcpy(char *dst, const char *src);
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類
#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)
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;
}