C++知識概要

範中豪發表於2021-05-21
  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 成員變數指向對方。弱引用並不對物件的記憶體進行管理,在功能上類似於普通指標,然而一個比較大的區別是,弱引用能檢測到所管理的物件是否已經被釋放,從而避免訪問非法記憶體

相關文章