C++編程思想重點筆記(下)

上篇請看:C++編程思想重點筆記(上)

  1. 宏的好處與壞處html

    • 宏的好處:#與##的使用ios

      三個有用的特徵:字符串定義、字符串串聯和標誌粘貼。編程

      字符串定義的完成是用#指示,它允許設一個標識符並把它轉化爲字符串,然而字符串串聯發生在當兩個相鄰的字符串沒有分隔符時,在這種狀況下字符串組合在一塊兒。在寫調試代碼時,這兩個特徵是很是有效的。數組

      #define DEBUG(X) cout<<#X " = " << X << endl

      上面的這個定義能夠打印任何變量的值。安全

      咱們也能夠獲得一個跟蹤信息,在此信息裏打印出它們執行的語句。框架

      #define TRACE(S) cout << #S << endl; S

      #S定義了要輸出的語句。第2個S重申了語句,因此這個語句被執行。固然,這可能會產生問題,尤爲是在一行for循環中。ide

      for (int i = 0 ; i < 100 ; i++ )
         TRACE(f(i)) ;

      由於在TRACE( )宏裏實際上有兩個語句,因此一行for循環只執行第一個。函數

       
          
      for (int i = 0 ; i < 100 ; i++ )
             cout << "f(i)" << endl; 
             f(i);  // 第二條語句脫離了for循環,所以執行不到
       

      解決方法是在宏中用逗號代替分號優化

      標誌粘貼在寫代碼時是很是有用的,用##表示。它讓咱們設兩個標識符並把它們粘貼在一塊兒自動產生一個新的標識符。例如:this

      #define FIELD(A) char *A##_string;int A##_size

      此時下面的代碼:

      class record{  
            FIELD(one);  
            FIELD(two);  
            FIELD(three);  
            //...  
      };

      就至關於下面的代碼:

      class record{  
            char *one_string,int one_size;  
            char *two_string,int two_size;  
            char *three_string,int three_size;    
            //...  
      };

       

    • 宏的很差:容易出錯
      下面舉個例子便可說明:

       
          
        #define band(x) (((x)>5 && (x)<10) ? (x) :0)
        int main() {
           for(int i = 4; i < 11; i++) {
             int a = i;
             cout << "a = " << a << "\t";
             cout << "band(++a)" << band(++a) << "\t";
             cout << "a = " << a << endl;
           }
      
          return 0;
         }
       

      輸出:

      a = 4 band(++a)0 a = 5
      a = 5 band(++a)8 a = 8
      a = 6 band(++a)9 a = 9
      a = 7 band(++a)10 a = 10
      a = 8 band(++a)0 a = 10
      a = 9 band(++a)0 a = 11
      a = 10 band(++a)0 a = 12

  2. 存儲類型指定符
    經常使用的有staticextern
    不經常使用的有兩個:一是auto,人們幾乎不用它,由於它告訴編譯器這是一個局部變量,實際上編譯器老是能夠從 變量定義時的上下文中判斷出這是一個局部變量。因此auto是多餘的。還有一個是register,它也是局部變量,但它告訴編譯器這個特殊的變量要常常用到,因此編譯器應該儘量地讓它保存在寄存器中。它用於優化代碼。各類編譯器對這種類型的變量處理方式也不盡相同,它們有時會忽略這種存儲類型的指定。通常,若是要用到這個變量的地址, register指定符一般都會被忽略。應該避免用register類型,由於編譯器在優化代碼方面一般比咱們作得更好。

  3. 位拷貝(bitcopy)與值拷貝的區別(很重要)
    由1個例子來講明:一個類在任什麼時候候知道它存在多少個對象,能夠經過包含一個static成員來作到,以下代碼所示:

    #include <iostream>
     using namespace std;
     class test {
         static int object_count;
     public:
         test() {
             object_count++;
             print("test()");
         }
         static void print(const char *msg = 0) {
             if(msg) cout << msg << ": ";
             cout << "object_count = " << object_count << endl;
         }
         ~test() {
             object_count--;
             print("~test()");
         }
     };
     int test::object_count = 0;
     // pass and return by value.
     test f(test x) {
         x.print("x argument inside f()");
         return x;
     }
     int main() {
         test h;
         test::print("after construction of h");
         test h2 = f(h);
         test::print("after call to f()");
         return 0;
     }

    然而輸出並非咱們指望的那樣:

    test(): object_count = 1
    after construction of h: object_count = 1
    x argument inside f(): object_count = 1
    ~test(): object_count = 0
    after call to f(): object_count = 0
    ~test(): object_count = -1
    ~test(): object_count = -2

    在h生成之後,對象數是1,這是對的。咱們但願在f()調用後對象數是2,由於h2也在範圍內。然而,對象數是0,這意味着發生了嚴重的錯誤。這從結尾兩個析構函數執行後使得對象數變爲負數的事實獲得確認,有些事根本就不該該發生。

    讓咱們來看一下函數f()經過傳值方式傳入參數那一處。原來的對象h存在於函數框架以外,同時在函數體內又增長了一個對象,這個對象是傳值方式傳入的對象的拷貝,這屬於位拷貝,調用的是默認拷貝構造函數,而不是調用構造函數。然而,參數的傳遞是使用C的原始的位拷貝的概念,但test類須要真正的初始化來維護它的完整性。因此,缺省的位拷貝不能達到預期的效果。

    當局部對象出了調用的函數f()範圍時,析構函數就被調用,析構函數使object_count減少。 因此,在函數外面, object_count等於0。h2對象的建立也是用位拷貝產生的(也是調用默認拷貝構造函數),因此,構造函數在這裏也沒有調用。當對象h和h2出了它們的做用範圍時,它們的析構函數又使object_count值變爲負值。

    總結:

    • 位拷貝拷貝的是地址(也叫淺拷貝),而值拷貝則拷貝的是內容(深拷貝)。
    • 深拷貝和淺拷貝能夠簡單理解爲:若是一個類擁有資源,當這個類的對象發生複製過程的時候,資源從新分配,這個過程就是深拷貝,反之,沒有從新分配資源,就是淺拷貝。
    • 默認的拷貝構造函數」和「缺省的賦值函數」均採用「位拷貝」而非「值拷貝」的方式來實現,假若類中含有指針變量,這兩個函數註定將出錯。

    關於位拷貝和值拷貝的深刻理解能夠參考這篇文章:C++中的位拷貝與值拷貝淺談

    爲了達到咱們指望的效果,咱們必須本身定義拷貝構造函數:

    test(const test& t) {
        object_count++;
        print("test(const test&)");
    }

    這樣輸出才正確:

    test(): object_count = 1
    after construction of h: object_count = 1
    test(const test&): object_count = 2
    x argument inside f(): object_count = 2
    test(const test&): object_count = 3
    ~test(): object_count = 2
    after call to f(): object_count = 2
    ~test(): object_count = 1
    ~test(): object_count = 0

    引伸

    • 若是在main中加一句「f(h);」,即忽略返回值,那麼返回的時候還會調用拷貝構造函數嗎?
      答案是:會調用。這時候會產生一個臨時對象,因爲該臨時對象沒有用處,所以會立刻調用析構函數銷燬掉。這時候輸出就會像下面這樣:

      test(): object_count = 1
      after construction of h: object_count = 1
      test(const test&): object_count = 2
      x argument inside f(): object_count = 2
      test(const test&): object_count = 3
      ~test(): object_count = 2
      after call to f(): object_count = 2
      test(const test&): object_count = 3
      x argument inside f(): object_count = 3
      test(const test&): object_count = 4
      ~test(): object_count = 3
      ~test(): object_count = 2
      ~test(): object_count = 1
      ~test(): object_count = 0

    • 若是一個類由其它幾個類的對象組合而成,若是此時該類沒有自定義拷貝構造函數,那麼編譯器遞歸地爲全部的成員對象和基本類調用拷貝構造函數。若是成員對象也含有別的對象,那麼後者的拷貝構造函數也將被調用。

    • 怎樣避免調用拷貝構造函數?僅當準備用傳值的方式傳遞類對象時,才須要拷貝構造函數。有兩種解決方法:

      • 防止傳值方法傳遞
        有一個簡單的技術防止經過傳值方式傳遞:聲明一個私有private拷貝構造函數。咱們甚至沒必要去定義它,除非咱們的成員函數或友元函數須要執行傳值方式的傳遞。若是用戶試圖用傳值方式傳遞或返回對象,編譯器將會發出一個出錯信息。這是由於拷貝構造函數是私有的。由於咱們已顯式地聲明咱們接管了這項工做,因此編譯器再也不建立缺省的拷貝構造函數。
        class noCC {
         int i;
         noCC(const noCC&); // private and no definition
        public:
         noCC(int I = 0) : i(I) {}
        };
        void f(noCC);
        main() {
         noCC n;
        //! f(n);        // error: copy-constructor called
        //! noCC n2 = n; // error: c-c called
        //! noCC n3(n);  // error: c-c called
        }

        注意這裏`n2 = n`也調用拷貝構造函數,注意這裏要和賦值函數區分。

      • 改變外部對象的函數
        使用引用傳遞:好比void get(const Slice&);
  4. 非自動繼承的函數
    構造函數、析構函數和賦值函數(operator=)不能被繼承。

  5. 私有繼承的目的
    private繼承的目的是什麼,由於在類中選擇建立一個private對象彷佛更合適。將private繼承包含在該語言中只是爲了語言的完整性。可是,若是沒有其餘理由,則應當減小混淆,因此一般建議用private成員而不是private繼承
    然而,private繼承也不是一無用處。
    這裏可能偶然有這種狀況,便可能想產生像基類接口同樣的接口,而不容許處理該對象像處理基類對象同樣。private繼承提供了這個功能

    引伸

    能對私有繼承成員公有化嗎?
    當私有繼承時,基類的全部public成員都變成了private。若是但願它們中的任何一個是可視的,能夠辦到嗎?答案是能夠的,只要用派生類的public選項聲明它們的名字便可(新的標準中使用using關鍵字)。

     
      
    #include <iostream>
     class base {
     public:
         char f() const { return 'a'; }
         int g() const { return 2; }
         float h() const { return 3.0; }
     };
     class derived : base {
     public:
         using base::f; // Name publicizes member
         using base::h; 
     };
     int main() {
         derived d;
         d.f();
         d.h();
      //    d.g(); // error -- private function
         return 0;
     }
     

    這樣,若是想要隱藏這個類的基類部分的功能,則private繼承是有用的

  6. 多重繼承注意向上映射的二義性。好比base(有個f()方法)有兩個子對象d1和d2,且都重寫了base的f()方法,此時子類dd若是也有f()方法則不能同時繼承自d1和d2,由於f()方法存在二義性,不知道該繼承哪一個f()方法。
    解決方法是對dd類中的f()方法從新定義以消除二義性,好比明確指定使用d1的f()方法。
    固然也不能將dd類向上映射爲base類,這能夠經過使用虛繼承解決,關鍵字virtual,base中的f()方法改爲虛函數且d1和d2的繼承都改成虛繼承,固然dd繼承d1和d2用public繼承便可。

  7. C語言中如何關閉assert斷言功能?
    頭文件:<assert.h>或<cassert>
    在開發過程當中,使用它們,完成後用#define NDEBUG使之失效,以便推出產品,注意必須在頭文件以前關閉纔有效。

     
      
     #define NDEBUG
     #include <cassert>
     

     

  8. C++如何實現動態捆綁?—即多態的實現(很重要)

    C++中爲了實現多態,編譯器對每一個包含虛函數的類建立一個表(稱爲VTABLE,虛表)。在 VTABLE中,編譯器放置特定類的虛函數地址。在每一個帶有虛函數的類中,編譯器祕密地置一指針,稱爲vpointer(縮寫爲VPTR),指向這個對象的VTABLE。經過基類指針作虛函數調用時(也就是作多態調用時),編譯器靜態地插入取得這個VPTR,並在VTABLE表中查找函數地址的代碼,這樣就能調用正確的函數使晚捆綁發生。
    爲每一個類設置VTABLE、初始化VPTR、爲虛函數調用插入代碼,全部這些都是自動發生的,因此咱們沒必要擔憂這些。利用虛函數,這個對象的合適的函數就能被調用,哪怕在編譯器還不知道這個對象的特定類型的狀況下。

    在vtable表中,編譯器放置了在這個類中或在它的基類中全部已聲明爲virtual的函數的地址。若是在這個派生類中沒有對在基類中聲明爲virtual的函數進行從新定義,編譯器就使用基類的這個虛函數地址。
    下面舉個例子說明:

    #include <iostream>
     enum note { middleC, Csharp, Cflat };
    
     class instrument {
     public:
         virtual void play(note) const {
             cout << "instrument::play" << endl;
         }
         virtual char* what() const {
             return "instrument";
         }
         // assume this will modify the object:
         virtual void adjust(int) {}
     };
    
     class wind : public instrument {
     public:
         void play(note) const {
             cout << "wind::play" << endl;
         }
         char* what() const {
             return "wind";
         }
         void adjust(int) {}
     };
    
     class percussion : public instrument {
     public:
         void play(note) const {
             cout << "percussion::play" << endl;
         }
         char* what() const {
             return "percussion";
         }
         void adjust(int) {}
     };
    
     class string : public instrument {
     public:
         void play(note) const {
             cout << "string::play" << endl;
         }
         char* what() const {
             return "string";
         }
         void adjust(int) {}
     };
    
     class brass : public wind {
     public:
         void play(note) const {
             cout << "brass::play" << endl;
         }
         char* what() const {
             return "brass";
         }
     };
    
     class woodwind : public wind {
     public:
         void play(note) const {
             cout << "woodwind::play" << endl;
         }
         char* what() const {
             return "woodwind";
         }
     };
    
     instrument *A[] = {
         new wind,
         new percussion,
         new string,
         new brass
     };

    下圖畫的是指針數組A[]。

    指針數組A
    指針數組A

    下面看到的是經過instrument指針對於brass調用adjust()。instrument引用產生以下結果:

    動態綁定
    動態綁定

    編譯器從這個instrument指針開始,這個指針指向這個對象的起始地址。全部的instrument對象或由instrument派生的對象都有它們的VPTR,它在對象的相同的位置(經常在對象的開頭),因此編譯器可以取出這個對象的VPTR。VPTR指向VTABLE的開始地址。全部的VTABLE有相同的順序,無論何種類型的對象。 play()是第一個,what()是第二個,adjust()是第三個。因此編譯器知道adjust()函數必在VPTR + 2處。這樣,不是「以instrument :: adjust地址調用這個函數」(這是早捆綁,是錯誤活動),而是產生代碼,「在VPTR + 2處調用這個函數」。由於VPTR的效果和實際函數地址的肯定發生在運行時,因此這樣就獲得了所但願的晚捆綁。向這個對象發送消息,這個對象能判定它應當作什麼。

    引伸 — 對象切片

    當多態地處理對象時,傳地址與傳值有明顯的不一樣。全部在這裏已經看到的例子和將會看到的例子都是傳地址的,而不是傳值的。這是由於地址都有相同的長度,傳派生類型(它一般稍大一些)對象的地址和傳基類(它一般小一點)對象的地址是相同的。如前面解釋的,使用多態的目的是讓對基類對象操做的代碼也能操做派生類對象。
    若是使用對象而不是使用地址或引用進行向上映射,發生的事情會使咱們吃驚:這個對象 被「切片」,直到所剩下來的是適合於目的的子對象。在下面例子中能夠看到經過檢查這個對象的長度切片剩下來的部分。

     
      
    #include <iostream>
     using namespace std;
     class base {
         int i;
     public:
         base(int I = 0) : i(I) {}
         virtual int sum() const { return i; }
     };
     class derived : public base {
         int j;
     public:
         derived(int I = 0, int J = 0) : base(I), j(J) {}
         virtual int sum() const { return base::sum() + j; }
     };
     void call(base b) {
         cout << "sum = " << b.sum() << endl;
     }
     main() {
         base b(10);
         derived d(10, 47);
         call(b);
         call(d);
     }
     

    函數call( )經過傳值傳遞一個類型爲base的對象。而後對於這base對象調用虛函數sum( )。 咱們可能但願第一次調用產生10,第二次調用產生57。實際上,兩次都產生10。 在這個程序中,有兩件事情發生了

      • 第一,call( )接受的只是一個base對象,因此全部在這個函數體內的代碼都將只操做與base相關的數。 對call( )的任何調用都將引發一個與base大小相同的對象壓棧並在調用後清除。這意味着,若是一個由base派生來類對象被傳給call,編譯器接受它,但只拷貝這個對象對應於base的部分,切除這個對象的派生部分,如圖:
        對象切片
        如今,咱們可能對這個虛函數調用感到奇怪:這裏,這個虛函數既使用了base(它仍存在), 又使用了derived的部分(derived再也不存在了,由於它被切片)。 其實咱們已經從災難中被解救出來,這個對象正安全地以值傳遞。由於這時編譯器認爲它知道這個對象的確切的類型(這個對象的額外特徵有用的任何信息都已經失去)。
      • 另外,用值傳遞時,它對base對象使用拷貝構造函數,該構造函數初始化vptr指向base vtable,而且只拷貝這個對象的base部分。這裏沒有顯式的拷貝構造函數,因此編譯器自動地爲咱們合成一個。因爲上述諸緣由,這個對象在切片期間變成了一個base對象。

        對象切片其實是去掉了對象的一部分,而不是象使用指針或引用那樣簡單地改變地址的內容。所以,對象向上映射不常作,事實上,一般要提防或防止這種操做。咱們能夠經過在基 類中放置純虛函數來防止對象切片。這時若是進行對象切片就將引發編譯時的出錯信息。

        最後注意:虛機制在構造函數中不工做。即在構造函數中調用虛函數沒有結果。

  9. RTTI—運行時類型識別(很重要)

    • 概念

      運行時類型識別(Run-time type identification, RTTI)是在咱們只有一個指向基類的指針或引用時肯定一個對象的準確類型

    • 使用方法
      通常狀況下,咱們並不須要知道一個類的確切類型,虛函數機制能夠實現那種類型的正確行爲。可是有些時候,咱們有指向某個對象的基類指針,肯定該對象的準確類型是頗有用的。
      RTTI與異常同樣,依賴駐留在虛函數表中的類型信息。若是試圖在一個沒有虛函數的類上用RTTI,就得不到預期的結果。

      RTTI的兩種使用方法

      • 第一種使用typeid(),就像sizeof()同樣,看上都像一個函數。但實際上它是由編譯器實現的。typeid()帶有一個參數,它能夠是一個對象引用或指針,返回全局typeinfo類的常量對象的一個引用。能夠用運算符「==」和「!=」來互相比較這些對象。也能夠用name()來得到類型的名稱。注意,若是給typeid( )傳遞一個shape*型參數,它會認爲類型爲shape*,因此若是想知道一個指針所指對象的精確類型,咱們必須逆向引用這個指針。好比,s是個shape*, 那麼:
        cout << typeid(*s).name()<<endl;
        將顯示出s所指向的對象類型。
        也能夠用before(typeinfo&)查詢一個typeinfo對象是否在另外一個typeinfo對象的前面(以定義實現的排列順序),它將返回true或false。若是寫:
        if(typeid(me).before(typeid(you))) //...
        那麼表示咱們正在查詢me在排列順序中是否在you以前。
      • RTTI的第二個用法叫「安全類型向下映射」。使用dynamic_cast<>模板。

      兩種方法的使用舉例以下:

      #include <iostream>
      #include <typeinfo>
      using namespace std;
      class base {
         int i;
      public:
         base(int I = 0) : i(I) {}
         virtual int sum() const { return i; }
      };
      class derived : public base {
         int j;
      public:
         derived(int I = 0, int J = 0) : base(I), j(J) {}
         virtual int sum() const { return base::sum() + j; }
      };
      main() {
         base *b = new derived(10, 47);
         // rtti method1
         cout << typeid(b).name() << endl; // P4base
         cout << typeid(*b).name() << endl; // 7derived
         if(typeid(b).before(typeid(*b)))
             cout << "b is before *b" << endl;
         else
             cout << "*b is before b" << endl;
         // rtti method2
         derived *d = dynamic_cast<derived*>(d);
         if(d) cout << "cast successful" << endl;
      }

      注意1:這裏若是沒有多態機制,則RTTI可能運行的結果不是咱們想要的,好比若是沒有虛函數,則這裏兩個都顯示base,通常但願RTTI用於多態類。

      注意2:運行時類型的識別對一個void型指針不起做用。void *確實意味着「根本沒有類型信息」。

      void *v = new stimpy;
      stimpy* s = dynamic_cast<stimpy*>(v);  // error
      cout << typeid(*v).name() << endl;     // error

       

    • RTTI的實現
      典型的RTTI是經過在VTABLE中放一個額外的指針來實現的。這個指針指向一個描述該特定類型的typeinfo結構(每一個新類只產生一個typeinfo的實例),因此typeid( )表達式的做用實際上很簡單。VPTR用來取typeinfo的指針,而後產生一個結果typeinfo結構的一個引用—這是一個決定性的步驟—咱們已經知道它要花多少時間。

      對於dynamic_cast<目標* > <源指針>,多數狀況下是很容易的,先恢復源指針的RTTI信息再取出目標*的類型RTTI信息,而後調用庫中的一個例程判斷源指針是否與目標*相同或者是目標*類型的基類。它可能對返回的指針作了一點小的改動,由於目的指針類可能存在多重繼承的狀況,而源指針類型並非派生類的第一個基類。在多重繼承時狀況會變得複雜些,由於一個基類在繼承層次中可能出現一次以上,而且可能有虛基類。
      用於動態映射的庫例程必須檢查一串長長的基類列表,因此動態映射的開銷比typeid()要大(固然咱們獲得的信息也不一樣,這對於咱們的問題來講可能很關鍵),而且這是非肯定性的,由於查找一個基類要比查找一個派生類花更多的時間。另外動態映射容許咱們比較任何類型,不限於在同一個繼承層次中比較兩個類。這使得動態映射調用的庫例程開銷更高了。

      映射類型 含義
      static_cast 爲了「行爲良好」和「行爲較好」而使用的映射,包括一些咱們可能如今不用的映射(如向上映射和自動類型轉換)
      const_cast 用於映射常量和變量(const和volatile)
      const_cast 爲了安全類型的向下映射(本章前面已經介紹)
      reinterpret_cast 爲了映射到一個徹底不一樣的意思。這個關鍵詞在咱們須要把類型映射回原有類型時要用到它。咱們映射到的類型僅僅是爲了故弄玄虛和其餘目的。這是全部映射中最危險的

      注意:若是想把一個const 轉換爲非const,或把一個volatile轉換成一個非volatile(勿遺忘這種狀況),就要用到const_cast。這是能夠用const_cast的惟一轉換。若是還有其餘的轉換牽涉進來,它必須分開來指定,不然會有一個編譯錯誤。

相關文章
相關標籤/搜索