程序员面试宝典
第一章 類
1.1 為什么構造函數不可以是虛函數
①從存儲空間角度
? ? 虛函數對應一個vtable(虛函數表),這大家都知道,可是這個vtable其實是存儲在對象的內存空間的。問題出來了,如果構造函數是虛的,就需要通過 vtable來調用,可是對象還沒有實例化,也就是內存空間還沒有,無法找到vtable,所以構造函數不能是虛函數。
②從使用角度
? ? ? ? 虛函數主要用于在信息不全的情況下,能使重載的函數得到對應的調用。構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。
虛函數的作用在于通過父類的指針或者引用來調用它的時候能夠變成調用子類的那個成員函數。而構造函數是在創建對象時自動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。
③構造函數不需要是虛函數,也不允許是虛函數,因為創建一個對象時我們總是要明確指定對象的類型,盡管我們可能通過實驗室的基類的指針或引用去訪問它。但析構卻不一定,我們往往通過基類的指針來銷毀對象。這時候如果析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。
④從實現上看,vbtl在構造函數調用后才建立,因而構造函數不可能成為虛函數 ?
? 從實際含義上看,在調用構造函數時還不能確定對象的真實類型(因為子類會調父類的構造函數);而且構造函數的作用是提供初始化,在對象生命期只執行一次,不是對象的動態行為,也沒有太大的必要成為虛函數
⑤當一個構造函數被調用時,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“當前”類的,而完全忽視這個對象后面是否還有繼承者。當編譯器為這個構造函數產生代碼時,它是為這個類的構造函數產生代碼- -既不是為基類,也不是為它的派生類(因為類不知道誰繼承它)。
? ? ? ? 所以它使用的V P T R必須是對于這個類的V TA B L E。而且,只要它是最后的構造函數調用,那么在這個對象的生命期內, V P T R將保持被初始化為指向這個V TA B L E, 但如果接著還有一個更晚派生的構造函數被調用,這個構造函數又將設置V P T R指向它的 V TA B L E,等.直到最后的構造函數結束。V P T R的狀態是由被最后調用的構造函數確定的。這就是為什么構造函數調用是從基類到更加派生 類順序的另一個理由。
? ? ? ? 但是,當這一系列構造函數調用正發生時,每個構造函數都已經設置V P T R指向它自己的 V TA B L E。如果函數調用使用虛機制,它將只產生通過它自己的V TA B L E的調用,而不是最后的V TA B L E(所有構造函數被調用后才會有最后的V TA B L E)。
1.2、為什么析構函數可以是虛函數
? ? ? 編譯器總是根據類型來調用類成員函數。但是一個派生類的指針可以安全地轉化為一個基類的指針。這樣刪除一個基類的指針的時候,C++不管這個指針指向一個基類對象還是一個派生類的對象,調用的都是基類的析構函數而不是派生類的。如果你依賴于派生類的析構函數的代碼來釋放資源,而沒有重載析構函數,那么會有資源泄漏。
? ? ? 所以建議的方式是將析構函數聲明為虛函數。如果你使用MFC,并且以CObject或其派生類為基類,那么MFC已經為你做了這件事情;CObject的析構函數是虛函數。一個函數一旦聲明為虛函數,那么不管你是否加上virtual 修飾符,它在所有派生類中都成為虛函數。但是由于理解明確起見,建議的方式還是加上virtual 修飾符。
? ? ? C++不把虛析構函數直接作為默認值的原因是虛函數表的開銷以及和C語言的類型的兼容性。有虛函數的對象總是在開始的位置包含一個隱含的虛函數表指針成員。如果是對于MFC類CPoint和CSize這樣的小型類,增加一個指針就增加了很多內存占用,而且使得其內存表示和基類POINT和SIZE不一致。
2.虛函數表
? ? ?請移步:https://www.cnblogs.com/LUO77/p/5771237.html
3.動態綁定與靜態綁定
- 靜態綁定發生在編譯期,動態綁定發生在運行期;
- 對象的動態類型可以更改,但是靜態類型無法更改;
- 要想實現動態,必須使用動態綁定;
- 在繼承體系中只有虛函數使用的是動態綁定,其他的全部是靜態綁定;
- 靜態多態是指通過模板技術或者函數重載技術實現的多態,其在編譯器確定行為。動態多態是指通過虛函數技術實現在運行期動態綁定的技術
動態綁定:有一個基類,兩個派生類,基類有一個virtual函數,兩個派生類都覆蓋了這個虛函數。現在有一個基類的指針或者引用,當該基類指針或者引用指向不同的派生類對象時,調用該虛函數,那么最終調用的是該被指向對象對應的派生類自己實現的虛函數。
4.虛函數表是針對類的還是針對對象的?同一個類的兩個對象的虛函數表是怎么維護的?
編譯器為每一個類維護一個虛函數表(本質是一個函數指針數組,數組里面存放了一系列函數地址 ),每個對象的首地址保存著該虛函數表的指針,同一個類的不同對象實際上指向同一張虛函數表。調用形式:*(this指針+調整量)[虛函數在vftable內的偏移]()
在類內部添加一個虛擬函數表指針,該指針指向一個虛擬函數表,該虛擬函數表包含了所有的虛擬函數的入口地址,每個類的虛擬函數表都不一樣,在運行階段可以循此脈絡找到自己的函數入口。純虛函數相當于占位符, 先在虛函數表中占一個位置由派生類實現后再把真正的函數指針填進去。除此之外和普通的虛函數沒什么區別。
在單繼承形式下,子類的完全獲得父類的虛函數表和數據。子類如果重寫了父類的虛函數(如fun),就會把虛函數表原本fun對應的記錄(內容MyClass::fun)覆蓋為新的函數地址(內容MyClassA::fun),否則繼續保持原本的函數地址記錄。
使用這種方式,就可以實現多態的特性。假設我們使用如下語句:
MyClass*pc= new MyClassA;
pc->fun();??
因為虛函數表內的函數地址已經被子類重寫的fun函數地址覆蓋了,因此該處調用的函數正是MyClassA::fun,而不是基類的MyClass::fun。
如果使用MyClassA對象直接訪問fun,則不會出發多態機制,因為這個函數調用在編譯時期是可以確定的,編譯器只需要直接調用MyClassA::fun即可。
注:對象不包含虛函數表,只有虛指針,類才包含虛函數表,派生類會生成一個兼容基類的虛函數表
詳情可以參考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
第二章 其他
2.?extern關鍵字的作用
? ? ?2.1 概述
? ? ? ?extern置于變量或函數前,用于標示變量或函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。它只要有兩個作用:
- 當它與“C”一起連用的時候,如:extern "C" void fun(int a,int b);則告訴編譯器在編譯fun這個函數時候按著C的規矩去翻譯,而不是C++的(這與C++的重載有關,C++語言支持函數重載,C語言不支持函數重載,函數被C++編譯器編譯后在庫中的名字與C語言的不同) #ifdef __cplusplus extern "C" { #endif/*...*/#ifdef __cplusplus } #endif
- 當extern不與“C”在一起修飾變量或函數時,如:extern int g_Int;它的作用就是聲明函數或全局變量的作用范圍的關鍵字,其聲明的函數和變量可以在本模塊或其他模塊中使用。記住它是一個聲明不是定義!也就是說B模塊(編譯單元)要是引用模塊(編譯單元)A中定義的全局變量或函數時,它只要包含A模塊的頭文件即可,在編譯階段,模塊B雖然找不到該函數或變量,但它不會報錯,它會在連接時從模塊A生成的目標代碼中找到此函數。
? ?2.2 .為什么不將全局變量的定義放在頭文件中
? ? ? ?首先要說明什么是全局變量,c語言中全局變量一般是指定義在函數體外的變量。全局變量按可訪問性可分為外部變量和內部變量。
? ? ? 內部變量是指?使用了static關鍵字修飾的全局變量,它的可訪問范圍(作用域)被限定在本源文件所在的鏈接文件模塊中,不能被其它文件模塊引用。反之沒有被static關鍵字修飾的全局變量則是外部變量,其它文件模塊可以通過extern關鍵字引用該全局變量并訪問。
要說明的是全局變量?無論是內部變量還是外部變量,的存儲類別都是靜態的,也就是放到靜態內存區域中,它編譯鏈接階段就已經分配好了固定的內存。
搞清楚上面的內容,就很容易得出若把全局變量放在頭文件會有哪些問題;
一 對內部變量來說,每個include該頭文件的文件模塊中都會單獨為這個內部變量分配靜態內存空間,這個空間是相對獨立的,是一種空間浪費,同時還失去了全局變量訪問一致性的特點,實在沒有什么意義。如果這個頭文件只被一個模塊使用,對于這個文件模塊來說應該沒啥問題。
二 對外部變量來講,這個頭文件被多個文件模塊include的情況下,鏈接過程會報錯,因為符號沖突,所有include這個頭文件的模塊都會有這個全局符號。在這個頭文件僅僅只被一個模塊include的時候可以正常使用。
經上分析得出要避免全局變量定義在頭文件中,因為當這個頭文件被多方include的時候會產生一些不必要的麻煩,就這么多。
全局變量作用域范圍較廣,被錯誤修改后排查定位問題比較困難,若非必要盡少使用。
下面說一下比較好的方式就是全局變量只定義在實現文件(.c,.m)中,對內部變量沒啥說的它只在文件模塊內部使用,對外部變量可以在該模塊頭文件中使用extern關鍵字修飾一下,這樣其它文件模塊只要直接include該頭文件就可以使用模塊中的外部變量了。
2.2.static關鍵字的作用
- ? 修飾局部變量
? ??static修飾局部變量時,使得被修飾的變量成為靜態變量,存儲在靜態區。存儲在靜態區的數據生命周期與程序相同,在main函數之前初始化,在程序退出時銷毀。(無論是局部靜態還是全局靜態)
- ? 修飾全局變量
? ? 全局變量本來就存儲在靜態區,因此static并不能改變其存儲位置。但是,static限制了其鏈接屬性。被static修飾的全局變量只能被該包含該定義的文件訪問(即改變了作用域)。
- ? 修飾函數
? ? ?static修飾函數使得函數只能在包含該函數定義的文件中被調用。對于靜態函數,聲明和定義需要放在同一個文件夾中。
- ? 修飾成員變量
? ? ??用static修飾類的數據成員使其成為類的全局變量,會被類的所有對象共享,包括派生類的對象,所有的對象都只維持同一個實例。?因此,static成員必須在類外進行初始化(初始化格式:int base::var=10;),而不能在構造函數內進行初始化,不過也可以用const修飾static數據成員在類內初始化。
- ? 修飾成員函數
? ? ?用static修飾成員函數,使這個類只存在這一份函數,所有對象共享該函數,不含this指針,因而只能訪問類的static成員變量。靜態成員是可以獨立訪問的,也就是說,無須創建任何對象實例就可以訪問。例如可以封裝某些算法,比如數學函數,如ln,sin,tan等等,這些函數本就沒必要屬于任何一個對象,所以從類上調用感覺更好,比如定義一個數學函數類Math,調用Math::sin(3.14);還可以實現某些特殊的設計模式:如Singleton;
- 最重要的特性:隱藏
? ? 當同時編譯多個文件時,所有未加static前綴的全局變量和函數都具有全局可見性,其它的源文件也能訪問。利用這一特性可以在不同的文件中定義同名函數和同名變量,而不必擔心命名沖突。static可以用作函數和變量的前綴,對于函數來講,static的作用僅限于隱藏。
不可以同時用const和static修飾成員函數。
? ?C++編譯器在實現const的成員函數的時候為了確保該函數不能修改類的實例的狀態,會在函數中添加一個隱式的參數const this*。但當一個成員為static的時候,該函數是沒有this指針的。也就是說此時const的用法和static是沖突的。我們也可以這樣理解:兩者的語意是矛盾的。static的作用是表示該函數只作用在類型的靜態變量上,與類的實例沒有關系;而const的作用是確保函數不能修改類的實例的狀態,與類型的靜態變量沒有關系。因此不能同時用它們。
2.3.const的作用
- 定義變量為只讀變量,不可修改
- 修飾函數的參數和返回值(后者應用比較少,一般為值傳遞)
- const成員函數(只需要在成員函數參數列表后加上關鍵字const,如char?get()?const;)可以訪問const成員變量和非const成員變量,但不能修改任何變量。在聲明一個成員函數時,若該成員函數并不對數據成員進行修改操作,應盡可能將該成員函數聲明為const成員函數。
-
const對象只能訪問const成員函數,而非const對象可以訪問任意的成員函數,包括const成員函數.即對于class A,有const A a;那么a只能訪問A的const成員函數。而對于:A b;b可以訪問任何成員函數。
使用const關鍵字修飾的變量,一定要對變量進行初始化
2.4.指針與引用的區別
- 指針只是一個變量,只不過這個變量存儲的是一個地址;而引用跟原來的變量實質上是同一個東西,只不過是原變量的一個別名而已,不占用內存空間。
- 引用必須在定義的時候初始化,而且初始化后就不能再改變;而指針不必在定義的時候初始化,初始化后可以改變。
- 指針可以為空,但引用不能為空(這就意味著我們拿到一個引用的時候,是不需要判斷引用是否為空的,而拿到一個指針的時候,我們則需要判斷它是否為空。這點經常在判斷函數參數是否有效的時候使用。)
- “sizeof 引用" = 指向變量的大小 , "sizeof 指針"= 指針本身的大小
- 指針可以有多級,而引用只能是一級
2.5.new與malloc的區別
- malloc與free是C++/C語言的標準庫函數,new/delete是C++的運算符。它們都可用于申請動態內存和釋放內存。
- 對于非內部數據類型的對象而言,光用malloc/free無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。
- new可以認為是malloc加構造函數的執行。new出來的指針是直接帶類型信息的。而malloc返回的都是void指針。
2.6.智能指針怎么實現?什么時候改變引用計數?
- 構造函數中計數初始化為1;
- 拷貝構造函數中計數值加1;
- 賦值運算符中,左邊的對象引用計數減一,右邊的對象引用計數加一;
- 析構函數中引用計數減一;
- 在賦值運算符和析構函數中,如果減一后為0,則調用delete釋放對象。
2.7.內聯函數,宏定義和普通函數的區別
- 內聯函數要做參數類型檢查,這是內聯函數跟宏相比的優勢
- 宏定義是在預編譯的時候把所有的宏名用宏體來替換,簡單的說就是字符串替換,?內聯函數則是在編譯的時候進行代碼插入,編譯器會在每處調用內聯函數的地方直接把內聯函數的內容展開,這樣可以省去函數的調用的壓棧出棧的開銷,提高效率。
- 內聯函數是指嵌入代碼,就是在調用函數的地方不是跳轉,而是把代碼直接寫到那里去。對于短小簡單的代碼來說,內聯函數可以帶來一定的效率提升,而且和C時代的宏函數相比,內聯函數?更安全可靠。可是這個是以增加空間消耗為代價的
- const與#define的區別:宏在預處理階段替換,const在編譯階段替換;宏沒有類型,不做安全檢查,const有類型,在編譯階段進行安全檢查
2.8. C++內存管理
棧:? 存放函數參數以及局部變量 , 在出作用域時 , 將自動被釋放 . 棧內存分配運算內置于處理器的指令集中 , 效率 很 高 , 但分配的內存容量有限 .
堆?:new 分配的內存塊 ( 包括數組 , 類實例等 ), 需 delete 手動釋放 . 如果未釋放 , 在整個程序結束后 ,OS 會幫你回收掉 .
自由存儲區:?malloc 分配的內存塊 , 需 free 手動釋放 . 它和堆有些相似 .
全局/靜態區:?保存自動全局變量和static變量(包括static全局和局部變量)。靜態區的內容在整個程序的生命周期內都存在,有編譯器在編譯的時候分配(數據段(存儲全局數據和靜態數據)和代碼段(可執行的代碼/只讀常量))。
常量存儲區:?常量 (const) 存于此處 , 此存儲區不可修改 .
棧與堆的區別:?? ? ? ?
管理方式不同:?棧是編譯器自動管理的,堆需手動釋放
空間大小不同:?在32位OS下,堆內存可達到4GB的的空間,而棧就小得可憐.(VC6中,棧默認大小是1M,當然,你可以修改它)
能否產生碎片不同:對于棧來說,進棧/出棧都有著嚴格的順序(先進后出),不會產生碎片;而堆頻繁的new/delete,會造成內存空間的不連續,容易產生碎片.
生長方向不同:棧向下生長,以降序分配內存地址;堆向上生長,以升序分配內在地址.
分配方式不同:堆動態分配,無靜態分配;棧分為靜態分配和動態分配,比如局部變量的分配,就是動態分配(alloca函數),函數參數的分配就是動態分配(我想的…).
分配效率不同:棧是系統提供的數據結構,計算機會在底層對棧提供支持,進棧/出棧都有專門的指令,這就決定了棧的效率比較高.堆則不然,它由C/C++函數庫提供,機制復雜,堆的效率要比棧低得多.
可以看出,棧的效率要比堆高很多,所以,推薦大家盡量用棧.不過,雖然棧有如此多的好處,但遠沒有堆使用靈活.
第四章 實戰
4.1 .手寫strcpy,memcpy,strcat,strcmp等函數
https://blog.csdn.net/gao1440156051/article/details/51496782
https://blog.csdn.net/wilsonboliu/article/details/7919773
16.i++是否為原子操作?
不是。操作系統原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷,分為兩種情況(兩種都應該滿足)
?(1) 在單線程中, 能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生于指令之間。
?(2) 在多線程中,不能被其它進程(線程)打斷的操作就叫原子操作。
i++分為三個階段:
內存到寄存器
寄存器自增
寫回內存
這三個階段中間都可以被中斷分離開.
17.有關數組,指針,函數的三者結合問題
數組指針和指針數組的區別:https://blog.csdn.net/men_wen/article/details/52694069
右左法則的說明:http://www.cnblogs.com/zhangjing0502/archive/2012/06/08/2542059.html
指針常量和常量指針:https://www.zhihu.com/question/19829354
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://blog.csdn.net/xingjiarong/article/details/47282563
注意:所謂指向常量的指針或引用(即常量引用、常量指針),不過是指針或引用“自以為是”罷了,它們覺得自己指向了常量,所以自覺地不去改變所指對象的值,但這些對象卻可以通過其他途徑改變。
const int *a; ?等價于int const *a; ? ?const在前面所以內容不可以改變,但是指針指向可以改變。也就是常量指針
int *const a; ?表示的是指針指向不可改變,但是指針所存放的內容可以改變,也即是指針常量
補充:
18.C++中類與結構體的區別?
- 最本質的一個區別就是默認的訪問控制:?struct作為數據結構的實現體,它默認的數據訪問控制是public的,而class作為對象的實現體,它默認的成員變量訪問控制是private的。
- “class”這個關鍵字還用于定義模板參數,就像“typename”。但關鍵字“struct”不用于定義模板參數。
19.析構函數的作用?
析構函數是用來釋放所定義的對象中使用的指針,默認的析構函數不用顯示調用,自建的析構函數要在程序末尾調用。
如果你的類里面只用到的基本類型,如int char double等,系統的默認析構函數其實什么都沒有做
但如果你使用了其他的類如vector,string等,系統的默認析構函數就會調用這些類對象的析構函數
如果是自己寫析構函數的話,如果你的類里面分配了系統資源,如new了內存空間,打開了文件等,那么在你的析構函數中就必須釋放相應的內存空間和關閉相關的文件;這樣系統就會自動調用你的析構函數釋放資源,避免內存泄漏
例如:
class A
{
private:
char *data;
public:
A()
{
data = new char[ 10];
}
~A()
{
delete[] data;
}
};
- 1
A? a;
a?中將?new?10個?char
當?a?這個變量消亡的時候,將自動執行?~A(),釋放空間
對象消亡時,自動被調用,用來釋放對象占用的空間,避免內存泄漏
20.虛函數的作用?
虛函數可以讓成員函數操作一般化,用基類的指針指向不同的派生類的對象時,基類指針調用其虛成員函數,則會調用其真正指向對象的成員函數,而不是基類中定義的成員函數(只要派生類改寫了該成員函數)。若不是虛函數,則不管基類指針指向的哪個派生類對象,調用時都會調用基類中定義的那個函數。虛函數是C++多態的一種表現,可以進行靈活的動態綁定。? ?? 重點可參考:https://www.cnblogs.com/wangxiaobao/p/5850949.html
? ? ? ? ? ? ? ? ? ? ?http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
21.操作系統和編譯器如何區分全局變量和局部變量?
? ? ? ?操作系統只管調度進程,編譯器通過內存分配的位置來知道的,全局變量分配在全局數據段并且在程序開始運行的時候被加載。局部變量則分配在棧里面 。
22. Makefile文件的作用?
? ?makefile關系到了整個工程的編譯規則。一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯,甚至于進行更復雜的功能操作,因為makefile就像一個Shell腳本一樣,其中也可以執行操作系統的命令。
23.結構體和聯合體的區別?
? ? ? 結構和聯合都是由多個不同的數據類型成員組成,?但在任何同一時刻,?聯合中只存放了一個被選中的成員(所有成員共用一塊地址空間),?而結構的所有成員都存在(不同成員的存放地址不同)。?
? ? ?對于聯合的不同成員賦值,?將會對其它成員重寫,?原來成員的值就不存在了,?而對于結構的不同成員賦值是互不影響的。
24.列表初始化問題?
? ? ??使用初始化列表主要是基于性能問題,對于內置類型,如int, float等,使用初始化列表和在構造函數體內初始化差別不是很大;但是對于類類型來說,最好使用初始化列表。這樣就可以直接調用拷貝構造函數初始化,省去了一次調用默認構造函數的過程。
struct Test1
{
Test1() // 無參構造函數
{
cout << "Construct Test1" << endl ;
}
Test1( const Test1& t1) // 拷貝構造函數
{
cout << "Copy constructor for Test1" << endl ;
this->a = t1.a ;
}
Test1& operator = ( const Test1& t1) // 賦值運算符
{
cout << "assignment for Test1" << endl ;
this->a = t1.a ;
return * this;
}
int a ;
};
struct Test2 //普通初始化
{
Test1 test1 ;
Test2(Test1 &t1)
{
test1 = t1 ;
}
};
- 1
struct Test2 //2.列表初始化
{
Test1 test1 ;
Test2(Test1 &t1):test1(t1){}
}
- 1
Test1 t1 ; //調用
Test2 t2(t1) ;
- 1
普通初始化:
列表初始化:
下列情況一定要使用初始化成員列表
- 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表里面
- 引用類型,引用必須在定義的時候初始化,并且不能重新賦值,所以也要寫在初始化列表里面
- 需要初始化的數據成員是對象的情況
? 參考地址:https://www.cnblogs.com/weizhixiang/p/6374430.html?
25. 重載與重寫的區別?
? ? 從定義上來說:重載:是指允許存在多個同名函數,而這些函數的參數表不同(或許參數個數不同,或許參數類型不同,或許兩者都不同)。重寫:是指子類重新定義父類虛函數的方法。
? ?從實現原理上來說:重載:編譯器根據函數不同的參數表,對同名函數的名稱做修飾,然后這些同名函數就成了不同的函數。重寫:當子類重新定義了父類的虛函數后,父類指針根據賦給它的不同的子類指針,動態的調用屬于子類的該函數,這樣的函數調用在編譯期間是無法確定的(調用的子類的虛函數的地址無法給出)。
? ?補充:“隱藏”是指派生類的函數屏蔽了與其同名的基類函數。規則如下:?
(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。?
(2)如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual 關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。
26.類型安全以及C++中的類型轉換?
? ? ?類型安全很大程度上可以等價于內存安全,類型安全的代碼不會試圖訪問自己沒被授權的內存區域。C只在局部上下文中表現出類型安全,比如試圖從一種結構體的指針轉換成另一種結構體的指針時,編譯器將會報告錯誤,除非使用顯式類型轉換。然而,C中相當多的操作是不安全的。
?詳情可以移步:https://blog.csdn.net/chengonghao/article/details/50974022
? ?四種類型轉換:
- static_cast <T*> (content)? 靜態轉換.在編譯期間處理,可以實現C++中內置基本數據類型之間的相互轉換。如果涉及到類的話,static_cast只能在有相互聯系的類型中進行相互轉換,不一定包含虛函數。
- dynamic_cast<T*>(content) 動態類型轉換;也是向下安全轉型;是在運行的時候執行;基類中一定要有虛函數,否則編譯不通過。在類層次間進行上行轉換時(如派生類指針轉為基類指針),dynamic_cast和static_cast的效果是一樣的。在進行下行轉換時(如基類指針轉為派生類指針),dynamic_cast具有類型檢查的功能,比static_cast更安全。
- const_cast<T*>(content) 去常轉換;編譯時執行;
- reinterpret_cast<T*>(content) 重解釋類型轉換;
詳情可以移步:https://blog.csdn.net/u010025211/article/details/48626687
? ? ? ? ? ? ? ? ? ? ? ? https://blog.csdn.net/xtzmm1215/article/details/46475565
? ? ? ? ? ? ? ? ? ? ? ??https://blog.csdn.net/xingkongfenqi/article/details/49148885
27.內存對齊的原則以及作用?
- 結構體內的成員按自身長度自對齊(32位機器上,如char=1,short=2,int=4,double=8),所謂自對齊是指該成員的起始地址必須是它自身長度的整數倍。如int只能以0,4,8這類地址開始。
- 結構體的總大小為結構體的有效對齊值的整數倍(默認以結構體中最長的成員長度為有效值的整數倍,當用#pragrma pack(n)指定時,以n和結構體中最長的成員的長度中較小者為其值)。即sizeof的值,必須是其內部最大成員的整數倍,不足的要補齊。
?例如:
class A
{
char c;
int a;
char d;
};
cout << sizeof(A) << endl;
class B
{
char c;
char d;
int a;
};
cout << sizeof(B) << endl;
- 1
sizeof(A)=12,sizeof(B)=8;
因為左邊是1+(3)+4+1+(3)=12,而右邊是1+1+(2)+4=8。括號中為補齊的字節。
內存對齊的作用:
1、平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2、性能原因:經過內存對齊后,CPU的內存訪問速度大大提升。
詳情可以移步:https://blog.csdn.net/chy19911123/article/details/48894579
28.關鍵字registr,typdef的作用?
register關鍵字的作用:
請求CPU盡可能讓變量的值保存在CPU內部的寄存器中,減去CPU從內存中抓取數據的時間,提高程序運行效率。
使用register關鍵字應注意什么?
1.只有局部變量才可以被聲明用register修飾
(register不能修飾全局變量和函數的原因:全局變量可能被多個進程訪問,而用register修飾的變量,只能被當前進程訪問)
2.不能用取地址獲取用register修飾的變量的地址(原因:變量保存在寄存器中,而取地址獲取的地址的是內存的地址)
3. 用register修飾的變量一定要是CPU所接受的數據類型
typedef關鍵字的作用:
給數據類型定義一個新名字,
1.? 提高了移植性
2.? 簡化復雜的類型聲明,提高編碼效率
3.? 解釋數據類型的作用
29.什么情況下需要將析構函數定義為虛函數?
? ? 當基類指針指向派生類的對象(多態性)時。如果定義為虛函數,則就會先調用該指針指向的派生類析構函數,然后派生類的析構函數再又自動調用基類的析構函數,這樣整個派生類的對象完全被釋放。如果析構函數不被聲明成虛函數,則編譯器實施靜態綁定,在刪除基類指針時,只會調用基類的析構函數而不調用派生類析構函數,這樣就會造成派生類對象析構不完全。所以,將析構函數聲明為虛函數是十分必要的。
? 詳情可以移步:https://blog.csdn.net/jiadebin890724/article/details/7951461
30.有關純虛函數的理解?
? ? ?純虛函數是為你的程序制定一種標準,純虛函數只是一個接口,是個函數的聲明而已,它要留到子類里去實現。
class A{
protected:
void foo(); //普通類函數
virtual void foo1(); //虛函數
virtual void foo2() = 0; //純虛函數
}
- 1
? ? 帶純虛函數的類抽象類,這種類不能直接生成對象,而只有被繼承,并重寫其虛函數后,才能使用。
? ? 虛函數是為了繼承接口和默認行為
? ? 純虛函數只是繼承接口,行為必須重新定義
? ?(在很多情況下,基類本身生成對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常? 理。所以引入了純虛函數的概念)
? ? 詳情可以參考:https://blog.csdn.net/ybhjx/article/details/51788396
30.基類指針指向派生類,派生類指針指向基類?
? ? 基類指針可以指向派生類對象,從而實現多態,例如:
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0; //純虛函數
};
class Square : public Shape {
double size;
public:
Square( double s) {
size = s;
}
virtual double area() const {
return size * size;
}
};
class Circle : public Shape {
double radius;
public:
Circle( double r) {
radius = r;
}
virtual double area() const {
return 3.14159 * radius * radius;
}
};
int main()
{
Shape* array[ 2]; //定義基類指針數組
Square Sq(2.0);
Circle Cir(1.0);
array[ 0] = &Sq;
array[ 1] =&Cir;
for ( int i = 0; i < 2; i++) /
{
cout << array[i]->area() << endl;
}
return 0;
}
- 1
? ? ?上面的不同對象Sq,Cir(來自繼承同一基類的不同派生類)接受同一消息(求面積,來自基類的成員函數area()),但是卻根據自身情況調用不同的面積公式(執行了不同的行為,它是通過虛函數實現的)。我們可以理解為,繼承同一基類的不同派生對象,對來自基類的同一消息執行了不同的行為,這就是多態,它是通過繼承和虛函數實現的。而接受同一消息的實現就是基于基類指針。?
? ? ?但是要注意的是,這個指針只能用來調用基類的成員函數。
? ? ?如果試圖通過基類指針調用派生類才有的成員函數,則編譯器會報錯。
? ? ?為了避免這種錯誤,必須將基類指針強制轉化為派生類指針。然后派生類指針可以用來調用派生類的功能。這稱為向下強制類型轉換,這是一種潛在的危險操作。
? ? ?派生類指針不可以指向基類對象,例如:
有個people類是基類,成員有姓名和身份證號,有個派生類學生student,添加了成員學號,現在如果你說的這個情況成立student的指針----pt讓他指向people成員t,則t只有兩個成員變量,而*pt有3個,現在pt->學號這個變量在pt下是可以使用的,但它指向的實體卻沒有這個變量,所以出錯,于是C++直接就避免了這樣的隱式轉換。 所以根據上述信息我們可以知道: 進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;進行下行轉換(把基類指針或引用轉換成派生類表示)是不安全的。? ? 參考鏈接:https://blog.csdn.net/flyingbird_sxf/article/details/41358737
? ? ? ? ? ? ? ? ? ? ?https://www.cnblogs.com/rednodel/p/5800142.html
? ?31.?繼承機制中引用和指針之間如何轉換??
? ? 基類——>派生類:用dynamic_cast轉換(顯示轉換),首先檢查基類指針(引用)是否真正指向一個派生類對象,然后再做相應處理,對指針進行dynamic_cast,成功返回派生類對象,失敗返回空指針,對引用進行dynamic_cast,成功返回派生類對象,失敗拋出一個異常。 不允許隱式轉換。
? ? 派生類——>基類:可以用dynamic_cast或者直接進行類型轉換(直接賦值)。
? ??32.c語言和c++有什么區別??
? ? C語言是結構化的編程語言,它是面向過程的,而C++是面向對象的。?
? ??封裝:將數據和函數等集合在一個單元中(即類)。被封裝的類通常稱為抽象數據類型。封裝的意義在于保護或者防止代碼(數據)被我們無意中破壞。?
? ??繼承:繼承主要實現重用代碼,節省開發時間。它可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展。?
? ??多態:同一操作作用于不同的對象,可以有不同的解釋,產生不同的執行結果。在運行時,可以通過指向派生類的基類指針,來調用實現派生類中的方法。有編譯時多態和運行時多態。
???33.C++中的公有,私有,保護的問題??
| 類 | 類對象 | 公有繼承派生類 | 私有繼承派生類 | 保護繼承派生類 | 公有繼承派生類對象 | 私有繼承派生類對象 | 保護繼承派生類對象 | |
| 公有成員 | √ | √ | √ | √ | √ | √ | X | X |
| 私有成員 | √ | X | X | X | X | X | X | X |
| 保護成員 | √ | X | √ | √ | √ | X | X | X |
? ?√:代表可以訪問,X代表不能訪問。? ? ?
?參考鏈接:https://zhidao.baidu.com/question/551075894.html
34.如何實現類對象只能靜態分配或動態分配?
?C++中建立類的對象有兩種方式:
(1)靜態建立,例如 A a;
? ? ?靜態建立一個類對象,就是由編譯器為對象在棧空間中分配內存。使用這種方法,是直接調用類的構造函數。
(2)動態建立,例如 A* p = new A();
? ? ?動態建立一個類對象,就是使用new運算符為對象在堆空間中分配內存。這個過程分為兩步:第一步執行operator new( )函數,在堆空間中搜索一塊內存并進行分配;第二步調用類的構造函數構造對象。這種方法是間接調用類的構造函數。
只能動態分配:??
? ? ? 其實,編譯器在為類對象分配棧空間時,會先檢查類的析構函數的訪問性(其實不光是析構函數,只要是非靜態的函數,編譯器都會進行檢查)。如果類的析構函數在類外部無法訪問,則編譯器拒絕在棧空間上為類對象分配內存。 因此,可以將析構函數設為private,這樣就無法在棧上建立類對象了。但是為了子類可以繼承,最好設置成protected。
只能靜態分配:
? ??只有使用new運算符,對象才會被建立在堆上。因此只要限制new運算符就可以實現類對象只能建立在棧上。可以將new運算符設為私有。
35.explicit關鍵字的作用?
C++中, 一個參數的?構造函數(或者除了第一個參數外其余參數都有默認值的多參構造函數), 承擔了兩個角色。 1 是個?構造器?,2 是個默認且隱含的類型轉換操作符。
所以, 有時候在我們寫下如 AAA = XXX, 這樣的代碼, 且恰好XXX的類型正好是AAA單參數構造器的參數類型, 這時候?編譯器就自動調用這個構造器, 創建一個AAA的對象。
這樣看起來好象很酷, 很方便。 但在某些情況下(見下面權威的例子), 卻違背了我們(程序員)的本意。 這時候就要在這個構造器前面加上explicit修飾, 指定這個構造器只能被明確的調用/使用, 不能作為類型轉換操作符被隱含的使用。
class Test1
{
public:
Test1( int n)
{
num=n;
} //普通構造函數
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
} //explicit(顯式)構造函數
private:
int num;
};
int main()
{
Test1 t1= 12; //隱式調用其構造函數,成功
Test2 t2= 12; //編譯錯誤,不能隱式調用其構造函數
Test2 t2(12); //顯式調用成功
return 0;
}
- 1
Test1的?構造函數帶一個int型的參數,代碼23行會隱式轉換成調用Test1的這個構造函數。而Test2的構造函數被聲明為explicit(顯式),這表示不能通過隱式轉換來調用這個構造函數,因此代碼24行會出現編譯錯誤。
普通構造函數能夠被?隱式調用。而explicit構造函數只能被顯式調用。
36.內存溢出,內存泄漏的原因?
? ? 內存溢出是指程序在申請內存時,沒有足夠的內存空間供其使用。原因可能如下:
- ?內存中加載的數據量過于龐大,如一次從數據庫取出過多數據
- ?代碼中存在死循環或循環產生過多重復的對象實體
- ?遞歸調用太深,導致堆棧溢出等
- ?內存泄漏最終導致內存溢出
? ??內存泄漏是指向系統申請分配內存進行使用(new),但是用完后不歸還(delete),導致占用有效內存。常見的幾種情況:
? ?(1)?在類的構造函數和析構函數中沒有匹配的調用new和delete函數
? ? ????兩種情況下會出現這種內存泄露:一是在堆里創建了對象占用了內存,但是沒有顯示地釋放對象占用的內存;二是在類的構造函數中動態的分配了內存,但是在析構 函數中沒有釋放內存或者沒有正確的釋放內存
? ?(2)?在釋放對象數組時在delete中沒有使用方括號
? ? ??方括號是告訴編譯器這個指針指向的是一個對象數組,同時也告訴編譯器正確的對象地址值病調用對象的析構函數,如果沒有方括號,那么這個指針就被默認為只指向一個對象,對象數組中的其他對象的析構函數就不會被調用,結果造成了內存泄露。
? ? (3)沒有將基類的析構函數定義為虛函數
? ? ???當基類指針指向子類對象時,如果基類的析構函數不是virtual,那么子類的析構函數將不會被調用,子類的資源沒有正確是釋放,因此造成內存泄露
? ? ?參考鏈接: https://blog.csdn.net/hyqwmxsh/article/details/52813307? ??
? ? ?緩沖區溢出(棧溢出)
? ? ?程序為了臨時存取數據的需要,一般會分配一些內存空間稱為緩沖區。如果向緩沖區中寫入緩沖區無法容納的數據,機會造成緩沖區以外的存儲單元被改寫,稱為緩沖區溢出。而棧溢出是緩沖區溢出的一種,原理也是相同的。分為上溢出和下溢出。其中,上溢出是指棧滿而又向其增加新的數據,導致數據溢出;下溢出是指空棧而又進行刪除操作等,導致空間溢出。
37.auto_ptr類與shared_ptr類?
??? ??從c++11開始, auto_ptr已經被標記為棄用, 常見的替代品為shared_ptr。shared_ptr的不同之處在于引用計數, 在復制(或賦值)時不會像auto_ptr那樣直接轉移所有權。?兩者都是模板類,卻可以像指針一樣去使用。只是在指針上面的一層封裝。
?? ???auto_ptr實際也是一種類, 擁有自己的析構函數, 生命周期結束時能自動釋放資源,正因為能自動釋放資源, 特別適合在單個函數內代替new/delete的調用, 不用自己調用delete,也不用擔心意外退出造成內存的泄漏。
? ?atuo_ptr的缺陷:
- ?auto_ptr不能共享所有權,即不要讓兩個auto_ptr指向同一個對象(因為它采用的是轉移語義的拷貝,原指針會變為NULL)。
- ?auto_ptr不能管理對象數組(因為它內部的析構函數調用的是delete而不是delete[])。
- ?auto_ptr不能作為容器對象,STL容器中的元素經常要支持拷貝,賦值等操作,在這過程中auto_ptr會傳遞所有權。
? ?詳情原因可以參考:https://blog.csdn.net/uestclr/article/details/51316001
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??https://blog.csdn.net/kezunhai/article/details/38514823
? ? ?
? ? ?shared_ptr 使用引用計數的方式來實現對指針資源的管理。同一個指針資源,可以被多個 shared_ptr 對象所擁有,直到最后一個 shared_ptr 對象析構時才釋放所管理的對象資源。
? ? ? 可以說,shared_ptr 是最智能的智能指針,因為其特點最接近原始的指針。不僅能夠自由的賦值和拷貝,而且可以安全的用在標準容器中。
38.???有4種情況,編譯器必須為未聲明的constructor的classes合成一個default constructor:
l? “帶有默認構造函數”的成員對象
l? “帶有默認構造函數”的基類
l? “帶有虛函數”的類
l? “帶有虛擬基類”的類
被合成的構造函數只能滿足編譯器(而非程序員)的需要。在合成默認的構造函數中,只有基類的子對象和成員對象會被初始化,其他非靜態的數據成員(如整數,指針等)都不會被初始化。
所以并不是任何的類如果沒有定義默認的構造函數,都會被合成一個出來。
39.???虛基類
在C++中,如果在多條繼承路徑上有一個公共的基類,那么在這些路徑中的某幾條路徑的匯合處,這個公共的基類就會產生多個實例(從而造成二義性).如果想使這個公共的基類只產生一個實例,則可將這個基類說明為虛基類. 這要求在從base類派生新類時,使用關鍵字virtual將base類說明為虛基類.
用例子說明吧。
class base{protected:int b};
clase base1:public base{..};
clase base2:public base{..};
clase derived:public base1,public base2 {..};
derived d;
d.b //錯誤.
d.base::b //錯誤. 因為不知是用d.base1::b還是d.base2::b
=================================================
class base{protected:int b..};
clase base1:virtual public base{..}; //說明base為base1虛基類
clase base2:virtual public base{..}; //說明base為base2虛基類
clase derived:public base1,public base2 {..};
derived d;
d.b //對.
d.base::b //對. 因為d.base::b和d.base1::b還是d.base2::b都是引用同一虛基類成員b,具有相同的值.
40.??模板的特例化
引入原因:編寫單一的模板,它能適應大眾化,使每種類型都具有相同的功能,但對于某種特定類型,如果要實現其特有的功能,單一模板就無法做到,這時就需要模板特例化。?
定義:是對單一模板提供的一個特殊實例,它將一個或多個模板參數綁定到特定的類型或值上。
函數模板特例化:必須為原函數模板的每個模板參數都提供實參,且使用關鍵字template后跟一個空尖括號對<>,表明將原模板的所有模板參數提供實參。
1.???template?<typename?T>??
2.???void?fun(T?a)??
3.?? {??
4.?? ????cout?<<?"The?main?template?fun():?"?<<?a?<<?endl;??
5.?? }??
6.?? ??
7.???template?<>???//?對int型特例化??
8.???void?fun(int?a)??
9.?? {??
10. ????cout?<<?"Specialized?template?for?int?type:?"?<<?a?<<?endl;??
11. }??
12. ??
13.?int?main()??
14. {??
15. ????fun<char>('a');??
16. ????fun<int>(10);??
17. ????fun<float>(9.15);??
18. ????return?0;??
19. }??
對于除int型外的其他數據類型,都會調用通用版本的函數模板fun(T a);對于int型,則會調用特例化版本的fun(int a)。注意,一個特例化版本的本質是一個實例,而非函數的重載。因此,特例化不影響函數匹配。
?
類模板的特例化:
1.???template?<typename?T>??
2.???class?Test{??
3.???public:??
4.?? ????void?print(){??
5.?? ????????cout?<<?"General?template?object"?<<?endl;??
6.?? ????}??
7.?? };??
8.?? ??
9.???template<>???//?對int型特例化??
10.?class?Test<int>{??
11.?public:??
12. ????void?print(){??
13. ????????cout?<<?"Specialized?template?object"?<<?endl;??
14. ????}??
15. };??
另外,與函數模板不同,類模板的特例化不必為所有模板參數提供實參。我們可以只指定一部分而非所有模板參數,這種叫做類模板的偏特化?或部分特例化(partial specialization)。例如,C++標準庫中的類vector的定義:
[cpp]?view plain?copy
1.???template?<typename?T,?typename?Allocator>??
2.???class?vector??
3.?? {??
4.?? ????/*......*/??
5.?? };??
6.?? ??
7.?? //?部分特例化??
8.???template?<typename?Allocator>??
9.???class?vector<bool,?Allocator>??
10. {??
11. ????/*......*/??
12. };??
在vector這個例子中,一個參數被綁定到bool類型,而另一個參數仍未綁定需要由用戶指定。注意,一個類模板的部分特例化版本仍然是一個模板,因為使用它時用戶還必須為那些在特例化版本中未指定的模板參數提供實參。?
第二部分:嵌入式
總結
- 上一篇: MSB8036 The Windows
- 下一篇: 一种抑制undershoot/overs