string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
//根據其形參的取值,getFcn函數返回指向sumLength或者largerLength的指針
decltype(sumLength) *getFcn(const string&);
第2章 變量和基本類型
- 對象是指一塊能存儲數據並具有某種類型的內存空間。
- 初始化不是賦值,初始化的含義是創建變量時賦予其一個初始值,而賦值的含義是把對象的當前值擦除,而以一個新值來替代。
- 如果是內置類型的變量未被顯式初始化,它的值由定義的位置決定。定義於任何函數體之外的變量被初始化爲0,定義在函數體內部的內置類型變量將不被初始化。
- C++語言將聲明和定義的區分開來。聲明(declaration)使得名字爲程序所知,一個文件如果想使用別處定義的名字則必須包含對那個名字的聲明。而定義(definition)負責創建與名字關聯的實體。變量聲明規定了變量的類型和名字,在這一點上定義與之相同。但是除此之外,定義還申請了內存空間,也可能會爲變量賦一個初始值。
- 如果想聲明一個變量而非定義它,就在變量名前添加關鍵字extern,而且不要顯示地初始化變量:
extern int i; //聲明i而非定義i int j; //聲明並定義j
- 引用的類型都要和與之綁定的對象嚴格匹配。而且,引用只能綁定在對象上,而不能與字面值或某個表達式的計算結果綁定在一起。
- 引用的類型必須與其所引用對象的類型一致,但是有兩個例外。第一個例外情況就是在初始化常量引用時允許用任意表達式作爲初始值,只要該表達式的結果能轉換成引用的類型即可。尤其,允許爲一個常量引用綁定非常量的對象、字面值,甚至是個一般表達式:
int i = 42; const int &r1 = i; //允許將const int&綁定到一個普通int對象上 const int &r2 = 42; //正確:r2是一個常量引用 const int &r3 = r1 * 2;//正確:r3是一個常量引用 int &r4 = r1 * 2; //錯誤:r4是一個普通的非常量引用
- 常量引用僅對引用可參與的操作做出了限定,對於引用的對象本身是不是一個常量未作限定。因爲對象可能是個非常量,所以允許通過其他途徑改變它的值
int i = 42; int &r1 = i; //引用ri綁定對象i const int &r2 = i; //r2也綁定對象i,但是不允許通過r2修改i的值 r1 = 0; //r1並非常量,i的值修改爲0 r2 = 0; //錯誤:r2是一個常量引用
- 頂層const(top-level const)表示指針本身是個常量,底層const(low-level const)表示指針所指的對象是一個常量。
- decltype的作用是選擇並返回操作數的數據類型
decltype(f()) sum; //sum的類型是f()的返回值類型 const int ci = 0; decltype(ci) x = 0; //x的類型是const int;
- decltype((variable))(注意是雙層括號)的結果永遠是引用,而decltype(variable)結果只有當variable本身就是一個引用時纔是引用。
- 預處理變量,#define, #ifdef, #ifndef, #endif
第3章 字符串、向量和數組
- 頭文件不應該包含using聲明,因爲頭文件的內容會拷貝到所有引用它的文件中去,如果頭文件裏有某個using聲明,那麼每個使用了該頭文件的文件就都會有這個聲明。可能會產生始料未及的名字衝突。
- >>操作符會自動忽略開頭的空白並從第一個真正的字符開始讀起,直到遇見下一處空白爲止(以空白分開),getline()函數從給定的的輸入流中讀入內容,直到遇到換行符爲止(注意換行符也被讀進來了),然後把所讀的內容存入到那個string對象中去(注意不存換行符)。
- 當把string對象和字符字面值及字符串字面值混在一條語句中使用時,必須確保每個加法運算符(+)的兩側的運算對象至少有一個是string:
string s4 = s1 + ","; //正確:把一個string對象和一個字面值相加 string s5 = "hello" + ","; //錯誤:兩個運算對象都不是string string s6 = s1 + ", " + "world"; //正確:每個加法運算符都有一個運算對象是string string s7 = "hello" + "," + s2; //錯誤:左邊的加法是字面值+字面值
- C++語言中的字符串字面值並不是標準庫類型string的對象。切記字符串字面值與string是不同的類型。
- C語言的頭文件形如name.h,C++則將這些文件命名爲cname,去掉了.h後綴。
- string::size_type, ptrdiff_t, size_t, difference_type
- 內置的下標運算符所用的索引值不是無符號類型,這一點與vector和string(標準庫類型)不一樣。
int ia[] = {0, 2, 4, 6, 8};
int *p = &ia[2]; //p指向索引爲2的元素
int k = p[-2]; //p[-2]是ia[0]表示的那個元素
第4章 表達式
- 右值,左值,左值可以位於賦值語句的左側,右值則不能。
- 對於邏輯與運算符來說,當且僅當左側運算對象爲真時纔對右側運算對象求值。對於邏輯或運算符來說,當且僅當左側運算對象爲假時纔對右側運算對象求值。
- 複合賦值運算符(+=, -+, *=, /=, %= 算術運算符,<<=, >>=, &=, ^=, |=位運算符),都等價於a = a op b:使用複合運算符只求值一次,使用普通的運算符則求值兩次。一次是作爲右邊子表達式的一部分求值,另一次是作爲賦值運算的左側運算對象求值。
- 後置遞增運算符的優先級高於解引用運算符。
- 如果一條子表達式改變了某個運算對象的值,另一條子表達式又要使用該值的話,運算對象的求值順序就很關鍵了。
for(auto it = s.begin(); it != s.end() && !isspace(*it); ++it) *it = toupper(*it); //該循環的行爲是未定義的! while(beg != s.end() && !isspace(*beg)) *beg = toupper(*beg++); //錯誤:該賦值語句未定義 問題在於:賦值運算符左右兩端的運算對象都用到了beg,並且右側的運算對象還改變了beg的值,所以該賦值語句是未定義的。編譯器可能按照下面的任意一種思路處理該表達式: *beg = toupper(*beg); //如果先求左側的值 *(beg + 1) = toupper(*beg); //如果先求右側的值
- 解引用運算符的優先級低於點運算符,所以執行解引用運算的子表達式兩端必須加上括號。
- 嵌套條件運算符,允許在條件運算符的內部嵌套另外一個條件運算符。
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";
- 條件運算符的優先級非常低,因此當一條長表達式中嵌套了條件運算子表達式時,通常需要在它兩端加上括號。
cout << ((grade < 60) ? "fail" : "pass"); //輸出pass或者fail cout << (grade < 60) ? "fail" : "pass"; //輸出1或者0 cout << grade < 60 ? "fail" : "pass"; //錯誤:試圖比較cout和60
- 對數組執行sizeof運算得到整個數組所佔空間的大小,等價於對數組中所有的元素各執行一次sizeof運算並將所得結果求和。注意,sizeof運算不會把數組轉換成指針來處理。
- 對string對象或vector對象執行sizeof運算只返回該類型固定部分的大小,不會計算對象中的元素佔用了多少空間。(sizeof(string) = 32; sizeof(vector<int>) = 24)
- 隱式類型轉換,在大多數用到數組的表達式中,數組自動轉換成指向數組首元素的指針,當數組被用作decltype,或者取地址符(&)、sizeof及typeid等運算符的運算對象時,不會發生轉換。
- 強制類型轉換,static_cast,dynamic_cast,const_cast,reinterpret_cast
第5章 語句
- 因爲do while先執行語句或者塊,後判斷條件,所以不循序在條件部分定義變量
do { mumble(foo); }while(int foo = get_foo()); //錯誤,將變量聲明放在了do的條件部分
- 如果一段程序沒有try語句塊且發生了異常,系統會調用terminate函數並終止當前程序的執行。
- 異常發生時,調用者請求的一部分計算可能已經完成了,另一部分則尚未完成。通常情況下,略過部分程序意味着某些對象處理到一半就戛然而止,從而導致對象處於無效或未完成的狀態,或者資源沒有正確釋放,等等。那些在異常發生期間正確執行了“清理”工作的程序被稱作異常安全的代碼。
- 4個頭文件:<exception>, <stdexcept>, <type_info>, <new>
第6章 函數
- 形參和函數體內部定義的變量稱爲局部變量。
- 局部靜態對象在程序的執行路徑第一次經過對象定義語句時初始化,並且直到程序終止才被銷燬,在此期間及時對象所在的函數結束執行也不會對它有影響。
- 熟悉C的程序員常常使用指針類型的形參訪問函數外部的對象。在C++語言中,建議使用引用類型的形參替代指針。
- 拷貝大的類類型對象或者容器對象比較低效,甚至有的類類型(包括IO類型在內)根本不支持拷貝操作。當某種類型不支持拷貝操作時,函數智能通過引用形參訪問該類型的對象。如果函數無需改變引用形參的值,最好將其聲明爲常量引用。
- 一個函數只能返回一個值,然而有時函數需要同時返回多個值,引用形參爲我們一次返回多個結果提供了有效的途徑。
- 當用實參初始化形參時會忽略掉頂層const。換句話說,形參的頂層const被忽略掉了。當形參有頂層const時,傳給它常量對象或者非常量對象都是可以的
void func(const int i){/* func能夠讀取i,但是不能向i寫值,既可以傳入const int也可以傳入int*/}
- 數組兩個特性:不允許拷貝數組,以及在使用數組時會將其轉換成指針。因爲數組會被轉換成指針,所以當我們爲函數傳遞 一個數組時,實際上傳遞的是指向數組首元素的指針。
- main 處理命令行選項:當使用argv中的實參時,一定要記得可選的實參從argv[1]開始;argv[0]保存程序的名字,而非用戶輸入。
- 如果函數的實參數量未知但是全部實參的類型都相同,可以使用initializer_list類型的形參。和vector不一樣的是,initializer_list對象中的元素永遠是常量值,我們無法改變initialzer_list對象中的值。
- 不要返回局部對象的引用或指針。函數完成後,它所佔用的存儲空間也隨之被釋放掉。因此,函數終止意味着局部變量的引用將指向不再有效的內存區域。
- main函數的返回值可以看做是狀態指示器。返回0表示執行成功,返回其他值表示執行失敗,其中非0值得具體含義依機器而定。爲了使返回值與機器無關,cstdlib頭文件定義了兩個預處理變量,可以使用者兩個變量分別表示成功與失敗。(EXIT_FAILURE/EXIT_SUCCESS)
- 聲明一個返回數組指針的函數:
Type (*function(parameter_list)) [dimension]; int (*func(int i)) [10]; //表示數組中的元素是int類型
- C++11標準寫法,尾置返回類型:
//func接受一個int類型的實參,返回一個指針,該指針指向含有10個整數的數組 auto func(int i) -> int(*)[10];
- 一旦某個形參被賦予了默認值,它後面的所有形參都必須有默認值。
- _ _func_ _ :存放函數的名字;_ _FILE_ _:存放文件名的字符創字面值;_ _LINE_ _:存放當前行號的整形字面值;_ _TIME_ _:存放文件編譯時間的字符串字面值;_ _DATE_ _:存放文件編譯日期的字符串字面值。
- 調用重載函數時應儘量避免強制類型轉換。如果在實際應用中確實需要強制類型轉換,則說明我們設計的形參集合不合理。(含有多個形參的函數匹配,參考書中6.6例子)
- 函數指針
bool lengthCompare(const string&, const string&); bool (*pf)(const string&, const string&); //未初始化 pf = lengthCompare; pf = &lengthCompare; //等價 bool b1 = pf("hello", "goodbye"); bool b2 = (*pf)("hello", "goodbye"); bool b3 = lengthCompare("hello", "goodbye");//等價
- 函數指針形參 和數組類似,雖然不能定義函數類型的形參,但是形參可以是指向函數的指針。
void useBigger(const string& s1, const string& s2, bool pf(const string&, const string&));//第三個形參時函數類型,會自動地轉換成指向函數的指針 void useBigger(const string& s1, const string& s2, bool (*pf)(const string&, const string&)); typedef bool Func(const string&, const string&); typedef decltype(lengthCompare) Func2;//等價的類型 typedef bool (*FuncP)(const string&, const string&); typedef decltype(lengthCompare) *FuncP2; //等價類型
- 返回指向函數的指針
using F = int(int*, int); //F是函數類型,不是指針 using PF = int(*)(int*, int);//PF是指針類型 PF f1(int); //正確:PF是指向函數的指針,f1返回指向函數的指針 F f1(int); //錯誤:F是函數類型,f1不能返回一個函數 F *f1(int); //正確:顯示地制定返回類型是指向函數的指針 int (*f1(int))(int*, int); //由內向外的順序閱讀這條聲明語句:f1有形參列表,所以f1是個函數;f1前面有*,所以f1返回一個指針;進一步觀察發現,指針的類型本身也是包含形參列表,因此指針指向函數,該函數的返回類型是int. auto f1(int) -> int (*)(int*, int); //尾置返回類型,等價
- decltype 作用於某個函數時,它返回函數類型而非指針類型。因此,顯示地加上*表明我們需要返回指針,而非函數本身。
第7章 類
- IO類屬於不能拷貝類型,因此需要通過引用來傳遞。
istream& read(istream& is, Sales_data& item) { double price = 0; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.units_sold; return is; } ostream& print(ostream& os, const Sales_data& item) { os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; }
- 只有當類沒有聲明任何構造函數時,編譯器纔會自動地生成默認構造函數。一旦定義了一些其他的構造函數,除非再定義一個默認的構造函數,否則類將沒有默認構造函數。
- 如果類包含有內置類型或者複合類型的成員,則只有當這些成員全都被賦予了類內的初始值時,這個類才適合於使用合成的默認構造函數。否則他們的值將是未定義的,合成的默認構造函數可能執行錯誤操作。
- 在C++11新標準中,如果我們需要默認行爲,那麼可以通過在參數列表後面寫上 = default來要求編譯器生成構造函數。如果=default 在類的內部,則默認構造函數是內聯的;如果它在類的外部,則該成員默認情況下不是內聯的。
- 使用class和struct定義類唯一的區別就是默認的訪問權限。
- 友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限。一般來說,最好在類定義開始或結束前的位置集中聲明友元。
- 友元的聲明僅僅指定了訪問的權限,而非一個通常意義上的函數聲明。如果我們希望類的用戶能夠調用某個友元函數,那麼我們就必須在友元聲明之外再專門對函數進行一次聲明。爲了使友元對類的用戶可見,我們通常把友元的聲明與類本身放置在同一個頭文件中(類的外部)。
- 可以在類的內部把inline作爲聲明的一部分顯式地聲明成員函數,也能在類的外部用inline關鍵字修飾函數的定義
class Screen{ public: typedef std::string::size_type pos; Screen() = default; Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c){} char get() const{ return contents[cursor]; } //隱式內聯 inline char get(pos ht, pos wd) const; //顯式內聯 Screen &move(pos r, pos c); //能在之後被設爲內聯 private: pos cursor = 0; pos height = 0, width = 0; std::string contents; }; inline Screen& Screen::move(pos r, pos c) //可以在函數的定義處指定inline { pos row = r * width; cursor = row + c; return *this; }
- 一個可變數據成員(mutable data member)永遠不會是const,即使它是const對象的成員。
class Screen{ public: void some_member() const; private: mutable size_t access_ctr; //即使在一個const對象內也能被修改 }; void Screen::some_member() const { ++access_ctr; }
- 返回*this的成員函數:下面例子如果定義的返回類型不是引用,則move的返回值將是*this的副本,因此調用set只能改變臨時副本,不能改變myScreen的值。
class Screen{ public: Screen &set(char); Screen &set(pos, pos, char); }; inline Screen &Screen::set(char c) { contents[cursor] = c; return *this; //將this對象作爲左值返回 } inline Screen &Screen::set(pos r, pos col, char ch) { contents[r * width + col] = ch; return *this; } Screen temp = myScreen.move(4, 0); temp.set('#'); //不會改變myScreen的contents
- 一個const成員函數如果以引用的形式返回*this,那麼它的返回類型將是常量引用。
- class Screen; 前向聲明。在Screen聲明之後定義之前是一個不完全類型。不完全類型只能在非常有限的情景下使用:可以定義指向這種類型的指針或引用,也可以聲明(但是不能定義)以不完全類型作爲參數或者返回類型的函數。只能聲明不能定義,因爲編譯器不知道存儲該數據成員需要多少空間。
- 一般來說,內層作用域可以重新定義外層作用域中的名字,即使該名字已經在內層作用域中使用過。然而在類中,如果成員使用了外層作用域中的某個名字,而該名字代表一種類型,則類不能再之後重新定義該名字
typedef double Money; class Account { public: Money balance() { return bal; } //使用外層作用域的Money private: typedef double Money; //錯誤:不能重新定義Money Money bal; }
- 如果沒有在構造函數的初始值列表中顯示地初始化成員,則該成員將在構造函數體之前執行默認初始化。(理解初始化和賦值的區別,成員是const或者引用的話,必須初始化)
Sales_data::Sales_data(const string &s, unsigned cnt, double price) { bookNo = s; units_sold = cnt; revenue = cnt * price; } //區別於初始化列表是:原來的版本初始化了它的數據成員,而這個版本是對數據成員執行了賦值操作。
- 如果成員是const、引用,或者屬於某種未提供默認構造函數的類類型,我們必須通過構造函數初始值列表爲這些成員提供初值,所以類內的const和引用可以在構造函數的初始化列表裏進行初始化
class ConstRef { public: ConstRef(int ii):i(ii), ci(ii), ri(i){} private: int i; const int ci; int &ri; }
- 成員的初始化順序與它們在類定義中的出現順序一致:第一個成員先被初始化,然後第二個,以此類推。構造函數初始值列表中初始值的前後位置關係不會影響實際的初始化順序。如果一個成員是用另外一個成員來初始化的,那麼這兩個成員的初始化順序就很關鍵了。
- 如果一個構造函數爲所有參數都提供了默認實參,則實際上也定義了默認構造函數。
- 委託構造函數:使用它所屬類的其他構造函數執行它自己的初始化過程,或者說它把它自己的一些(或者全部)職責委託給了其他函數。
class Sales_data { public: Sales_data(std::string s, unsigned cnt, double price):bookNo(s), units_sold(cnt), revenue(cnt*price){} Sales_data():Sales_data("", 0, 0){} Sales_data(std::string s): Sales_data(s, 0, 0){} Sales_data(std::istream &is):Sales_data() { read(is, *this); } }
- 當對象被默認初始化或值初始化時自動執行默認構造函數。默認初始化在以下情況下發生:a.當我們在塊作用域內不使用任何初始值定義一個非靜態變量或者數組時;b.當一個類本身含有類類型的成員且使用合成的默認構造函數時;c.當類類型的成員沒有在構造函數初始值列表中顯式地初始化時。
- 值初始化在以下情況下發生:a.在數組初始化的過程中如果我們提供的初始值數量少於數組的大小時;b.當我們不使用初始值定義一個局部靜態變量時;c.當我們通過書寫形如T()的表達式顯式地請求值初始化時,其中T是類型名。
- 隱式的類類型轉換,能通過一個實參調用的構造函數定義了一條從構造函數的參數類型向類類型隱式轉換的規則。
string null_book = "9-9990-999"; //用一個string的實參調用了Sales_data的combine成員,編譯器用給定的string自動創建了一個Sales_data對象,新生成的這個臨時Sales_data對象被傳遞給combine. item.combine(null_book); //item.combine(Sales_data(null_book));
- 抑制構造函數定義的隱式轉換,將構造函數聲明爲explicit加以阻止。關鍵字explicit只對一個實參的構造函數有效。需要多個實參的構造函數不能用於隱式轉換,所以無須將這些構造函數指定爲explicit。
class Sales_data{ public: Sales_data() = default; Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(p * n){} explicit Sales_data(const std::string &s):bookNo(s) {} explicit Sales_data(std::istream&); }
- 編譯器不會將explicit的構造函數用於隱式轉換過程,但是可以使用這樣的構造函數顯式地強制進行轉換
//正確:實參是一個顯式構造的Sales_data對象 item.combine(Sales_data(null_book)); //正確:static_cast可以使用explicit的構造函數 item.combine(static_cast<Sales_data>(cin));
- 因爲靜態數據成員不屬於類的任何一個對象,所以它們並不是在創建類的對象時被定義的。這意味着它們不是由類的構造函數初始化的。一般來說,我們不能在類的內部初始化靜態成員。必須在類的外部定義和初始化每個靜態成員,一個靜態數據成員只能定義一次。
- 靜態數據成員可以是不完全類型,靜態數據成員的類型可以就是它所屬的類類型。而非靜態數據成員則受到限制,只能聲明它所屬類的指針或引用
class Bar{ public: private: static Bar mem1; // 正確:靜態成員可以是不完全類型 Bar *mem2; // 正確:指針成員可以是不完全類型 Bar mem2; // 錯誤:數據成員必須是完全類型 };
-
第8章 IO庫
- IO對象無拷貝或賦值,因此不能將形參或返回類型設置爲流類型,進行IO操作的函數通常以引用方式傳遞和返回流。讀寫一個IO對象會改變其狀態,因此傳遞和返回的引用不能是const的。