標籤:傳回值 unique 編寫 執行個體化 開始 部分 存取控制 基類 其他
#類的this
假設有一個類A,total是A的一個對象,isbn()是A的一個成員函數,當我們使用
total.isbn()時,實際上是在替某個對象調用isbn()。成員函數通過一個名為this的額外隱式函數來訪問調用它的那個對象。當我們調用一個成員函數時,用請求該函數的對象地址初始化this,例如:
調用total.isbn(),則編譯器負責把total的地址傳遞給isbn的隱式形參this,可以等價的認為
A::isbn(&total)
因為this的目的總是指向“這個”對象,所以this是一個常量指標A *const this,即this的指向的地址不能改變
那麼如果想讓this為一個const A *const this呢?this是隱式的不會出現在參數列表中,要在哪裡聲明?
C++的做法是允許把const放在成員函數的參數列表之後,這個const表示this是一個指向常量的指標,這樣使用const的成員函數也叫常量成員函數
因為這時候this是const A *const的,所以不能在這個函數裡面改變調用它的對象的內容。
常量對象,常量對象的引用或指標都只能調用常量成員函數
編譯器處理類的步驟:先編譯成員的聲明,再是成員函數體,所以成員函數體可以隨意使用類中的其他成員而不用在意成員的次序。
類外部定義的成員的名字必須包含它所屬的類名。
#建構函式
類通過一個或幾個特殊的成員函數來控制其對象的初始化過程,叫做建構函式。
建構函式的名字和類名相同,並且沒有傳回型別。
建構函式不能被聲明為const的,當我們建立一個const對象時,直到建構函式完成初始化過程,對象才能取得起“const”屬性。
只有類沒有顯式地定義建構函式,編譯器才會為我們隱式地定義一個預設建構函式。
如果類中包含一個其他類類型的成員且這個成員的類型沒有預設建構函式,那麼編譯器無法初始化該成員。
建構函式初始值列表。
#拷貝,賦值和析構
#存取控制與封裝
public:成員在整個程式內可被訪問,定義類的介面
private:成員可以被類的成員函數訪問,封裝了類的實現細節
一個類對於某個訪問說明符能出現的次數沒有限定。
##我們可以使用class或struct定義類,唯一的一點區別是:struct和class的預設存取權限不太一樣。
##類可以在它的第一個訪問說明符之前定義成員,對這種成員的存取權限依賴於類定義的方式。
##使用struct,預設是public的
##使用class,預設是private的
#友元
類可以允許其他類或者函數訪問它的非共有成員,方法是令其他類或函數成為它的友元,在類裡加上friend關鍵字開始的函式宣告語句。記住,友元聲明只能出現在類定義的內部。
在類裡面變數的聲明中加入mutable關鍵字表示這是一個可變資料成員,一個可變資料成員永遠不會是const,即使它是const對象的成員,所以,一個const成員函數可以改變一個可變資料成員的值。
##一個const成員函數如果以引用的方式返回*this,那麼它的傳回型別是常量引用。
所以經常要基於const的重載。
對於一個類來說,在我們建立它的對象之前必須被定義,而不能只是聲明,否則編譯器無法確定這樣的對象需要多少儲存空間。
編譯器處理完類中的全部聲明之後才會處理成員函數的定義。
在類的建構函式執行的時候,如果成員是const,引用,或者屬於某種未提供預設建構函式的類類型,必須通過建構函式初始值列表初始化,而不能在函數體內賦值。
建構函式初始值列表只說明用於初始化成員的值,而不限定初始化的具體執行順序。
成員的初始化順序和它們在類定義中出現順序一致:第一個成員先被初始化,然後第2個。。。
#委託建構函式
委託建構函式使用它所屬類的其他建構函式執行它自己的初始化過程。
方式:在成員初始化列表只有一個入口,調用其他建構函式。
轉換建構函式
#類的靜態成員
static
靜態成員可以是public,private,可以是常量,引用,指標,類類型等
類的靜態成員存在於任何對象之外,對象中不包含任何與待用資料成員有關的資料。
同樣,靜態成員函數也不與任何對象綁定在一起,它們不包含this指標。
所以,靜態成員函數不能聲明成const的,也不能在static函數體內使用this。
使用範圍運算子直接存取靜態成員。
雖然靜態成員不屬於類的某個對象,但是我們依然可以使用類的對象,引用,指標來訪問靜態成員。
#順序容器
一個容器就是一些特定類型對象的集合。
容器操作:
iterator 此容器類型的迭代器類型
const_iterator 可以讀取元素,但不能修改元素的迭代器類型
c.insert(a) 將a中的元素拷貝進c
c.emplace(a) 使用a構造c中的一個元素
c.begin() c.end()
c.cbegin() c.cend() 返回const_iterator
c.rbegin() c.rend()
對容器使用swap函數,常數時間,因為元素本身沒有交換,只是交換了2個容器的內部資料結構。
##容器操作可能會使迭代器失效
一個失效的指標,引用,迭代器將不再表示任何元素。
標準庫容器沒有給每個容器都定義成員函數來實現一些操作,而是定了一組泛型演算法,這些演算法大多數獨立於任何特定的容器,是通用的。
##unique,去重,但是沒有真正的刪除元素,只是覆蓋相鄰的重複元素,使得
##不重複元素出現在序列開始的部分,之後的位置的元素的值未知
##比如unique(a.begin(),a.end())
##返回指向不重複地區之後一個位置的迭代器
#lambda運算式
可以將lambda運算式理解為一個未命名的內嵌函式
##與函數相同之處:具有一個傳回型別,一個參數列表,一個函數體
##與函數不同指出:lambda可能定義在函數內部
##形式
##[capture list]/(parameter list/) ->return type /{function body/}
##python的形式是:lambda x : f(x)
##capture list,捕獲列表,是一個lambda所在函數中定義的局部變數的列表,常為空白
##可以忽略參數列表和傳回型別,但是capture list和函數體不能忽略
auto f = [] {return 42;}
調用和普通函數一樣使用調用運算子:f()
lambda的參數列表不能有預設參數
[]/(const string &a,const string &b/){return a.size() < b.size() ; }
##一個lambda只有在其捕獲列表中捕獲一個它所在函數中的局部變數,才能在函數體中使用該變數。
##捕獲列表只用於局部非static變數,lambda可以直接使用局部static變數和在它所在函數之外聲明的名字。
##捕獲方式可以是值或引用。
##值捕獲時,與函數傳參不同,被捕獲的變數的值是在lambda建立時拷貝,而不是調用時拷貝。
##引用捕獲時,使用的是引用所綁定的對象。這種情況藥保證在lambda執行時變數依然存在。
##可以從一個函數返回lambda。
隱式捕獲:可以讓編譯器根據lambda體中的代碼來推斷我們使用的變數。
[=]採用值捕獲
[&]採用引用捕獲
預設對於一個值被拷貝的變數,lambda不會改變其值,如果想要改變一個被捕獲的變數的值,必須在參數列表後加上mutable關鍵字。
#動態記憶體
動態分配的對象的生存期與它們在哪裡建立是無關的,只有當顯式地被釋放時,這些對象才會銷毀。
new 在動態記憶體中為對象分配空間並返回一個指向該對象的指標。
delete 接受一個動態對象的指標,銷毀該對象,並釋放與之關聯的記憶體。
2種智能指標,負責自動釋放所指向的對象。
#拷貝控制
拷貝建構函式,拷貝賦值運算子
移動建構函式,移動賦值運算子
解構函式
##拷貝建構函式
如果一個建構函式的第一個參數是自身類類型的引用,且任何額外參數都有預設值,則此建構函式是拷貝建構函式。
拷貝建構函式的第一個參數必須是一個參考型別。
一般情況,編譯器合成的拷貝建構函式會將其參數的非static成員逐個拷貝到正在建立的對象中。
每個成員的類型決定了它如何拷貝:對類類型的成員,會使用其拷貝建構函式,內建類型的成員則直接拷貝,雖然不能直接拷貝數組,但合成拷貝建構函式會逐元素地拷貝一個數群組類型的成員。
拷貝初始化發生的情況:
用=定義變數
將一個對象作為實參傳遞給一個非參考型別的形參
從一個傳回型別為非參考型別的函數返回一個對象
用花括弧列表初始化一個數組中的元素或一個彙總類中的成員
某些類類型還會對它們所分配的對象使用拷貝初始化
##拷貝建構函式被用來初始化非引用類型別參數,這一特性解釋了為什麼拷貝建構函式自己的參數必須是參考型別
因為如果其參數不是參考型別,則調用永遠不會成功-為了調用拷貝建構函式,我們必須拷貝它的實參,但為了拷貝實參,我們又需要調用拷貝建構函式,無限迴圈。
和拷貝建構函式一樣,如果類沒有定義自己的拷貝賦值運算子,編譯器會為它合成一個。
重載運算子本質上是函數,也有一個傳回型別和一個參數列表。
##賦值運算子通常返回一個指向其左側運算對象的引用。
##解構函式
解構函式釋放對象使用的資源,並銷毀對象的非static資料成員。
##名字由~接類名購成,沒有傳回值,不接受參數。
由於解構函式不接受參數,不能重載。
在一個解構函式中,首先執行函數體,然後銷毀成員。成員按初始化順序的逆序銷毀。
##隱式銷毀一個內建指標類型的成員不會delete它所指向的對象。
##當指向一個對象的引用或指標離開範圍時,解構函式不會執行。
無論何時一個對象被銷毀,就會自動調用其解構函式。
解構函式體自身並不直接銷毀成員,成員是在解構函式體之後隱含的析構階段被銷毀的。
##定義刪除的函數
我們可以通過將拷貝建構函式和拷貝賦值運算子定義為deleted function來阻止拷貝,deleted function是這樣一種函數:我們雖然聲明了它們,但是不能以任何方式使用它們。在函數的參數列表後面加=delete來指出我們希望將它定義為deleted。
解構函式定義為deleted function的話,這個類對象無法銷毀。
如果一個類有資料成員不能預設構造,拷貝,複製或銷毀,則對應的成員函數將被定義為刪除的。
也可以通過將其拷貝建構函式和拷貝賦值運算子聲明為private來阻止拷貝。
編寫賦值運算子時,需要注意:
如果將一個對象賦予它自身,賦值運算子必須能正確工作。
大多數賦值運算子組合了解構函式和拷貝建構函式的工作。
#重載運算與類型轉換
重載的運算子是具有特殊名字的函數:operator + 運算子號 組成名字。
當然也包含傳回型別,參數列表,函數體。
重載運算子的參數數量與該運算子作用的運算對象數量一樣多,是成員函數的話顯式參數就要比總數少一個。
因為它的左側運算對象綁定到隱式的this指標上。
除了operator()外,其他重載運算子不能有預設實參。
對於一個運算子函數來說,它或者是類的成員,或者至少含有一個類類型的參數。
只能重載已有的運算子,不能發明新的運算子。
+-*&既是一元也是二元,從參數的數量可以推斷定義的是哪種運算子。
當我們把運算子定義成成員函數時,它的左側運算對象必須是運算子所屬類的一個對象。
通常,輸出運算子的<<第一個形參是一個非常量ostream對象的引用,是非常量是因為流寫入內容會改變其狀態,是引用是因為我們無法直接複製一個ostream對象。
##輸入輸出運算子必須是非成員函數。
假設輸入輸出運算子是某個類的成員,則它們也必須是istream和ostream的成員,但是,這2個類屬於標準庫,我們無法給標準庫中的類添加任何成員。
##輸入運算子必須處理輸入可能失敗的情況,而輸出運算子不需要。
下標運算子[]必須是成員函數。
遞增遞減運算子的重載都有前置和後置版本,而且函數都為:
A &operator++(){}
這樣就沒有辦法重載了,因為重載沒有辦法區分,為瞭解決這個問題,後置版本接受一個額外的不被使用的int類型的形參,當我們使用後置運算子時,編譯器為這個形參提供一個值為0的實參。儘管從文法上來說函數可以使用這個形參,但是在實際過程中不會去使用它。因為這個形參的唯一作用就是區分前置版本和後置版本的函數。
##前置一般返回的是一個引用,後置一般返回的是一個值。
##函數調用運算子
如果類重載了函數調用運算子,我們可以像使用函數一樣使用該類的對象。
operator()
函數調用運算子必須是成員函數。
如果類定義了調用運算子,則該類的對象稱作函數對象。
##lambda是函數對象
當我們編寫了一個lambda後,編譯器將該運算式翻譯成一個未命名類的未命名物件,在lambda運算式產生的類中含有一個重載的函數調用運算子。
對於lambda的捕獲列表,如果是值捕獲,以類的資料成員的形式在類中,如果是引用捕獲,編譯器可以直接使用該引用。
lambda運算式產生的類不含預設建構函式,賦值運算子和預設解構函式。
C++的可調用的對象:函數,函數指標,lambda運算式,bind建立的對象,重載了函數調用運算子的類。
##類類型轉換
轉換建構函式和類型轉換運算子
類型轉換運算子是類的一種特殊成員函數,它負責將一個類類型的值轉換成其他類型
operator type() const;
類型轉換運算子面向任意類型(除了void)進行定義,只要該類型能作為函數的傳回型別,所以,不允許轉換成數組或者函數類型,但允許轉換成指標或者參考型別。
類類型轉換運算子沒有顯式的傳回型別,也沒有形參,而且必須定義成類的成員函數。
##類型轉換運算子是隱式執行的,所以無法傳遞實參
##顯式的類型轉換運算子
explicit operator type() const;
編譯器通常不會將一個顯式的類型轉換運算子用於隱式類型轉換
當類型轉換運算子是顯式時,我們也能執行類型轉換,不過必須通過顯式的強制類型轉換才行。
例外:當運算式被用作條件,編譯器會將顯式的類型轉換自動應用於它。
#物件導向程式設計
3個基本概念:資料抽象,繼承,動態綁定。
資料抽象:將類的介面和實現分離
繼承:相似關係
動態綁定:一定程度忽略相似類型的區別,以統一的方式使用它們的對象
##繼承
通過繼承聯絡在一起的類購成一種層次關係。通常在根部有一個基類(base class)。
其他類則直接或間接從基類繼承,繼承得到的類稱為衍生類別(derived class)。
基類負責定義在層次關係中所有類共同擁有的成員,而每個衍生類別定義各自特有的成員。
對於某些函數,基類希望它的衍生類別各自訂適合自身的版本,此時基類就將這些函式宣告成虛函數(virtual function)。
衍生類別必須通過使用類衍生的資料行表(class derivation list) 明確指出它是從哪些基類繼承而來的。
形式:在類名後一個冒號:,然後是逗號分隔的基類列表,基類前面可以有訪問說明符(public,private等)
衍生類別必須在其內部對所有重新定義的虛函數進行聲明。
##C++中,我們使用基類的引用或指標調用一個虛函數時將發生動態綁定(dynamic binding)
作為繼承關係中根節點的類通常都會定義一個虛解構函式,即使該函數不執行任何實際操作也要。
##任何建構函式之外的非靜態函數都可以是虛函數。
virtual只能出現在類內部的聲明語句之前,不能用於類外部的函數定義。
##如果基類把一個函式宣告為virtual,則該函數在衍生類別中隱式地也是virtual
如果成員函數沒有被聲明為虛函數,則其解析過程發生在編譯時間而不是運行時。
衍生類別可以繼承定義在基類中的成員,但是衍生類別的成員函數不一定有許可權訪問從基類繼承而來的成員。
##衍生類別能訪問public,protected,但是不能訪問private
##如果衍生類別沒有覆蓋其基類中的某個虛函數,該虛函數的行為類似於其他的普通成員,衍生類別會直接繼承其在基類中的版本。
##衍生類別到基類的轉換(編譯器隱式執行)
我們可以將衍生類別的對象當成基類對象來使用,也能將基類的指標或引用綁定到衍生類別對象的基類部分上。
雖然衍生類別對象中含有從基類繼承而來的成員,但是衍生類別不能直接初始化這些成員,需要使用基類的建構函式來初始化它們。
##每個類控制它自己的成員初始化過程。
衍生類別可以訪問基類的public 和 protected
##如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義,無論從基類中派生出多少個衍生類別,對於每個靜態成員來說都只存在唯一的執行個體。
##一個類不能派生它本身。
##C++11,如果我們不想讓一個類作為一個基類,我們可以在類定義的類名後面加final
class A final {};
衍生類別向基類的自動類型轉換隻對指標或參考型別有效,在衍生類別類型和基類類型之間不存在這樣的轉換。
當我們用一個衍生類別對象為一個基類對象初始化或賦值時,只有該衍生類別對象中的基類部分會被拷貝,移動或賦值,它的衍生類別部分將被忽略掉。
##虛函數
在C++中,當我們使用基類的引用或指標調用一個虛成員函數時會執行動態綁定,因為我們知道運行時才能知道到底調用了哪個版本的虛函數,所以所有虛函數都必須有定義。
當某個虛函數通過指標或引用調用時,編譯器產生的代碼直到運行時才能確定應該調用哪個版本的函數,被調用的函數是與綁定到指標或引用上的對象的動態類型相匹配的那一個。
##動態綁定只有當我們通過指標或引用調用虛函數的時候才會發生。
一個衍生類別的函數如果覆蓋了某個繼承而來的虛函數,則它的形參類型,傳回型別必須與被它覆蓋的基類函數完全一致。不過,當類的虛函數傳回型別是類本身的指標或引用時例外。
衍生類別如果定義了一個函數與基類中虛函數的名字相同但是形參列表不同,編譯器將認為新定義的這個函數與基類中原有的函數是相互獨立的。
##override
##final
如果我們已經把函數定義成final了,則之後任何嘗試覆蓋該函數的操作都將引發錯誤。
override和final說明符出現在形參列表之後。
虛函數也可以有預設實參。如果某次函數調用了預設實參,則該實參由本次調用的靜態類型決定。
如果我們希望對虛函數的調用不要動態綁定,而是強迫執行虛函數的某個特定版本的話,使用範圍運算子可以實現這個目的。
##pure virtual
一個純虛函數不用定義,我們通過在聲明語句的分號之前加上=0就可以將一個虛函數說明為純虛函數。
##=0隻能出現在類內部的虛函式宣告語句處,純虛函數的函數體如果要定義的話,必須定義在類的外部。
也就是說,我們不能在一個類的內部為一個=0的函數提供函數體。
含有或者未經覆蓋直接繼承純虛函數的類是抽象基類(abstract base class),抽象基類負責定義介面,後續的其他類可以覆蓋該介面。我們不能直接建立一個抽象基類的對象。
##在一個衍生類別內部,衍生類別訪問說明符對於衍生類別的成員和友元能否訪問其直接基類的成員沒有影響,對基類成員的存取權限只與基類中的訪問說明符有關。
##派生訪問說明符的目的是控制衍生類別使用者對於基類用成員的存取權限。
##不能繼承友元關係,每個類負責控制各自成員的存取權限。
#using語句還可以改變存取權限,各種規則。
##預設情況下,使用class關鍵字定義的衍生類別是私人繼承,使用struct關鍵字定義的衍生類別是公有繼承的。
##函數調用的解析過程
如果我們調用p->mem() ,或者obj.mem() ,步驟:
1.首先確定p或obj的靜態類型,因為調用的是一個成員,所以該類型必須是類類型。
2.在p或obj的靜態類型對應的類中尋找mem,如果找不到,依次在直接基類中不斷尋找直到到達繼承鏈的頂端,如果仍然找不到,編譯器報錯。
3.如果找到了mem,進行常規的類型檢查,以確認對於當前找到的mem,這次調用是否合法
4.如果調用合法,編譯器根據調用的是否是虛函數而產生不同的代碼:
如果mem是虛函數 && 通過引用或指標調用的,編譯器產生的代碼將在運行時確定運行虛函數的哪個版本,依據是對象的動態類型。
如果mem不是虛函數或者是通過對象進行的調用,則編譯器產生一個常規函數調用。
解構函式的虛屬性也會被繼承。
##如果基類的解構函式不是虛函數,則delete一個指向衍生類別對象的基類指標將產生未定義的行為
對象銷毀的順序和其建立的順序相反,衍生類別解構函式首先執行,然後是基類的解構函式。
#模板與泛型程式設計
一個函數模板就是一個公式,用來產生針對特定類型的函數版本。
template< typename T> or
template< class T>
含義相同,一個模板參數列表也可以同時使用typename和class
模板定義中,模板參數列表不可為空。
T的實際類型在編譯時間根據compare的使用方式來確定。
當我們調用一個函數模板時,編譯器通常用函數實參來為我們推斷模板實參。
然後編譯器用推斷出的模板參數來為我們產生一個特定版本的函數。
這些編譯器產生的版本通常被稱為模板的執行個體。
還可以在模板中定義非型別參數,一個非型別參數表示一個值而不是一個類型,我們通過一個特定的類型名來指定非型別參數。
當編譯器遇到一個模板定義時,它並不產生代碼,只有當我們執行個體化出模板的一個特定版本時,編譯器才會產生代碼。
函數模板和類模板成員函數的定義通常放在標頭檔中。
一個類模板的每個執行個體都形成一個獨立的類。
和其他任何類相同,我們可以在類模板內部和類模板外部為其定義成員函數,且定義在類模板內的成員函數被隱式聲明為內嵌函式。
預設,一個類模板的成員函數只有當程式用到它的時候才進行執行個體化。
當一個類包含一個友元聲明時,類與友元各自是否是模板是互相無關的,如果一個類模板包含一個非模板友元,則友元被授權可以訪問所有模板執行個體,如果友元自身是模板,類可以授權給所有友元模板執行個體,也可以只授權給特定執行個體。
《C++ primer 5th》筆記