最近會放出cpp成神之路的所有總結,大家感興趣的可以收藏一波。
?歷史文章:
超硬核!十萬字c++題,讓你秒殺老師和面試官
位運算
若一個數m滿足 m = 2^n;那么k%m=k&(m-1)
為什么內存對齊
- 平臺原因(移植原因)
- 不是所有的硬件平臺都能訪問任意地址上的任意數據的;
- 某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異
2、性能原因:
- 數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。
- 原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
???????函數調用過程棧的變化,返回值和參數變量哪個先入棧?
1、調用者函數把被調函數所需要的參數按照與被調函數的形參順序相反的順序壓入棧中,即:從右向左依次把被調函數所需要的參數壓入棧;
2、調用者函數使用call指令調用被調函數,并把call指令的下一條指令的地址當成返回地址壓入棧中(這個壓棧操作隱含在call指令中);
3、在被調函數中,被調函數會先保存調用者函數的棧底地址(push ebp),然后再保存調用者函數的棧頂地址,即:當前被調函數的棧底地址(mov ebp,esp);
4、在被調函數中,從ebp的位置處開始存放被調函數中的局部變量和臨時變量,并且這些變量的地址按照定義時的順序依次減小,即:這些變量的地址是按照棧的延伸方向排列的,先定義的變量先入棧,后定義的變量后入棧;
???????怎樣判斷兩個浮點數是否相等?
對兩個浮點數判斷大小和是否相等不能直接用==來判斷,會出錯!明明相等的兩個數比較反而是不相等!對于兩個浮點數比較只能通過相減并與預先設定的精度比較,記得要取絕對值!浮點數與0的比較也應該注意。與浮點數的表示方式有關。
???????宏定義一個取兩個數中較大值的功能
#define MAX(x,y)((x>y?)x:y)
???????define、const、typedef、inline使用方法?
???????const與#define的區別:
- const定義的常量是變量帶類型,而#define定義的只是個常數不帶類型;
- define只在預處理階段起作用,簡單的文本替換,而const在編譯、鏈接過程中起作用;
- define只是簡單的字符串替換沒有類型檢查。而const是有數據類型的,是要進行判斷的,可以避免一些低級錯誤;
- define預處理后,占用代碼段空間,const占用數據段空間;
- const不能重定義,而define可以通過#undef取消某個符號的定義,進行重定義;
- define獨特功能,比如可以用來防止文件重復引用。
???????#define和別名typedef的區別
- 執行時間不同,typedef在編譯階段有效,typedef有類型檢查的功能;#define是宏定義,發生在預處理階段,不進行類型檢查;
- 功能差異,typedef用來定義類型的別名,定義與平臺無關的數據類型,與struct的結合使用等。#define不只是可以為類型取別名,還可以定義常量、變量、編譯開關等。
- 作用域不同,#define沒有作用域的限制,只要是之前預定義過的宏,在以后的程序中都可以使用。而typedef有自己的作用域。
???????define與inline的區別
- #define是關鍵字,inline是函數;
- 宏定義在預處理階段進行文本替換,inline函數在編譯階段進行替換;
- inline函數有類型檢查,相比宏定義比較安全;
???????printf實現原理?
在C/C++中,對函數參數的掃描是從后向前的。C/C++的函數參數是通過壓入堆棧的方式來給函數傳參數的(堆棧是一種先進后出的數據結構),最先壓入的參數最后出來,在計算機的內存中,數據有2塊,一塊是堆,一塊是棧(函數參數及局部變量在這里),而棧是從內存的高地址向低地址生長的,控制生長的就是堆棧指針了,最先壓入的參數是在最上面,就是說在所有參數的最后面,最后壓入的參數在最下面,結構上看起來是第一個,所以最后壓入的參數總是能夠被函數找到,因為它就在堆棧指針的上方。printf的第一個被找到的參數就是那個字符指針,就是被雙引號括起來的那一部分,函數通過判斷字符串里控制參數的個數來判斷參數個數及數據類型,通過這些就可算出數據需要的堆棧指針的偏移量了,下面給出printf("%d,%d",a,b);(其中a、b都是int型的)的匯編代碼.
???????#include 的順序以及尖叫括號和雙引號的區別
表示編譯器只在系統默認目錄或尖括號內的工作目錄下搜索頭文件,并不去用戶的工作目錄下尋找,所以一般尖括號用于包含標準庫文件;
表示編譯器先在用戶的工作目錄下搜索頭文件,如果搜索不到則到系統默認目錄下去尋找,所以雙引號一般用于包含用戶自己編寫的頭文件。
???????lambda函數
- 利用lambda表達式可以編寫內嵌的匿名函數,用以替換獨立函數或者函數對象;
- 每當你定義一個lambda表達式后,編譯器會自動生成一個匿名類(這個類當然重載了()運算符),我們稱為閉包類型(closure type)。那么在運行時,這個lambda表達式就會返回一個匿名的閉包實例,其實一個右值。所以,我們上面的lambda表達式的結果就是一個個閉包。閉包的一個強大之處是其可以通過傳值或者引用的方式捕捉其封裝作用域內的變量,前面的方括號就是用來定義捕捉模式以及變量,我們又將其稱為lambda捕捉塊。
- lambda表達式的語法定義如下:[capture] (parameters) mutable ->return-type {statement};
- lambda必須使用尾置返回來指定返回類型,可以忽略參數列表和返回值,但必須永遠包含捕獲列表和函數體;
???????hello world 程序開始到打印到屏幕上的全過程?
1.用戶告訴操作系統執行HelloWorld程序(通過鍵盤輸入等)
2.操作系統:找到helloworld程序的相關信息,檢查其類型是否是可執行文件;并通過程序首部信息,確定代碼和數據在可執行文件中的位置并計算出對應的磁盤塊地址。
3.操作系統:創建一個新進程,將HelloWorld可執行文件映射到該進程結構,表示由該進程執行helloworld程序。
4.操作系統:為helloworld程序設置cpu上下文環境,并跳到程序開始處。
5.執行helloworld程序的第一條指令,發生缺頁異常
6.操作系統:分配一頁物理內存,并將代碼從磁盤讀入內存,然后繼續執行helloworld程序
7.helloword程序執行puts函數(系統調用),在顯示器上寫一字符串
8.操作系統:找到要將字符串送往的顯示設備,通常設備是由一個進程控制的,所以,操作系統將要寫的字符串送給該進程
9.操作系統:控制設備的進程告訴設備的窗口系統,它要顯示該字符串,窗口系統確定這是一個合法的操作,然后將字符串轉換成像素,將像素寫入設備的存儲映像區
10.視頻硬件將像素轉換成顯示器可接收和一組控制數據信號
11.顯示器解釋信號,激發液晶屏
12.OK,我們在屏幕上看到了HelloWorld
???????模板類和模板函數的區別是什么?
函數模板的實例化是由編譯程序在處理函數調用時自動完成的,而類模板的實例化必須由程序員在程序中顯式地指定。即函數模板允許隱式調用和顯式調用而類模板只能顯示調用。在使用時類模板必須加<T>,而函數模板不必
???????為什么模板類一般都是放在一個h文件中
- 模板定義很特殊。由template<…>處理的任何東西都意味著編譯器在當時不為它分配存儲空間,它一直處于等待狀態直到被一個模板實例告知。在編譯器和連接器的某一處,有一機制能去掉指定模板的多重定義。所以為了容易使用,幾乎總是在頭文件中放置全部的模板聲明和定義。
- 在分離式編譯的環境下,編譯器編譯某一個.cpp文件時并不知道另一個.cpp文件的存在,也不會去查找(當遇到未決符號時它會寄希望于連接器)。這種模式在沒有模板的情況下運行良好,但遇到模板時就傻眼了,因為模板僅在需要的時候才會實例化出來,所以,當編譯器只看到模板的聲明時,它不能實例化該模板,只能創建一個具有外部連接的符號并期待連接器能夠將符號的地址決議出來。然而當實現該模板的.cpp文件中沒有用到模板的實例時,編譯器懶得去實例化,所以,整個工程的.obj中就找不到一行模板實例的二進制代碼,于是連接器也黔驢技窮了。
???????C++中類成員的訪問權限和繼承權限問題。
- 三種訪問權限
- public:用該關鍵字修飾的成員表示公有成員,該成員不僅可以在類內可以被 ?訪問,在類外也是可以被訪問的,是類對外提供的可訪問接口;
- ?private:用該關鍵字修飾的成員表示私有成員,該成員僅在類內可以被訪問,在類體外是隱藏狀態;
- ?protected:用該關鍵字修飾的成員表示保護成員,保護成員在類體外同樣是隱藏狀態,但是對于該類的派生類來說,相當于公有成員,在派生類中可以被訪問。
- 三種繼承方式
- 若繼承方式是public,基類成員在派生類中的訪問權限保持不變,也就是說,基類中的成員訪問權限,在派生類中仍然保持原來的訪問權限;
- ?若繼承方式是private,基類所有成員在派生類中的訪問權限都會變為私有(private)權限;
- 若繼承方式是protected,基類的共有成員和保護成員在派生類中的訪問權限都會變為保護(protected)權限,私有成員在派生類中的訪問權限仍然是私有(private)權限。
???????cout和printf有什么區別?
cout<<是一個函數,cout<<后可以跟不同的類型是因為cout<<已存在針對各種類型數據的重載,所以會自動識別數據的類型。輸出過程會首先將輸出字符放入緩沖區,然后輸出到屏幕。
cout是有緩沖輸出:
cout < < "abc " < <endl;
或cout < < "abc\n ";cout < <flush; 這兩個才是一樣的.
endl相當于輸出回車后,再強迫緩沖輸出。
flush立即強迫緩沖輸出。
printf是無緩沖輸出。有輸出時立即輸出
???????重載運算符?
- 我們只能重載已有的運算符,而無權發明新的運算符;對于一個重載的運算符,其優先級和結合律與內置類型一致才可以;不能改變運算符操作數個數;
- .??? :: ???: ?sizeof?? typeid ?**不能重載;
- 兩種重載方式,成員運算符和非成員運算符,成員運算符比非成員運算符少一個參數;下標運算符、箭頭運算符必須是成員運算符;
- 引入運算符重載,是為了實現類的多態性;
- 當重載的運算符是成員函數時,this綁定到左側運算符對象。成員運算符函數的參數數量比運算符對象的數量少一個;至少含有一個類類型的參數;
- 從參數的個數推斷到底定義的是哪種運算符,當運算符既是一元運算符又是二元運算符(+,-,*,&);
- 下標運算符必須是成員函數,下標運算符通常以所訪問元素的引用作為返回值,同時最好定義下標運算符的常量版本和非常量版本;
- 箭頭運算符必須是類的成員,解引用通常也是類的成員;重載的箭頭運算符必須返回類的指針;
???????函數重載函數匹配原則
- 名字查找
- 確定候選函數
- 尋找最佳匹配
???????定義和聲明的區別
- 如果是指變量的聲明和定義
從編譯原理上來說,聲明是僅僅告訴編譯器,有個某類型的變量會被使用,但是編譯器并不會為它分配任何內存。而定義就是分配了內存。 - 如果是指函數的聲明和定義
聲明:一般在頭文件里,對編譯器說:這里我有一個函數叫function() 讓編譯器知道這個函數的存在。
定義:一般在源文件里,具體就是函數的實現過程 寫明函數體。
???????C++類型轉換有四種
- static_cast能進行基礎類型之間的轉換,也是最常看到的類型轉換。它主要有如下幾種用法:
1 . 用于類層次結構中父類和子類之間指針或引用的轉換。進行上行轉換(把子類的指針或引用轉換成父類表示)是安全的;
2 . 進行下行轉換(把父類指針或引用轉換成子類指針或引用)時,由于沒有動態類型檢查,所以是不安全的;
3 . 用于基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。
4 . 把void指針轉換成目標類型的指針(不安全!!)
5 . 把任何類型的表達式轉換成void類型。
- const_cast運算符用來修改類型的const或volatile屬性。除了去掉const 或volatile修飾之外, type_id和expression得到的類型是一樣的。但需要特別注意的是const_cast不是用于去除變量的常量性,而是去除指向常數對象的指針或引用的常量性,其去除常量性的對象必須為指針或引用。
- reinterpret_cast它可以把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針(先把一個指針轉換成一個整數,在把該整數轉換成原類型的指針,還可以得到原先的指針值)。
- dynamic_cast 主要用在繼承體系中的安全向下轉型。它能安全地將指向基類的指針轉型為指向子類的指針或引用,并獲知轉型動作成功是否。轉型失敗會返回null(轉型對象為指針時)或拋出異常bad_cast(轉型對象為引用時)。?dynamic_cast 會動用運行時信息(RTTI)來進行類型安全檢查,因此 dynamic_cast 存在一定的效率損失。當使用dynamic_cast時,該類型必須含有虛函數,這是因為dynamic_cast使用了存儲在VTABLE中的信息來判斷實際的類型,RTTI運行時類型識別用于判斷類型。typeid表達式的形式是typeid(e),typeid操作的結果是一個常量對象的引用,該對象的類型是type_info或type_info的派生。
??????????????全局變量和static變量的區別
1、全局變量(外部變量)的說明之前再冠以static就構成了靜態的全局變量。全局變量本身就是靜態存儲方式,靜態全局變量當然也是靜態存儲方式。
這兩者在存儲方式上并無不同。這兩者的區別在于非靜態全局變量的作用域是整個源程序,當一個源程序由多個原文件組成時,非靜態的全局變量在各個源文件中都是有效的。而靜態全局變量則限制了其作用域,即只在定義該變量的源文件內有效,在同一源程序的其它源文件中不能使用它。由于靜態全局變量的作用域限于一個源文件內,只能為該源文件內的函數公用,因此可以避免在其他源文件中引起錯誤。static全局變量與普通的全局變量的區別是static全局變量只初始化一次,防止在其他文件單元被引用。
2.static函數與普通函數有什么區別?
static函數與普通的函數作用域不同。盡在本文件中。只在當前源文件中使用的函數應該說明為內部函數(static),內部函數應該在當前源文件中說明和定義。對于可在當前源文件以外使用的函數應該在一個頭文件中說明,要使用這些函數的源文件要包含這個頭文件。
??? static函數與普通函數最主要區別是static函數在內存中只有一份,普通靜態函數在每個被調用中維持一份拷貝程序的局部變量存在于(堆棧)中,全局變量存在于(靜態區)中,動態申請數據存在于(堆)
???????靜態成員與普通成員的區別
- 生命周期
靜態成員變量從類被加載開始到類被卸載,一直存在;
普通成員變量只有在類創建對象后才開始存在,對象結束,它的生命期結束;
共享方式
靜態成員變量是全類共享;普通成員變量是每個對象單獨享用的;
定義位置
普通成員變量存儲在棧或堆中,而靜態成員變量存儲在靜態全局區;
初始化位置
普通成員變量在類中初始化;靜態成員變量在類外初始化;
- 默認實參
可以使用靜態成員變量作為默認實參,
?
???????說一下理解 ifdef? endif
- 一般情況下,源程序中所有的行都參加編譯。但是有時希望對其中一部分內容只在滿足一定條件才進行編譯,也就是對一部分內容指定編譯的條件,這就是“條件編譯”。有時,希望當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。?
- 條件編譯命令最常見的形式為:?
#ifdef 標識符?
程序段1?
#else?
程序段2?
#endif
它的作用是:當標識符已經被定義過(一般是用#define命令定義),則對程序段1進行編譯,否則編譯程序段2。?
其中#else部分也可以沒有,即:?
#ifdef?
程序段1?
#denif
在一個大的軟件工程里面,可能會有多個文件同時包含一個頭文件,當這些文件編譯鏈接成一個可執行文件上時,就會出現大量“重定義”錯誤。在頭文件中使用#define、#ifndef、#ifdef、#endif能避免頭文件重定義。
???????隱式轉換,如何消除隱式轉換?
- C++的基本類型中并非完全的對立,部分數據類型之間是可以進行隱式轉換的。所謂隱式轉換,是指不需要用戶干預,編譯器私下進行的類型轉換行為。很多時候用戶可能都不知道進行了哪些轉換
- ?C++面向對象的多態特性,就是通過父類的類型實現對子類的封裝。通過隱式轉換,你可以直接將一個子類的對象使用父類的類型進行返回。在比如,數值和布爾類型的轉換,整數和浮點數的轉換等。某些方面來說,隱式轉換給C++程序開發者帶來了不小的便捷。C++是一門強類型語言,類型的檢查是非常嚴格的。
- 基本數據類型?基本數據類型的轉換以取值范圍的作為轉換基礎(保證精度不丟失)。隱式轉換發生在從小->大的轉換中。比如從char轉換為int。從int->long。自定義對象?子類對象可以隱式的轉換為父類對象。
- C++中提供了explicit關鍵字,在構造函數聲明的時候加上explicit關鍵字,能夠禁止隱式轉換。
- 如果構造函數只接受一個參數,則它實際上定義了轉換為此類類型的隱式轉換機制。可以通過將構造函數聲明為explicit加以制止隱式類型轉換,關鍵字explicit只對一個實參的構造函數有效,需要多個實參的構造函數不能用于執行隱式轉換,所以無需將這些構造函數指定為explicit。