C/C++ 知識總結
目錄
C/C++
const
作用
- 修飾變數,說明該變數不可以被改變;
- 修飾指標,分為指向常量的指標和指標常量;
- 常量引用,經常用於形參型別,即避免了拷貝,又避免了函式對值的修改;
- 修飾成員函式,說明該成員函式內不能修改成員變數。
使用
const 使用
static
作用
- 修飾普通變數,修改變數的儲存區域和生命週期,使變數儲存在靜態區,在 main 函式執行前就分配了空間,如果有初始值就用初始值初始化它,如果沒有初始值系統用預設值初始化它。
- 修飾普通函式,表明函式的作用範圍,僅在定義該函式的檔案內才能使用。在多人開發專案時,為了防止與他人命令函式重名,可以將函式定位為 static。
- 修飾成員變數,修飾成員變數使所有的物件只儲存一個該變數,而且不需要生成物件就可以訪問該成員。
- 修飾成員函式,修飾成員函式使得不需要生成物件就可以訪問該函式,但是在 static 函式內不能訪問非靜態成員。
this 指標
this
指標是一個隱含於每一個非靜態成員函式中的特殊指標。它指向正在被該成員函式操作的那個物件。- 當對一個物件呼叫成員函式時,編譯程式先將物件的地址賦給
this
指標,然後呼叫成員函式,每次成員函式存取資料成員時,由隱含使用this
指標。 - 當一個成員函式被呼叫時,自動向它傳遞一個隱含的引數,該引數是一個指向這個成員函式所在的物件的指標。
this
指標被隱含地宣告為:ClassName *const this
,這意味著不能給this
指標賦值;在ClassName
const
成員函式中,this
指標的型別為:const ClassName* const
,這說明不能對this
指標所指向的這種物件是不可修改的(即不能對這種物件的資料成員進行賦值操作);this
並不是一個常規變數,而是個右值,所以不能取得this
的地址(不能&this
)。- 在以下場景中,經常需要顯式引用
this
指標:- 為實現物件的鏈式引用;
- 為避免對同一物件進行賦值操作;
- 在實現一些資料結構時,如
list
。
inline 行內函數
特徵
- 相當於把行內函數裡面的內容寫在呼叫行內函數處;
- 相當於不用執行進入函式的步驟,直接執行函式體;
- 相當於巨集,卻比巨集多了型別檢查,真正具有函式特性;
- 不能包含迴圈、遞迴、switch 等複雜操作;
- 在類宣告中定義的函式,除了虛擬函式的其他函式都會自動隱式地當成行內函數。
使用
inline 使用
編譯器對 inline 函式的處理步驟
- 將 inline 函式體複製到 inline 函式呼叫點處;
- 為所用 inline 函式中的區域性變數分配記憶體空間;
- 將 inline 函式的的輸入引數和返回值對映到呼叫方法的區域性變數空間中;
- 如果 inline 函式有多個返回點,將其轉變為 inline 函式程式碼塊末尾的分支(使用 GOTO)。
優缺點
優點
- 行內函數同巨集函式一樣將在被呼叫處進行程式碼展開,省去了引數壓棧、棧幀開闢與回收,結果返回等,從而提高程式執行速度。
- 行內函數相比巨集函式來說,在程式碼展開時,會做安全檢查或自動型別轉換(同普通函式),而巨集定義則不會。
- 在類中宣告同時定義的成員函式,自動轉化為行內函數,因此行內函數可以訪問類的成員變數,巨集定義則不能。
- 行內函數在執行時可除錯,而巨集定義不可以。
缺點
- 程式碼膨脹。內聯是以程式碼膨脹(複製)為代價,消除函式呼叫帶來的開銷。如果執行函式體內程式碼的時間,相比於函式呼叫的開銷較大,那麼效率的收穫會很少。另一方面,每一處行內函數的呼叫都要複製程式碼,將使程式的總程式碼量增大,消耗更多的記憶體空間。
- inline 函式無法隨著函式庫升級而升級。inline函式的改變需要重新編譯,不像 non-inline 可以直接連結。
- 是否內聯,程式設計師不可控。行內函數只是對編譯器的建議,是否對函式內聯,決定權在於編譯器。
虛擬函式(virtual)可以是行內函數(inline)嗎?
Are "inline virtual" member functions ever actually "inlined"?
- 虛擬函式可以是行內函數,內聯是可以修飾虛擬函式的,但是當虛擬函式表現多型性的時候不能內聯。
- 內聯是在編譯器建議編譯器內聯,而虛擬函式的多型性在執行期,編譯器無法知道執行期呼叫哪個程式碼,因此虛擬函式表現為多型性時(執行期)不可以內聯。
inline virtual
唯一可以內聯的時候是:編譯器知道所呼叫的物件是哪個類(如Base::who()
),這隻有在編譯器具有實際物件而不是物件的指標或引用時才會發生。
虛擬函式內聯使用
assert()
斷言,是巨集,而非函式。assert 巨集的原型定義在 <assert.h>
(C)、<cassert>
(C++)中,其作用是如果它的條件返回錯誤,則終止程式執行。可以通過定義 NDEBUG
來關閉 assert,但是需要在原始碼的開頭,include <assert.h>
之前。
assert() 使用
sizeof()
- sizeof 對陣列,得到整個陣列所佔空間大小。
- sizeof 對指標,得到指標本身所佔空間大小。
#pragma pack(n)
設定結構體、聯合以及類成員變數以 n 位元組方式對齊
#pragma pack(n) 使用
位域
Bit mode: 2; // mode 佔 2 位
類可以將其(非靜態)資料成員定義為位域(bit-field),在一個位域中含有一定數量的二進位制位。當一個程式需要向其他程式或硬體裝置傳遞二進位制資料時,通常會用到位域。
- 位域在記憶體中的佈局是與機器有關的
- 位域的型別必須是整型或列舉型別,帶符號型別中的位域的行為將因具體實現而定
- 取地址運算子(&)不能作用於位域,任何指標都無法指向類的位域
volatile
volatile int i = 10;
- volatile 關鍵字是一種型別修飾符,用它宣告的型別變量表示可以被某些編譯器未知的因素(作業系統、硬體、其它執行緒等)更改。所以使用 volatile 告訴編譯器不應對這樣的物件進行優化。
- volatile 關鍵字宣告的變數,每次訪問時都必須從記憶體中取出值(沒有被 volatile 修飾的變數,可能由於編譯器的優化,從 CPU 暫存器中取值)
- const 可以是 volatile (如只讀的狀態暫存器)
- 指標可以是 volatile
extern "C"
- 被 extern 限定的函式或變數是 extern 型別的
- 被
extern "C"
修飾的變數和函式是按照 C 語言方式編譯和連線的
extern "C"
的作用是讓 C++ 編譯器將 extern "C"
宣告的程式碼當作 C 語言程式碼處理,可以避免 C++ 因符號修飾導致程式碼不能和C語言庫中的符號進行連結的問題。
extern "C" 使用
struct 和 typedef struct
C 中
// c typedef struct Student { int age; } S;
等價於
// c struct Student { int age; }; typedef struct Student S;
此時 S
等價於 struct Student
,但兩個識別符號名稱空間不相同。
另外還可以定義與 struct Student
不衝突的 void Student() {}
。
C++ 中
由於編譯器定位符號的規則(搜尋規則)改變,導致不同於C語言。
一、如果在類識別符號空間定義了 struct Student {...};
,使用 Student me;
時,編譯器將搜尋全域性識別符號表,Student
未找到,則在類識別符號內搜尋。
即表現為可以使用 Student
也可以使用 struct Student
,如下:
// cpp struct Student { int age; }; void f( Student me ); // 正確,"struct" 關鍵字可省略
二、若定義了與 Student
同名函式之後,則 Student
只代表函式,不代表結構體,如下:
typedef struct Student { int age; } S; void Student() {} // 正確,定義後 "Student" 只代表此函式 //void S() {} // 錯誤,符號 "S" 已經被定義為一個 "struct Student" 的別名 int main() { Student(); struct Student me; // 或者 "S me"; return 0; }
C++ 中 struct 和 class
總的來說,struct 更適合看成是一個數據結構的實現體,class 更適合看成是一個物件的實現體。
區別
- 最本質的一個區別就是預設的訪問控制
- 預設的繼承訪問許可權。struct 是 public 的,class 是 private 的。
- struct 作為資料結構的實現體,它預設的資料訪問控制是 public 的,而 class 作為物件的實現體,它預設的成員變數訪問控制是 private 的。
union 聯合
聯合(union)是一種節省空間的特殊的類,一個 union 可以有多個數據成員,但是在任意時刻只有一個數據成員可以有值。當某個成員被賦值後其他成員變為未定義狀態。聯合有如下特點:
- 預設訪問控制符為 public
- 可以含有建構函式、解構函式
- 不能含有引用型別的成員
- 不能繼承自其他類,不能作為基類
- 不能含有虛擬函式
- 匿名 union 在定義所在作用域可直接訪問 union 成員
- 匿名 union 不能包含 protected 成員或 private 成員
- 全域性匿名聯合必須是靜態(static)的
union 使用
C 實現 C++ 類
explicit(顯式)建構函式
explicit 修飾的建構函式可用來防止隱式轉換
explicit 使用
friend 友元類和友元函式
- 能訪問私有成員
- 破壞封裝性
- 友元關係不可傳遞
- 友元關係的單向性
- 友元宣告的形式及數量不受限制
using
using 宣告
一條 using 宣告
語句一次只引入名稱空間的一個成員。它使得我們可以清楚知道程式中所引用的到底是哪個名字。如:
using namespace_name::name;
建構函式的 using 宣告【C++11】
在 C++11 中,派生類能夠重用其直接基類定義的建構函式。
class Derived : Base { public: using Base::Base; /* ... */ };
如上 using 宣告,對於基類的每個建構函式,編譯器都生成一個與之對應(形參列表完全相同)的派生類建構函式。生成如下型別建構函式:
derived(parms) : base(args) { }
using 指示
using 指示
使得某個特定名稱空間中所有名字都可見,這樣我們就無需再為它們新增任何字首限定符了。如:
using namespace_name name;
儘量少使用 using 指示
汙染名稱空間
一般說來,使用 using 命令比使用 using 編譯命令更安全,這是由於它只匯入了制定的名稱。如果該名稱與區域性名稱發生衝突,編譯器將發出指示。using編譯命令匯入所有的名稱,包括可能並不需要的名稱。如果與區域性名稱發生衝突,則區域性名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。另外,名稱空間的開放性意味著名稱空間的名稱可能分散在多個地方,這使得難以準確知道添加了哪些名稱。
using 使用
:: 範圍解析運算子
分類
- 全域性作用域符(
::name
):用於型別名稱(類、類成員、成員函式、變數等)前,表示作用域為全域性名稱空間 - 類作用域符(
class::name
):用於表示指定型別的作用域範圍是具體某個類的 - 名稱空間作用域符(
namespace::name
):用於表示指定型別的作用域範圍是具體某個名稱空間的
:: 使用
enum 列舉型別
限定作用域的列舉型別
enum class open_modes { input, output, append };
不限定作用域的列舉型別
enum color { red, yellow, green }; enum { floatPrec = 6, doublePrec = 10 };
decltype
decltype 關鍵字用於檢查實體的宣告型別或表示式的型別及值分類。語法:
decltype ( expression )
decltype 使用
引用
左值引用
常規引用,一般表示物件的身份。
右值引用
右值引用就是必須繫結到右值(一個臨時物件、將要銷燬的物件)的引用,一般表示物件的值。
右值引用可實現轉移語義(Move Sementics)和精確傳遞(Perfect Forwarding),它的主要目的有兩個方面:
- 消除兩個物件互動時不必要的物件拷貝,節省運算儲存資源,提高效率。
- 能夠更簡潔明確地定義泛型函式。
引用摺疊
X& &
、X& &&
、X&& &
可摺疊成X&
X&& &&
可摺疊成X&&
巨集
- 巨集定義可以實現類似於函式的功能,但是它終歸不是函式,而巨集定義中括弧中的“引數”也不是真的引數,在巨集展開的時候對 “引數” 進行的是一對一的替換。
成員初始化列表
好處
- 更高效:少了一次呼叫預設建構函式的過程。
- 有些場合必須要用初始化列表:
- 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表裡面
- 引用型別,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裡面
- 沒有預設建構函式的類型別,因為使用初始化列表可以不必呼叫預設建構函式來初始化,而是直接呼叫拷貝建構函式初始化。
initializer_list 列表初始化【C++11】
用花括號初始化器列表列表初始化一個物件,其中對應建構函式接受一個 std::initializer_list
引數.
initializer_list 使用
面向物件
面向物件程式設計(Object-oriented programming,OOP)是種具有物件概念的程式程式設計典範,同時也是一種程式開發的抽象方針。
面向物件三大特徵 —— 封裝、繼承、多型
封裝
- 把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。
- 關鍵字:public, protected, friendly, private。不寫預設為 friendly。
關鍵字 | 當前類 | 包內 | 子孫類 | 包外 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
friendly | √ | √ | × | × |
private | √ | × | × | × |
繼承
- 基類(父類)——> 派生類(子類)
多型
- 多型,即多種狀態,在面嚮物件語言中,介面的多種不同的實現方式即為多型。
- C++ 多型有兩種:靜態多型(早繫結)、動態多型(晚繫結)。靜態多型是通過函式過載實現的;動態多型是通過虛擬函式實現的。
- 多型是以封裝和繼承為基礎的。
靜態多型(早繫結)
函式過載
class A { public: void do(int a); void do(int a, int b); };
動態多型(晚繫結)
- 虛擬函式:用 virtual 修飾成員函式,使其成為虛擬函式
注意:
- 普通函式(非類成員函式)不能是虛擬函式
- 靜態函式(static)不能是虛擬函式
- 建構函式不能是虛擬函式(因為在呼叫建構函式時,虛表指標並沒有在物件的記憶體空間中,必須要建構函式呼叫完成後才會形成虛表指標)
- 行內函數不能是表現多型性時的虛擬函式,解釋見:虛擬函式(virtual)可以是行內函數(inline)嗎?
動態多型使用
虛解構函式
虛解構函式是為了解決基類的指標指向派生類物件,並用基類的指標刪除派生類物件。
虛解構函式使用
純虛擬函式
純虛擬函式是一種特殊的虛擬函式,在基類中不能對虛擬函式給出有意義的實現,而把它宣告為純虛擬函式,它的實現留給該基類的派生類去做。
virtual int A() = 0;
虛擬函式、純虛擬函式
- 類裡如果聲明瞭虛擬函式,這個函式是實現的,哪怕是空實現,它的作用就是為了能讓這個函式在它的子類裡面可以被覆蓋,這樣的話,這樣編譯器就可以使用後期繫結來達到多型了。純虛擬函式只是一個介面,是個函式的宣告而已,它要留到子類裡去實現。
- 虛擬函式在子類裡面也可以不過載的;但純虛擬函式必須在子類去實現。
- 虛擬函式的類用於 “實作繼承”,繼承介面的同時也繼承了父類的實現。當然大家也可以完成自己的實現。純虛擬函式關注的是介面的統一性,實現由子類完成。
- 帶純虛擬函式的類叫虛基類,這種基類不能直接生成物件,而只有被繼承,並重寫其虛擬函式後,才能使用。這樣的類也叫抽象類。抽象類和大家口頭常說的虛基類還是有區別的,在 C# 中用 abstract 定義抽象類,而在 C++ 中有抽象類的概念,但是沒有這個關鍵字。抽象類被繼承後,子類可以繼續是抽象類,也可以是普通類,而虛基類,是含有純虛擬函式的類,它如果被繼承,那麼子類就必須實現虛基類裡面的所有純虛擬函式,其子類不能是抽象類。
虛擬函式指標、虛擬函式表
- 虛擬函式指標:在含有虛擬函式類的物件中,指向虛擬函式表,在執行時確定。
- 虛擬函式表:在程式只讀資料段(
.rodata section
,見:目標檔案儲存結構),存放虛擬函式指標,如果派生類實現了基類的某個虛擬函式,則在虛表中覆蓋原本基類的那個虛擬函式指標,在編譯時根據類的宣告建立。
虛繼承
虛繼承用於解決多繼承條件下的菱形繼承問題(浪費儲存空間、存在二義性)。
底層實現原理與編譯器相關,一般通過虛基類指標和虛基類表實現,每個虛繼承的子類都有一個虛基類指標(佔用一個指標的儲存空間,4位元組)和虛基類表(不佔用類物件的儲存空間)(需要強調的是,虛基類依舊會在子類裡面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類裡面了);當虛繼承的子類被當做父類繼承時,虛基類指標也會被繼承。
實際上,vbptr 指的是虛基類表指標(virtual base table pointer),該指標指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝,節省了儲存空間。
虛繼承、虛擬函式
- 相同之處:都利用了虛指標(均佔用類的儲存空間)和虛表(均不佔用類的儲存空間)
- 不同之處:
- 虛繼承
- 虛基類依舊存在繼承類中,只佔用儲存空間
- 虛基類表儲存的是虛基類相對直接繼承類的偏移
- 虛擬函式
- 虛擬函式不佔用儲存空間
- 虛擬函式表儲存的是虛擬函式地址
- 虛繼承
模板類、成員模板、虛擬函式
- 模板類中可以使用虛擬函式
- 一個類(無論是普通類還是類模板)的成員模板(本身是模板的成員函式)不能是虛擬函式
抽象類、介面類、聚合類
- 抽象類:含有純虛擬函式的類
- 介面類:僅含有純虛擬函式的抽象類
- 聚合類:使用者可以直接訪問其成員,並且具有特殊的初始化語法形式。滿足如下特點:
- 所有成員都是 public
- 沒有有定於任何建構函式
- 沒有類內初始化
- 沒有基類,也沒有 virtual 函式
記憶體分配和管理
malloc、calloc、realloc、alloca
- malloc:申請指定位元組數的記憶體。申請到的記憶體中的初始值不確定。
- calloc:為指定長度的物件,分配能容納其指定個數的記憶體。申請到的記憶體的每一位(bit)都初始化為 0。
- realloc:更改以前分配的記憶體長度(增加或減少)。當增加長度時,可能需將以前分配區的內容移到另一個足夠大的區域,而新增區域內的初始值則不確定。
- alloca:在棧上申請記憶體。程式在出棧的時候,會自動釋放記憶體。但是需要注意的是,alloca 不具可移植性, 而且在沒有傳統堆疊的機器上很難實現。alloca 不宜使用在必須廣泛移植的程式中。C99 中支援變長陣列 (VLA),可以用來替代 alloca。
malloc、free
用於分配、釋放記憶體
malloc、free 使用
new、delete
- new / new[]:完成兩件事,先底層呼叫 malloc 分了配記憶體,然後呼叫建構函式(建立物件)。
- delete/delete[]:也完成兩件事,先呼叫解構函式(清理資源),然後底層呼叫 free 釋放空間。
- new 在申請記憶體時會自動計算所需位元組數,而 malloc 則需我們自己輸入申請記憶體空間的位元組數。
new、delete 使用
定位 new
定位 new(placement new)允許我們向 new 傳遞額外的引數。
new (palce_address) type new (palce_address) type (initializers) new (palce_address) type [size] new (palce_address) type [size] { braced initializer list }
palce_address
是個指標initializers
提供一個(可能為空的)以逗號分隔的初始值列表
delete this 合法嗎?
Is it legal (and moral) for a member function to say delete this?
合法,但:
- 必須保證 this 物件是通過
new
(不是new[]
、不是 placement new、不是棧上、不是全域性、不是其他物件成員)分配的 - 必須保證呼叫
delete this
的成員函式是最後一個呼叫 this 的成員函式 - 必須保證成員函式的
delete this
後面沒有呼叫 this 了 - 必須保證
delete this
後沒有人使用了
如何定義一個只能在堆上(棧上)生成物件的類?
只能在堆上
方法:將解構函式設定為私有
原因:C++ 是靜態繫結語言,編譯器管理棧上物件的生命週期,編譯器在為類物件分配棧空間時,會先檢查類的解構函式的訪問性。若解構函式不可訪問,則不能在棧上建立物件。
只能在棧上
方法:將 new 和 delete 過載為私有
原因:在堆上生成物件,使用 new 關鍵詞操作,其過程分為兩階段:第一階段,使用 new 在堆上尋找可用記憶體,分配給物件;第二階段,呼叫建構函式生成物件。將 new 操作設定為私有,那麼第一階段就無法完成,就不能夠在堆上生成物件。
智慧指標
C++ 標準庫(STL)中
標頭檔案:#include <memory>
C++ 98
std::auto_ptr<std::string> ps (new std::string(str));
C++ 11
- shared_ptr
- unique_ptr
- weak_ptr
- auto_ptr(被 C++11 棄用)
- Class shared_ptr 實現共享式擁有(shared ownership)概念。多個智慧指標指向相同物件,該物件和其相關資源會在 “最後一個 reference 被銷燬” 時被釋放。為了在結構較複雜的情景中執行上述工作,標準庫提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等輔助類。
- Class unique_ptr 實現獨佔式擁有(exclusive ownership)或嚴格擁有(strict ownership)概念,保證同一時間內只有一個智慧指標可以指向該物件。你可以移交擁有權。它對於避免記憶體洩漏(resource leak)——如 new 後忘記 delete ——特別有用。
shared_ptr
多個智慧指標可以共享同一個物件,物件的最末一個擁有著有責任銷燬物件,並清理與該物件相關的所有資源。
- 支援定製型刪除器(custom deleter),可防範 Cross-DLL 問題(物件在動態連結庫(DLL)中被 new 建立,卻在另一個 DLL 內被 delete 銷燬)、自動解除互斥鎖
weak_ptr
weak_ptr 允許你共享但不擁有某物件,一旦最末一個擁有該物件的智慧指標失去了所有權,任何 weak_ptr 都會自動成空(empty)。因此,在 default 和 copy 建構函式之外,weak_ptr 只提供 “接受一個 shared_ptr” 的建構函式。
- 可打破環狀引用(cycles of references,兩個其實已經沒有被使用的物件彼此互指,使之看似還在 “被使用” 的狀態)的問題
unique_ptr
unique_ptr 是 C++11 才開始提供的型別,是一種在異常時可以幫助避免資源洩漏的智慧指標。採用獨佔式擁有,意味著可以確保一個物件和其相應的資源同一時間只被一個 pointer 擁有。一旦擁有著被銷燬或程式設計 empty,或開始擁有另一個物件,先前擁有的那個物件就會被銷燬,其任何相應資源亦會被釋放。
- unique_ptr 用於取代 auto_ptr
auto_ptr
被 c++11 棄用,原因是缺乏語言特性如 “針對構造和賦值” 的 std::move
語義,以及其他瑕疵。
auto_ptr 與 unique_ptr 比較
- auto_ptr 可以賦值拷貝,複製拷貝後所有權轉移;unqiue_ptr 無拷貝賦值語義,但實現了
move
語義; - auto_ptr 物件不能管理陣列(析構呼叫
delete
),unique_ptr 可以管理陣列(析構呼叫delete[]
);
強制型別轉換運算子
static_cast
- 用於非多型型別的轉換
- 不執行執行時型別檢查(轉換安全性不如 dynamic_cast)
- 通常用於轉換數值資料型別(如 float -> int)
- 可以在整個類層次結構中移動指標,子類轉化為父類安全(向上轉換),父類轉化為子類不安全(因為子類可能有不在父類的欄位或方法)
向上轉換是一種隱式轉換。
dynamic_cast
- 用於多型型別的轉換
- 執行行執行時型別檢查
- 只適用於指標或引用
- 對不明確的指標的轉換將失敗(返回 nullptr),但不引發異常
- 可以在整個類層次結構中移動指標,包括向上轉換、向下轉換
const_cast
- 用於刪除 const、volatile 和 __unaligned 特性(如將 const int 型別轉換為 int 型別 )
reinterpret_cast
- 用於位的簡單重新解釋
- 濫用 reinterpret_cast 運算子可能很容易帶來風險。 除非所需轉換本身是低級別的,否則應使用其他強制轉換運算子之一。
- 允許將任何指標轉換為任何其他指標型別(如
char*
到int*
或One_class*
到Unrelated_class*
之類的轉換,但其本身並不安全) - 也允許將任何整數型別轉換為任何指標型別以及反向轉換。
- reinterpret_cast 運算子不能丟掉 const、volatile 或 __unaligned 特性。
- reinterpret_cast 的一個實際用途是在雜湊函式中,即,通過讓兩個不同的值幾乎不以相同的索引結尾的方式將值對映到索引。
bad_cast
- 由於強制轉換為引用型別失敗,dynamic_cast 運算子引發 bad_cast 異常。
bad_cast 使用
執行時型別資訊 (RTTI)
dynamic_cast
- 用於多型型別的轉換
typeid
- typeid 運算子允許在執行時確定物件的型別
- type_id 返回一個 type_info 物件的引用
- 如果想通過基類的指標獲得派生類的資料型別,基類必須帶有虛擬函式
- 只能獲取物件的實際型別
type_info
- type_info 類描述編譯器在程式中生成的型別資訊。 此類的物件可以有效儲存指向型別的名稱的指標。 type_info 類還可儲存適合比較兩個型別是否相等或比較其排列順序的編碼值。 型別的編碼規則和排列順序是未指定的,並且可能因程式而異。
- 標頭檔案:
typeinfo
typeid、type_info 使用
Effective C++
- 視 C++ 為一個語言聯邦(C、Object-Oriented C++、Template C++、STL)
- 寧可以編譯器替換前處理器(儘量以
const
、enum
、inline
替換#define
) - 儘可能使用 const
- 確定物件被使用前已先被初始化(構造時賦值(copy 建構函式)比 default 構造後賦值(copy assignment)效率高)
- 瞭解 C++ 默默編寫並呼叫哪些函式(編譯器暗自為 class 建立 default 建構函式、copy 建構函式、copy assignment 操作符、解構函式)
- 若不想使用編譯器自動生成的函式,就應該明確拒絕(將不想使用的成員函式宣告為 private,並且不予實現)
- 為多型基類宣告 virtual 解構函式(如果 class 帶有任何 virtual 函式,它就應該擁有一個 virtual 解構函式)
- 別讓異常逃離解構函式(解構函式應該吞下不傳播異常,或者結束程式,而不是吐出異常;如果要處理異常應該在非析構的普通函式處理)
- 絕不在構造和析構過程中呼叫 virtual 函式(因為這類呼叫從不下降至 derived class)
- 令
operator=
返回一個reference to *this
(用於連鎖賦值) - 在
operator=
中處理 “自我賦值” - 賦值物件時應確保複製 “物件內的所有成員變數” 及 “所有 base class 成分”(呼叫基類複製建構函式)
- 以物件管理資源(資源在建構函式獲得,在解構函式釋放,建議使用智慧指標,資源取得時機便是初始化時機(Resource Acquisition Is Initialization,RAII))
- 在資源管理類中小心 copying 行為(普遍的 RAII class copying 行為是:抑制 copying、引用計數、深度拷貝、轉移底部資源擁有權(類似 auto_ptr))
- 在資源管理類中提供對原始資源(raw resources)的訪問(對原始資源的訪問可能經過顯式轉換或隱式轉換,一般而言顯示轉換比較安全,隱式轉換對客戶比較方便)
- 成對使用 new 和 delete 時要採取相同形式(
new
中使用[]
則delete []
,new
中不使用[]
則delete
) - 以獨立語句將 newed 物件儲存於(置入)智慧指標(如果不這樣做,可能會因為編譯器優化,導致難以察覺的資源洩漏)
- 讓介面容易被正確使用,不易被誤用(促進正常使用的辦法:介面的一致性、內建型別的行為相容;阻止誤用的辦法:建立新型別,限制類型上的操作,約束物件值、消除客戶的資源管理責任)
- 設計 class 猶如設計 type,需要考慮物件建立、銷燬、初始化、賦值、值傳遞、合法值、繼承關係、轉換、一般化等等。
- 寧以 pass-by-reference-to-const 替換 pass-by-value (前者通常更高效、避免切割問題(slicing problem),但不適用於內建型別、STL迭代器、函式物件)
- 必須返回物件時,別妄想返回其 reference(絕不返回 pointer 或 reference 指向一個 local stack 物件,或返回 reference 指向一個 heap-allocated 物件,或返回 pointer 或 reference 指向一個 local static 物件而有可能同時需要多個這樣的物件。)
- 將成員變數宣告為 private(為了封裝、一致性、對其讀寫精確控制等)
- 寧以 non-member、non-friend 替換 member 函式(可增加封裝性、包裹彈性(packaging flexibility)、機能擴充性)
- 若所有引數(包括被this指標所指的那個隱喻引數)皆須要型別轉換,請為此採用 non-member 函式
- 考慮寫一個不拋異常的 swap 函式
- 儘可能延後變數定義式的出現時間(可增加程式清晰度並改善程式效率)
- 儘量少做轉型動作(舊式:
(T)expression
、T(expression)
;新式:const_cast<T>(expression)
、dynamic_cast<T>(expression)
、reinterpret_cast<T>(expression)
、static_cast<T>(expression)
、;儘量避免轉型、注重效率避免 dynamic_casts、儘量設計成無需轉型、可把轉型封裝成函式、寧可用新式轉型) - 避免使用 handles(包括 引用、指標、迭代器)指向物件內部(以增加封裝性、使 const 成員函式的行為更像 const、降低 “虛吊號碼牌”(dangling handles,如懸空指標等)的可能性)
- 為 “異常安全” 而努力是值得的(異常安全函式(Exception-safe functions)即使發生異常也不會洩露資源或允許任何資料結構敗壞,分為三種可能的保證:基本型、強列型、不拋異常型)
- 透徹瞭解 inlining 的裡裡外外(inlining 在大多數 C++ 程式中是編譯期的行為;inline 函式是否真正 inline,取決於編譯器;大部分編譯器拒絕太過複雜(如帶有迴圈或遞迴)的函式 inlining,而所有對 virtual 函式的呼叫(除非是最平淡無奇的)也都會使 inlining 落空;inline 造成的程式碼膨脹可能帶來效率損失;inline 函式無法隨著程式庫的升級而升級)
- 將檔案間的編譯依存關係降至最低(如果使用 object references 或 object pointers 可以完成任務,就不要使用 objects;如果能過夠,儘量以 class 宣告式替換 class 定義式;為宣告式和定義式提供不同的標頭檔案)
- 確定你的 public 繼承塑模出 is-a 關係(適用於 base classes 身上的每一件事情一定適用於 derived classes 身上,因為每一個 derived class 物件也都是一個 base class 物件)
- 避免遮掩繼承而來的名字(可使用 using 宣告式或轉交函式(forwarding functions)來讓被遮掩的名字再見天日)
- 區分介面繼承和實現繼承(在 public 繼承之下,derived classes 總是繼承 base class 的介面;pure virtual 函式只具體指定介面繼承;非純 impure virtual 函式具體指定介面繼承及預設實現繼承;non-virtual 函式具體指定介面繼承以及強制性實現繼承)
- 考慮 virtual 函式以外的其他選擇(如 Template Method 設計模式的 non-virtual interface(NVI)手法,將 virtual 函式替換為 “函式指標成員變數”,以
tr1::function
成員變數替換 virtual 函式,將繼承體系內的 virtual 函式替換為另一個繼承體系內的 virtual 函式) - 絕不重新定義繼承而來的 non-virtual 函式
- 絕不重新定義繼承而來的預設引數值,因為預設引數值是靜態繫結(statically bound),而 virtual 函式卻是動態繫結(dynamically bound)
Google C++ Style Guide
Google C++ Style Guide 圖
STL
索引
容器
容器 | 底層資料結構 | 時間複雜度 | 有無序 | 可不可重複 | 其他 |
---|---|---|---|---|---|
array | 陣列 | 隨機讀改 O(1) | 無序 | 可重複 | 支援快速隨機訪問 |
vector | 陣列 | 隨機讀改、尾部插入、尾部刪除 O(1) 頭部插入、頭部刪除 O(n) |
無序 | 可重複 | 支援快速隨機訪問 |
list | 雙向連結串列 | 插入、刪除 O(1) 隨機讀改 O(n) |
無序 | 可重複 | 支援快速增刪 |
deque | 雙端佇列 | 頭尾插入、頭尾刪除 O(1) | 無序 | 可重複 | 一箇中央控制器 + 多個緩衝區,支援首尾快速增刪,支援隨機訪問 |
stack | deque / list | 頂部插入、頂部刪除 O(1) | 無序 | 可重複 | deque 或 list 封閉頭端開口,不用 vector 的原因應該是容量大小有限制,擴容耗時 |
queue | deque / list | 尾部插入、頭部刪除 O(1) | 無序 | 可重複 | deque 或 list 封閉頭端開口,不用 vector 的原因應該是容量大小有限制,擴容耗時 |
priority_queue | vector + max-heap | 插入、刪除 O(log2n) | 有序 | 可重複 | vector容器+heap處理規則 |
set | 紅黑樹 | 插入、刪除、查詢 O(log2n) | 有序 | 不可重複 | |
multiset | 紅黑樹 | 插入、刪除、查詢 O(log2n) | 有序 | 可重複 | |
map | 紅黑樹 | 插入、刪除、查詢 O(log2n) | 有序 | 不可重複 | |
multimap | 紅黑樹 | 插入、刪除、查詢 O(log2n) | 有序 | 可重複 | |
hash_set | 雜湊表 | 插入、刪除、查詢 O(1) 最差 O(n) | 無序 | 不可重複 | |
hash_multiset | 雜湊表 | 插入、刪除、查詢 O(1) 最差 O(n) | 無序 | 可重複 | |
hash_map | 雜湊表 | 插入、刪除、查詢 O(1) 最差 O(n) | 無序 | 不可重複 | |
hash_multimap | 雜湊表 | 插入、刪除、查詢 O(1) 最差 O(n) | 無序 | 可重複 |
演算法
演算法 | 底層演算法 | 時間複雜度 | 可不可重複 |
---|---|---|---|
find | 順序查詢 | O(n) | 可重複 |
sort | 內省排序 | O(n*log2n) | 可重複 |
資料結構
順序結構
順序棧(Sequence Stack)
順序棧資料結構和圖片
佇列(Sequence Queue)
佇列資料結構
非迴圈佇列
非迴圈佇列圖片
迴圈佇列
迴圈佇列圖片
順序表(Sequence List)
順序表資料結構和圖片
鏈式結構
鏈式資料結構
鏈佇列(Link Queue)
鏈佇列圖片
線性表的鏈式表示
單鏈表(Link List)
單鏈表圖片
雙向連結串列(Du-Link-List)
雙向連結串列圖片
迴圈連結串列(Cir-Link-List)
迴圈連結串列圖片
雜湊表
概念
雜湊函式:H(key): K -> D , key ∈ K
構造方法
- 直接定址法
- 除留餘數法
- 數字分析法
- 摺疊法
- 平方取中法
衝突處理方法
- 鏈地址法:key 相同的用單鏈錶鏈接
- 開放定址法
- 線性探測法:key 相同 -> 放到 key 的下一個位置,
Hi = (H(key) + i) % m
- 二次探測法:key 相同 -> 放到
Di = 1^2, -1^2, ..., ±(k)^2,(k<=m/2)
- 隨機探測法:
H = (H(key) + 偽隨機數) % m
- 線性探測法:key 相同 -> 放到 key 的下一個位置,
線性探測的雜湊表資料結構
線性探測的雜湊表資料結構和圖片
遞迴
概念
函式直接或間接地呼叫自身
遞迴與分治
- 分治法
- 問題的分解
- 問題規模的分解
- 折半查詢(遞迴)
- 歸併查詢(遞迴)
- 快速排序(遞迴)
遞迴與迭代
- 迭代:反覆利用變數舊值推出新值
- 折半查詢(迭代)
- 歸併查詢(迭代)
廣義表
頭尾連結串列儲存表示
廣義表的頭尾連結串列儲存表示和圖片
擴充套件線性連結串列儲存表示
擴充套件線性連結串列儲存表示和圖片
二叉樹
性質
- 非空二叉樹第 i 層最多 2(i-1) 個結點 (i >= 1)
- 深度為 k 的二叉樹最多 2k - 1 個結點 (k >= 1)
- 度為 0 的結點數為 n0,度為 2 的結點數為 n2,則 n0 = n2 + 1
- 有 n 個結點的完全二叉樹深度 k = ⌊ log2(n) ⌋ + 1
- 對於含 n 個結點的完全二叉樹中編號為 i (1 <= i <= n) 的結點
- 若 i = 1,為根,否則雙親為 ⌊ i / 2 ⌋
- 若 2i > n,則 i 結點沒有左孩子,否則孩子編號為 2i
- 若 2i + 1 > n,則 i 結點沒有右孩子,否則孩子編號為 2i + 1
儲存結構
二叉樹資料結構
順序儲存
二叉樹順序儲存圖片
鏈式儲存
二叉樹鏈式儲存圖片
遍歷方式
- 先序遍歷
- 中序遍歷
- 後續遍歷
- 層次遍歷
分類
- 滿二叉樹
- 完全二叉樹(堆)
- 大頂堆:根 >= 左 && 根 >= 右
- 小頂堆:根 <= 左 && 根 <= 右
- 二叉查詢樹(二叉排序樹):左 < 根 < 右
- 平衡二叉樹(AVL樹):| 左子樹樹高 - 右子樹樹高 | <= 1
- 最小失衡樹:平衡二叉樹插入新結點導致失衡的子樹:調整:
- LL型:根的左孩子右旋
- RR型:根的右孩子左旋
- LR型:根的左孩子左旋,再右旋
- RL型:右孩子的左子樹,先右旋,再左旋
其他樹及森林
樹的儲存結構
- 雙親表示法
- 雙親孩子表示法
- 孩子兄弟表示法
並查集
一種不相交的子集所構成的集合 S = {S1, S2, ..., Sn}
平衡二叉樹(AVL樹)
性質
- | 左子樹樹高 - 右子樹樹高 | <= 1
- 平衡二叉樹必定是二叉搜尋樹,反之則不一定
- 最小二叉平衡樹的節點的公式:
F(n)=F(n-1)+F(n-2)+1
(1 是根節點,F(n-1) 是左子樹的節點數量,F(n-2) 是右子樹的節點數量)
平衡二叉樹圖片
最小失衡樹
平衡二叉樹插入新結點導致失衡的子樹
調整:
- LL 型:根的左孩子右旋
- RR 型:根的右孩子左旋
- LR 型:根的左孩子左旋,再右旋
- RL 型:右孩子的左子樹,先右旋,再左旋
紅黑樹
紅黑樹的特徵是什麼?
- 節點是紅色或黑色。
- 根是黑色。
- 所有葉子都是黑色(葉子是 NIL 節點)。
- 每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)(新增節點的父節點必須相同)
- 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。(新增節點必須為紅)
調整
- 變色
- 左旋
- 右旋
應用
- 關聯陣列:如 STL 中的 map、set
紅黑樹、B 樹、B+ 樹的區別?
- 紅黑樹的深度比較大,而 B 樹和 B+ 樹的深度則相對要小一些
- B+ 樹則將資料都儲存在葉子節點,同時通過連結串列的形式將他們連線在一起。
B 樹(B-tree)、B+ 樹(B+-tree)
B 樹、B+ 樹圖片
特點
- 一般化的二叉查詢樹(binary search tree)
- “矮胖”,內部(非葉子)節點可以擁有可變數量的子節點(數量範圍預先定義好)
應用
- 大部分檔案系統、資料庫系統都採用B樹、B+樹作為索引結構
區別
- B+樹中只有葉子節點會帶有指向記錄的指標(ROWID),而B樹則所有節點都帶有,在內部節點出現的索引項不會再出現在葉子節點中。
- B+樹中所有葉子節點都是通過指標連線在一起,而B樹不會。
B樹的優點
對於在內部節點的資料,可直接得到,不必根據葉子節點來定位。
B+樹的優點
- 非葉子節點不會帶上 ROWID,這樣,一個塊中可以容納更多的索引項,一是可以降低樹的高度。二是一個內部節點可以定位更多的葉子節點。
- 葉子節點之間通過指標來連線,範圍掃描將十分簡單,而對於B樹來說,則需要在葉子節點和內部節點不停的往返移動。
B 樹、B+ 樹區別來自:differences-between-b-trees-and-b-trees、B樹和B+樹的區別
八叉樹
八叉樹圖片
八叉樹(octree),或稱八元樹,是一種用於描述三維空間(劃分空間)的樹狀資料結構。八叉樹的每個節點表示一個正方體的體積元素,每個節點有八個子節點,這八個子節點所表示的體積元素加在一起就等於父節點的體積。一般中心點作為節點的分叉中心。
用途
- 三維計算機圖形
- 最鄰近搜尋
演算法
排序
排序演算法 | 平均時間複雜度 | 最差時間複雜度 | 空間複雜度 | 資料物件穩定性 |
---|---|---|---|---|
氣泡排序 | O(n2) | O(n2) | O(1) | 穩定 |
選擇排序 | O(n2) | O(n2) | O(1) | 陣列不穩定、連結串列穩定 |
插入排序 | O(n2) | O(n2) | O(1) | 穩定 |
快速排序 | O(n*log2n) | O(n2) | O(log2n) | 不穩定 |
堆排序 | O(n*log2n) | O(n*log2n) | O(1) | 不穩定 |
歸併排序 | O(n*log2n) | O(n*log2n) | O(n) | 穩定 |
希爾排序 | O(n*log2n) | O(n2) | O(1) | 不穩定 |
計數排序 | O(n+m) | O(n+m) | O(n+m) | 穩定 |
桶排序 | O(n) | O(n) | O(m) | 穩定 |
基數排序 | O(k*n) | O(n2) | 穩定 |
- 均按從小到大排列
- k:代表數值中的 “數位” 個數
- n:代表資料規模
- m:代表資料的最大值減最小值
- 來自:wikipedia . 排序演算法
查詢
查詢演算法 | 平均時間複雜度 | 空間複雜度 | 查詢條件 |
---|---|---|---|
順序查詢 | O(n) | O(1) | 無序或有序 |
二分查詢(折半查詢) | O(log2n) | O(1) | 有序 |
插值查詢 | O(log2(log2n)) | O(1) | 有序 |
斐波那契查詢 | O(log2n) | O(1) | 有序 |
雜湊查詢 | O(1) | O(n) | 無序或有序 |
二叉查詢樹(二叉搜尋樹查詢) | O(log2n) | ||
紅黑樹 | O(log2n) | ||
2-3樹 | O(log2n - log3n) | ||
B樹/B+樹 | O(log2n) |
圖搜尋演算法
圖搜尋演算法 | 資料結構 | 遍歷時間複雜度 | 空間複雜度 |
---|---|---|---|
BFS廣 |