程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 淺談C++開發中數據結構和算法的分離

淺談C++開發中數據結構和算法的分離

編輯:C++入門知識

相信每一個在windows下編過程序的人都或多或少地用過位圖,大多數人是從網上下載一些成熟完善的DIB類庫來使用例如CxImage、CDIB),少數人有一套自己封裝好的DIB類庫,方便以後的擴充和使用。近幾年GDI+異軍突起,在某些處理方面,如:縮放、旋轉、漸變填充等它提供無與倫比的速度和質量,但,如果你想做一個完善的圖像處理程序,直接使用它會給架構設計帶來困難,你可以用adapter模式封裝它後再使用)。

這時候,如果你需要一些圖像處理操作你會怎麼辦呢?很多沒有OO經驗的C++程序員例如一年前的我)可能會這樣做:在類中直接添加方法。

  1. int FClamp0255 (int nValue) {return max (0, min (0xFF, nValue));} // 飽和到0--255  
  2. class FCObjImage  
  3. {  
  4.  public :  
  5. Invert () ;  
  6. AdjustRGB (int R, int G, int B) ;  
  7. } ;  
  8. void FCObjImage::Invert ()  
  9. {  
  10.  if ((GetHandle() == NULL) || (ColorBits() < 24))  
  11. return ;  
  12.  int nSpan = ColorBits() / 8 ; // 每象素字節數3, 4  
  13.  for (int y=0 ; y < Height() ; y++)  
  14.  {  
  15. BYTE * pPixel = GetBits (y) ;  
  16. for (int x=0 ; x < Width() ; x++, pPixel += nSpan)  
  17. {  
  18.  pPixel[0] = ~pPixel[0] ;  
  19.  pPixel[1] = ~pPixel[1] ;  
  20.  pPixel[2] = ~pPixel[2] ;  
  21. }  
  22.  }  
  23. }  
  24. void FCObjImage::AdjustRGB (int R, int G, int B)  
  25. {  
  26.  if ((GetHandle() == NULL) || (ColorBits() < 24))  
  27. return ;  
  28.  int nSpan = ColorBits() / 8 ; // 每象素字節數3, 4  
  29.  for (int y=0 ; y < Height() ; y++)  
  30.  {  
  31. BYTE * pPixel = GetBits (y) ;  
  32. for (int x=0 ; x < Width() ; x++, pPixel += nSpan)  
  33. {  
  34.  pPixel[0] = FClamp0255 (pPixel[0] + B) ;  
  35.  pPixel[1] = FClamp0255 (pPixel[1] + G) ;  
  36.  pPixel[2] = FClamp0255 (pPixel[2] + R) ;  
  37. }  
  38. }  
  39. }  

這裡舉了兩個例子分別實現反色,調節RGB值功能),現實中會有大量的此類操作:亮度、對比度、飽和度......現在回想一下,你添加這些方法的步驟是什麼,Ooooooooo,RCP我同事的發明,全稱:rapid copy paste^-^),第一步一定是從上面復制一塊代碼下來,然後改掉其中的接口和處理部分。雖然這裡的示范代碼很短小,不會連同bug一起復制,但,定時炸彈卻又多了一個。有天,你的boss告訴你:我不能忍受長時間的等待,請給我加個進度條.....。你也許會加個全局變量,也許會給每個函數加個參數,但不變的是:你必須修改所有這些處理函數的代碼,內心的咒罵並不會使你少改其中的任何一個。而此時,bug已經在旁邊伺機而動了...然而苦日子遠沒熬到頭,一個月後,你心血來潮的老板會讓你在其中加上區域處理的功能,再一個月後......

回頭重新看看代碼?沒錯,除了紅色的代碼外,其他地方一摸一樣,那能不能把這些算法分離抽出來呢?可能我們馬上會想到標准庫中qsort和windows中常用的回調方法。好,讓我們實作一下:

  1. void Pixel_Invert (BYTE * pPixel)  
  2. {  
  3.  pPixel[0] = ~pPixel[0] ;  
  4.  pPixel[1] = ~pPixel[1] ;  
  5.  pPixel[2] = ~pPixel[2] ;  
  6. }  
  7. void FCObjImage::PixelProcess (void(__cdecl*PixelProc)(BYTE * pPixel))  
  8. {  
  9.  if ((GetHandle() == NULL) || (ColorBits() < 24))  
  10. return ;  
  11.  int nSpan = ColorBits() / 8 ; // 每象素字節數3, 4  
  12.  for (int y=0 ; y < Height() ; y++)  
  13.  {  
  14. BYTE * pPixel = GetBits (y) ;  
  15. for (int x=0 ; x < Width() ; x++, pPixel += nSpan)  
  16. {  
  17.  PixelProc (pPixel) ;  
  18. }  
  19.  }  
  20. }  
  21. void FCObjImage::Invert ()  
  22. {  
  23.  PixelProcess (Pixel_Invert) ;   

嗯,看樣子不錯,算法被剝離到一個單一函數中,我們似乎已經解決問題了。處理Invert它完成的非常好,但處理AdjustRGB時遇到了麻煩,RGB那三個調節參數怎麼傳進去呢?我們的接口參數只有一個,通過添加全局變量/成員變量?這是一個辦法,但隨著類方法的增加,程序的可讀性和維護性會急劇的下降,反而倒不如改之前的效果好。

那麼如何實現高度的抽象和良好的接口呢?我們現場請來OOobject orient),請它來講一下它的實現。設計如下派生關系:

  1. class FCSinglePixelProcessBase  
  2. {  
  3.  public :  
  4. virtual void ProcessPixel (int x, int y, BYTE * pPixel) PURE ;  
  5. } ;  
  6. class FCPixelInvert : public FCSinglePixelProcessBase  
  7. {  
  8.  public :  
  9. virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;  
  10. } ;   
  11. void FCPixelInvert::ProcessPixel (int x, int y, BYTE * pPixel)  
  12. {  
  13.  pPixel[0] = ~pPixel[0] ; pPixel[1] = ~pPixel[1] ; pPixel[2] = ~pPixel[2] ;  
  14. }   
  15. class FCPixelAdjustRGB : public FCSinglePixelProcessBase  
  16. {  
  17.  public :  
  18. FCPixelAdjustRGB (int DeltaR, int DeltaG, int DeltaB) ;  
  19. virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;  
  20.  protected :  
  21. int m_iDeltaR, m_iDeltaG, m_iDeltaB ;  
  22. } ;   
  23. void FCPixelAdjustRGB::ProcessPixel (int x, int y, BYTE * pPixel)  
  24. {  
  25.  pPixel[0] = FClamp0255 (pPixel[0] + m_iDeltaB) ;  
  26.  pPixel[1] = FClamp0255 (pPixel[1] + m_iDeltaG) ;  
  27.  pPixel[2] = FClamp0255 (pPixel[2] + m_iDeltaR) ;  
  28. }  

然後我們修改image類如下:

  1. #include "PixelProcessor.h"  
  2. class FCObjImage  
  3. {  
  4.  public :  
  5. void PixelHandler (FCSinglePixelProcessBase & PixelProcessor, FCObjProgress * progress = NULL) ;  
  6. } ;  
  7. void FCObjImage::PixelHandler (FCSinglePixelProcessBase & PixelProcessor, FCObjProgress * progress)  
  8. {  
  9.  if (GetHandle() == NULL)  
  10. return ;  
  11.  int nSpan = ColorBits() / 8 ; // 每象素字節數3, 4  
  12.  for (int y=0 ; y < Height() ; y++)  
  13.  {  
  14. BYTE * pPixel = GetBits (y) ;  
  15. for (int x=0 ; x < Width() ; x++, pPixel += nSpan)  
  16. {  
  17.  PixelProcessor.ProcessPixel (x, y, pPixel) ;  
  18. }  
  19. if (progress != NULL)  
  20.  progress->SetProgress (y * 100 / Height()) ;  
  21.  }  
  22. }  
  23. void FCObjImage::Invert (FCObjProgress * progress)  
  24. {  
  25.  PixelHandler (FCPixelInvert(), progress) ;  
  26. }   
  27. void FCObjImage::AdjustRGB (int R, int G, int B, FCObjProgress * progress)  
  28. {  
  29.  PixelHandler (FCPixelAdjustRGB (R,G,B), progress) ;  

以上只是一個基本框架,你可以很輕易的把區域處理的參數添加進去-通過構造時傳遞一個RECT參數。)

對象真的是一個很奇妙的東西,它可以對外提供一個簡單的接口,而自身又可以封裝上很多附加信息。

好,現在讓我們來檢驗一下剛才的成果:添加一個給圖像奇數行置黑,給偶數行置白的操作。

  1. class FCPixelTest : public FCSinglePixelProcessBase  
  2. {  
  3.  public :  
  4. virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;  
  5. } ;   
  6. void FCPixelTest::ProcessPixel (int x, int y, BYTE * pPixel)  
  7. {  
  8.  if (y % 2) pPixel[0]=pPixel[1]=pPixel[2] = 0 ;   
  9.  // 奇數行   
  10.  else   
  11. pPixel[0]=pPixel[1]=pPixel[2] = 0xFF ;   
  12. // 偶數行  

然後進行如下調用:

  1. PixelHandler (FCPixelTest(), progress) ; 

多麼的和諧美妙,設計算法的人員只需寫出自己的算法,而不用去考慮怎麼讓它支持進度條和區域這些問題。感覺這就象一把設計優良的AK,你可以不斷的往裡添加子彈對象)^-^

至此,我們應該已經大功告成了。還有問題嗎?

等等,別忙,有些地方不太對,我添加這個算法後,怎麼編譯這麼久啊。

問題就出在那個不起眼的:

  1. #include "PixelProcessor.h" 

image是圖像處理的最底層對象,工程中的所有文件都直接或間接地包含它,因此,任何對image.h本身及它所包含的.h的修改都會引起幾乎整個工程的build,這當然是無法忍受的,解決的辦法是使用“前置聲明”,因為在PixelHandler接口中我們只需要它的引用也即是說:我接口)並不需要知道傳給我的類的內部結構,給我一個32(64)的內存地址就OK了)。

因此我們把

  1. #include "PixelProcessor.h" 

替換成:

  1. class FCSinglePixelProcessBase ; // external class 前置聲明 

然後在.cpp文件中再包含PixelProcessor.h,這樣,對PixelProcessor.h的改變僅僅會導致.cpp文件的重新編譯,大大節約了編譯時間。

總結:

1)可能的話,在編程中永遠也別去想“拷貝代碼”這個字眼。畢竟,OO就是為了抽象和代碼重用才誕生的。

2)除非必要,否則類的成員變量和函數的參數盡量用指針或引用代替,這樣做可以在.h中盡可能地少包含其他.h文件,而用前置聲明來替代,以此來減少編譯時間和以後可能會產生的交叉包含。

3)最後說一下效率問題:有些朋友可能會說每個像素都調用虛函數會影響性能,這的確,但實際的損失遠沒有想象的大。我實測了一下:對1024*768的圖片進行反片處理,速度只有5%左右的損失,進行復雜處理亮度/對比度/gamma)時損失可完全忽略,畢竟多出來的那部分代碼只是進出棧和查表,而不是浮點除這樣耗時的指令。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved