微軟軟體實現技術授課系列內容之二:C++ Best Coding Practice

來源:互聯網
上載者:User
                  C++ Best Coding Practice

   隨著電腦語言的發展,我們現在編寫一個程式越來越容易了。利用一些軟體開發工具,往往只要通過滑鼠的拖拖點點,電腦就會自動幫你產生許多代碼。但在很多時候,電腦的這種能力被濫用了,我們往往只考慮把這個程式搭起來,而不去考慮程式的效能如何,程式是否足夠的健壯。而此節課的目的主要是介紹一些編碼的經驗,讓大家編寫的程式更加健壯和高效能。   1 、Prefer const and inline to #define      在C++編程中應該盡量使用const和inline來代替#define,盡量做到能不用#define就不用。#define常見的用途有“定義常量”以及“定義宏”,但其中存在諸多的弊病。  第一,查錯不直觀,不利於調試。Define的定義是由預先處理程式處理的,作的是完全的文本替換,不做任何的類型檢查。在編譯器處理階段,define定義的東西已經被完全替換了,這樣在debug的時候就看不到任何的相關資訊,即跟蹤時不能step into宏。例如,把ASPECT_RATIO用define定義成1.653,編譯器就看不到ASPECT_RATIO這個名字了。如果編譯器報1.653錯,那麼就無從知道此1.653來自於何處。在真正編碼的時候應該使用如下的語句來定義:static const double ASPECT_RATIO = 1.653;      第二,沒有任何類型資訊,不是type safe。因為它是文本層級的替換,這樣不利於程式的維護。  第三,define的使用很容易造成汙染。比如,如果有兩個標頭檔都定義了ASPECT_RATIO, 而一個CPP檔案又同時包含了這兩個標頭檔,那麼就會造成衝突。更難查的是另外一種錯誤,比如有如下的代碼:   // in header file def.h         #define Apple 1         #define Orange 2    #define Pineapple 3         …  // in some cpp file that includes the def.h       enum Colors {White, Black, Purple, Orange}; 在.h檔案中Orange被定義成水果的一種,而在.cpp檔案中Orange又成為了一種顏色,那麼編譯器就會把此處的Orange替換成2,編譯可能仍然可以通過,程式也能夠運行,但是這就成了一個bug,表現出古怪的錯誤,且很難查錯。再比如定義了一個求a與b哪個數大的宏,#define max(a,b) ((a) > (b) ? (a) : (b))   int a = 5, b = 0;   max(++ a, b);   max(++ a, b + 10);  在上面的操作中,max(++ a, b); 語句中a被++了兩次,而max(++ a, b + 10); 語句中a只加了一次,這樣在程式處理中就很有可能成為一個bug,且此bug也非常的難找。在實際編碼時可以使用如下的語句來做:  template<class T>   inline const T&   max(const T& a, const T& b) { return a > b ? a : b; }    2 、Prefer C++-style casts   在程式中經常會需要把一種類型轉換成另外一種類型,在C++中應該使用static_cast、const_cast、dynamic_cast、reinterpret_cast關鍵字來做類型轉換。因為這有以下好處,一是其本身就是一種注釋,在代碼中看到上面這些關鍵字就可馬上知道此處是進行類型轉換。二是C語言中類型轉換通常是很難進行搜尋的,而通過關鍵字cast則可以很容易的找到程式中出現類型轉換的地方了。   3 、Distinguish between prefix and postfix forms of increment and decrement operators   通常對於作業系統或編譯器自身支援的類型,prefix(首碼,如++i)與postfix(尾碼,如i++)的效果是一樣的。因為現在的編譯器都很聰明,它會自動做最佳化,這兩者的彙編代碼是一樣的,效能不會有差別。但有時候也會有不同的,如一些重載了操作符的類型。下面是類比prefix與postfix的操作過程,可以發現在postfix操作中會產生一個臨時變數,而這一臨時變數是會佔用額外的時間和開銷的。   // prefix form: increment and fetch       UPInt& UPInt::operator++()    {          *this += 1; // increment          return *this; // fetch    }   // postfix form: fetch and increment    const UPInt UPInt::operator++(int)    {          UPInt oldValue = *this; // fetch          ++(*this); // increment           return oldValue; // return what was fetched      }   一般情況下不需要區分是先++,還是後++,但是我們在編寫程式的時候最好能習慣性的將其寫成++i的形式,如在使用STL中的iterator時,prefix與postfix會有相當大的效能差異。請不要小看這些細節,實際在編寫程式的時候,若不注意具體細節,你會發現程式的效能會非常的低。但要注意,雖然在大多數情況下可以用prefix來代替postfix,但有一種情況例外,那就是有[]操作符時,比如gzArray [++index] 是不等於 gzArray[index++]的。      4 、Minimizing Compile-time Dependencies      有些人在編寫程式時,往往喜歡將一個.h檔案包含到另一個.h檔案,而實踐證明在做大型軟體時這是一個非常不好的習慣,因這樣會造成很多依賴的問題,包含較多的.h檔案,別人又使用了這個class,而在他的那個工程中可能並不存在這些.h檔案,這樣很可能就編譯不能通過。而且這樣做,還可能造成很難去更新一個模組的情況。因為一個.h檔案被很多模組包含的話,如果修改了此.h檔案,在編譯系統的時候,編譯器會去尋找哪些模組依賴於某個被修改過的.h檔案,那麼就導致了所有包含入此.h檔案的模組全都要進行重新編譯。在項目比較小的時候,大家可能還感覺不到差別,但是如果說是在大型的軟體系統裡,你可能編譯一遍源碼需要七、八個小時。如果你這個.h檔案被很多模組包含的話,就算在.h檔案中加了一行注釋,在編譯時間編譯器檢查哪些檔案被改動,那麼所有包含入此.h檔案的模組都會被重新編譯,造成巨大的時間和精力負擔。對於此問題,解決的方法就是讓.h檔案自包含,也就是說讓它包含盡量少的東西。所謂盡量少是指如刪掉任何一個它包含進來的.h檔案,都將無法正常進行工作。其實在很多情況下,並不需要一個.h檔案去包含另一個.h檔案,完全可以通過class聲明來解決依賴關係的這種問題。再來看下面這個例子:   #include "a.h"  // class A  #include "b.h"  // class B  #include "c.h"  // class C  #include "d.h"  // class D  #include "e.h"  // class E  class X : public A, private B  {   public:       E  SomeFunctionCall(E someParameter);     private:       D  m_dInstance;  }; 當類X從類A和類B中派生時,需要知道X在記憶體中都有哪些data,通常在記憶體中前面是基類的data,後面緊跟的是此衍生類別自身定義的data,因此就必須知道類A與類B的內部細節,要不然編譯器就無法來安排記憶體了。但是在處理參數以及參數傳回值的時候,實際上並不需要知道這些資訊,在此處定義的SomeFunctionCall()只需知道E是個class就足夠了,並不需要知道類E中的data如長度等的具體細節。上面的代碼應該改寫成如下的形式,以減少依賴關係:   #include "a.h"  // class A  #include "b.h"  // class B  #include "c.h"  // class C  #include "d.h"  // class D  class E;  class X : public A, private B  {   public:       E  SomeFunctionCall(E someParameter);     private:       D  m_dInstance;  };       5 、Never treat arrays polymorphically      不要把數組和多態一起使用,請看下面的例子。   class BST { ... };   class BalancedBST: public BST { ... };   void printBSTArray(ostream& s, const BST array[], int numElements)    {          for (int i = 0; i < numElements; ++i)          {             s << array[i];             // this assumes an  operator<< is defined for BST         }   }   BalancedBST bBSTArray[10];   printBSTArray(cout, bBSTArray, 10);    數組在記憶體中是一個連續的記憶體空間,而在數組中應該如何來定位一個元素呢?過程是這樣的,編譯器可以知道每個資料類型的長度大小,如果數組的index是0,則會自動去取第一個元素;如果是指定了某個index,編譯器則會根據此index與該資料類型的長度自動去算出該元素的位置。  在printBSTArray()函數中,儘管傳入的參數是BalancedBST類型,但由於其本來定義的類型是BST,那麼它依然會根據BST來計算類型的長度。而通常衍生類別執行個體所佔的記憶體要比基類執行個體所佔的記憶體大一些,因此該程式在編譯時間會報錯。請記住,永遠不要把數組和C++的多態性放在一起使用。   6 、Prevent exceptions from leaving destructors   解構函式中一定不要拋出異常。通常有兩種情況會導致解構函式的調用,一種是當該類的對象離開了它的域,或delete運算式中一個該類對象的指標,另一種是由於異常而引起解構函式的調用。     如果解構函式被調用是由於exception引起,而此時在解構函式中又拋出了異常,程式會立即被系統終止,甚至都來不及進行記憶體釋放。因此如果在解構函式中拋出異常的話,就很容易混淆引起異常的原因,且這樣的軟體也會讓使用者非常惱火。由於解構函式中很可能會調用其它的一些函數,所以在寫解構函式的時候一定要注意,對這些函數是否會拋出異常要非常清楚,如果會的話,就一定要小心了。比如下面這段代碼:   Session::~Session()      {          logDestruction(this);     }  比如logDestruction()函數可能會拋出異常,那麼我們就應該採用下面這種代碼的形式:   Session::~Session()      {          try          {            logDestruction(this);          }         catch (...)          {          }      }  這樣程式出錯的時候不會被立即關掉,可以給使用者一些其它的選擇,至少先讓他把目前在做的工作儲存下來。   7 、Optimization:Remember the 80-20 rule   在軟體界有一個20-80法則,其實這是一個很有趣的現象,比如一個程式中20%的代碼使用了該程式所佔資源的80%;一個程式中20%的代碼佔用了總已耗用時間的80%;一個程式中20%的代碼使用了該程式所佔記憶體的80%;在20%的代碼上面需要花費80%的維護力量,等等。這個規律還可以被繼續推廣下去,不過這個規律無法被證明,它是人們在實踐中觀察得出的結果。從這個規律出發,我們在做程式最佳化的時候,就有了針對性。比如想提高代碼的運行速度,根據這個規律可以知道其中20%的代碼佔用了80%的已耗用時間,因此我們只要找到這20%的代碼,並進行相應的最佳化,那麼我們程式的運行速度就可以有較大的提高。再如有一個函數,佔用了程式80%的已耗用時間,如果把這個函數的執行速度提高10倍,那麼對程式整體效能的提高,影響是非常巨大的。如果有一個函數已耗用時間只佔總時間的1%,那就算把這個函數的運行速度提高1000倍,對程式整體效能的提高也是影響不大的。所以我們的基本思想就是找到佔用已耗用時間最大的那個函數,然後去最佳化它,哪怕只是改進了一點點,程式的整體效能也可以被提高很多。     要想找出那20%的代碼,我們的方法就是使用Profiler,它實際上是一些公司所開發的工具,可以檢查程式中各個模組所分配記憶體的使用方式,以及每個函數所啟動並執行時間等。常見的Profiler有Intel公司開發的VTune,微軟公司開發的Visual Studio profiler,DevPartner from Compuware等。
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.