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 成員變量指向對方。弱引用並不對對象的內存進行管理,在功能上類似於普通指針,然而一個比較大的區別是,弱引用能檢測到所管理的對象是否已經被釋放,從而避免訪問非法內存


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM