這篇文章也許會持續(xù)更新,也歡迎大家提出問題,一起探討。原文地址AC4Fun,轉載請注明出處。
****************分割線************************************
按照侯捷先生在《Effective C++》的觀點,以及自己的一些理解,可以將互聯網技術崗位關于C++的知識點歸納為以下五個部分:
- C++基礎知識
- 面向過程的特性
- 面向對象的特性
- 泛型編程的特性
- 標準模板庫和算法
這是我第一次在簡書寫文章,如果大家有什么意見的話,歡迎隨時向我提出來。必須要說明的是,C++知識繁雜磅礴,面試題中可被問到的很多,如果想要成功拿到心儀的offer,除了掌握C++這個工具外,需要結合一些其它領域知識,如基礎數據結構和基本的算法,網絡編程,多線程編程,(Linux)Shell腳本(awk, sed),編譯器使用(gcc/clang, makefile, automake/cmake等),數據庫(SQL語言,數據庫理論如存儲引擎,索引實現,范式等)視頻處理,設計模式,機器學習等知識,如果后續(xù)有時間,也會對相關知識進行一下歸納。
本文主要側重于紀錄分析C++面試中的基礎知識,主要包括:關鍵字使用,
1. 關鍵字
auto
C++98以前,auto關鍵字是繼承B語言而來,C++11中已被廢棄,新auto主要用于自動類型推斷和返回值占位
static
static修飾的變量和函數存儲在內存的靜態(tài)文本區(qū),與全局變量存儲的位置一致。而靜態(tài)文本區(qū)的字節(jié)默認都是0x00
- 修飾局部變量
對局部變量添加static修飾符后,變量的存儲區(qū)由棧改為靜態(tài)文本區(qū),它的生存周期得到了改變。 - 修飾全局變量
對全局變量添加static,生存周期不會改變,但是會影響作用域。普通全局變量的作用域是整個源程序, 當一個源程序由多個源文件組成時,非靜態(tài)的全局變量在各個源文件中都是有效的。 而靜態(tài)全局變量則限制了其作用域, 即只在定義該變量的源文件內有效, 在同一源程序的其它源文件中不能使用它。由于靜態(tài)全局變量的作用域局限于一個源文件內,只能為該源文件內的函數公用,因此可以避免在其它源文件中引起錯誤。這一點,有人稱作“隱藏” - 修飾函數
static函數與普通函數作用域不同,僅在本文件。只在當前源文件中使用的函數應該說明為內部函數(static修飾的函數),內部函數應該在當前源文件中說明和定義。對于可在當前源文件以外使用的函數,應該在一個頭文件中說明,要使用這些函數的源文件要包含這個頭文件.
static函數在內存中只有一份,普通函數在每個被調用中維持一份拷貝
const
const關鍵字可以修飾變量,引用,函數,對象等:
常變量: const 類型說明符 變量名
常引用: const 類型說明符 &引用名
常對象: 類名 const 對象名
常成員函數: 類名::fun(形參) const
常數組: 類型說明符 const 數組名[大小]
常指針: const 類型說明符* 指針名 ,類型說明符* const 指針名
在常變量(const 類型說明符 變量名)、常引用(const 類型說明符 &引用名)、常對象(類名 const 對象名)、 常數組(類型說明符 const 數組名[大小]), const” 與 “類型說明符”或“類名”(其實類名是一種自定義的類型說明符) 的位置可以互換。
需要注意的概念其實是“常量指針”和“指針常量”,也就是const修飾一個指針變量的時候產生的兩種差異。我們知道,一個指針變量,使用的時候需要考慮該指針本身和被它所指的對象,看如下例子:
char *const pc; //到char的const指針
char const *pc1; //到const char的指針
const char *pc2; //到const char的指針(后兩個聲明是等同的)
從右向左讀的記憶方式:
pc is a const pointer to char. 故pc不能指向別的字符串,但可以修改其指向的字符串的內容。pc是一個指向字符類型的常指針,pc的值不可變,但是pc值(也就是pc指向的地址)所代表的內存空間的內容是可以變的,所以pc是一個指針常量(const pointer)。
pc2 is a pointer to const char. 故pc2的內容不可以改變,但pc2可以指向別的字符串。也就是說pc2是指向一個不可變內容空間(常量)的指針,也就是常量指針(pointer to const)。pc2++可行,但pc2 = "hello world"不可行。當然,只是說不能通過pc2去修改那段內容,別的方式是可以的。
其實,const只對它左邊的東西起作用,唯一的例外就是const本身就是最左邊的修飾符,那么它才會對右邊的東西起作用。
理解之后,再來看下面兩個,就很容易明白了。
vector<string>::const_iterator iter //iter is a pointer to const string
//iter值可變,但是*iter不可重新被賦值,常量指針
const vector<string>::iterator cit //cit is a const pointer to string
//cit值不可變,但是*cit可被重新賦值,指針常量
常量函數 常量函數是C++對常量的一個擴展,它很好的確保了C++中類的封裝性。在C++中,為了防止類的數據成員被非法訪問,將類的成員函數分成了兩類,一類是常量成員函數(也被稱為觀察者);另一類是非常量成員函數(也被成為變異者)。在一個函數的簽名后面加上關鍵字const后該函數就成了常量函數。對于常量函數,最關鍵的不同是編譯器不允許其修改類的數據成員。
當然,我們可以繞過編譯器的錯誤去修改類的數據成員。但是C++也允許我們在數據成員的定義前面加上mutable,以允許該成員可以在常量函數中被修改。當存在同名同參數和返回值的常量函數和非常量函數時,具體調用哪個函數是根據調用對象是常量對像還是非常量對象來決定的。常量對象調用常量成員;非常量對象調用非常量的成員。
另外,需要注意C++中用const定義了一個常量后,不會分配一個空間給它,而是將其寫入符號表(symbol table),這使得它成為一個編譯期間的常量,沒有了存儲與讀內存的操作,使得它的效率也很高。只有當使用extern或者取地址操作的時候,才會分配空間,但是這不會影響到常量本身的值,因為用到a的時候,編譯器根本不會去進行內存空間的讀取。這就是c++的常量折疊(constant folding),即將const常量放在符號表中,而并不給其分配內存。編譯器直接進行替換優(yōu)化。其值仍舊從符號表中讀取,不管常量對應的存儲空間中的值如何變化,都不會對其值產生影響。
宏定義與const
C++中定義常量的時候不再采用define,因為define只做簡單的宏替換,并不提供類型檢查.
四種類型轉換
static_cast
static_cast 很像 C 語言中的舊式類型轉換。它能進行基礎類型之間的轉換,也能將帶有可被單參調用的構造函數或用戶自定義類型轉換操作符的類型轉換,還能在存有繼承關系的類之間進行轉換(即可將基類轉換為子類,也可將子類轉換為基類),還能將 non-const對象轉換為 const對象(注意:反之則不行,那是const_cast的職責。)。
注意:static_cast 轉換時并不進行運行時安全檢查,所以是非安全的,很容易出問題。因此 C++ 引入 dynamic_cast 來處理安全轉型。dynamic_cast
dynamic_cast 主要用來在繼承體系中的安全向下轉型。它能安全地將指向基類的指針轉型為指向子類的指針或引用,并獲知轉型動作成功是否。如果轉型失敗會返回null(轉型對象為指針時)或拋出異常(轉型對象為引用時)。dynamic_cast 會動用運行時信息(RTTI)來進行類型安全檢查,因此 dynamic_cast 存在一定的效率損失。
class CBase { };
class CDerived: public CBase { };
CBase b;
CBase* pb;
CDerived d;
CDerived* pd;
pb = dynamic_cast<CBase*>(&d); // ok: derived-to-base
pd = dynamic_cast<CDerived*>(&b); // error: base-to-derived
上面的代碼中最后一行 VS2010 會報如下錯誤:
error C2683: 'dynamic_cast' : 'CBase' is not a polymorphic typeIntelliSense: the operand of a runtime dynamic_cast must have a polymorphic class type
這是因為 dynamic_cast 只有在基類帶有虛函數的情況下才允許將基類轉換為子類。當然,允許轉換也不代表可以轉換成功。
class CBase
{
virtual void dummy() {}
};
class CDerived: public CBase
{
int a;
};
int main ()
{
CBase * pba = new CDerived;
CBase * pbb = new CBase;
CDerived * pd1, * pd2;
pd1 = dynamic_cast<CDerived*>(pba);
pd2 = dynamic_cast<CDerived*>(pbb);
return 0;
}
結果是:上面代碼中的 pd1 不為 null,而 pd2 為 null。
dynamic_cast 也可在 null 指針和指向其他類型的指針之間進行轉換,也可以將指向類型的指針轉換為 void 指針(基于此,我們可以獲取一個對象的內存起始地址 *const void * rawAddress = dynamic_cast<const void > (this);)。
-
const_cast
前面提到 const_cast 可去除對象的常量性(const),它還可以去除對象的易變性(volatile)。const_cast 的唯一職責就在于此,若將 const_cast 用于其他轉型將會報錯。 -
reinterpret_cast
reinterpret_cast 用來執(zhí)行低級轉型,如將執(zhí)行一個 int 的指針強轉為 int。其轉換結果與編譯平臺息息相關,不具有可移植性,因此在一般的代碼中不常見到它。reinterpret_cast 常用的一個用途是轉換函數指針類型,即可以將一種類型的函數指針轉換為另一種類型的函數指針,但這種轉換可能會導致不正確的結果??傊瑀einterpret_cast 只用于底層代碼,一般我們都用不到它,如果你的代碼中使用到這種轉型,務必明白自己在干什么。
總結來看,需要類型轉換的時候,優(yōu)先使用C++的這種風格進行類型轉換,基礎類型轉換的時候,使用static_cast, 子類與父類之間進行轉換的時候,尤其是基類向子類轉換的時候,使用dynamic_cast。其它情況,如轉換為void指針,使用dynamic_cast, int型指針到int,以及函數指針之間的轉換使用reinterpret_cast, const_cast只用于去除對象的常量性(const)和易變性(volatile)
拋開C++為了兼容C而允許隱式類型轉換(隱蔽,不安全,易引起非預期的函數調用,對象切割等等),我傾向于認為C++是一種強類型(傾向于不允許隱式類型轉換),靜態(tài)類型(編譯前已經知道數據類型)的語言。
重載與重寫
重載(Overload)
同一可訪問區(qū)內被聲明的幾個具有不同參數列(參數的類型,個數,順序不同)的同名函數,根據參數列表確定調用哪個函數,重載不關心函數返回類型。成員函數被重載的特征:
(1)相同的范圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual關鍵字可有可無。重寫(Override),也叫覆蓋(Overwrite)
重寫是指派生類函數重寫基類函數,是C++的多態(tài)的表現,特征是:
(1)不同的范圍(分別位于派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual關鍵字。
說到底,這兩個概念其實并沒有太大的關聯,重載是編譯多態(tài)的一種實現,重寫與虛函數相關,用于實現動態(tài)綁定,屬于編譯時多態(tài)的實現。
重寫與隱藏的關系
“隱藏”是指派生類的函數屏蔽了與其同名的基類函數,規(guī)則如下:
(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏。
(2)如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏。如果有virtual關鍵字,函數同名,參數相同,就是“重寫”了
指針和引用的區(qū)別
從概念上講。指針從本質上講就是存放變量地址的一個變量,在邏輯上是獨立的,它可以被改變,包括其所指向的地址的改變和其指向的地址中所存放的數據的改變。
而引用是一個別名,它在邏輯上不是獨立的,它的存在具有依附性,所以引用必須在一開始就被初始化,而且其引用的對象在其整個生命周期中是不能被改變的(自始至終只能依附于同一個變量)。
再來詳細解釋一下指針和引用的區(qū)別(這里要感謝一下某位大神的指點)。
C++里,一個重要的概念是object,不是“面向對象”的對象,是“存儲空間”的意思?;旧?,凡是有名字的東西都有存儲空間。而指針是對存儲空間的引用(C++規(guī)格書的原話)。比如int p = &a;,那么p里面存著一個指針,它指向a的存儲空間。C++的表達式的值分為左值(l-value)和右值(r-value),其中左值是對應存儲空間的,而右值就不對應。p這個名字本身對應著存儲空間,里面存著一個指針。所以p也是左值。表達式(p)也是左值。意思是“p指向的那個存儲空間”。(p)和a對應的存儲空間是一樣的。因為指針也是值,所以,可以把p的值傳來傳去。然后再用(p)這樣的表達式操作a的存儲空間。這是指針的工作原理??偨Y一下:C++里,用int a;這種方法定義的標識符a對應存儲空間,類型就是int。存儲空間里存儲值。指針本身是一種值,它指向存儲空間。(*p)這個表達式對應的存儲空間就是p指向的存儲空間。
然后再說C++里的“引用”。
如果用int &b = a;這種方式定義,那么b這個標識符的存儲空間和a的存儲空間是一樣的。就是這么簡單。b并沒有自己的存儲空間,可以認為b就是a這個空間的別名?!耙谩币坏┒x,你就不能讓b再去“指向”別的存儲空間——在寫下int &b = a;的時候,b的存儲空間就固定了。但是“指針”則不然。如果定義int *p = &a;,那么p本身是個存儲空間,里面存著一個指針。你可以再找另一個指針,存到p里。比如int c; p = &c;這樣p就指向c的存儲空間了。所以,區(qū)別就是“指針本身是一個值,這個值執(zhí)行一個存儲空間;但引用只是另一個存儲空間的別名而已,本身并不是單獨的值”。以上是指針和引用的語義。
指針傳遞參數和引用傳遞參數的區(qū)別
答案是:本質上沒有區(qū)別。
內存對齊
每個特定平臺上的編譯器都有自己的默認“對齊系數”(也叫對齊模數,32位gcc 4.7上默認為8,32位VS2010上默認為8)。程序員可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊系數”。
規(guī)則:
1、數據成員對齊規(guī)則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset為0的地方,以后每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規(guī)則:在數據成員完成各自對齊之后,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。
3、結合1、2可推斷:當#pragma pack的n值等于或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。
如果我們不想編譯器自動為我們添加補齊位,可以將對齊系數設為1
#pragma pack(push, 1)
// code...
#pragma pack(pop)
四種智能指針
* auto_ptr
* unique_ptr
* shared_ptr
* weak_ptr
智能指針產生的原因在于解決常規(guī)指針可能產生的內存泄漏問題,
將基本類型指針封裝為類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),并在析構函數里編寫delete語句刪除指針指向的內存空間
STL一共給我們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暫不討論)。模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,并提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,如果您的編譯器不支持其他兩種解決力案,auto_ptr將是唯一的選擇。
使用auto_ptr仍然會存在,程序將試圖刪除同一個對象兩次的問題。要避免這種問題,方法有多種:
- 定義陚值運算符,使之執(zhí)行深復制。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未采用此方案。
- 建立所有權(ownership)概念。對于特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。然后讓賦值操作轉讓所有權。這就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。
- 創(chuàng)建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數。例如,賦值時,計數將加1,而指針過期時,計數將減1,。當減為0時才調用delete。這是shared_ptr采用的策略。
當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這么做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這么做 unique_ptr比auto_ptr優(yōu)秀的地方。
如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。
如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。
explicit
規(guī)避可被單參調用的構造函數引起的隱式類型轉換
所有的智能指針類都有一個explicit構造函數,以指針作為參數.因此不能自動將指針轉換為智能指針對象,必須顯式調用
內存管理
* new和delete
* malloc和free
*
面向對象
class CMyString
{
public:
CMyString(char* pData = NULL);
CMyString(const CMyString &str);
~CMyString(void);
private:
char * m_pData;
對象的大小
繼承(為什么要繼承?單繼承 多繼承)
如何實現一個不能被繼承的類?
類成員變量初始化順序
析構函數與構造函數的執(zhí)行順序
常見對象->構造函數(缺省構造函數,有參構造函數,復制構造函數)
銷毀對象->析構函數
拷貝構造函數
CMyString::CMyString(const CMyString &str)
{
m_pData = new char[ strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
}
為什么拷貝構造函數的參數一定是引用?避免拷貝構造函數不限制的遞歸復制下去。
賦值構造函數
CMyString& CMyString::operator = (const CMyString &str)
{
if(this != &str)
{
CMyString strTemp(str);
char * pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
其實我在想,這里的指針pTemp使用會不會有問題呢?
虛函數與運行時多態(tài)
- 純虛函數
- 虛函數列表
函數重載與編譯時多態(tài)
友元函數
歸納面向對象三大特征:封裝 繼承 多態(tài)
面向對象設計之SOLID五大原則?
泛型編程
編寫能夠正確處理各種不同數據類型參數的代碼,只要參數的數據類型滿足特定的語法和語義需求。對于C++而言,實現泛型編程的方式就是使用模板。
template<class T>
class vector {
}
標準模板庫(Standard Template Library)
一群優(yōu)秀的人寫的一個優(yōu)秀的函數庫
六大組件
容器
迭代器
適配器
算法
函數對象
空間適配器
vector
map
參考文獻
static的作用
C++中指針和引用的區(qū)別
C++的重載(overload)與重寫(override)
C/C++中const關鍵字詳解
C++智能指針簡單剖析
拷貝構造函數的參數為什么必須使用引用類型
C++11新特性——auto的使用