程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++箴言:將強制轉型減到最少

C++箴言:將強制轉型減到最少

編輯:C++入門知識
C++ 的規則設計為保證不會發生類型錯誤。在理論上,假如你的程序想順利地通過編譯,你就不應該試圖對任何對象做任何不安全的或無意義的操作。這是一個非常有價值的保證,你不應該輕易地放棄它。
  
   <!-- frame contents --> <!-- /frame contents -->   不幸的是,強制轉型破壞了類型系統。它會引起各種各樣的麻煩,其中一些輕易被察覺,另一些則格外地微妙。假如你從 C,Java,或 C# 轉到 C++,請一定注重,因為強制轉型在那些語言中比在 C++ 中更有必要,危險也更少。但是 C++ 不是 C,也不是 Java,也不是 C#。在這一語言中,強制轉型是一個你必須聚精會神才可以靠近的特性。
  
  我們就從回顧強制轉型的語法開始,因為對於同樣的強制轉型通常有三種不同的寫法。C 風格(C-style)強制轉型如下:
  
  
  
  (T) eXPression // cast expression to be of type T
  
  函數風格(Function-style)強制轉型使用這樣的語法:
  
  
  
  T(expression) // cast expression to be of type T
  
  這兩種形式之間沒有本質上的不同,它純粹就是一個把括號放在哪的問題。我把這兩種形式稱為舊風格(old-style)的強制轉型。
  
  C++ 同時提供了四種新的強制轉型形式(通常稱為新風格的或 C++ 風格的強制轉型):
  
  
  
  const_cast(expression)
  
  dynamic_cast(expression)
  
  reinterpret_cast(expression)
  
  static_cast(expression)
  
  每一種適用於特定的目的:
  
  ·const_cast 一般用於強制消除對象的常量性。它是唯一能做到這一點的 C++ 風格的強制轉型。
  
  ·dynamic_cast 主要用於執行“安全的向下轉型(safe downcasting)”,也就是說,要確定一個對象是否是一個繼續體系中的一個特定類型。它是唯一不能用舊風格語法執行的強制轉型。也是唯一可能有重大運行時代價的強制轉型。(過一會兒我再提供細節。)
  
  ·reinterpret_cast 是特意用於底層的強制轉型,導致實現依靠(implementation-dependent)(就是說,不可移植)的結果,例如,將一個指針轉型為一個整數。這樣的強制轉型在底層代碼以外應該極為罕見。在本書中我只用了一次,而且還僅僅是在討論你應該如何為裸內存(raw memory)寫一個調諧分配者(debugging allocator)的時候。
  
  ·static_cast 可以被用於強制隱型轉換(例如,non-const 對象轉型為 const 對象(就像 Item 3 中的),int 轉型為 double,等等)。它還可以用於很多這樣的轉換的反向轉換(例如,void* 指針轉型為有類型指針,基類指針轉型為派生類指針),但是它不能將一個 const 對象轉型為 non-const 對象。(只有 const_cast 能做到。)
  
  舊風格的強制轉型依然合法,但是新的形式更可取。首先,在代碼中它們更輕易識別(無論是人還是像 grep 這樣的工具都是如此),這樣就簡化了在代碼中尋找類型系統被破壞的地方的過程。第二,更精確地指定每一個強制轉型的目的,使得編譯器診斷使用錯誤成為可能。例如,假如你試圖使用一個 const_cast 以外的新風格強制轉型來消除常量性,你的代碼將無法編譯。
  
  當我要調用一個 explicit 構造函數用來傳遞一個對象給一個函數的時候,大概就是我僅有的使用舊風格的強制轉換的時候。例如:
  
  
  
  class Widget {
  
  public:
  
  explicit Widget(int size);
  
  ...
  
  };
  
  
  
  void doSomeWork(const Widget& w);
  
  
  
  doSomeWork(Widget(15)); // create Widget from int
  
  // with function-style cast
  
  
  
  doSomeWork(static_cast(15)); // create Widget from int
  
  // with C++-style cast
  
  
  
   更多內容請看C/C++技術專題專題,或 由於某種原因,有條不紊的對象創建感覺上不像一個強制轉型,所以在這個強制轉型中我多半會用函數風格的強制轉型代替 static_cast。反過來說,在你寫出那些導致核心崩潰(core dump)的代碼時,你通常都感覺你有恰當的原因,所以你最好忽略你的感覺並始終都使用新風格的強制轉型。
  
  
   <!-- frame contents --> <!-- /frame contents -->   很多程序員認為強制轉型除了告訴編譯器將一種類型看作另一種之外什麼都沒做,但這是錯誤的。任何種類的類型轉換(無論是通過強制轉型的顯式的還是編譯器添加的隱式的)都會導致運行時的可執行代碼。例如,在這個代碼片斷中,
  
  
  
  int x, y;
  
  ...
  
  double d = static_cast(x)/y; // divide x by y, but use
  
  // floating point division
  
  int x 到 double 的強制轉型理所當然要生成代碼,因為在大多數系統架構中,一個 int 的底層表示與 double 的不同。這可能還不怎麼令人吃驚,但是下面這個例子可能會讓你稍微開一下眼:
  
  
  
  class Base { ... };
  
  
  
  class Derived: public Base { ... };
  
  
  
  Derived d;
  
  
  
  Base *pb = &d; // implicitly convert Derived* → Base*
  
  這裡我們只是創建了一個指向派生類對象的基類指針,但是有時候,這兩個指針的值並不相同。在當前情況下,會在運行時在 Derived* 指針上應用一個偏移量以得到正確的 Base* 指針值。
  
  這後一個例子表明一個單一的對象(例如,一個類型為 Derived 的對象)可能會有不止一個地址(例如,它的被一個 Base* 指針指向的地址和它的被一個 Derived* 指針指向的地址)。這在 C 中就不會發生,也不會在 Java 中發生,也不會在 C# 中發生,它僅在 C++ 中發生。實際上,假如使用了多繼續,則一定會發生,但是在單繼續下也會發生。與其它事情合在一起,就意味著你應該總是避免對 C++ 如何擺放事物做出假設,你當然也不應該基於這樣的假設執行強制轉型。例如,將一個對象的地址強制轉型為 char* 指針,然後對其使用指針運算,這幾乎總是會導致未定義行為。
  
  但是請注重我說一個偏移量是“有時”被需要。對象擺放的方法和他們的地址的計算方法在不同的編譯器之間有所變化。這就意味著僅僅因為你的“我知道事物是如何擺放的”而使得強制轉型能工作在一個平台上,並不意味著它們也能在其它平台工作。這個世界被通過痛苦的道路學得這條經驗的可憐的程序員所布滿。 關於強制轉型的一件有趣的事是很輕易寫出看起來對(在其它語言中也許是對的)實際上錯的東西。例如,許多應用框架(application framework)要求在派生類中實現虛成員函數時要首先調用它們的基類對應物。假設我們有一個 Window 基類和一個 SpecialWindow 派生類,它們都定義了虛函數 onResize。進一步假設 SpecialWindow 的 onResize 被期望首先調用 Window 的 onResize。這就是實現這個的一種方法,它看起來正確實際並不正確:
  
  
  
  class Window { // base class
  
  public:
  
  virtual void onResize() { ... } // base onResize impl
  
  ...
  
  };
  
  
  
  class SpecialWindow: public Window { // derived class
  
  public:
  
  virtual void onResize() { // derived onResize impl;
  
  static_cast(*this).onResize(); // cast *this to Window,
  
  // then call its onResize;
  
  // this doesn’t work!
  
  
  
  ... // do SpecialWindow-
  
  } // specific stuff
  
  
  
  ...
  
  
  
  };
  
  我突出了代碼中的強制轉型。(這是一個新風格的強制轉型,但是使用舊風格的強制轉型也於事無補。)正像你所期望的,代碼將 *this 強制轉型為一個 Window。因此調用 onResize 的結果就是調用 Window::onResize。你也許並不期待它沒有調用當前對象的那個函數!作為替代,強制轉型創建了一個 *this 的基類部分的新的,臨時的拷貝,然後調用這個拷貝的 onResize!上面的代碼沒有調用當前對象的 Window::onResize,然後再對這個對象執行 SpecialWindow 特有的動作——它在對當前對象執行 SpecialWindow 特有的動作之前,調用了當前對象的基類部分的一份拷貝的 Window::onResize。假如 Window::onResize 改變了當前對象(可能性並不小,因為 onResize 是一個 non-const 成員函數),當前對象並不會改變。作為替代,那個對象的一份拷貝被改變。假如 SpecialWindow::onResize 改變了當前對象,無論如何,當前對象將被改變,導致的境況是那些代碼使當前對象進入一種病態,沒有做基類的變更,卻做了派生類的變更。
  
  解決方法就是消除強制轉型,用你真正想表達的來代替它。你不應該哄騙編譯器將 *this 當作一個基類對象來處理,你應該調用當前對象的 onResize 的基類版本。就是這樣:
  
  
  
  class SpecialWindow: public Window {
  
  
  public:
  
  virtual void onResize() {
  
  Window::onResize(); // call Window::onResize
  
  ... // on *this
  
  }
  
  ...
  
  
  
  };
  
  這個例子也表明假如你發現自己要做強制轉型,這就是你可能做錯了某事的一個信號。在你想用 dynamic_cast 時尤其如此。
  
  在探究 dynamic_cast 的設計意圖之前,值得留意的是很多 dynamic_cast 的實現都相當慢。例如,至少有一種通用的實現部分地基於對類名字進行字符串比較。假如你在一個位於四層深的單繼續體系中的對象上執行 dynamic_cast,在這樣一個實現下的每一個 dynamic_cast 都要付出相當於四次調用 strcmp 來比較類名字的成本。對於一個更深的或使用了多繼續的繼續體系,付出的代價會更加昂貴。一些實現用這種方法工作是有原因的(它們不得不這樣做以支持動態鏈接)。盡管如此,除了在普遍意義上警惕強制轉型外,在性能敏感的代碼中,你應該非凡警惕 dynamic_casts。
  
  對 dynamic_cast 的需要通常發生在這種情況下:你要在一個你確信為派生類的對象上執行派生類的操作,但是你只能通過一個基類的指針或引用來操控這個對象。有兩個一般的方法可以避免這個問題。
  
  
   更多內容請看C/C++技術專題專題,或 第一個,使用存儲著直接指向派生類對象的指針的容器,從而消除通過基類接口操控這個對象的需要。例如,假如在我們的 Window/SpecialWindow 繼續體系中,只有 SpecialWindows 支持 blinking,對於這樣的做法:
  
  
  
  class Window { ... };
  
  
  
   <!-- frame contents --> <!-- /frame contents -->   class SpecialWindow: public Window {
  
  public:
  
  void blink();
  
  ...
  
  };
  
  typedef // see Item 13 for info
  
  std::vector > VPW; // on tr1::shared_ptr
  
  
  
  VPW winPtrs;
  
  
  
  ...
  
  
  
  for (VPW::iterator iter = winPtrs.begin(); // undesirable code:
  
  iter != winPtrs.end(); // uses dynamic_cast
  
  ++iter) {
  
  if (SpecialWindow *psw = dynamic_cast(iter->get()))
  
  psw->blink();
  
  }
  
  設法用如下方法代替:
  
  
  
  typedef std::vector > VPSW;
  
  
  
  VPSW winPtrs;
  
  
  
  ...
  
  
  
  for (VPSW::iterator iter = winPtrs.begin(); // better code: uses
  
  iter != winPtrs.end(); // no dynamic_cast
  
  ++iter)
  
  (*iter)->blink();
  
  當然,這個方法不答應你在同一個容器中存儲所有可能的 Window 的派生類的指針。為了與不同的窗口類型一起工作,你可能需要多個類型安全(type-safe)的容器。
  
  一個候選方法可以讓你通過一個基類的接口操控所有可能的 Window 派生類,就是在基類中提供一個讓你做你想做的事情的虛函數。例如,盡管只有 SpecialWindows 能 blink,在基類中聲明這個函數,並提供一個什麼都不做的缺省實現或許是有意義的:
  
  
  
  class Window {
  
  public:
  
  virtual void blink() {} // default impl is no-op;
  
  ... // see Item 34 for why
  
  }; // a default impl may be
  
  // a bad idea
  
  
  
  class SpecialWindow: public Window {
  
  public:
  
  virtual void blink() { ... }; // in this class, blink
  
  ... // does something
  
  
     };
  
  
  
  typedef std::vector > VPW;
  
  
  
  VPW winPtrs; // container holds
  
  // (ptrs to) all possible
  
  ... // Window types
  
  
  
  for (VPW::iterator iter = winPtrs.begin();
  
  iter != winPtrs.end();
  
  ++iter) // note lack of
  
  (*iter)->blink(); // dynamic_cast
  
  無論哪種方法——使用類型安全的容器或在繼續體系中上移虛函數——都不是到處適用的,但在很多情況下,它們提供了 dynamic_casting 之外另一個可行的候選方法。當它們可用時,你應該加以利用。
  
  你應該絕對避免的一件東西就是包含了極聯 dynamic_casts 的設計,也就是說,看起來類似這樣的任何東西:
  
  
  
  class Window { ... };
  
  
  
  ... // derived classes are defined here
  
  
  
  typedef std::vector > VPW;
  
  
  
  VPW winPtrs;
  
  
  
  ...
  
  
  
  for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
  
  {
  
   if (SpecialWindow1 *psw1 = dynamic_cast(iter->get())) { ... }
  
  
  
   else if (SpecialWindow2 *psw2 = dynamic_cast(iter->get())) { ... }
  
  
  
   else if (SpecialWindow3 *psw3 = dynamic_cast(iter->get())) { ... }
  
  
  
  ...
  
  }
  
  這樣的 C++ 會生成的代碼又大又慢,而且很脆弱,因為每次 Window 類繼續體系發生變化,所有這樣的代碼都要必須被檢查,以確認是否需要更新。(例如,假如增加了一個新的派生類,在上面的極聯中或許就需要加入一個新的條件分支。)看起來類似這樣的代碼應該總是用基於虛函數的調用的某種東西來替換。 好的 C++ 極少使用強制轉型,但在通常情況下完全去除也不實際。例如,從 int 到 double 的強制轉型,就是對強制轉型的合理運用,雖然它並不是絕對必要。(那些代碼應該被重寫,聲明一個新的類型為 double 的變量,並用 x 的值進行初始化。)就像大多數可疑的結構成分,強制轉型應該被盡可能地隔離,典型情況是隱藏在函數內部,用函數的接口保護調用者遠離內部的污穢的工作。
  
  Things to Remember
  
  ·避免強制轉型的隨時應用,非凡是在性能敏感的代碼中應用 dynamic_casts,假如一個設計需要強制轉型,設法開發一個沒有強制轉型的侯選方案。
  
  ·假如必須要強制轉型,設法將它隱藏在一個函數中。客戶可以用調用那個函數來代替在他們自己的代碼中加入強制轉型。
  
  ·盡量用 C++ 風格的強制轉型替換舊風格的強制轉型。它們更輕易被注重到,而且他們做的事情也更加明確。
  
  
  
   更多內容請看C/C++技術專題專題,或
 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved