C++知識概要

  1. static的用法和作用
  • 在全局變數前加上關鍵字 static,全局變數就定義成一個全局靜態變數。存儲在靜態存儲區,在整個程式運行期間一直存在。同時全局靜態變數在聲明他的文件之外是不可見的
  • 在局部變數之前加上關鍵字 static,局部變數就成為一個局部靜態變數。存儲在靜態存儲區,作用域仍為局部作用域,當定義它的函數或者語句塊結束的時候,作用域結束。但是當局部靜態變數離開作用域後,並沒有銷毀,而是仍然駐留在記憶體當中,只不過我們不能再對它進行訪問,直到該函數再次被調用,並且該過程中值保持不變。
  • 在函數返回類型前加 static,函數就定義為靜態函數。函數的定義和聲明在默認情況下都是 extern 的,但靜態函數只是在聲明他的文件當中可見,不能被其他文件所用。
  • 在類中,靜態成員可以實現多個對象之間的數據共享,並且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此,靜態成員是類的所有對象中共享的成員,而不是某個對象的成員。對多個對象來說,靜態數據成員只存儲一處,供所有對象共用
  • 靜態成員函數和靜態數據成員一樣,它們都屬於類的靜態成員,它們都不是對象成員。因此,對靜態成員的引用不需要用對象名
    static 成員函數不能被 virtual 修飾,static 成員不屬於任何對象或實例,所以加上 virtual 沒有任何實際意義;靜態成員函數沒有 this 指針,虛函數的實現是為每一個對象分配一個 vptr 指針,而 vptr 是通過 this 指針調用的,所以不能為 virtual;虛函數的調用關係,this->vptr->ctable->virtual function。
  1. 靜態變數初始化

靜態局部變數和全局變數一樣,數據都存放在全局區域,所以在主程式之前,編譯器已經為其分配好了記憶體。在 C++ 中,初始化是在執行相關程式碼時才會進行初始化。

  1. 虛函數可以聲明為 inline 嗎

不可以
虛函數用於實現運行時的多態,或者稱為晚綁定或動態綁定。而內聯函數用於提高效率。內聯函數的原理是,在編譯期間,對調用內聯函數的地方的程式碼替換成函數程式碼。內聯函數對於程式中需要頻繁使用和調用的小函數非常有用。
虛函數要求在運行時進行類型確定,而內聯函數要求在編譯期完成相關的函數替換

  1. static 修飾符

static 修飾成員變數,在數據段分配記憶體。
static 修飾成員函數,在程式碼區分配記憶體。

  1. 一個派生類構造函數的執行順序如下
  1. 虛擬基類的構造函數(多個虛擬基類則按照繼承的順序執行構造函數)
  2. 基類的構造函數(多個普通基類也按照繼承的順序執行構造函數)
  3. 類類型的成員對象的構造函數(按照初始化順序)
  4. 派生類自己的構造函數
  1. 必須使用成員列表初始化的四種情況
  • 當初始化一個引用成員時
  • 當初始化一個常量成員時
  • 當調用一個基類的構造函數,而它擁有一組參數時
  • 當調用一個成員類的構造函數,而它擁有一組參數時
  1. 構造函數為什麼不能為虛函數
  • 虛函數對應一個指向虛函數表的指針,但是這個指向vtable 的指針事實上是存儲在對象的記憶體空間的。問題出來了,假設構造函數是虛的,就須要通過 vtable 來調用,但是對象還沒有實例化,也就是記憶體空間還沒有,怎麼找 vtable 呢?所以構造函數不能是虛函數。
  • 因為構造函數本來就是為了明確初始化對象成員才產生的,然而 virtual function 主要是為了在不完全了解細節的情況下也能正確處理對象。另外,virtual 函數是在不同類型的對象產生不同的動作,現在對象還沒有產生,也就不能使用 virtual 函數來完成你想完成的動作
  1. 析構函數為什麼要虛函數

C++中基類採用 virtual 虛析構函數是為了防止記憶體泄漏。具體地說,如果派生類中申請了記憶體空間,並在其析構函數中對這些記憶體空間進行釋放。假設基類中採用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。那麼在這種情況下,派生類中申請的空間就得不到釋放從而產生記憶體泄漏。

  1. 構造函數析構函數可以調用虛函數嗎

在構造函數和析構函數中最好不要調用虛函數
構造函數或者析構函數調用虛函數並不會發揮虛函數動態綁定的特性,跟普通函數沒區別
即使構造函數或者析構函數如果能成功調用虛函數, 程式的運行結果也是不可控的

  1. 空類的大小是多少?為什麼
  • C++空類的大小不為 0,不同編譯器設置不一樣,vs 設置為 1
  • C++標準指出,不允許一個對象(當然包括類對象)的大小為 0,不同的對象不能具有相同的地址
  • 帶有虛函數的 C++類大小不為 1,因為每一個對象會有一個 vptr 指向虛函數表,具體大小根據指針大小確定
  • C++中要求對於類的每個實例都必須有獨一無二的地址,那麼編譯器自動為空類分配一個位元組大小,這樣便保證了每個實例均有獨一無二的記憶體地址
  1. 移動構造函數
A(A&& b){
  ***
}
// a = std::move(b)
  1. 移動賦值
A& operator=(A&& b){
  ***
  return *this;
}
  1. 類如何實現只能靜態分配和只能動態分配

前者是把 new、delete 運算符重載為 private 屬性。後者是把構造、析構函數設為 protected 屬性,再用子類來動態創建
建立類的對象有兩種方式:

  1. 靜態建立,靜態建立一個類對象,就是由編譯器為對象在棧空間中分配記憶體;
  2. 動態建立,就是使用 new 運算符為對象在堆空間中分配記憶體。這個過程分為兩步,第一步執行operator new()函數,在堆中搜索一塊記憶體並進行分配;第二步調用類構造函數構造對象
  1. 什麼情況會自動生成默認構造函數

帶有默認構造函數的類成員對象
帶有默認構造函數的基類
帶有一個虛函數的類
帶有一個虛基類的類
合成的默認構造函數中,只有基類子對象和成員類對象會被初始化。所有其他的非靜態數據成員都不會被初始化

  1. 如何消除隱式轉換

C++中提供了 explicit 關鍵字,在構造函數聲明的時候加上 explicit 關鍵字,能夠禁止隱式轉換
如果構造函數只接受一個參數,則它實際上定義了轉換為此類類型的隱式轉換機制。可以通過將構造函數聲明為 explicit 加以制止隱式類型轉換,關鍵字 explicit 只對一個實參的構造函數有效,需要多個實參的構造函數不能用於執行隱式轉換,所以無需將這些構造函數指定為explicit。

  1. 派生類指針轉換為基類指針,指針值會不會變

將一個派生類的指針轉換成某一個基類指針,編譯器會將指針的值偏移到該基類在對象記憶體中的起始位置

  1. C 語言的編譯鏈接過程

源程式碼-->預處理-->編譯-->優化-->彙編-->鏈接–>可執行文件

  • 預處理
    讀取 c 源程式,對其中的偽指令(以#開頭的指令)和特殊符號進行處理。包括宏定義替換、條件編譯指令、頭文件包含指令、特殊符號
  • 編譯
    編譯程式所要作得工作就是通過詞法分析和語法分析,在確認所有的指令都符合語法規則之後,將其翻譯成等價的中間程式碼表示或彙編程式碼
  • 彙編
    彙編過程實際上指把彙編語言程式碼翻譯成目標機器指令的過程
  • 鏈接階段
    鏈接程式的主要工作就是將有關的目標文件彼此相連接,也即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成為一個能夠被作業系統裝入執行的統一整體。
  1. 容器內部刪除一個元素
  1. 順序容器
    erase 迭代器不僅使所指向被刪除的迭代器失效,而且使被刪元素之後的所有迭代器失效(list 除外),所以不能使用 erase(it++)的方式,但是erase 的返回值是下一個有效迭代器;
    it = c.erase(it);
  2. 關聯容器
    erase 迭代器只是被刪除元素的迭代器失效,但是返回值是 void,所以要採用 erase(it++)的方式刪除迭代器;
    c.erase(it++)
  1. vector 越界訪問下標,map 越界訪問下標?vector 刪除元素時會不會釋放空間

通過下標訪問 vector 中的元素時不會做邊界檢查,即便下標越界。也就是說,下標與 first 迭代器相加的結果超過了 finish 迭代器的位置,程式也不會報錯,而是返回這個地址中存儲的值。如果想在訪問 vector 中的元素時首先進行邊界檢查,可以使用 vector 中的 at 函數。通過使用 at 函數不但可以通過下標訪問 vector 中的元素,而且在 at 函數內部會對下標進行邊界檢查
map 的下標運算符[]的作用是:將 key 作為下標去執行查找,並返回相應的值;如果不存在這個 key,就將一個具有該 key 和 value 的默認值插入這個 map
erase()函數,只能刪除內容,不能改變容量大小; erase 成員函數,它刪除了 itVect 迭代器指向的元素,並且返回要被刪除的 itVect 之後的迭代器,迭代器相當於一個智慧指針,之後迭代器將失效。;clear()函數,只能清空內容,不能改變容量大小;如果要想在刪除內容的同時釋放記憶體,那麼你可以選擇 deque 容器

int main(){
  vector<int> vec(10, 0);
  int arr[10] = {0,0,0,0,0,0,0,0,0,0};
  cout << vec[11] << endl; // 輸出值
  cout << *(vec.begin()+11) << endl; // 輸出值
  cout << vec.at(11); // 報錯,越界
  cout << arr[11]; // 輸出值
}
  1. vector 的增加刪除都是怎麼做的?為什麼是 1.5 倍

vector 通過一個連續的數組存放元素,如果集合已滿,在新增數據的時候,就要分配一塊更大的記憶體,將原來的數據複製過來,釋放之前的記憶體,再插入新增的元素
初始時刻 vector 的 capacity 為 0,塞入第一個元素後 capacity 增加為 1
不同的編譯器實現的擴容方式不一樣,VS2015 中以 1.5 倍擴容,GCC 以 2 倍擴容
對比可以發現採用成倍方式擴容,可以保證常數的時間複雜度,而增加指定大小的容量只能達到 O(n)的時間複雜度,因此,使用成倍的方式擴容
以 2 倍的方式擴容,導致下一次申請的記憶體必然大於之前分配記憶體的總和,導致之前分配的記憶體不能再被使用,所以最好倍增長因子設置為(1,2)之間
向量容器 vector 的成員函數 pop_back()可以刪除最後一個元素
而函數 erase()可以刪除由一個 iterator 指出的元素,也可以刪除一個指定範圍的元素
還可以採用通用演算法 remove()來刪除 vector 容器中的元素
採用 remove 一般情況下不會改變容器的大小,而 pop_back()與 erase()等成員函數會改變容器的大小,使得之後所有迭代器、引用和指針都失效

  1. 函數指針

函數指針指向的是特殊的數據類型,函數的類型是由其返回的數據類型和其參數列表共同決定的,而函數的名稱則不是其類型的一部分
函數指針聲明

int (*pf)(const int&, const int&);

上面的 pf 就是一個函數指針,指向所有返回類型為 int,並帶有兩個 const int & 參數的函數。應該注意的是 *pf 兩邊的括弧是必須的否則就是聲明了一個返回int *類型的函數
函數指針賦值

指針名 = 函數名;
指針名 = &函數名;
  1. c/c++的記憶體分配,詳細說一下棧、堆、靜態存儲區

程式碼段
只讀,可共享; 程式碼段(code segment/text segment )通常是指用來存放程式執行程式碼的一塊記憶體區域。這部分區域的大小在程式運行前就已經確定,並且記憶體區域通常屬於只讀, 某些架構也允許程式碼段為可寫,即允許修改程式。在程式碼段中,也有可能包含一些只讀的常數變數,例如字元串常量等
數據段
儲存已被初始化了的靜態數據。數據段(data segment )通常是指用來存放程式中已初始化的全局變數的一塊記憶體區域。數據段屬於靜態記憶體分配。
BSS 段
未初始化的數據段。BSS 段(bss segment )通常是指用來存放程式中未初始化的全局變數的一塊記憶體區域。BSS 是英文 Block Started by Symbol 的簡稱。BSS 段屬於靜態記憶體分配(BSS 段 和 data 段的區別是 ,如果一個全局變數沒有被初始化(或被初始化為 0),那麼他就存放在 bss 段;如果一個全局變數被初始化為非 0,那麼他就被存放在 data 段)
堆(heap )
堆是用於存放進程運行中被動態分配的記憶體段,它的大小並不固定,可動態擴張或縮減。當進程調用 malloc 等函數分配記憶體時,新分配的記憶體就被動態添加到堆上(堆被擴張);當利用 free 等函數釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)
棧(stack)
棧又稱堆棧,是用戶存放程式臨時創建的局部變數,也就是說我們函數括弧「{} 」中定義的變數(但不包括 static 聲明的變數,static 意味著在數據段中存放變數)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束後,函數的返回值也會被存放回棧中。由於棧的先進先出特點,所以棧特別方便用來保存/ 恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的記憶體區。
共享記憶體映射區域
棧和堆之間,有一個共享記憶體的映射的區域。這個就是共享記憶體存放的地方。一般共享記憶體的默認大小是 32M

綜上:
棧區(stack) — 由編譯器自動分配釋放,存放函數的參數值,局部變數的值等其操作方式類似於數據結構中的棧
堆區(heap) — 一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由 OS(作業系統)回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表
全局區(靜態區)(static) — 全局變數和靜態變數的存儲是放在一塊的,初始化的全局變數和靜態變數在一塊區域,未初始化的全局變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後由系統釋放
文字常量區 — 常量字元串就是放在這裡的。程式結束後由系統釋放
程式程式碼區 — 存放函數體的二進位程式碼

  1. 堆與棧的區別

管理方式:對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程式設計師控制,容易產生 memory leak
空間大小:一般來講在 32 位系統下,堆記憶體可以達到 4G 的空間,但是對於棧來講,一般都是有一定的空間大小的
碎片問題:對於堆來講,頻繁的 new/delete 勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個問題,因為棧是先進後出的隊列,他們是如此的一一對應,以至於永遠都不可能有一個記憶體塊從棧中間彈出,在他彈出之前,在他上面的後進的棧內容已經被彈出
生長方向:對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長。
分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有 2 種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變數的分配。動態分配由 alloca 函數進行分配,但是棧的動態分配和堆是不同的,它的動態分配是由編譯器進行釋放,無需我們手工實現
分配效率:棧是機器系統提供的數據結構,電腦會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高,堆則是 C/C++函數庫提供的

  1. 野指針是什麼?

野指針:指向記憶體被釋放的記憶體或者沒有訪問許可權的記憶體的指針。它的成因有三個:1. 指針變數沒有被初始化。2. 指針 p 被 free 或者 delete 之後,沒有置為 NULL。3.指針操作超越了變數的作用範圍 (覺得存在錯誤)

  1. 懸空指針和野指針有什麼區別

野指針:野指針指,訪問一個已刪除或訪問受限的記憶體區域的指針,野指針不能判斷是否為 NULL 來避免。指針沒有初始化,釋放後沒有置空,越界
懸空指針:一個指針的指向對象已被刪除,那麼就成了懸空指針。野指針是那些未初始化的指針

  1. 記憶體泄漏

記憶體泄漏 是指由於疏忽或錯誤造成了程式未能釋放掉不再使用的記憶體的情況。記憶體泄漏並非指記憶體在物理上消失,而是應用程式分配某段記憶體後,由於設計錯誤,失去了對該段記憶體的控制 (記憶體泄露的排查診斷與解決)

  1. new 和 delete 的實現原理, delete 是如何知道釋放記憶體的大小的
  1. new 表達式調用一個名為 operator new(operator new[])函數,分配一塊足夠大的、原始的、未命名的記憶體空間
  2. 編譯器運行相應的構造函數以構造這些對象,並為其傳入初始值
  3. 對象被分配了空間並構造完成,返回一個指向該對象的指針

new 簡單類型直接調用 operator new 分配記憶體;而對於複雜結構,先調用 operator new 分配記憶體,然後在分配的記憶體上調用構造函數;對於簡單類型,new[]計算好大小後調用 operator new;對於複雜數據結構,new[] 先調用 operator new[]分配記憶體,然後在 p 的前四個位元組寫入數組大小 n,然後調用 n 次構造函數,針對複雜類型,new[]會額外存儲數組大小
delete 簡單數據類型默認只是調用 free 函數;複雜數據類型先調用析構函數再調用 operator delete;針對簡單類型,delete 和 delete[]等同。假設指針 p 指向 new[]分配的記憶體。因為要 4 位元組存儲數組大小,實際分配的記憶體地址為[p-4],系統記錄的也是這個地址。delete[]實際釋放的就是 p-4 指向的記憶體。而 delete 會直接釋放 p 指向的記憶體,這個記憶體根本沒有被系統記錄,所以會崩潰
需要在 new [] 一個對象數組時,需要保存數組的維度,C++ 的做法是在分配數組空間時多分配了 4 個位元組的大小,專門保存數組的大小,在delete [] 時就可以取出這個保存的數,就知道了需要調用析構函數多少次了

  1. 使用智慧指針管理記憶體資源,RAII

RAII 全稱是「Resource Acquisition is Initialization」,直譯過來是「資源獲取即初始化」,也就是說在構造函數中申請分配資源,在析構函數中釋放資源。因為 C++的語言機制保證了,當一個對象創建的時候,自動調用構造函數,當對象超出作用域的時候會自動調用析構函數。所以,在 RAII 的指導下,我們應該使用類來管理資源,將資源和對象的生命周期綁定
智慧指針(std::shared_ptr 和 std::unique_ptr)即 RAII 最具代表的實現,使用智慧指針,可以實現自動的記憶體管理,再也不需要擔心忘記 delete 造成的記憶體泄漏。毫不誇張的來講,有了智慧指針,程式碼中幾乎不需要再出現 delete 了

  1. 記憶體對齊
  1. 分配記憶體的順序是按照聲明的順序。
  2. 每個變數相對於起始位置的偏移量必須是該變數類型大小的整數倍,不是整數倍空出記憶體,直到偏移量是整數倍為止
  3. 最後整個結構體的大小必須是裡面變數類型最大值的整數倍
class A{
   int a;
   double b;
};

class B{
   int a, b;
   double c;
};

class C{
   int a;
   double b;
   int c;
};
class D{
   int a;
   double b;
   int c,d;
};

int main(){
   cout << sizeof(int) << " " << sizeof(double) << endl;
   cout << sizeof(A) << " " << sizeof(B) << " " << sizeof(C) << " " << sizeof(D) << endl;
}
// out
/*
4 8
16 16 24 24
*/
  1. 為什麼記憶體對齊

平台原因(移植原因)

  • 不是所有的硬體平台都能訪問任意地址上的任意數據的;
  • 某些硬體平台只能在某些地址處取某些特定類型的數據,否則拋出硬體異常

性能原因:

  • 數據結構(尤其是棧)應該儘可能地在自然邊界上對齊
  • 原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問
  1. 宏定義一個取兩個數中較大值的功能
#define MAX(x,y) (x > y ? x:y)
  1. define 與 inline 的區別

define 是關鍵字,inline 是函數

宏定義在預處理階段進行文本替換,inline 函數在編譯階段進行替換
inline 函數有類型檢查,相比宏定義比較安全

  1. printf 實現原理

在 C/C++中,對函數參數的掃描是從後向前的。C/C++的函數參數是通過壓入堆棧的方式來給函數傳參數的,所以最後壓入的參數總是能夠被函數找到,因為它就在堆棧指針的上方。printf 的第一個被找到的參數就是那個字元指針,就是被雙引號括起來的那一部分,函數通過判斷字元串里控制參數的個數來判斷參數個數及數據類型,通過這些就可算出數據需要的堆棧指針的偏移量了。

  1. hello world 程式開始到列印到螢幕上的全過程
  • 用戶告訴作業系統執行 HelloWorld 程式(通過鍵盤輸入等)
  • 作業系統:找到 helloworld 程式的相關資訊,檢查其類型是否是可執行文件;並通過程式首部資訊,確定程式碼和數據在可執行文件中的位置並計算出對應的磁碟塊地址。
  • 作業系統:創建一個新進程,將 HelloWorld 可執行文件映射到該進程結構,表示由該進程執行 helloworld 程式。
  • 作業系統:為 helloworld 程式設置 cpu 上下文環境,並跳到程式開始處。
  • 執行 helloworld 程式的第一條指令,發生缺頁異常
  • 作業系統:分配一頁物理記憶體,並將程式碼從磁碟讀入記憶體,然後繼續執行 helloworld 程式
  • helloword 程式執行 puts 函數(系統調用),在顯示器上寫一字元串
  • 作業系統:找到要將字元串送往的顯示設備,通常設備是由一個進程式控制制的,所以,作業系統將要寫的字元串送給該進程
  • 作業系統:控制設備的進程告訴設備的窗口系統,它要顯示該字元串,窗口系統確定這是一個合法的操作,然後將字元串轉換成像素,將像素寫入設備的存儲映像區
  • 影片硬體將像素轉換成顯示器可接收和一組控制數據訊號
  • 顯示器解釋訊號,激發液晶螢幕
  • OK,我們在螢幕上看到了 HelloWorld
  1. 模板類和模板函數的區別是什麼

函數模板的實例化是由編譯程式在處理函數調用時自動完成的,而類模板的實例化必須由程式設計師在程式中顯式地指定。即函數模板允許隱式調用和顯式調用而類模板只能顯示調用。在使用時類模板必須加<T>,而函數模板不必

  1. C++四種類型轉換
  • static_cast 能進行基礎類型之間的轉換,也是最常看到的類型轉換。它主要有如下幾種用法:1. 用於類層次結構中父類和子類之間指針或引用的轉換,2. 進行下行轉換(把父類指針或引用轉換成子類指針或引用)時,由於沒有動態類型檢查,所以是不安全的,3. 用於基本數據類型之間的轉換,如把 int 轉換成 char,把 int 轉換成 enum,4. 把 void 指針轉換成目標類型的指針(不安全!!) 5. 把任何類型的表達式轉換成 void 類型
  • const_cast 運算符用來修改類型的 const 或 volatile 屬性。將一個 const 的指針或引用轉換為非 const。除了去掉 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 的派生。C 的強制轉換表面上看起來功能強大什麼都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。
  1. 全局變數和 static 變數的區別

全局變數(外部變數)的說明之前再冠以 static 就構成了靜態的全局變數。全局變數本身就是靜態存儲方式,靜態全局變數當然也是靜態存儲方式。這兩者在存儲方式上並無不同。這兩者的區別在於非靜態全局變數的作用域是整個源程式,當一個源程式由多個原文件組成時,非靜態的全局變數在各個源文件中都是有效的。而靜態全局變數則限制了其作用域,即只在定義該變數的源文件內有效,在同一源程式的其它源文件中不能使用它。由於靜態全局變數的作用域限於一個源文件內,只能為該源文件內的函數公用,因此可以避免在其他源文件中引起錯誤。static 全局變數與普通的全局變數的區別是 static 全局變數只初始化一次,防止在其他文件單元被引用。
static 函數與普通的函數作用域不同。只在當前源文件中使用的函數應該聲明為內部函數(static),內部函數應該在當前源文件
中說明和定義。對於可在當前源文件以外使用的函數應該在一個頭文件中說明,要使用這些函數的源文件要包含這個頭文件。static 函數與普通函數最主要區別是 static 函數在記憶體中只有一份,普通靜態函數在每個被調用中維持一份拷貝,程式的局部變數存在於(堆棧)中,全局變數存在於(靜態區)中,動態申請數據存在於(堆)中

  1. 迭代器++it, it++ 哪個好

前置返回一個引用,後置返回一個對象
前置不會產生臨時對象,後置必須產生臨時對象,臨時對象會導致效率降低
++i實現

int& operator++()
{
  *this += 1;
  return *this; 
}

i++實現

int operator++(int) 
{
  int temp = *this; 
  ++*this; 
  return temp; 
}
  1. 模板和實現可不可以不寫在一個文件裡面?為什麼?

因為在編譯時模板並不能生成真正的二進位程式碼,而是在編譯調用模板類或函數的 CPP 文件時才會去找對應的模板聲明和實現,在這種情況下編譯器是不知道實現模板類或函數的 CPP 文件的存在,所以它只能找到模板類或函數的聲明而找不到實現,而只好創建一個符號寄希望於鏈接程式找地址。但模板類或函數的實現並不能被編譯成二進位程式碼,結果鏈接程式找不到地址只好報錯了。
模板定義很特殊。由template<…>處理的任何東西都意味著編譯器在當時不為它分配存儲空間,它一直處於等待狀態直到被一個模板實例告知。在編譯器和連接器的某一處,有一機制能去掉指定模板的多重定義。所以為了容易使用,幾乎總是在頭文件中放置全部的模板聲明和定義。

  1. 執行 int main(int argc, char *argv[])時的記憶體結構

參數的含義是程式在命令行下運行的時候,需要輸入 argc 個參數,每個參數是以 char 類型輸入的,依次存在數組裡面,數組是 argv[],所有的參數在指針char * 指向的記憶體中,數組的中元素的個數為 argc 個,第一個參數為程式的名稱。

  1. 大端小端,如何檢測

大端模式:是指數據的高位元組保存在記憶體的低地址中,而數據的低位元組保存在記憶體的高地址端。
小端模式,是指數據的高位元組保存在記憶體的高地址中,低位位元組保存在在記憶體的低地址端。
檢測1直接讀取存放在記憶體中的十六進位數值,取低位進行值判斷

int a = 0x12345678;
int *c = &a;
c[0] == 0x12 大端模式
c[0] == 0x78 小端模式
  1. 有了 malloc/free,為什麼還要 new/delete

對於類類型的對象而言,用 malloc/free 無法滿足要求的。對象在創建的時候要自動執行構造函數,消亡之前要調用析構函數。由於 malloc/free 是庫函數而不是運算符,不在編譯器控制之內,不能把執行構造函數和析構函數的任務強加給它,因此,C++還需要 new/delete。

  1. 為什麼拷貝構造函數必須傳引用不能傳值

拷貝構造函數的作用就是用來複制對象的,在使用這個對象的實例來初始化這個對象的一個新的實例。對於內置數據類型的傳遞時,直接賦值拷貝給形參(注意形參是函數內局部變數);對於類類型的傳遞時,需要首先調用該類的拷貝構造函數來初始化形參(局部對象)。拷貝構造函數用來初始化一個非引用類類型對象,如果用傳值的方式進行傳參數,那麼構造實參需要調用拷貝構造函數,而拷貝構造函數需要傳遞實參,所以會一直遞歸。

  1. this 指針調用成員變數時,堆棧會發生什麼變化

當在類的非靜態成員函數訪問類的非靜態成員時,編譯器會自動將對象的地址傳給作為隱含參數傳遞給函數,這個隱含參數就是 this 指針。即使你並沒有寫 this 指針,編譯器在鏈接時也會加上 this 的,對各成員的訪問都是通過 this 的。例如你建立了類的多個對象時,在調用類的成員函數時,你並不知道具體是哪個對象在調用,此時你可以通過查看 this 指針來查看具體是哪個對象在調用。This 指針首先入棧,然後成員函數的參數從右向左進行入棧,最後函數返回地址入棧。

  1. 智慧指針怎麼用?智慧指針出現循環引用怎麼解決?
  1. shared_ptr
    調用一個名為 make_shared 的標準庫函數,shared_ptr<int> p = make_shared<int>(42); 通常用 auto 更方便,
    auto p = …;shared_ptr<int> p2(new int(2));
    每個 shared_ptr 都有一個關聯的計數器,通常稱為引用計數,一旦一個 shared_ptr 的計數器變為 0,它就會自動釋放自己所管理的對象; shared_ptr 的析構函數就會遞減它所指的對象的引用計數。如果引用計數變為 0,shared_ptr 的析構函數就會銷毀對象,並釋放它佔用的記憶體。
  2. unique_ptr
    一個 unique_ptr 擁有它所指向的對象。某個時刻只能有一個 unique_ptr指向一個給定對象。當 unique_ptr 被銷毀時,它所指向的對象也被銷毀。
  3. weak_ptr
    weak_ptr 是一種不控制所指向對象生存期的智慧指針,它指向由一個 shared_ptr 管理的對象,將一個 weak_ptr 綁定到一個 shared_ptr 不會改變引用計數,一旦最後一個指向對象的 shared_ptr 被銷毀,對象就會被釋放,即使有 weak_ptr 指向對象,對象還是會被釋放。
  4. 弱指針用於專門解決 shared_ptr 循環引用的問題,weak_ptr 不會修改引用計數,即其存在與否並不影響對象的引用計數器。循環引用就是:兩個對象互相使用一個 shared_ptr 成員變數指向對方。弱引用並不對對象的記憶體進行管理,在功能上類似於普通指針,然而一個比較大的區別是,弱引用能檢測到所管理的對象是否已經被釋放,從而避免訪問非法記憶體
Tags: