C++ Primer#
- 單文件編譯
g++ -o 輸出文件名 待編譯文件名
- 讀取、輸出到文件
程序名 < 輸入文件
程序名 > 輸出文件
數據類型#
- 選用的經驗:
數值不為負選擇無符號型 (unsigned)
整數運算選用int
,超過表示範圍的話選用long long
浮點數運算選用double
- 不要混用有符號類型和無符號類型,帶符號數會自動轉換為無符號數,等於初始值對無符號類型所能表示的數值總數取模後的餘數
- 字符型字面值常量的類型可以通過前綴指定,整型、浮點型可以通過後綴指定
變量#
- 初始化和賦值存在本質上的巨大區別
- 使用列表初始化
{}
,在可能的數據丟失時會使編譯器給出警告 - 聲明 (declaration) 和定義 (definition) 是不同的,聲明使名字為程序所知,定義則為變量申請存儲空間或賦初值
- 使用
extern
關鍵字來聲明變量 - 變量能且只能被定義一次,但可以被多次聲明
- C++ 是靜態語言 —— 在編譯階段檢查類型
- 全局變量在塊作用域內用
::
前綴可以顯式訪問(屏蔽塊作用域內的局部變量) - 局部變量最好不要同全局變量同名
複合類型#
- 通過在變量名前加
&
來定義引用類型,引用必須初始化 - 引用即別名
- 引用只能綁定在對象上
- 指針存放對象的地址,使用取地址符
&
獲取地址 - 利用指針訪問對象,使用解引用符
*
訪問對象 &
和*
出現在聲明和表達式中的含義截然不同- 初始化空指針 (=nullptr、=0、=NULL)
- 賦值永遠改變的是等號左側的對象
void*
可以存放任意對象的地址- 類型修飾符 (
*
和&
) 僅修飾其後的第一個變量標識符
const 限定符#
- 常量引用是對 const 的引用
- 在初始化常量引用時允許使用任意表達式作為初始值,只要該表達式的結果能夠轉換成引用的類型即可
- 對 const 的引用可能引用一個並非 const 的對象
- 所謂指向常量的指針或引用不過是指針或引用 **“自以為是”** 地認為自己指向常量,所以自覺地不去改變所指對象的值
- 弄清聲明的含義最有效的方法是從右向左閱讀
*
放在 const 之前說明指針是一個常量 - 不變的是指針本身的值而非指向的那個值- 指針本身是一個常量並不意味著不能通過指針修改其所指向對象的值
- 非常量可以轉換為常量 反之則不行
- 常量表達式是指值不會改變並且在編譯過程中就能得到計算結果的表達式
- 一個對象是不是常量表達式由它的數據類型和初始值共同決定
- 頂層 const 表示指針本身是個常量;底層 const 表示指針所指的對象是個常量(僅示例,頂層和底層 const 適用於各種類型)
- 當執行對象的拷貝操作時,頂層 const 和底層 const 區別明顯
處理類型#
- 類型別名:
typedef 前者 後者
—— 後者是前者的同義詞 - 注意 typedef 中使用
*
的情況(並不是簡單的替換關係),const
是對給定類型的修飾 - 別名聲明:
using 前者 = 後者
—— 前者是後者的同義詞 auto
根據初值自動推斷數據類型(僅保留底層 const)頂層 const 需要在 auto 前加以修飾*
和&
只從屬於某個聲明符而非基本數據類型的一部分decltype
推斷表達式類型而不使用其作為初值(保留變量的全部類型)- 如果 decltype 使用的表達式不是一個變量,則 decltype 返回表達式結果對應的類型
- 如果表達式的內容是解引用操作,則 decltype 將得到引用類型
- 對於 decltype 所用的表達式來說,在變量名上加括號與不加括號得到的類型會有不同
decltype((變量))
的結果永遠是引用,decltype(變量)
只有在變量本身是引用時才是引用
自定義數據結構#
struct 類名 類體 ;
- 預處理器保證頭文件多次包含仍能安全工作 —— 頭文件保護符
#define
把一個名字設定為預處理變量#ifdef
當且僅當變量已定義時為真#ifndef
當且僅當變量未定義時為真#endif
檢查結果為真時執行後續操作直到出現此命令- 一般將預處理變量名全部大寫以保證其唯一性
- 頭文件一旦改變,相關的源文件必須重新編譯以獲取更新過的聲明
using 聲明#
using namespace::name;
- 頭文件中不應包含 using 聲明
string#
std::string
可變長字符序列- 在執行讀取操作時,string 對象會自動忽略開頭的空白(空格符、換行符、制表符等)並從第一個真正的字符開始讀起,直到遇見下一處空白為止
- 常用操作:
getline(a,b)
—— 從 a 中讀取一行(以換行符為界)賦給 bs.empty()
—— 判斷 s 是否為空s.size()
- size 函數的返回值是一個無符號整型數(類型為
string::size_type
) 注意避免 int 和 unsigned 混用 - string 的比較:1. 字符相同時,較短 string 小於較長 string;2. 字符相異時,第一對相異字符的比較
- 當 string 對象和字符 / 字符串字面值混在一條語句中時,必須確保每個 + 兩側的運算對象至少有一個是 string
- 字符串字面值與 string 是不同的類型
- 使用 C++ 版本的 C 標準庫頭文件
ctype.h
=>cctype
- cctype 中包含一系列字符的判斷和處理函數
- 基於範圍的 for 語句
for (declaration : expression)
類似於 python 中的 for 語句 - string 中的字符可以通過下標訪問
- 始終注意檢查下標的合法性(是否在正確的範圍內)
vector#
std::vector
表示對象的集合(所有對象的類型相同),也被稱為容器- vector 是一個類模板而非類型
vector<類型> 容器名;
- vector 豐富的初始化方式:列表 (
vector<T> v5{a,b,c...}
或vector<T> v5={a,b,c...}
)、拷貝 (vector<T> v2(v1)
或vector<T> v2 = v1
)、構造 (vector<T> v3(n,val)
或vector<T> v3(n)
)... push_back(值)
將值作為 vector 的尾元素壓到 vector 的尾端- vector 能高效地快速添加元素(沒有必要為其指定容量)
- 如果循環體內部包含有向 vector 添加元素的語句,則不能使用範圍 for 循環
- empty 和 size 函數與 string 的類似
- size 函數的返回值也是屬於 vector 的特殊類型
size_type
但需要指出 vector 的元素類型 - vector 的比較法則也與 string 類似
- vector 不能使用下標添加元素,只能使用下標訪問已存在的元素
迭代器#
- 有迭代器的類型同時擁有返回迭代器的成員
begin()
返回指向第一個元素的迭代器end()
返回指向尾元素下一位置 **(尾後)** 的迭代器- 一般來說,我們不清楚迭代器的準確類型(使用
auto
來定義變量) *iter
返回迭代器所指元素的引用iter->mem
++iter
/--iter
指示容器的下一個 / 上一個元素- 泛型編程:所有標準庫容器的迭代器都定義了
==
和!=
,所以在 for 循環中使用!=
而非<
,因為這種編程風格在標準庫提供的所有容器中都有效 const_iterator
只能讀元素,不能寫元素->
即為解引用和成員訪問的結合,it->mem 等價於 (*it).mem- 任何一種可能改變 vector 容量的操作,都会使該 vector 的迭代器失效
- 凡是使用了迭代器的循環體,都不要向迭代器所屬的容易添加元素
- 兩個迭代器相減的結果為
difference_type
(帶符號整型數)
陣列#
- 與 vector 類似的是存放類型相同的對象的容器;不同的是陣列的大小確定不變,不能隨意向其增加元素
- 陣列中元素的個數也是陣列類型的一部分,所以需要為常量表達式
- 陣列的初始化:列表初始化,不允許拷貝
- 字符陣列可以使用字符串字面值初始化,但要注意字符串字面值結尾處自帶一個空字符
- 默認情況下,類型修飾符從右向左依次綁定;但對於陣列而言,由(括號)內向外閱讀更有意義
- 在使用陣列下標時,通常將其定義為
size_t
類型 - 使用陣列類型的對象其實是使用一個指向該陣列首元素的指針
- 當使用陣列作為一個 auto 變量的初始值時,推斷得到的類型是指針而非陣列;但是 decltype 不會發生上述轉換
- 陣列可以使用下標索引尾元素後那個並不存在的元素
begin(陣列名)
/end(陣列名)
能安全地返回首元素指針 / 尾後元素指針- 兩個指針相減的結果為
ptrdiff_t
- 如果兩個指針分別指向不相關的對象,則不能比較
- 內置的下標運算符所用的索引值不是無符號類型,這與 vector 和 string 是不同的
- C 風格字符串存放在字符陣列中並以空字符(
\0
)結束 - 頭文件
cstring
中定義的函數可以操作 C 風格字符串 - 使用標準庫 string 比使用C 風格字符串更安全高效
- 儘量使用標準庫類型而非陣列
多維陣列#
- 嚴格來說,C++ 中沒有多維陣列,通常所說的多維陣列其實是陣列的陣列
- 使用
{}
括起來的一組值初始化多維陣列,花括號嵌套與否完全等價(嵌套只是為了更清晰地閱讀) - 可以僅初始化部分元素,其它元素執行默認初始化
- 使用範圍 for 語句處理多維陣列,除了最內層的循環外,其他所有循環的控制變量都應該是引用類型
- 當程序使用多維陣列的名字時,會自動將其轉換成指向陣列首元素的指針,即指向第一個內層陣列的指針
表達式基礎#
- 左值和右值:左值可以位於賦值語句的左側,右值則不能(在 C++ 中並非如此簡單)
- 當一個對象被用作右值的時候,用的是對象的值(內容);當對象被用作左值的時候,用的是對象的身份(在內存中的位置)
- 賦值運算符需要一個非常量左值作為其左側運算符,得到的結果也為左值
- 取地址符作用於一個左值運算對象,返回一個指向該運算對象的指針,該指針為右值
- 解引用運算符、下標運算符的求值結果為左值
- 複合表達式中,括號無視優先級與結合律
- 求值順序在大多數運算符中沒有明確規定,除了
&&
、||
、?:
、,
外
運算符#
- 整數相除的商值無論正負一律向 0 取整(舍棄小數部分)
(-m)/n
和m/(-n)
都等價於-(m/n)
,m%(-n)
等價於m%n
,(-m)%n
等價於-(m%n)
- 除非必須,否則不用遞增遞減運算符的後置版本
ptr->mem
等價於(*ptr).mem
P.S. 解引用運算符的優先級低於點運算符- 條件運算符(
cond?expr1:expr2
)可嵌套 最好不超過兩到三層 - 條件運算符的優先級非常低,通常需要在它兩端加括號
- 僅將位運算符用於處理無符號類型
- 移位運算符(IO 運算符)的優先級不高不低:低於算術運算符,高於關係運算符、賦值運算符、條件運算符
sizeof
返回一條表達式或一個類型名字所占的字節數sizeof (type)
和sizeof expr
sizeof
並不實際計算其運算對象的值- 對 char 或類型為 char 的表達式執行 sizeof 運算結果為 1
- sizeof 運算不會把陣列轉換成指針來處理,等價於對陣列中所有元素執行一次 sizeof 運算並將所得結果求和
- 對 string 或 vector 對象執行 sizeof 運算只返回該類型固定部分的大小,不會計算對象中的元素佔用了多少空間
- 逗號運算符真正的結果是右側表達式的值
類型轉換#
- 算術轉換:一種算術類型 -> 另一種算數類型,如運算符的運算對象將轉換成最寬的類型,整數值將轉換成浮點類型
- 整型提升:小整數類型 -> 較大的整數類型,如
bool
、char
、short
提升為int
、long
等 - 強制類型轉換
cast-name<type>(expression)
- 任何具有明確定義的類型轉換,只要不包含底層 const,都可以使用
static_cast
- 當需要把一個較大的算術類型賦值給較小的類型時,
static_cast
非常有用 static_cast
對於編譯器無法自動執行的類型轉換也非常有用const_cast
只能改變運算對象的底層 const,只有const_cast
能改變表達式的常量屬性- 使用
reinterpret_cast
非常危險 - 要儘量避免強制類型轉換
- 舊式的強制類型轉換
type (expr)
和(type) expr
,不夠清晰明了,追蹤困難
運算符優先級#
條件語句#
else
與離它最近的未匹配的if
匹配,使用花括號可以強制匹配case
標籤必須是整形常量表達式- 如果在某處一個帶有初值的變量位於作用域之外,在另一處該變量位於作用域之內,則從前一處跳轉到后一處的行為為非法行為
迭代語句#
- 傳統 for 循環的執行流程:首先執行 init-statement;接下來判斷 condition;條件為真,執行循環體;最後執行 expression
- for 語句中的 init-statement 可定義多個對象,但只能有一條聲明語句,所以所有變量的基礎類型必須相同
- 在範圍 for 語句中,當範圍變量為引用類型時,才能對元素執行寫操作
- 在範圍 for 語句中,使用
auto
可以保證類型相容 - 範圍 for 語句的等價傳統 for 語句(不能用範圍 for 語句增加 vector 對象或其他容器的元素)
do while
與while
十分相似,只是先執行循環體後檢查條件
跳轉語句#
break
負責終止離它最近的while
、do while
、for
、switch
語句,並從這些語句後的第一條語句開始繼續執行continue
用於終止離它最近的for
、while
、do while
循環中的當前迭代並立即開始下一次迭代goto label;
label 是用於標識一條語句的標示符
try 語句塊和異常處理#
- 程序的異常檢測部分使用
throw
表達式引發一個異常 try
塊後跟隨一個或多個catch
子句,由try
中拋出的異常來選中對應的catch
子句- C 風格字符串(
const char*
) - 異常中斷了程序的正常流程,那些在異常發生期間正確執行了 “清理” 工作的程序被稱作異常安全的代碼
stdexcept
頭文件定義了幾種常用的異常類,另外幾種異常類型:exception
、bad_alloc
、bad_cast
- 後三種異常只能以默認初始化的方式初始化,不允許為這些對象提供初始值;其它異常類型使用 string 對象或者 C 風格字符串初始化這些類型的對象,不允許使用默認初始化的方式
- 異常類型只定義了一個成員函數
what
,該函數沒有參數,返回一個指向 C 風格字符串的const char*
,提供關於異常的信息
函數基礎#
- 函數形參列表中的形參通常用逗號隔開,其中每個形參都含有一個聲明符的聲明,即使兩個形參的類型一樣,也必須把兩個類型都寫出來
- 形參名是可選的,當函數確實有個別形參不會被用到時,此類形參通常不命名以表示在函數體內不會使用它
- 函數的返回類型不能是陣列類型或函數類型,但可以是指向陣列或函數的指針
- 名字有作用域,對象有生命週期
- 形參和函數體內部定義的變量為局部變量,僅在函數的作用域內可見,還會隱藏在外層作用域中同名的其他所有聲明中
- 只存在於塊執行期間的對象成為自動對象
- 局部靜態對象在程序的執行路徑第一次經過對象定義語句時初始化,並直到程序終止才被銷毀 ——
static
- 函數名需要在使用前聲明,函數只能定義一次,但可以聲明多次,函數聲明無需函數體,用
;
替代即可 - 函數聲明也稱作函數原型
- 含有函數聲明的頭文件應該被包含到定義函數的源文件中
- 分離式編譯允許程序分散在幾個文件中,每個文件獨立編譯
參數傳遞#
- 引用傳遞和值傳遞
- C++ 中建議使用引用類型的形參代替指針類型訪問函數外部的對象
- 使用引用來避免拷貝
- 當函數無須修改引用形參的值時最好使用常量引用
- 使用引用形參返回額外信息:一個函數只能返回一個值,但有時函數需要同時返回多個值,引用形參為我們一次返回多個結果提供了有效途徑
- 用實參初始化形參時會忽略掉頂層 const(作用於對象本身)
- 在 C++ 中允許定義若干具有相同名字的函數,前提是不同函數的形參列表有明顯的區別
- 可以用非常量初始化一個底層 const 對象,但反之不行;一個普通的引用必須用同類型的對象初始化
- C++ 允許使用字面值初始化常量引用
- 儘量使用常量引用,把函數不會改變的形參定義為普通引用是一種常見錯誤
- 陣列的特殊性:不允許拷貝陣列,使用陣列時會將其轉換成指針
- 儘管不能以值傳遞的方式傳遞陣列,但可以把形參寫成類似陣列的形式,本質上傳遞的還是指向陣列首元素的指針
- 管理指針形參(陣列實參)的三種技術:1. 使用標記指定陣列長度;2. 使用標準庫規範(傳遞指向陣列首元素和尾後元素的指針);3. 顯式傳遞一個表示陣列大小的形參
- 形參也可以是陣列的引用,如
int (&arr)[10]
,陣列大小是構成陣列類型的一部分 - 傳遞多維陣列,如
int matrix[][10]
,編譯器會忽略第一個維度,最好不要把它包括在形參列表內。matrix 的聲明看起來是一個二維陣列,實際上形參是指向含有 10 個整數的陣列的指針 int main(int argc, char *argv[]) {...}
第二個形參是一個陣列,其元素是指向 C 風格字符串的指針;第一個形參表示陣列中字符串的數量int main(int argc, char **argv) {...}
等價於上述代碼- 當使用 argv 中的實參時,可選的實參從
argv[1]
開始,argv[0]
保存程序名 - 如果函數的實參數量未知但全部實參的類型都相同,可以使用
initializer_list
類型的形參,使用方式類似於vector
initializer_list
中的元素永遠是常量值,無法改變initializer_list
對象中元素的值- 省略符形參是為了便於 C++ 程序訪問某些特殊的 C 代碼(使用了 C 標準庫 varargs)而設置的,形如
void foo(parm_list, ...);
和void foo(n...)
返回類型和 return 語句#
- 返回 void 的函數不必須要有 return,因為在最後總會隱式執行。若想讓函數提前退出,可以使用 return。
- 在含有 return 語句的循環後也應該有一條 return 語句,否則程序是錯誤的且難以被編譯器發現
- 不要返回局部對象的引用或指針
- 引用返回左值:調用一個返回引用的函數得到左值,其他返回類型得到右值
- C++11 規定,函數可以返回
{}
包圍的值的列表 (返回類型為vector<類型>
) - 頭文件
cstdlib
中定義了兩個預處理變量EXIT_FAILURE
、EXIT_SUCCESS
,可以作為 main 函數的返回值 - 遞歸:函數調用自身(main 函數不能調用自己)
- 函數不能返回陣列但可以返回陣列的指針或引用
- 要想定義一個返回陣列的指針或引用可以使用類型別名,如
typedef int arrT[10]
、等價寫法using arrT = int[10]
,此時,arrT* func(int i)
中的 func 函數即返回一個指向含有 10 個整數的陣列的指針 - 除了類型別名,返回陣列指針的函數形式如
Type (*function(parameter_list))[dimension]
- 還可以使用尾置返回類型,如
auto func(int i) -> int(*)[10]
- 或者使用
decltype(陣列名)*
來聲明函數
函數重載#
- 重載函數:同一作用域內的幾個函數名相同但形參列表不同(main 函數不能重載)
- 重載函數的返回類型需要一致,不允許同名函數返回不同的類型
- 頂層 const 形參並不區分重載函數而底層 const(指針、引用)可以區分重載函數
- 最好只重載非常相似的操作
const_cast
在重載函數的情景中最有用- 函數匹配也叫重載確定,在調用重載函數時可能的三種結果:最佳匹配、無匹配、二義性調用
- 在 C++ 中,名字查找發生在類型檢查之前
特殊用途語言特性#
- 一旦某個形參被賦予了默認值,它後面的所有形參都必須有默認值
- 默認實參負責填補函數調用缺少的尾部實參
- 當設計含有默認實參的函數時,其中一項任務是合理設置形參的順序,儘量讓不怎麼使用默認值的形參出現在前面,讓那些經常使用默認值的形參出現在後面
- 在給定的作用域中一個形參只能被賦予一次默認實參,函數的後續聲明只能為之前沒有默認值的形參添加默認實參,且該形參右側的所有形參必須都有默認值
- 應在函數聲明中指定默認實參,並將該聲明放在合適的頭文件中
- 只要表達式的類型能轉換成形參所需的類型,該表達式就能作為默認實參;用作默認實參的名字在函數聲明所在的作用域內解析,而這些名字的求值過程發生在函數調用時
- 內聯函數可以避免函數調用的開銷
- 在函數返回類型前加上
inline
就可將其聲明為內聯函數 - 內聯說明只是向編譯器發出請求,編譯器可以選擇忽略這個請求
- 內聯機制一般用於優化規模較小、流程直接、頻繁調用的函數
- constexpr 函數是指能用於常量表達式的函數,函數的返回類型及所有形參的類型都為字面值類型且函數體中有且只有一條 retuen 語句
- 在編譯過程中,constexpr 函數被隱式指定為內聯函數
- 允許 constexpr 函數的返回值並非一個常量
- 內聯函數和 constexpr 函數通常放在頭文件中
assert
預處理宏,用法:assert (expr)
,對 expr 求值,若為假,輸出信息並終止運行;若為真,什麼也不做- 預處理名字由預處理管理器而非編譯器管理,應直接使用預處理名字而無需 using 聲明
assert
的行為依賴於NDEBUG
預處理變量的狀態,當#define NDEBUG
時,assert
什麼也不做- 編譯器定義了一些局部靜態變量用於程序調試,
_ _func_ _
、_ _FILE_ _
、_ _LINE_ _
、_ _TIME_ _
、_ _DATE_ _
函數匹配#
- 候選函數:同名函數、聲明可見
- 可行函數:形參數量相等、形參類型相同
- 尋找最佳匹配
- 如果沒有一個函數脫穎而出,編譯器會因調用具有二義性而拒絕請求
- 調用重載函數應儘量避免強制類型轉換。如果在實際應用中確需強制類型轉換,則說明設計的形參集合不合理。
- 實參類型轉換的等級:1. 精確匹配;2. 通過 const 轉換實現的匹配;3. 通過類型提升實現的匹配;4. 通過算數類型轉換或指針轉換實現的匹配;5. 通過類型轉換實現的匹配
- 內置類型提升和轉換可能在函數匹配時產生意想不到的結果
- 所有算數類型轉換的級別都一樣
函數指針#
- 函數指針指向的是函數而非對象
- 函數
bool lengthCompare(const string &, const string &);
,聲明一個指向該函數的指針,bool (*pf)(const string &, const string &);
pf = lengthCompare;
等價於pf = &lengthCompare
bool b = pf("hello","goodbye");
等價於bool b = (*pf)("hello","goodbye");
等價於bool b = lengthCompare("hello","goodbye");
- 與陣列類似,雖然不能定義函數類型的形參,但形參可以是指向函數的指針,形參看起來是函數類型,實際上當作指針;可以直接把函數作為實參使用,它也會自動轉換為指針
- 使用類型別名和
decltype
可以簡化使用函數指針的代碼,如typedef decltype(lengthCompare) Func;
定義了函數類型,typedef decltype(lengthCompare) *FuncP;
定義了函數指針 using F = int(int*, int);
定義了函數類型 F,using PF = int(*)(int*, int);
定義了指向函數類型的指針 PF- 當
decltype
作用於函數時,它返回函數類型而非指針類型,需要顯式地加上*
來表示需要返回指針
定義抽象數據類型#
- 類 = 數據抽象 + 封裝;數據抽象 = 接口 + 實現
- 定義在類內部的函數是隱式的
inline
函數 - 成員函數的聲明必須在類的內部,它的定義既可以在類的內部也可以在類的外部;作為接口組成部分的非成員函數,它們的定義和聲明都在類的外部
- 成員函數通過一個名為
this
的額外的隱式參數來訪問調用它的那個對象。當我們調用一個成員函數時,用請求該函數的對象地址初始化this
- 因為
this
的目的總是指向 “這個” 對象,所以this
是一個常量指針,不允許改變this
中保存的地址 - 當把
const
關鍵字放在成員函數的參數列表之後,緊跟在參數列表後的const
的作用是修改隱式this
指針的類型,表示this
是一個指向常量的指針=> 這樣使用const
的成員函數稱為常量成員函數 - 常量成員函數不能改變調用它的對象的內容
- 常量對象,以及常量對象的引用或指針都只能調用常量成員函數
- 編譯器首先編譯成員的聲明,然後才輪到成員函數體。所以,成員函數體可以隨意使用類中的其它成員而無需在意這些成員出現的次序
- 成員函數的定義必須與它的聲明匹配,同時,類外部定義的成員的名字必須包含它所屬的類名
return *this
返回調用該函數的對象,函數的返回類型應為對應類型的引用- 如果非成員函數是類接口的組成部分,這些函數的聲明應該與類在同一個頭文件內
- 構造函數不能被聲明成
const
的 - 編譯器創建的構造函數又稱合成的默認構造函數
- 只有當類沒有聲明任何構造函數時,編譯器才會自動地生成默認構造函數
- 如果類包含有內置類型或者符合類型的成員,則只有當這些成員全都被賦予了類內的初始值時,這個類才適合於使用合成的默認構造函數
= default
要求編譯器生成默認構造函數- 構造函數初始值列表在
函數名(參數列表):
後,在{}
函數體之前 - 構造函數初始值列表是成員名的一個列表,每個名字後緊跟
()
括起來的成員初始值,不同成員的初始化通過逗號分隔 - 當某個數據成員被構造函數初始值列表忽略時,它將以與合成默認構造函數相同的方式隱式初始化
- 一般來說,編譯器生成的拷貝、賦值和析構操作將對對象的每個成員執行拷貝、賦值和銷毀操作
- 很多需要動態內存的類應該使用
vector
對象或string
對象管理必要的存儲空間,使用vector
或string
能避免分配和釋放內存帶來的複雜性 - 如果類包含
vector
或string
成員,則其拷貝、賦值和銷毀的合成版本能正常工作
訪問控制與封裝#
- 使用訪問說明符(
public
、private
)加強類的封裝性 class
和struct
關鍵字唯一的區別在於其默認訪問權限不太一樣,可以用任意一個來定義類struct
:第一個訪問說明符前的成員是public
的;class
:第一個訪問說明符前的成員是private
的- 類可以允許其它類或函數訪問其非公有成員 ->友元
- 友元聲明只需要在類內增加一條以
friend
關鍵字開始的函數聲明語句即可 - 友元聲明適用於類的接口組成部分非成員函數
- 友元聲明僅僅指定訪問權限,而非通常意義上的函數聲明。如果我們希望類的用戶能夠調用某個友元函數,必須在友元聲明外再專門聲明一次函數。
- 通常在類的頭文件中,除了類內部的友元聲明外獨立聲明友元函數(在類外)
- 最好在類定義開始或結束前的位置集中聲明友元
類的其它特性#
- 用來定義類型的成員必須先定義後使用,因此,類型成員通常出現在類開始的地方
- 最好只在類外部定義的地方說明
inline
,可以使類更易理解 - 只要函數在參數的數量和 / 或類型上有所區別,就可以重載成員函數
- 當我們希望能修改類的某個數據成員,即使是在一個
const
成員函數內,可以通過在變量聲明中加上mutable
實現 - 當我們提供一個類內初始值時,必須以
=
或{}
表示 - 一個
const
成員函數如果以引用的形式返回*this
,那麼它的返回類型將是常量引用 - 通過區分成員函數是否是
const
的,可以對其進行重載 - 建議:對於公共代碼使用私有功能函數 -> 避免在多處使用同樣的代碼
- 我們可以僅僅聲明類而暫時不定義它(類似於函數),如
class Screen;
->前向聲明 - 在 "聲明之後" "定義之前" 的類類型是一個不完全類型
- 前向聲明適用於當類的成員包含指向它自身類型的引用或指針
- 如果一個類指定了友元類
friend class 類名
,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員 - 每個類負責控制自己的友元類或友元函數,友元關係不存在傳遞性
- 當把一個成員函數聲明成友元時,必須明確指出該成員函數屬於哪個類,如
類名::成員函數名
- 要想令某個成員函數作為友元,我們必須仔細組織程序的結構以滿足聲明和定義的彼此依賴關係
- 如果一個類想把一組重載函數聲明成它的友元,它需要對這組函數中的每一個分別聲明
類的作用域#
- 一個類就是一個作用域的事實很好地解釋為什麼當我們在類的外部定義成員函數時必須同時提供類名和函數名
- 一旦遇到類名,定義的剩餘部分就在類的作用域之內了
- 返回類型必須指明它是哪個類的成員 (返回類型中使用的名字都位於類的作用域之外)
- 編譯器處理完類中的全部聲明後才會處理成員函數的定義
- 在類中,如果成員使用了外層作用域中的某個名字,而該名字代表一種類型,則類不能在之後重新定義該名字
- 當類的成員被隱藏時,可以通過加上類的名字或顯式地使用
this
指針來強制訪問成員,如this->成員變量名
或類名::成員變量名
- 建議不要把成員名作為參數或其它局部變量使用
- 當外部作用域的對象被隱藏時,可以使用作用域運算符訪問它
構造函數再探#
- 初始化和先定義後賦值在一些情況下有很大不同
- 如果成員是
const
或引用或屬於某種類類型而該類沒有定義默認構造函數時,必須將其初始化 - 應養成使用構造函數初始值的習慣
- 成員的初始化順序與它們在類定義中出現的順序一致,最好令構造函數初始值的順序與成員聲明的順序一致,如果可能的話儘量避免使用某些成員初始化其他成員
- 如果一個構造函數為所有參數都提供了默認實參,則它實際上也定義了默認構造函數
- 委託構造函數使用它所屬類的其它構造函數執行其自身的初始化過程,其成員初始值列表只有唯一的入口,即類名,如
Sales_data(): Sales_data("", 0, 0){函數體}
- 當對象被默認初始化或值初始化時自動執行默認構造函數,類必須包含一個默認構造函數以便在這些情況下使用
- 如果定義了其他構造函數,最好也提供一個默認構造函數
- 編譯器只會自動地執行一步類類型轉換
explicit
可以用來抑制構造函數的隱式轉換,只能在類內聲明構造函數時使用,explicit
只對一個實參的構造函數有效,需要多個實參的構造函數不能用於隱式轉換,無須為其指定- 當使用
explicit
關鍵字聲明構造函數時,它將只能以直接初始化的形式使用,且編譯器不會在自動轉換過程中使用該構造函數 - 儘管編譯器不會將
explicit
構造函數用於隱式轉換,但我們可以用這樣的構造函數顯式地強制轉換,如static_cast
- 聚合類:1. 所有成員
public
2. 未定義任何構造函數 3. 無類內初始值 4. 無基類,也無virtual
函數 - 聚合類可以使用由花括號括起來的成員初始值列表初始化,如
Data val1 = { 0, "Anna"}
,初始值的順序必須與聲明的順序一致,若初始值列表的元素個數少於類的成員數,則靠後的成員被值初始化 - 數據成員都是字面值類型的聚合類是字面值常量類
- 如果
- 數據成員都是字面值類型;
- 類至少含有一個
constexpr
構造函數; - 如果一個數據成員含有類內初始值,則內置類型成員的初始值是一條常量表達式,或者如果成員屬於某種類類型,則初始值使用成員自己的
constexpr
構造函數; - 類必須使用析構函數的默認定義
則它也是一個字面值常量類
constexpr
構造函數體一般是空的,使用前置關鍵字就可以聲明一個constexpr
構造函數
類的靜態成員#
- 使用
static
關鍵字使得成員與類本身直接相關,而不是與類的各個對象保持關聯 - 類的靜態成員存在於任何對象之外,對象中不包含任何與靜態數據成員有關的數據
- 靜態成員函數不與任何對象綁定在一起,不包含
this
指針,無法聲明為const
- 可以使用作用域運算符直接訪問靜態成員,也可以使用類的對象、引用或指針來訪問靜態成員,成員函數不用通過作用域運算符就能直接使用靜態成員
- 既可以在類內也可以在類外定義靜態成員函數,在類外定義時不能重複
static
關鍵字 - 必須在類的外部定義和初始化每個靜態成員
- 從類名開始,一條定義語句的剩餘部分就都位於類的作用域之內了
- 靜態數據成員的類型可以是它所屬的類類型,而非靜態數據成員只能聲明為它所屬類的指針或引用
- 靜態成員和普通成員的另一個重要區別是我們可以使用靜態成員作為默認實參,但非靜態成員不行,因為它的值本身屬於對象的一部分
IO 類#
iostream
、fstream
、sstream
三個頭文件分別定義了用於讀寫流、命名文件、內存 string 對象的類型- 標準庫通過繼承機制使我們能忽略這些不同類型的流之間的差異
- 不能拷貝或對 IO 對象賦值
- IO 類的條件狀態,確定一個流對象的狀態最簡單的方法是將其作為一個條件使用,如
while
循環檢查>>
表達式返回的流的狀態 - 流對象的
rdstate
成員返回一個iostate
值,對應流的當前狀態 - 流對象的
clear
成員可以復位所有錯誤標誌位(無參數)或設置流的新狀態(有參數) - 緩衝刷新:數據真正寫到輸出設備或文件,導致緩衝刷新的原因有很多
endl
完成換行並刷新緩衝區工作;flush
刷新緩衝區但不輸出任何額外字符;ends
向緩衝區插入一個空字符並刷新緩衝區- 如果程序崩潰,輸出緩衝區不會被刷新
cout<<unitbuf;
告訴流在每次寫操作後進行一次flush
;cout<<nounitbuf;
恢復為正常的緩衝區刷新機制- 當一個輸入流關聯到一個輸出流時,從輸入流讀取數據的操作會先刷新關聯的輸出流,
cout
和cin
就是關聯在一起的 x.tie(&o)
將流 x 關聯到輸出流 o- 既可以將
istream
關聯到ostream
,也可將ostream
關聯到ostream
- 每個流同時最多關聯到一個流,但多個流可以同時關聯到同一個
ostream
文件輸入輸出#
- 頭文件
fstream
定義了三個類型來支持文件 IO:ifstream
- 讀;ofstream
- 寫;fstream
- 讀寫 fstream
的各類特有操作ifstream in(ifile);
構造一個ifstream
並打開給定文件- 調用
open
可以將空文件流與文件相關聯,例如ofstream out;
、out.open(ofile);
。使用if (out)
可以判斷open
是否成功。 - 為了將文件流關聯到另一文件,必須先關閉已關聯文件,例如
in.close()
、in.open(ifile)
。 - 當一個
fstream
對象被銷毀時,close
會自動被調用。 - 文件模式:
in
- 讀方式;out
- 寫方式;app
- 每次寫操作前定位到文件末尾;ate
- 打開文件後定位到文件末尾;trunc
- 截斷文件;binary
- 以二進制方式進行 IO - 以
out
模式(ofstream
的默認模式)打開文件會丟失已有數據 - 阻止一個
ofstream
清空給定文件內容的方法是同時指定app
模式,例如ofstream app("file", ofstream::out| ofstream::app);
- 每次打開文件時,都要設置文件模式,否則使用默認值
string 流#
istringstream
- 讀;ostringstream
- 寫;stringstream
- 讀寫stringstream
的各類特有操作istringstrean
和ostringstream
的使用- string 流在拆分從文件中讀入的字符串時非常有用
順序容器概述#
vector
- 可變大小數組;deque
- 雙端隊列;list
- 雙向鏈表;forward_list
- 單向鏈表;array
- 固定大小數組;string
- 與vector
相似,但專門用於保存字符string
和vector
將元素保存在連續的內存空間中:由元素下標計算地址非常快速,但在中間位置添刪元素非常耗時list
和forward_list
令容器任何位置的添刪操作都很快速,但不支持元素的隨機訪問,額外內存開銷很大deque
更為複雜,支持快速的隨機訪問,在中間位置添刪元素代價很高,但在兩端添刪元素速度很快array
大小固定,不支持添刪元素和改變容器大小- 現代 C++ 程序應該使用標準庫容器,而不是原始的數據結構,如內置數組
- 除非有很好的理由選擇其他容器,否則使用
vector
- 如果程序有很多小元素,且空間額外開銷很重要,不要使用
list
或forward_list
- 如果程序要求隨機訪問元素,應使用
vector
或deque
- 如果程序要求在容器中間添刪元素,應使用
list
或forward_list
- 如果程序需要在頭尾添刪元素,但不需要在中間添刪元素,使用
deque
- 如果程序只有在讀取輸入時才需要在容器中間插入元素,隨後需要隨機訪問元素:首先確定是否真的需要在容器中間插入元素,在處理輸入數據時,可以很容易地向
vector
追加數據,再調用標準庫的sort
函數來重排容器內元素,從而避免在中間插入元素;如果必須在中間位置插入元素,考慮在輸入階段使用list
,一旦輸入完成,將list
中內容拷貝到vector
中。 - 不確定使用何種容器,可以在程序中只使用
vector
和list
的公共操作:使用迭代器,不使用下標操作,避免隨機訪問。這樣在必要時選擇使用vector
或list
都很方便
容器庫概覽#
-
每個容器都定義在一個頭文件中,文件名與類型名相同。容器均定義為模板類,大部分容器都需要額外提供元素類型信息。
-
順序容器幾乎可以保存任意類型的元素
-
容器操作:類型別名、構造函數、賦值與 swap、大小、增刪元素、獲取迭代器、反向容器的額外成員
-
迭代器範圍由一對迭代器表示,
[begin,end)
左閉右開區間,需要保證 end 不在 begin 之前,且指向同一個容器的元素或尾後元素 -
借助類型別名,可以在不了解容器中元素類型的情況下使用它,這在泛型編程中非常有用
-
begin
和end
操作生成指向容器中首元素和尾後元素的迭代器,形成一個包含容器中所有元素的迭代器範圍 -
begin
和end
有多個版本:list<string> a = {"Milton", "Shakespeare", "Austen"}; auto it1 = a.begin(); // list<string>::iterator auto it2 = a.rbegin(); // list<string>::reverse_iterator auto it3 = a.cbegin(); // list<string>::const_iterator auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
-
當不需要寫訪問時,應使用
cbegin
和cend
-
容器的定義和初始化:默認構造函數,拷貝初始化
c1(c2)
或c1=c2
,列表初始化c{a,b,c...}
或c={a,b,c...}
。只有順序容器的構造函數才能接受大小參數seq(n,t)
,關聯容器並不支持 -
拷貝初始化:1. 拷貝整個容器;2. 拷貝由一個迭代器對所指定的元素範圍
-
當將一個容器初始化為另一個容器的拷貝時,兩個容器的容器類型和元素類型都必須相同
-
順序容器提供的構造函數可以接受一個容器大小和一個元素初始值,例如
vector<int> ivec(10,-1);
-
定義一個
array
時,除了指定元素類型,還必須指定大小,例如array<int, 42>
-
雖然我們不能對內置陣列類型進行拷貝或對象賦值操作,但
array
並無此限制 -
容器的賦值運算可用於所有容器
-
賦值運算將左邊容器的全部元素替換為右邊容器元素的拷貝
-
swap
(交換元素) 通常比直接拷貝快得多,如swap(c1,c2)
、c1.swap(c2)
-
assign
(替換元素) 僅適用於順序容器,不支持關聯容器和array
,如seq.assign(b,e)
、seq.assign(il)
、seq.assign(n,t)
-
賦值相關運算會導致容器內部的迭代器、引用和指針失效,但
swap
不會 -
除
array
外,swap
不對任何元素進行拷貝刪除或插入,可以保證在常數時間完成 -
除
string
外,指向容器的迭代器、引用和指針在swap
操作後仍指向swap
操作前所指向的元素 -
交換兩個
array
所需時間與array
中的元素數目成正比 -
統一使用非成員版本的
swap
是一個好習慣 -
容器大小操作:
size
、empty
、max_size
-
比較兩個容器實際上進行元素的逐對比較,與
string
的關係運算類似:大小相同元素相等則相等;大小不同元素相等則小容器小於大容器;大小不同元素不等則取決於第一個不等元素的比較結果
順序容器操作#
- 添加元素:
push_back(t)
或emplace_back(args)
、push_front(t)
或emplace_front(args)
、insert(p,t)
或emplace(p,args)
及多種insert
操作 - 向一個
vector
、string
、deque
插入元素會使所有指向容器的迭代器、引用和指針失效 - 除
array
和forward_list
外,每個順序容器都支持push_back
- 當我們用一個對象來初始化容器時,或將一個對象插入到容器中時,實際上放入到容器的是對象值的一個拷貝,而不是對象本身。
list
、forward_list
、deque
還支持push_front
,deque
像vector
一樣提供隨機訪問元素的能力,但它提供了vector
所不支持的push_front
,deque
保證在容器收尾插入和刪除元素的操作都只花費常數時間,與vector
一樣,在deque
首尾之外的位置插入元素會很耗時- 將元素插入到
vector
、deque
、string
中的任何位置都是合法的,但可能很耗時 insert
的返回值是指向新插入元素的迭代器- 理解
emplace
:c.emplace_back("978-0590353403", 25, 15.99)
等價於c.push_back(Sales_data("978-0590353403", 25, 15.99))
emplace
在容器中直接構造元素。傳遞給emplace
的參數必須與元素類型的構造函數相匹配。front
和back
分別返回首元素和尾元素的引用,注意區分其與begin
和end
的區別 (後者是迭代器)- 訪問成員函數返回的是引用,如果容器是一個
const
對象,則返回值是const
的引用;如果容器不是const
的,則返回值是普通引用,我們可以用來改變元素的值 - 如果使用
auto
變量來保存和改變元素的值,必須將變量定義為引用類型 - at 和下標操作只適用於
string
、vector
、deque
和array
,每個順序容器都有一個front
成員函數,除forward_list
外所有順序容器都有一個back
成員函數。 - 刪除
deque
中除首尾外的任何元素都會使迭代器、引用、指針失效;指向vector
或string
中刪除點之後位置的迭代器、引用、指針都會失效 pop_front
和pop_back
分別刪除首元素和尾元素,vector
和string
不支持pop_front
,forward_list
不支持pop_back
erase
可以刪除由一個迭代器指定的單個元素,也可以刪除由一對迭代器指定範圍內的所有元素,返回指向刪除元素之後位置的迭代器forward_list
插入刪除操作:before_begin()
、cbefore_begin()
返回指向鏈表首元素之前不存在的元素的迭代器 (首前迭代器);insert_after()
在迭代器 p 之後的位置插入元素;emplace_after
使用 args 在 p 指定的位置後創建一個元素;erase_after
刪除 p 指向的位置後的元素- 順序容器改變大小:
c.resize(n)
、c.resize(n,t)
;vector
、string
、deque
進行resize
可能導致迭代器、指針和引用失效,在縮小容器時指向被刪除元素的迭代器、引用、指針會失效 - 由於向迭代器添加元素和從迭代器刪除元素的代碼可能會使迭代器失效,因此必須保證每次改變容器的操作之後都正確地重新定位迭代器,這對
vector
、string
和deque
尤為重要。 - 程序必須保證每個循環步中都更新迭代器、引用或指針,使用
insert
和erase
是一個好的選擇,因為它們會返回操作後的迭代器 (分別指向新添加元素和刪除元素之後) - 不要保存
end
返回的迭代器,而是反復調用它
vector 對象是如何增長的#
vector
將元素連續存儲,為了降低添加元素時所帶來的內存分配和釋放開銷,vector
和string
會預分配更大的內存空間來避免內存空間的重新分配- 管理容量的成員函數:
c.capacity()
- 不重新分配內存空間的話 c 可以保存多少元素;c.shrink_to_fit()
- 將capacity()
減少為與size()
相同大小;c.reserve(n)
- 分配能容納 n 個元素的內存空間 - 調用
reserve
永遠不會減少容器佔用的內存空間
額外的 string 操作#
- 構造 string 的其他方法:
string s(cp,n)
、string s(s2,pos2)
、string s(s2,pos2,len2)
s.substr(pos,n)
子字符串操作,返回 s 中從 pos 開始的 n 個字符的拷貝string
還定義了額外的insert
和erase
版本 (接受下標的版本):s.insert(s.size(), 5, 'i');
、s.erase(s.size()-5, 5);
string
還提供了接受 C 風格字符數組的insert
和assign
:s.assign(cp,7)
、s.insert(s.size(),cp+7)
,cp 是一個 const char*string
類還定義了append
和replace
來改變 string 的內容,s.append(string)
在末尾插入,s.replace(pos,n,string)
替換 pos 開始 n 個字符的內容find
函數完成最簡單的搜素,返回第一個匹配位置的下標,否則返回string::npos
find_first_of
、find_first_not_of
、find_last_of
、find_last_not_of
尋找與給定字符串中任何一個字符匹配的位置- 一種常見的程序設計模式是用這個可選的開始位置在字符串中循環地搜索子字符串出現的位置
- 使用
rfind
、find_last
來逆向搜索 - 與
strcmp
類似的compare
函數 - 數值轉換:
to_string
數值轉 string、stod
、stof
、stold
、stoi
、stol
、stoul
、stoll
、stoull
string 轉數值 - 要轉換為數值的 string 中第一個非空白符必須是數值中可能出現的數值:
d = stod(s2.substr(s2.find_first_of("+-.0123456789")))
容器適配器#
- 本質上,一個適配器是一種機制,能使某種事物的行為看起來像另外一種事物一樣。一個容器適配器接受一種已有的容器類型,使其行為看起來像一種不同的類型。
- 有一些操作和類型所有適配器都支持
- 三個順序容器適配器:
stack
、queue
、priority_queue
,默認情況下,前兩者基於deque
,後者基於vector
- 假定
deq
是一個deque<int>
,可以用stack<int> stk(deq);
初始化一個stack
- 在
vector
上實現的空棧:stack<string, vector<string>> str_stk;
- 棧默認基於
deque
實現,也可以在list
或vector
之上實現。 - 棧的其他操作:
s.pop()
刪除棧頂元素,但不返回該元素值;s.push(item)
/s.emplace(args)
創建一個新元素壓入棧頂,該元素通過拷貝或移動 item 而來,或者由 args 構造;s.top()
返回棧頂元素,但不將元素彈出棧 queue
默認基於deque
實現,priority_queue
默認基於vector
實現queue
也可以用list
或vector
實現,priority_queue
也可以用deque
實現- 隊列的其他操作:
q.pop()
返回queue
的首元素或priority_queue
的最高優先級的元素,但不刪除此元素;q.front()
/q.back()
返回首元素或尾元素,但不刪除此元素,只適用於queue
;q.top()
返回最高優先級元素,但不刪除該元素,只適用於priority_queue
;q.push(item)
/q.emplace(args)
在queue
末尾或priority_queue
中恰當的位置創建一個元素,其值為 item,或者由 grgs 構造
泛型算法概述#
- 大多數算法都定義在頭文件
algorithm
中。標準庫還在頭文件numeric
中定義了一組數值泛型算法。 - 迭代器令算法不依賴於容器
- 但算法依賴於容器類型的操作
- 算法永遠不會執行容器的操作,它只運行於迭代器之上,執行迭代器的操作
初識泛型算法#
- 對於只讀取而不改變元素的算法,通常最好使用
cbegin()
和cend()
。但是,如果你計劃使用算法返回的選代器來改變元素的值,就需要使用begin()
和end()
的結果作為參數。 - 那些只接受一單一選代器來表示第二個序列的算法,都假定第二個序列至少