下面是我看《C++ Primer Plus》第6版這本書後所做的筆記,作為備忘錄便于以後複習。
筆記部分
- C++的const比C語言#define更好的原因?
首先,它能夠明确指定類型,有類型檢查功能。
其次,可以使用C++的作用域規則将定義限制在特定的函數或檔案中。
第三,可以将const用于更複雜的類型,比如數組和結構。
- 不能簡單地将整數賦給指針,如下所示:
在這裡,左邊是指向int的指針,是以可以把它賦給位址,但右邊是一個整數。您可能知道,0xB8000000是老式計算機系統中視訊記憶體的組合段偏移位址,但這條語句并沒有告訴程式,這個數字就是一個位址。在C99标準釋出之前,C語言允許這樣指派。但C++在類型一緻方面的要求更嚴格,編譯器将顯示一條錯誤消息,通告類型不比對。要将數字值作為位址來使用,應通過強制類型轉換将數字轉換為适當的位址類型:int *ptr; ptr = 0xB8000000; // type mismatch
int *ptr; ptr = (int *) 0xB8000000; // type now match
這樣,指派語句的兩邊都是整數的位址,是以這樣指派有效。
注意,pt是int值的位址并不意味着pt本身的類型是int。例如,在有些平台中,int類型是個2位元組值,而位址是個4位元組值。
- 為什麼說字首++/--比字尾++/--的效率高?
對于内置類型和當代的編譯器而言,這看似不是什麼問題。然而,C++允許您針對類定義這些運算符,在這種情況下,使用者這樣定義字首函數:将值加1,然後傳回結果;但字尾版本首先複制一個副本,将其加1,然後将複制的副本傳回。是以,對于類而言,字首版本的效率比字尾版本高。
總之,對于内置類型,采用哪種格式不會有差别,但對于使用者定義的類型,如果有使用者定義的遞增和遞減運算符,則字首格式的效率更高。
-
逗号運算符
到目前為止,逗号運算符最常見的用途是将兩個或更多的表達式放到一個for循環表達式中。逗号運算符的特性有下面幾個:
-
它確定先計算第一個表達式,然後計算第二個表達式;
i = 20, j = 2 * i; // i set to 20, then j set to 40
- 逗号表達式的值是第二部分的值。例如,上面表達式的值為40。
-
在所有運算符中,逗号運算符的優先級是最低的。例如:
cats = 17, 240;
被解釋我:
(cats = 17), 240;
也就是說,将cats設定為17,後面的240不起作用。如果是cats = (17, 240);那麼cats就是240了。
-
-
有用的字元函數庫cctype
從C語言繼承而來,老式格式是ctype.h,常用的有:
C++ Primer Plus第6版重點筆記 -
快排中中值的選取:
将元素每5個一組,分别取中值。在n/5個中值裡面找到中值,作為partition的pivot。
為什麼*不每3個一組?保證pivot左邊右邊至少3n/10個元素,這樣最差O(n)。
-
C++存儲方案:C++三種,C++11四種
這些方案的差別就在于資料保留在記憶體中的時間。
自動存儲持續性:在函數定義中聲明的變量(包括函數參數)的存儲持續性為自動的。它們在程式開始執行其所屬的函數或代碼塊時被建立,在執行完函數或代碼塊時,它們使用的記憶體被釋放。C++有兩種存儲持續性為自動的變量。
靜态存儲持續性:在函數定義外定義的變量和使用關鍵字static定義的變量的存儲持續性都為靜态。它們在程式整個運作過程中都存在。C++有3種存儲持續性為靜态的變量。
線程存儲持續性(C++11):目前,多核處理器很常見,這些CPU可同時處理多個執行任務。這讓程式能夠将計算放在可并行處理的不同線程中。如果變量是使用關鍵字thread_local聲明的,則其生命周期與所屬的線程一樣長。本書不探讨并行程式設計。
動态存儲持續性:用new運算符配置設定的記憶體将一直存在,直到使用delete運算符将其釋放或程式結束為止。這種記憶體的存儲持續性為動态,有時被稱為自由存儲(free store)或堆(heap)。
- 自己寫string類注意事項:
-
關于記錄已有對象數object_count
不要在類聲明(即頭檔案)中初始化靜态成員變量,這是因為聲明描述了如何配置設定記憶體,但并不配置設定記憶體。對于靜态類成員,可以在類聲明之外使用單獨的語句來進行初始化,這是因為靜态類成員是單獨存儲的,而不是對象組成部分。請注意,初始化語句指出了類型int(不可缺少),并使用了作用域運算符,但沒有使用關鍵字static。
初始化是在方法檔案中,而不是在類聲明檔案中進行的,這是因為類聲明位于頭檔案中,可能被包含多次,這樣若在頭檔案中進行初始化靜态成員,将出現多個初始化語句副本,進而引發錯誤。
對于不能在類聲明中初始化靜态成員的一種例外情況是:靜态資料成員為整型或枚舉型const。即如果靜态資料成員是整型或枚舉型,則可以在類聲明中初始化。
-
注意重寫拷貝構造函數和指派運算符,其中指派運算符的原型為:
Class_name & Class_name::operator=(const Class_name &);
它接受并傳回一個指向類對象的引用,目的應該是友善串聯使用。
-
- 何時調用拷貝(複制)構造函數:
以上4中方式都将調用:StringBad(const StringBad &)StringBad ditto (motto); StringBad metoo = motto; StringBad also = StringBad(motto); StringBad * pStringBad = new StringBad (motto);
- 其中中間兩種聲明可能會使用複制構造函數直接建立metoo和also對象,也可能使用複制構造函數生成一個臨時對象,然後将臨時對象的内容賦給metoo和also,這取決于具體的實作。最後一種聲明使用motto初始化一個匿名對象,并将新對象的位址賦給pStringBad指針。
- 每當程式生成了對象副本時,編譯器都将使用複制構造函數。具體的說,當函數按值傳遞對象或函數傳回對象時,都将使用複制構造函數。記住,按值傳遞意味着建立原始變量的一個副本。
- 編譯器生成臨時對象時,也将使用複制構造函數。例如,将3個Vector對象相加時,編譯器可能生成臨時的Vector對象來儲存中間的結果。
- 另外,String sailor = sports;等價于String sailor = (String)sports;是以調用的是拷貝構造函數
- 何時調用指派運算符:
- 将已有的對象賦給另一個對象時,将調用重載的指派運算符。
- 初始化對象時,并不一定會使用指派操作符:
這裡,metoo是一個新建立的對象,被初始化為knot的值,是以使用指派構造函數。不過,正如前面指出的,實作時也可能分兩步來處理這條語句:使用複制構造函數建立一個臨時對象,然後通過指派操作符将臨時對象的值複制到新對象中。這就是說,初始化總是會調用複制構造函數,而使用=操作符時也可能調用指派構造函數。StringBad metoo=knot; // use copy constructor, possibly assignment, too
- 指派運算符和拷貝構造函數在實作上的差別:
- 由于目标對象可能引用了以前配置設定的資料,是以函數應使用delete[]來釋放這些資料。
- 函數應當避免将對象賦給自身;否則給對象重新指派前,釋放記憶體操作可能删除對象的内容。
- 函數傳回一個指向調用對象的引用(友善串聯使用),而拷貝構造函數沒有傳回值。
C++ Primer Plus第6版重點筆記 StringBad & StringBad::operator=(const StringBad & st) { if(this == & st) return * this; delete [] str; len = st.len; str = new char [len + 1]; strcpy(str,st.str); return *this; }
C++ Primer Plus第6版重點筆記 代碼首先檢查自我複制,這是通過檢視指派操作符右邊的位址(&s)是否與接收對象(this)的位址相同來完成的,如果相同,程式将傳回*this,然後結束。
如果不同,釋放str指向的記憶體,這是因為稍後将把一個新字元串的位址賦給str。如果不首先使用delete操作符,則上述字元串将保留在記憶體中。由于程式程式不再包含指向字元串的指針,一次這些記憶體被浪費掉。
接下來的操作與複制構造函數相似,即為新字元串配置設定足夠的記憶體空間,然後複制字元串。
指派操作并不建立新的對象,是以不需要調整靜态資料成員num_strings的值。
-
重載運算符最好聲明為友元
比如将比較函數作為友元,有助于将String對象與正常的C字元串進行比較。例如,假設answer是String對象,則下面的代碼:
if("love" == answer)
将被轉換為:
if(operator == ("love", answer))
然後,編譯器将使用某個構造函數将代碼轉換為:
if(operator == (String("love"), answer))
這與原型是相比對的。
-
在重寫string類時使用中括号通路字元時注意:
(1)為什麼重載的[]傳回值是個char &而不是char?
(2)為什麼有兩個重載[]的版本,另一個是const版本?
解答(1):
将傳回類制聲明為char &,便可以給特定元素陚值。例如,可以編寫這樣的代碼:
String means ("might");
means [9] = ' r';
第二條語句将被轉換為一個重載運算符函數調用:
means.operator[][0] = 'r';
這裡将r陚給方法的傳回值,而函數傳回的是指向means.str[0]的引用,是以上述代碼等同于下面的代碼:
means.str[0] = 'r';
代碼的最後一行通路的是私有資料,但由于operator 是類的一個方法,是以能夠修改數組的内容。 最終的結果是“might”被改為“right”。
解答(2):
假設有下面的常量對象:
const String answer("futile");
如果隻有上述operator定義,則下面的代碼将出錯:
cout << answer[1]; // compile-time error
原因是answer是常量,而上述方法無法確定不修改資料(實際上,有時該方法的工作就是修改資料, 是以無法確定不修改資料)。
但在重載時,C++将區分常量和非常量函數的特征标,是以可以提供另一個僅供const String對象使用 的 operator版本:
// for use with const String objects
const char & string::operator const {
return str[i];
}
有了上述定義後,就可以讀/寫正常String對象了 :而對于const Siring對象,則隻能讀取其資料。
- 靜态成員函數在類聲明外定義實作時不能再加static關鍵字,與靜态成員變量一樣。
- 實作has-a關系的兩種方法:
- 組合(或包含)方式。這是我們通常采用的方法。
-
c++還有另一種實作has-a關系的途徑—私有繼承。使用私有繼承,基類的公有成員和保護成員都将稱為派生類的私有成員。這意味着基類方法将不會稱為派生對象公有接口的一部分,但可以派生類的成員函數中使用它們。而使用公有繼承,基類的公有方法将稱為派生類的公有方法。簡言之,派生類将繼承基類的接口:這是is-a關系的一部分。使用私有繼承,基類的公有方法将稱為派生類的私有方法,即派生類不繼承基類的接口。正如從被包含對象中看到的,這種不完全繼承是has-a關系的一部分。
使用私有繼承,類将繼承實作。例如,如果從String類派生出Student類,後者将有一個String類元件,可用于儲存字元串。另外,Student方法可以使用String方法類通路String元件。
包含将對象作為一個命名的成員對象添加到類中,而私有繼承将對象作為一個未被命名的繼承對象添加到類中。我們使用術語子對象來表示同繼承或包含添加的對象。
是以,私有繼承提供的特性與包含相同:獲得實作,但不獲得接口。是以,私有繼承也可以用來實作has-a關系。
-
使用包含還是使用私有繼承?
由于既可以使用包含,也可以使用私有繼承來建立has-a關系,那麼應使用何種方式呢?大多數C++程式員傾向于使用包含。
通常,應使用包含來建立has-a關系;如果新類需要通路原有類的保護成員,或需要重新定義 虛函數,則應使用私有繼承。
- 首先,它易于了解。類聲明中包含表示被包含類的顯式命名對象,代碼可以通過名稱引用這些對象,而使用繼承将使關系更抽象。
-
其次,繼承會引起很多問題,尤其從多個基類繼承時,可能必須處理很多問題,如包含同名方法的獨立的基類或共亨祖先的獨立基類。
總之,使用包含不太可能遇到這樣的麻煩。
- 另外,包含能夠包括多個同類的子對象。如果某個類需要3個string對象,可以使用包含聲明3個獨立的string成員。而繼承則隻能使用一個這樣的對象(當對象都沒有名稱時,将難以區分)。
然而,私有繼承所提供的特性确實比包含多。例如,假設類包含保護成員(可以是資料成員,也可以是成員函數),則這樣的成員在派生類中足可用的,但在繼承層次結構外是不可用的。如果使用組合将這樣的類包含在另一個類中,則後者将不是派生類,而是位于繼承層次結構之外,是以不能通路保護成員。但通過繼承得到的将是派生類,是以它能夠通路保護成員。
另—種需要使用私有繼承的情況是需要重新定義虛函數。派生類可以重新定義虛函數,但包含類不能。使用私有繼承,重新定義的函數将隻能在類中使用,而不是公有的。
-
關于保護繼承
保護繼承是私有繼承的變體,保護繼承在列出基類時使用關鍵字protected;
class Student : protected std::string, protected std::valarray<double> { ... }
使用保護繼承時,基類的公有成員和保護成員都将成為派生類的保護成員,和私有繼承一樣,基類的接口在派生類中也是可用的,但在繼承層次結構之外是不可用的。當從派生類派生出另一個類的時,私有繼承和保護繼承
之間的主要差別便呈現出來了。使用私有繼承時,第三代将不能使用基類的接口,這是因為基類的共有方法在派生類中将變成私有方法;使用保護繼承時,基類的公有方法在第二代中将程式設計呢個受保護的,是以第三代派生類可以使用它們。
下表總結了公有、私有和保護繼承。隐式向上轉換意味着無需進行顯式類型轉換,就可以将基類指針或引用指向派生類對象。
C++ Primer Plus第6版重點筆記 -
智能指針相關
請參考:C++智能指針簡單剖析,推薦必看。
- C++中的容器種類:
- 序列容器(7個)
- vector:提供了自動記憶體管理功能(采用了STL普遍的記憶體管理器allocator),可以動态改變對象長度,提供随機通路。在尾部添加和删除元素的時間是常數的,但在頭部或中間就是線性時間。
- deque:雙端隊列(double-ended queue),支援随機通路,與vector類似,主要差別在于,從deque對象的開始位置插入和删除元素的時間也是常數的,是以若多數操作發生在序列的起始和結尾處,則應考慮使用deque資料結構。為實作在deque兩端執行插入和删除操作的時間為常數時間這一目的,deque對象的設計比vector更為複雜,是以,盡管二者都提供對元素的随機通路和在序列中部執行線性時間的插入和删除操作,但vector容器執行這些操作時速度更快些。
- list:雙向連結清單(是循環的)。目的是實作快速插入和删除。
- forward_list(C++11):實作了單連結清單,不可反轉。相比于list,forward_list更簡單,更緊湊,但功能也更少。
- queue:是一個擴充卡類。queue模闆讓底層類(預設是deque)展示典型的隊列接口。queue模闆的限制比deque更多,它不僅不允許随機通路隊列元素,甚至不允許周遊隊列。與隊列相同,隻能将元素添加到隊尾、從隊首删除元素、檢視隊首和隊尾的值、檢查元素數目和測試隊列是否為空。
-
priority_queue:是另一個擴充卡類,支援的操作與queue相同。
priority_queue模闆類是另一個擴充卡類,它支援的操作與queue相同。兩者之間的主要差別在于,在priority_queue中,最大的元素被移到對首。内部差別在于,預設的底層類是vector。可以修改用于确定哪個元素放到隊首的比較方式,方法是提供一個可選的構造函數參數:
stack:與queue相似,stack也是一個擴充卡類,它給底層類(預設情況下為vector)提供了典型的棧接口。priority_queue<int> pq1; // default version priority_queue<int> pg2(greater<int>); // use greater<int> to order greater<>函數是一個預定義的函數對象。
- 關聯容器
- 4種有序關聯容器:set、multiset、map和multimap,底層基于樹結構
- C++11又增加了4種無序關聯容器:unordered_set、unordered_multiset、unordered_map和unordered_multimap,底層基于hash。
- 序列容器(7個)