CPP2-基礎部分(1)

參考自《c++ primer 5th zh》,本系列將會接着將《The C++ Programming Language 4th Ed》《c++ primer plus 6th》加進來,暫時是抄書形式的這種,所以會引起大家的不適吧。初衷主要是爲了自己能將厚厚的一本書壓縮到幾個博客中,這樣看書回憶更容易了,因爲相比來說,一個版面的,就算再大也比折成一張一張的書籍,更能夠宏觀的把握,而且對於我來說,忽略了很多不必要的文字,(==!當然現在還是很多,所以其實通過對自己博客的書寫可以看出,哪個章節的文字多,那麼那個章節就是掌握的較差的一章)。


一、變量和基本內置類型

     1、cpp定義了算術類型和空類型,其中算術類型包括字符、整形數、布爾值和浮點數,算術類型在不同機器上所佔的比特數是不同的,下表爲cpp標準定義的比特最小值,當然編譯器可以允許更大的:


其中cpp語言規定int至少和short一樣大;long至少和int一樣大;longlong至少和long一樣大。通常float以1個字(32bit)表示,double以2個字表示,long double以3或者4個字表示,float和double分別有7和16個有效位;而且對於char、 signed char、unsigned char 來說,char默認爲哪一個是編譯器決定的。

        2、明確知道數值不爲負的時候,用無符號類型;一般使用int 。short有時候太小,而long和int一般有着一樣的位數,如果數值超過了int,用long long;算術表達式中不要用char或bool,因爲不同機器定義是有符號還是無符號不同,或者顯式指定;浮點運算用double,float精度不夠,而long double運行消耗較大。當對無符號類型賦值超出範圍的值時,採取的是將值對無符號類型表示數值最大值取模後的餘數,比如-1賦給8bit的unsigned char,結果爲255.

        3、如果兩個字符串字面值位置緊鄰,之間只有空格、縮進和換行符,那麼會被自動合併爲一個字符串字面值。對於轉義字符來說,\ 後面跟的是1-3個數字,那麼就是8進制;如果後面是\x那麼就是16進制。如果反斜線 \ 後面跟着的八進制數字超過3個,那麼只有前3個會被認爲八進制;而 \x 會用到後面所有的數字。其中nullptr是指針字面值。

        4、通過添加下面的前綴或者後綴,可以改變整型、浮點型和字符型字面值的默認類型,如:L'a'  、42ULL:


       5、對象和變量的稱呼,參考python中的詳細說明,對象表示的是內存中某種類型的空間,而變量就是作爲引用該空間的標識符。而且初始化不是賦值,初始化是創建變量時賦予其初始值;賦值時將對象的當前值擦除,用新值來代替。

       6、這四種都是初始化:int a = 0;   int a = {0};    int a{0};   int  a(0);其中{}表示的是列表初始化;而且用內置類型的變量作爲其他的初始化時:如果初始值存在丟失信息的風險,編譯器會報錯:long double ld = 3.1415926536;  int a{ld}, b = {ld}; //這時候會報錯 int c(ld), d = ld; //這時候不報錯。

      7、cpp支持分離式編譯(separate compilation),就是程序分成多個文件。爲了支持這個機制,可以將聲明和定義區分開,一個文件如果想使用別處定義的名字則必須包含對那個名字的聲明,而定義賦值創建與名字關聯的實體。變量聲明規定了變量的類型和名字,定義在這基礎上申請了內存空間,或者爲變量賦一個初始值(初始化、聲明、定義、初始值,都是不同的概念)。如果只想聲明一個變量而不定義它,在前面增加extern 關鍵字,而且不要顯式的初始化變量: extern int i ;//聲明未定義;int i ;//聲明且定義。而且在函數內部,初始化一個由extern關鍵字標記的變量,將引發錯誤。所以如果需要跨文件使用變量,那麼定義只能出現在一個文件中,而用到該變量的文件中必須有聲明,但卻不能重複定義。

     8、用戶定義的標識符中不能連續出現兩個下劃線,也不能以下劃線緊跟大寫字母開頭,定義在函數體外的標識符不能以下劃線開頭。變量名一般小寫字母,定義的類名一般是大寫字母開頭。下面是cpp的關鍵字和操作符替代名:


       9、::作用域符號,前面沒有域名的時候,就是指的全局作用域;c++11中增加了“右值引用”(在後面介紹),一般說的引用指的是“左值引用”,而且引用必須初始化。而且因爲引用本身不是一個對象,所以無法定義引用的引用。除了下面的”const的引用“和書中15.2.3中的兩種另外,其他的應用類型都需要與綁定的對象嚴格匹配。而且引用只能綁定在對象上,不能與字面值或某個表達式的計算結果綁定在一起;除了下面的"const的引用"和15.2.3節的兩種另外,其他所有指針類型都需要和指向的對象嚴格匹配,因爲在聲明語句中指針的類型實際上被用於指定它所指向對象的類型,所以必須匹配。c++11增加的nullptr作爲指針的字面值常量,可以被解釋成各種類型的指針。void*是一種特殊的指針類型,用來存放任意對象的地址。這個類型所能做的比較有限:拿它和別的指針作比較、作爲函數的輸入或輸出、或者賦給另一個void*指針。不能直接操作這個指針指向的對象,因爲沒解釋它指向的到底是什麼類型。

      10、引用本身不是對象,所以不能定義指向引用的指針。指針是對象,所以有對指針的引用int *&r = p;(p爲指針),解讀方法:按照結合方式,&r,說明這是一個引用,剩下的int *說明什麼類型的引用。

       11、const對象一旦創建後就不能改變它的值,所以const對象必須初始化,初始值可以是任意複雜的表達式:const  int i  = function();   const in i =42; 這兩種都是可以的。int i = 42;const int ci  = i; int j = ci;這三種都是正確的;const的常量特徵只有在執行改變ci操作的時候纔會阻止,如果發現並沒有改變值,那麼就支持這種操作。如果直接用字面值常量來初始化比如const int bufsize = 512;那麼其實在編譯階段,編譯器就會把所有這個變量的出現的地方換成512,然後在進行下一步編譯操作(常量摺疊)。默認情況下const的作用範圍只在文件內,多個文件出現同名的const變量,只是不同文件分別設立獨立的變量而已。如果想跨文件使用,比如得到函數返回值,那麼對於const 變量不管是聲明還是定義都添加extern關鍵字,然後在某個文件中定義一次就行; const int ci = 1024; int &r2 = ci;會報錯,因爲試圖用可以改變的變量來指向常量,const int &r1 = ci;是正確的。

      12、在上面9中說引用的兩個例外之一:a)在初始化常量引用時允許任意表達式作爲初始值,只要該表達式的結果能轉換成引用的類型就行。尤其,允許爲一個常量引用綁定非常量的對象、字面值,甚至是一個一般表達式:

要理解過程,最簡單的就是弄清楚當一個常量引用被綁定到另外一種類型上時到底發生了什麼:


這裏ri引用了一個int型的數,對ri的操作是整數運算,但是dval是double,所以爲了確保ri綁定到一個整數上,編譯器把上述代碼變成如下形式:


所以其實是 ri 綁定到一個臨時量對象,這個對象是由編譯器檢測到需要空間暫存表達式的求值結果時臨時創建一個未命名的對象。當ri不是const 類型時,如果執行上面的引用那麼就是需要可以對ri 賦值,可是ri 綁定的是臨時變量,所以改變的卻不是我們想要的,所以這種操作是非法的(即非const 不支持中間轉換,必須完全匹配)。下面這種:常量引用僅對引用可參與的操作做出限定,對引用本身是不是常量不做限定,(其實下面的這種情況也可以用上面的臨時變量來解釋,因爲i 是int類型,而r2是const int 類型,中間需要轉換):


        13、指向常量的指針不能用於改變其所指對象的值,要存放常量對象的地址,只能使用指向常量的指針:


指針類型必須與所指對象的類型一致,但有兩個另外:一個是允許令一個指向常量的指針指向一個非常量對象:


指向此昂了的指針僅僅要求不能通過該指針改變對象的值,所以可以用指向常量的指針去指向非常量的對象。

         14、將const分成兩種說法:頂層const,用於表示自身是常量;底層const,用於表示指向的對象是常量。當進行復制的時候,頂層const不受到影響,底層const就有了,因爲對於頂層來說,非常量可以轉換成常量,而常量不能轉換成非常量。(p58)

         15、常量表達式指的是值不變,並且在編譯的時候就知道結果的表達式。int sta = 27;這不屬於常量表達式,因爲sta是個int 類型,對於編譯器來說,可能後面會改變值,沒法直接固定。在複雜的系統中要求的常量表達式初始化別人,很難做到,c++11允許將變量聲明爲constexpr類型,讓編譯器來驗證變量值是否是常量表達式。聲明爲constexpr的變量一定是一個常量,而且必須用常量表達式初始化:


雖然不能用普通函數作爲constexpr變量的初始值,但是如6.5.2部分一樣,允許定義一種特殊的constexpr函數,這種函數足夠簡單而且編譯的時候就能計算結果,這樣就能用該函數初始化constexpr變量。constexpr指針的初始值必須是nullptr或者0,或者直接是地址值。6.1.1說道函數體內定義的變量一般不存放在固定的地址中,所以constexpr指針不能指向這樣的變量,而全局對象一般地址不變,所以可以,而且函數定義的其中一種具有固定地址的變量也可以作爲其初始值。在constexpr聲明中如果定義了一個指針,限定符constexpr僅僅對指針有效,對其指向的對象無關。所以可以將其當做頂層const指針對待。

           16、類型別名(type alias),含有typedef的聲明語句定義的不再是變量而是類型別名。而且,新規定引入一種新的方法,使用別名聲明(alias declaration)來定義類型的別名:


這種方法用關鍵字using作爲別名聲明的開始,後面緊跟別名和等號,作用是把等號左側的名字規定成等號右側類型的別名。在追本溯源的時候往往是將原有的typedef定義放入原來位置考慮,對於其他情況都較好理解,對於指針就不太好了,這裏技巧就是,將指針當成一個類型,將* 與p綁定在一起考慮,而不是簡單的帶入。

         17、爲了解決編程時將表達式的值賦給變量,而不知道表達式的類型的尷尬,c++11引入了auto類型說明符讓編譯器去分析表達式所屬的類型,顯然auto定義的變量必須有初始值:


而auto支持的一條語句中聲明多個變量,這時候這些變量的初始基本數據類型都必須一樣。當推斷出auto的類型和要求的不太一樣時,編譯器會適當的轉換。


而且使用引用其實是使用引用的對象,特別是當引用被用作初始值時,真正參與初始化的是引用對象的值,此時編譯器以引用對象的類型作爲auto類型,其次auto一般會忽略掉頂層const,同時底層const會保留下來,比如當初始值時一個指向常量的指針時:



如果希望auto明確指出類型是個頂層const,必須明確指出:


還可以將引用的類型設爲auto,這時候之前的初始化規則仍然適用:


設置一個類型爲auto引用時,初始值中的頂層常量屬性仍然保留。而且如下面,auto的多個聲明中必須類型統一,因爲符號&和×只屬於標識符,而不是基本數據類型,所以初始值必須同一種類型:


      18、有時候希望從表達式的類型推斷出要定義的變量的類型,但是卻不想用該表達式的值初始化變量。c++11引入了第二種類型說明符decltype。在此過程中,編譯器分析表達式並得到它的類型,卻不實際計算表達式的值:


decltype處理頂層const和引用的方式與auto有些不同。如果decltype使用的表達式是一個變量,則decltype返回該變量的類型(包括頂層const和引用在內):


這裏要注意的是,引用從來都作爲其所指對象的同義詞出現,只有用在decltype處是一個例外。如果decltype使用的表達式不是一個變量,則decltype返回表達式結果對應的類型。如4.1.1部分,有些表達式將向decltype返回一個引用類型。當這種情況發生時,意味着該表達式的結果對象能作爲一條賦值語句的左值:


上面前兩行就不解釋了,第三行是如果表達式的內容是解引用操作,則decltype將得到引用類型。解引用指針可以得到指針所指向的對象,而且還能給這個對象賦值。因此第三行的結果類型就是int&,而不是int。decltype和auto的另一個重要區別是,decltype的結果類型與表達式形式密切相關,不過有個另外:對於decltype所用的表達式來說,如果變量名加上一對括號,則得到的類型與不加的時候不同,不加的話得到的是該變量的類型;加一層或多層括號,編譯器會把它當成一個表達式。變量是一種可以作爲賦值語句左值的特殊表達式,所以這樣的decltype就會得到引用類型:


ps:decltype((var))雙括號的結果永遠是引用,而decltype(var)結果只有當var本身是引用的時候纔是引用。

     19、c++新標準規定可以爲數據成員提供一個類內初始值。創建對象時,類內初始值將用於初始化數據成員。沒有初始值的成員將默認初始化。不過對類內初始值的限制與之前類似:放在花括號裏,或者等號右邊,不過不能用圓括號(上面6)

      20、對於分離式編譯來說,爲了確保各個文件中類的定義一致,類通常定義在頭文件中,而且類所在頭文件的名字應該與類的名字一樣。頭文件中通常頁用到其他頭文件的功能,對於顯式包含和間接包含都會造成編譯器識別成重定義,所以要做適當處理。也就是預處理器,在編譯之前執行的一段程序,比如#include的替換,和這裏的頭文件保護符,#define 指令將一個名字設定爲預處理變量,:#ifdef當且僅當變量已定義時爲真,#ifndef當且僅當變量未定義時爲真。一旦檢查結果爲真,則執行後續操作直至遇到#endif指令爲止。ps:不要太在乎程序需不需要這個頭文件都應該習慣性的加上頭文件保護符。

二、字符串、向量和數組

    1、c++很多內置類型是計算機硬件本身支持的能力,而標準庫定義的一些更高級的類型是未直接實現到計算機硬件中的。

    2、使用using聲明來指定所需要訪問的命名空間的名字,如下形式,不過記得每個名字都需要獨立的using聲明,而且每個都需要分號結尾:



值得注意的是頭文件中不應該包含using聲明,會引起一些名字衝突,也就是用到才使用(頭文件會被其他源文件包含),下面附帶在《c++ primer 5th 》中涉及到的標準庫名字和頭文件以供查閱:





    3、使用標準庫的string類型,需要包含其#include <string>頭文件,還要使用using命令usingstd::string。(標準庫對庫類型提供的操作做了詳細的規定;而且也對庫的實現作出了一些性能上的需求,所以要想實現自己的庫,最好也遵循要求)。

    3.1下面是string初始化的方式:

很多時候,特別是後面的類類型的時候,可以顯式的創建一個臨時對象來用於複製:string s8 = string(10,‘c’),這個觀點在後面的隱式類轉換上會用到。下面是string的大部分操作:

上面操作中:對於string的流讀取來說,會自動忽略開頭的空白(空格符、換行符、製表符),讀取之後,直到下一個空白前停止。(這種空白的討論是很有必要的,很多時候不但是可讀性的問題,也涉及到一些安全問題,比如在c的某些函數上)。因爲流讀取的結果會返回左側的對象,所以可以連續流讀取,cin>>str1>>str2;對於getline函數來說:會從當前輸入流緩衝區中讀取數據,直到遇到換行符(也被讀取進來),然後賦值給string對象(丟棄最後的換行符),如果當前輸入流緩衝區中只有一個換行符,那麼當前str就是個空字符串。和上面的流操作一樣,getline函數也會返回流參數,所以可以用作whine(getline())中的條件判斷。

        3.2、對於size()函數來說,其返回的是string:size_type類型(無符號整數類型),標準庫中定義自己配套的類型,就是爲了保證與機器無關的特性。在c++11中允許使用auto或者decltype來推斷變量的類型:auto len = line.size();值得注意的是,如果用一個負值,比如s.size()<n(負數n),那麼結果肯定是true,因爲負值會轉換成無符號值(肯定很大)。

        3.3 、標準庫允許字符和字符串字面值在運算中轉換成string對象,不過與string對象混合使用時,記得每個加法運算兩側至少有一個是string對象。而且因爲加法是從左到右的操作,所以如果表達式的第一個就是string對象,那麼後面按照連續返回機制,其實後面可以都是字符串或者字符字面值。

          3.4、處理string對象中的字符會涉及到語言和庫的多方面,在cctype頭文件中就定義了標準庫函數處理這部分的工作,下面是主要的函數名和含義:

頭文件如果是cname,那麼就是從c中繼承行爲而改寫的,並且包含在std中,不然name.h就是c版本的標準庫。在c++11中引入新的語句:範圍for(range for,估計採用了matlab python的一些特性吧):

其中declaration是變量,expression是個可迭代的序列,當執行一次,變量就變成了序列中的下一個值。比如string str(“abcdef”);for (auto c : str)/*操作部分*/(個人:朝着python近了一步);而且如果想這種方式改變string對象中的每個值,可以採用之前的auto &c的方式:for(auto &c :str) c = toupper(c);ps:對字符串操作前檢測當前字符串是否爲空是個好習慣。

         4、標準庫類型vector表示對象的集合,其中所有對象的類型都相同。如之前一樣需要#include<vector>然後還有 using std::vector;編譯器根據模板創建類或函數的過程稱爲實例化。而vector就是類模板,需要實例化的時候提供存放對象的類型:vector<int> ivec;vector能容納絕大多數類型的對象做元素,不過引用不是對象,所以不存在包含引用的vector(個人:估計就是在實例化的時候沒法採取<int &>的形式,因爲引用首先不是對象,其次必須初始化),而且相比早期的標準,vector<vector<int> 空格>;現在的可以vector<vector<int>>,最後兩個相鄰了。

         4.1、下面是vector對象的常用初始化方法:


根據新標準的形式,列表初始化可以如下:vector<string> articles = {“a”,”an”,”the”};也就是vector對象包含三個元素:每個元素對應着不同的字符串。其中()這種初始化形式只支持1個元素,而多個值的初始化需要用到{};上表中的默認初始化就是按照不同的類型會有不同的初始化,內置類型或者調用類的構造函數。不過這裏有個問題值得注意的:


對比可以發現上面的v8看着怪怪的,可是還是允許的,v6是違反了vector容器的()規定,而v7,v8是當花括號無法執行的時候,考慮使用圓括號規則代替。Ps:如果循環體內部包含有向vector對象添加元素的語句,則不能使用範圍for循環。具體原因在本書的5.4.3中(個人:應該是vector爲了支持動態增長,在改變容器大小的時候,內存位置會變化的,所以讀取的迭代器會可能失效)。

        4.2 vector的一些重要的操作:

只有當元素的值支持可比較時,才能比較,比如vector<string>,而比如vector<Myclass>,自己定義的類中並沒有運算符重載,那麼就不支持比較了。

        5、除了vector之外,標準庫定義了其他幾種容器,所有都支持迭代器,只有少數才能使用下標運算符。string不是容器類型,不過卻支持與容器類似的操作,比如迭代器。Begin()指向第一個元素,end()指向最後一個元素的下一個位置,如果容器爲空,則begin()==end()。爲了方便,可以儘可能的使用auto b = vec.begin();讓編譯器自己決定迭代器的類型。下面就是標準容器迭代器的運算符:

解引用迭代器的前提是該迭代器指向某個元素,試圖解引用一個非法迭代器或者尾後迭代器都是未被定義的行爲。Ps:對於cpp老程序員來說習慣在比較的時候使用!=和==,而不是使用小於<來觀察是否到了最後,因爲這兩個是所有容器都有效的,其他的不保證有效。

        5.1下面是非常量和常量迭代器的類型的例子:

沒const 的支持讀寫,有const 的只能讀取(類似於指針常量一樣,不能通過該渠道去改變對象)。迭代器名詞混淆點:可能是迭代器概念本身;容器定義的迭代器類型;某個迭代器對象。比如下面的:vector<int> v;


上面的一個是表示vector內存放的是否是常量,從而返回對應的迭代器,不過更多時候就算是可改變的容器,我們也想在某個部分使用迭代器常量,C++11引入新的兩個函數:cbegin()和cend()用以返回const_iterator類型。

          5.2、迭代器本身的運算,支持的操作有iter++;++iter;iter+n;iter-n;iter+=n;iter-=n;iter1-iter2;>;>=;<;<=;其中必須保證得到的結果是有效迭代器,也就是在範圍內或者尾後迭代器。其中iter1-iter2的結果的類型爲difference_type類型,是帶符號的整型數,表示兩個迭代器的差分距離,可正可負。

        6、對於數組的維度大小初始化來說,如果變量或者函數是constexpr的話可以作爲其值:string strs[get_size()];當get_size是constexpr時正確,否則出錯。而且定義數組時,必須指定數組的類型,不允許用auto由初始值的列表推斷類型,而且也不存在引用的數組。一些編譯器支持數組的賦值,這是編譯器擴展,不過這對於需要移植的代碼來說,是不好的。

        6.1、定義指針數組、數組指針、數組引用(沒有引用數組):假設int arr[10];那麼對應爲int *ptr[10];int (*parray)[10] = &arr;int (&arrRef)[10]= arr;對於這種嵌套的來說,最好從數組的名字開始按照由內向外的順序閱讀。對於數組下標來說,通常將大小定義爲size_t類型,該類型在cstddef頭文件中。

         6.2 在很多地方使用數組類型的對象其實就是使用一個指向該數組首元素的指針。所以當使用數組作爲一個auto變量的初始值時,推斷得到的類型是指針而非數組:

編譯器在這其中執行的是auto ia2(&ia[0])這樣的操作;而使用decltype關鍵字的時候卻不會轉換成指針:

對於int *e = &arr[10];來說,也能做一個尾後指針,不過該指針只能當作哨兵的作用,不能解引用或者遞增。,可以如for(int *be = arr,*en = arr+10; be!=en;++be){};爲了更安全,c++11引入兩個新的函數begin和end,int *beg =begin(ia);int *last = end(ia);這兩個函數在<iterator>頭文件中。對應的指針相減:auto n = end(arr) – begin(arr);相減的類型爲ptrdiff_t類型,也是定義在cstddef頭文件中。數組中的指針和迭代器一樣,範圍爲頭和“尾後”之間。標準庫類型string和vector的下標必須是無符號的,而數組的下標無這個要求。

        6.3、對於string對象,可以用字符串字面值或者空字符結尾的字符數組來初始化,逆向是str.c_str(),該函數返回指向空字符結尾的字符數組,而且如果後續對str改變了,那麼該函數的返回值也會改變,從而前面的會失效,所以最好深度複製需要的str返回的字符串數組。

        6.4、對於數組的理解:a)從數組名出發,比如intia[2][3],那麼就是ia往後結合,最後觀察數組元素,即是一個2維數組,每個元素是一個3維數組,其中每個具體元素是int值。如果綁定int (&row)[4] = ia[1];就是隻引用ia數組第2行:

使用範圍for循環處理多維數組,除了最內層的循環外,其他所有循環的控制變量都必須是引用類型,這是爲了避免數組被自動轉成指針,否則外層的ia首先轉換成大小爲4的數組類型int (*p)[4],然後接着再次轉換成數組內部的首地址類型,也就是int*類型,所以內層就只能在第一個int*內循環了,也就是第一行(編譯器也會報錯),而且對於ia來說,其在使用的時候會自動轉換成第一個內層數組的指針。如果上面都沒有引用,那麼row是int*類型。


如果通過類型別名的話,可以讓上面的工作更簡化。

三、表達式

1、左值右值好繞;邏輯運算符和關係運算符的運算對象和求值結果都是右值,而且返回值(即比較結果)都是布爾類型。

2、優先級規定了運算對象的組合方式,但是沒說對象按照什麼順序求值,而且大多數情況下,不會明確指定求值的順序,所以不要在同一個運算符上使用依賴同一個變量或者參數的函數,因爲無法知道最後的求值順序,所以下面這種:cout<<i<<” “<<++i<<endl;的結果是依賴於編譯器的。只有四種:&&、||、?:、,。這四種的求值順序是可以保證的(個人:注意p123和p133頁,不要在所有的運算對象中使用修改同一個對象的表達式)。

3、在算術運算中,%爲取餘,也就是其中的運算對象必須都是整型。在c++11中規定在除法運算中,如果兩個對象符號爲正,則小數直接刪除,如果有一個負值,則結果有小數的,也是一樣直接切除小數。而且新標準中(m/n)*n+m%n==m;(-m)/n==m/(-n) ==-(m/n);m%(-n)==m%n;(-m)%n==-(m%n)。

對於if(var==true)這個來說,會先把true提升到int類型的1,然後在var與1比較,也就是如果var的值是2的話,也是不相等的。所以老手一般寫成if(!var)或者直接if(var)。

        4、int k=0;然後k={3.14};//錯誤:窄化轉換。如果左側是內置類型,那麼初始值列表最多隻有一個值,而且該值即使轉換,其所佔空間也不能大於目標類型的空間,(個人:double的空間比int的大)。無論左側的類型是是什麼,初始值列表爲空的話,編譯器都會創建一個初始化的臨時值,然後在賦給左側運算對象(比如類類型)。

        5、對於前置自增和後置自增,這兩種都必須用於左值運算對象,前置將對象本身作爲左值返回,後置的將對象原始值副本作爲右值返回。而且對於*p++和*p—來說,是先進行自增自減操作,然後在解引用,不過後置的會返回改變前的值,其實就是相當於先取p指向的值,然後自增自減。

        6、要非常注意<<和>>與不同的預算符之間的優先級。比如下面三個就完全不同:

       7、對於符號位如何處理沒有明確規定,所以位運算最好都用來處理無符號類型。對於移位來說,右側的運算對象必須不爲負的,而且值必須小於結果的位數。幾乎位操作在運算的時候,都會先提升。而且記得移位運算符的優先級就是用作輸入輸出運算符的優先級。

       8、sizeof運算符滿足右結合律,所得的值爲size_t類型的常量表達式,有兩種形式:sizeof (type);sizeof expr。後者是返回表達式結果類型的大小,而且sizeof並不實際計算運算對象的值。所以其expr是否是個有效的指針,是否合法解引用,它並不關心。新標準允許使用作用域運算符獲取類成員的大小,所以可以直接sizeof Myclass:ivar(數據成員)。Sizeof 不會將數組轉換成指針處理,所以得到的是整個數組的佔空間大小。Ps:對string對象或vector對象執行sizeof,只返回該類型固定部分大小,不會計算對象中元素佔了多少空間(個人:難道和opencv中固定的部分指的就是矩陣頭,而元素對應數據部分,所以這裏的結果就類似矩陣頭那種固定部分?)。而且返回值可以用作數組的維度聲明因爲它的返回值是常量。

        9、隱式轉換:a)大多數表達式中,比int小的整型首先提升到較大的整型;b)條件中,非布爾值轉換成布爾值;c)初始化過程中,初始值轉換成變量的類型;在賦值中,右側運算對象轉換成左側運算對象的類型;d)算術運算或關係運算對象有多種類型,先提升到精度較高的同一類型;e)函數調用時,也會發生類型轉換。

        9.1、算術轉換中運算對象將轉換成最寬類型,比如有個是long double,那麼都會轉換到這個類型,如果有浮點數和整型,那麼直接轉換到浮點類型。整型的提升:bool、char、signed char、unsigned char、short和unsigned short等類型,只要所有可能的值能存放在int中,就會提升成int,否則提升成unsigned int類型;較大的char類型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的能夠容納原類型所有可能的值的類型。

        9.2、某個運算對象的類型是無符號類型,那麼轉換的結果會依賴於機器中各個整數類型的相對大小。如果兩個運算對象提升後的類型要麼都是帶符號的,要麼都是無符號的,則小類型的運算對象轉換成較大的類型。如果一個是無符號,一個是有符號,則無符號的類型不小於帶符號類型,那麼帶符號的運算對象轉換成無符號的。比如一個是unsigned int一個是int,那麼int轉換成unsigned int類型。如果int類型值恰好是負的,那麼會帶來副作用;當帶符號的大於無符號的,此時轉換依賴於機器,如果無符號類型的所有值都能存放在該帶符號類型中,則無符號類型的運算對象轉換成帶符號類型。如果不能,那麼帶符號類型的運算對象轉換成無符號類型。例如,如果兩個運算對象的類型分別爲long和unsigned int,並且int和long的大小一樣,那麼long轉換成unsigned int;如果long佔用的比int多,那麼unsigned int類型轉換成long類型。

        9.3、當數組用在decltype、取地址(&)、sizeof和typeid中都不會轉換成指針。指向任意非常量的指針能轉換成void*,指向任意對象的指針能轉換成const void*。在強制類型轉換中,形式如下:cast-name<type>(expression);type是轉換的目標類型,expression是要轉換的值,如果type是引用類型,則結果是左值。Cast-name可以是:static_cast、dynamic_cast、const_cast、reinterpret_cast中的一種。第二個支持運行時類型識別。任何具有明確定義的類型轉換,只要不包含底層const,則可以使用第一個。而且在編譯器無法自動執行的類型轉換也很有用,比如:double d = 1.0;void *p = &d;double *dp = static_cast<double*>(p);對於第三個來說,只能改變運算對象的底層const:

如果對象本身不是一個常量,使用這個可以獲得寫權限是合法行爲;如果對象是一個常量,再使用const_cast執行寫操作就會產生未定義的後果。只有const_cast能改變表達式的常量屬性,使用其他形式的命名強制類型轉換改變表達式的常量屬性都將引發編譯器錯誤。同樣的,不能用const_cast改變表達式的類型:

這個常常用於函數重載的上下文中;而對於最後一個轉換,通常爲運算對象的位模式提供較低層次的重新解釋。假設有如下轉換:

我們必須記得pc指向的真實對象其實是個int而非字符,如果將pc當初字符指針使用就存在運行時發生錯誤:string str(pc);使用這個轉換是非常危險的,因爲類型變了,而編譯器沒提示,而且這其實是不對的,使用這個轉換必須對類型和編譯器實現轉換的過程都非常瞭解才行。早期的cpp標準中,有兩種顯示轉換形式:type (expr);(type)expr。舊的對應着const_cast、static_cast、reinterpret_cast相似的行爲。如果換成前面兩個合法,則行爲對應,如果不合法就是對應第三個reinterpret_cast。比如char*pc = (char*) ip;//ip是指向整數的指針.這時候使用的類似第三個。

        10、運算符優先表:



四、語句

        1、對於switch,語句首先對括號裏面的表達式求值,該表達式緊跟switch後面,可以是一個初始化過的變量聲明,表達式的值轉換成整數類型,然後與每個case 標籤的值比較:switch (ch){case  ‘a’: /*操作*/;break;default:break;}。case關鍵字和他對應的值一起被稱爲case標籤。Case標籤必須是整型常量表達式;case 3.14://錯誤,case標籤不是個整數;case ival(前面int ival =42)://錯誤,case標籤不是個常量。。。標籤不應該孤零零的出現,後面必須有一條語句或者另外一個case標籤,所以default後面必須有東西,如果沒有也得寫個空語句(;)或者一個空塊({})。

        1.1、如果在switch中某處一個帶有初值的變量位於作用域之外,另一處該變量位於作用域之內,則從前一處跳轉到後一處的行爲是非法的:

上面的語句是非法的,下面的纔是合法情況:

類似於自產自銷,不外流給其他case。

        2、傳統的for語句,中間的條件部分,如果第一次求值結果爲false、,那麼for語句一次都不會執行。對於新式的for,其中for(declaration:expression)statement;中expression可以是花括號的初始值列表、數組、vector或者string等類型的對象。它們的共同點就是擁有能返回迭代器的begin和end成員。

        3、do{   statement   } while( condition);    。記得最後的分號。使用的判定循環變量必須定義在do外面,不能在裏面。

        4、goto語句:goto label;很多行 label: 語句;。這裏的“label:語句”中的label叫做標籤標識符,獨立於變量和其他標識符的名字,所以何以和程序中其他實體的標識符使用相同的名字,而且不會互相干擾。goto語句和控制權轉向的那條帶標籤的語句必須位於同一個函數之內。同樣和switch一樣,不能往前跳過定義語句而直接使用,不過往後還是可以的,因爲這表示銷燬該變量(所以不是重定義):



        5、異常處理機制爲程序中異常檢測和異常處理這兩個部分:a)throw表達式,異常檢測部分使用throw表達式來表示遇到的問題,也就是throw拋出了異常;b)try語句塊,異常處理部分使用try語句塊處理異常。以關鍵字try開始,然後一個或多個catch子句結束。Try語句塊中代碼拋出的異常通常會被某個catch子句處理。C)一套異常類,用於在throw表達式和相關的catch子句之間傳遞異常的具體信息。

        5.1、throw表達式包含關鍵字throw和緊跟的一個表達式,其中表達式的類型就是拋出的異常類型。表達式後面緊跟一個分號,構成一條語句:

runtime_error是標準庫異常類型的一種,定義在stdexcept頭文件中。必須初始化這個對象,所以這裏傳遞了個字符串。

         5.2、try語句塊的通用語法形式:

比如上面的是Sales_item的對象加法代碼,在某個地方,而在與用戶交互的代碼中,負責處理髮生的異常:



比如上面的try中,就可以調用之前的item相加的函數代碼,然後拋出異常返回上一層,也就是try。每個異常類的都有一個成員函數what(),返回的類型是const char*,也就是c風格的字符串。不過runtime_error的what返回的是string對象的副本。當異常拋出時,首先搜索拋出該異常的函數,如果沒有匹配的catch子句,則終止該函數,並在調用該函數的函數中繼續尋找,如果還沒有,則終止該函數,接着往上,最後到了上無可上的時候,調用terminate的標準庫函數,啓動系統異常。

         5.3、標準異常。通常在4個頭文件中有標準庫定義好的一組類:a)exception頭文件定義了最通用的異常類exception。它只報告異常的發生,不提供任何額外的信息;b)stdexcept頭文件定義了幾種常用的異常類;c)new頭文件定義了bad_alloc異常類型;d)type_info頭文件定義了bad_cast異常類型:

標準庫異常類只定義了幾種運算,包括創建或拷貝異常類型的對象,以及爲異常類型的對象賦值。對於exception、bad_alloc和bad_cast對象,不允許爲這些對象提供初始值;而其他的需要使用string或者c風格的字符串初始化,但不允許使用默認初始化的方式。所以對於前者,what的返回內容由編譯器決定。


五、函數

        1、執行函數的第一步就是(隱式的)定義並初始化它的形參爲實參的值。其中實參的求值順序並沒有規定,而且實參的類型必須與對應的形參類型匹配。

         2、函數最外層作用域中的局部變量不能使用與函數形參一樣的名字。形參和函數體內部定義的變量統稱爲局部變量。函數的返回類型不能是數組類型或者函數類型,不過可以是指向數組或函數的指針。而且在函數內部,內置類型的未初始化局部變量產生未定義的值。

        3、函數聲明也叫做函數原型。其中形參的名字可以省略。記得含有函數聲明的頭文件應該被包含到定義函數的源文件中。在參數傳遞的時候,有引用傳遞和值傳遞兩種,前者是會改變實參本身,後者只是複製實參的值而已。而且當參數是指針的時候,指針作爲一個類型,如果不加引用,也是會重新創建一個實參指針的副本,只是這個副本中的地址值爲實參指針中的地址值。而且在傳遞例如類類型的時候,使用引用是極好的,這樣避免了較大的複製操作,如果確定不會改變該實參,可以將該形參設定爲常量引用。

       4 、正如本書的2.4.3節的頂層const的討論,頂層const作用於對象本身:

和其他初始化過程一樣,當用實參初始化形參時會忽略掉頂層const,也就是形參的頂層const被忽略掉了,當形參有頂層const時,傳遞給它常量對象或者非常量都是可以的。(這裏針對的是const與否,沒有結合引用的時候,結合了引用的const就不一樣了,因爲用於聲明引用的const都是底層const。)。

所以上面的例子就成了重定義的形式了,因爲參數上忽略了const,所以完全相同的函數原型。只是對於內部來說第一個參數內部無法改變i的值,而第二個可以,不過在接收實參上是一視同仁的。

         4.1、在對形參的初始化方式上其實和變量的初始化方式是一樣的,可以使用非常量初始化一個底層const對象,可是反過來就不行了,而且對於一個非常量的引用來說,也必須使用同類型的對象初始化:

對應到形參的初始化上爲:

其中函數原型爲:void reset(int &i );

要想調用引用版本的reset只能使用int類型的對象,而不能使用字面值、求值結果爲int的表達式、需要轉換的對象或者const int類型的對象,因爲都不匹配。所以要想調用指針版本的reset,也只能使用int*。

        4.2、c++允許將變量定義成數組的引用,即int (&arr)[10],記得這裏的圓括號不能省,不然就是非法的引用數組,這並不存在。同樣的在傳遞參數多維數組的時候可以void print(int matrix[][10]),其中除了第一維,後面的所有維度都不能省略,這裏表達的是形參爲指向含有10個整數的數組的指針。

        4.3、爲了能編寫處理不同數量實參的函數,新標準提供了兩種主要的方法:a)如果所有的實參類型相同,可以傳遞一個名爲initializer_list的標準庫類型;b)如果實參的類型不同,可以編寫一種特殊的函數,也就是可變參數模板,在後面介紹。不過c++還有一種特殊的形參類型,即省略符,可以用它傳遞可變數量的實參,不過這種功能一般只用於與c函數交互的接口程序。Initializer_list是一種標準庫類型,用於表示某種特定類型的值的數組,該類型定義在同名的頭文件中,它的操作如下:

和vector一樣,它也是個模板類型,定義的時候必須說明所含元素的類型:不過與vector不同的是,它對象中的元素永遠是常量值,所以無法改變。可以如下來編寫錯誤信息輸出的函數:

當然了,如果想要調用這個函數,也就是給形參賦值,那麼必須把序列放在一對花括號中,比如:error_msg({“functionX”,”okay”,”no error”})。對於省略符形參來說,是爲了便於c++程序訪問某些特殊的c代碼而設置的,使用了名爲varargs的c標準庫功能。通常,省略符形參不應該用於其他目的,僅僅用於c和c++通用的類型。特別值得注意的是,大多數類類型的對象在傳遞給省略符形參時都無法正確的複製。這種類型的函數形式差不多都是這兩種:

具體的可查閱c語言的這個標準庫資料。

        5、對於函數的返回值來說,返回一個值的方式和初始化一個變量或者形參的方式完全一樣:返回值用於初始化調用點的一個臨時量,該臨時量就是函數調用的結果,最後將該臨時量賦值給調用該函數的左值。C++新標準規定,函數可以返回花括號包圍的值的列表,此次的列表也用來對錶示函數返回的臨時量進行初始化。如果列表爲空,臨時量執行值初始化;否則,返回的值由函數的返回類型決定。不過,如果函數返回的是內置類型,而不是類類型,那麼花括號包圍的列表最多包含一個值,而且該值所佔空間不應大於目標類型的空間(比如上面的窄化轉換錯誤)。對於main函數,cstdlib頭文件定義了兩個預處理變量,EXIT_FAILURE;EXIT_SUCCESS,用來作爲return的表達式參數。

        5.1、而且因爲數組不能被複制,所以函數無法返回數組,不過可以返回數組的指針或引用:比如int (*func(int i))[10],表示的就是一個參數爲int的函數,該函數返回的是一個維度爲10的數組指針。C++11標準引入了新的叫做尾置返回類型,任何函數的定義都能使用尾置返回,不過這種形式對返回類型比較複雜的函數最有效,比如數組的指針或者數組的引用:

因爲把函數的返回類型放在了形參列表之後,所以可以很清楚的看到func函數返回的是一個指針,該指針指向含有10個整數的數組。或者使用decltype,這是知道函數返回的指針將指向哪個數組的時候,比如下面:

arrPtr使用關鍵字decltype表示它的返回類型是個指針,該指針所指的對象與odd的類型一致,不過decltype並不負責把數組類型轉換成對應的指針,所以返回的結果是個數組,要想表達返回的是指針,還需要顯式的加個*。

       6、重載函數只有形參的數量和類型上不同才能定義。而且頂層const不影響傳入函數的對象,所以這個是無法區分的。而對於形參是某種類型的指針或引用,那麼區分其是常量對象還是非常量對象可以實現重載,這時候const是底層的。

        6.1、對於之前的const_cast在重載函數的情景中最有用,比如下面的函數:

如果想寫它的重載版本,也就是形參是非const的,可是想返回的結果是個普通的引用,而不是const引用:

上面這個重載函數中,調用了之前的那個版本。

        6.2、在cpp中,名字查找發生在類型檢查之前。

        7、對於函數的聲明來說,通常放在頭文件中,並且一個函數只聲明一次,但是多次聲明同一個函數也是合法的,不過在給定的作用域中一個形參只能被賦予一次默認實參。也就是函數的後續聲明只能爲之前那些沒有默認值的形參添加默認實參,而且該形參右邊所有形參必須都有默認值。用作默認實參的名字在函數聲明所在的作用域內解析,而且名字的求值過程發生在函數調用時。比如在開始先int  wd = 80; char def =‘ ’;int ht();然後是函數原型string screen(int = ht(), int = wd, char = def);按照上面的原則在一個函數中(下面using sz = int;):




       7.1、constexpr函數是隻能用於常量表達式的函數,這種函數的定義的要求有:函數的返回類型及所有形參的類型都是字面值類型,而且函數體中必須有且只有一條return語句:

編譯器把對constexpr函數的調用替換成其結果值,爲了能在編譯過程中隨時展開,該函數被隱式的指定爲內聯函數。該函數體內也可以包含其他語句,只要這些語句在運行時不執行任何操作就行,比如空語句、類型別名以及using聲明等。當然可以返回的不是常量:

只是這時候對於intarr[scale[i]]這個就會報錯,如果是int arr[scale[2]],這是不會報錯的。Ps:內聯函數和constexpr函數可以在程序中多次定義,不過這多次定義必須完全一致,所以一般來說是完整的定義放在頭文件中,不聲明和定義分離的。

        8、assert是一種預定義宏,行爲就不說了,這個宏依賴於NDEBUG這個預處理變量的狀態,如果定義了NDEBUG那麼assert宏就什麼都不做,默認狀態下是沒定義的,這個可以用來在代碼發佈的時候進行屏蔽測試代碼。編譯器爲每個函數都定義了變量__func__用以輸出當前的函數的名稱:直接在函數中cout<<__func__(個人:在vs2010中未通過),預處理器還定義了另外4個有用的名字:

        9、對於能夠容易區分的函數匹配來說,就不介紹了,對於那些形參可以轉換的函數匹配來說,還是不那麼容易的。a)首先,選定調用對應的重載函數集,集合中的函數稱爲候選函數,只要當前可見,同名就都收集起來;b)宣傳形參數量與實參數量相等而且類型相同或者能夠轉換的可行函數集合;c)尋找最匹配函數;d)在沒有最匹配的時候,尋找某個函數的每個實參的匹配都不劣於其他可行函數或者至少有一個實參的匹配優於其他可行函數;e)編譯器報二義性錯誤。其中在最匹配階段,編譯器將按照下面的順序來決定哪個函數:a)實參類型和形參類型完全相同、數組類型或函數類型轉換成指針類型、添加或刪除頂層const;b)通過const轉換實現的匹配;c)類型提升實現的匹配;d)算術類型轉換(所有的算術類型的轉換級別都一樣,不會因爲這個轉換成int,那個long就不一樣)或者指針轉換實現的匹配;e)類類型轉換實現的匹配。

        10、指向不同函數的指針之間不能轉換,這裏涉及的有形參的數量,類型和返回值類型。函數指針類型與函數之間必須精確匹配,這裏不支持形參的類型的升級或者轉換。雖然不能定義函數類型的形參,但是形參可以是指向函數的指針,也就是形參是函數類型,實際上是轉換成指針的:

上面的函數原型中第三個參數看起來太繁瑣了,可以採用類型別名的方式:

其中lengthCompare是個函數名。decltype不會自動轉換成指針,所以後面需要顯式加上*。不過要注意的是在返回值部分,編譯器不會自動的將函數類型轉換成指針,所以這個位置必須顯式的轉換成函數指針作爲新函數的返回值類型聲明:

上面的f1可以直接寫出下面形式:




2015年09月05日 第0次修改!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章