淺談C++開發(fā)中數(shù)據(jù)結(jié)構(gòu)和算法的分離
相信每一個在windows下編過程序的人都或多或少地用過位圖,大多數(shù)人是從網(wǎng)上下載一些成熟完善的DIB類庫來使用(例如CxImage、CDIB),少數(shù)人有一套自己封裝好的DIB類庫,方便以后的擴充和使用。(近幾年GDI+異軍突起,在某些處理方面,如:縮放、旋轉(zhuǎn)、漸變填充等它提供無與倫比的速度和質(zhì)量,但,如果你想做一個完善的圖像處理程序,直接使用它會給架構(gòu)設(shè)計帶來困難,你可以用adapter模式封裝它后再使用)。
這時候,如果你需要一些圖像處理操作你會怎么辦呢?很多沒有OO經(jīng)驗的C++程序員(例如一年前的我)可能會這樣做:在類中直接添加方法。
- int FClamp0255 (int nValue) {return max (0, min (0xFF, nValue));} // 飽和到0--255
- class FCObjImage
- {
- public :
- Invert () ;
- AdjustRGB (int R, int G, int B) ;
- } ;
- void FCObjImage::Invert ()
- {
- if ((GetHandle() == NULL) || (ColorBits() < 24))
- return ;
- int nSpan = ColorBits() / 8 ; // 每象素字節(jié)數(shù)3, 4
- for (int y=0 ; y < Height() ; y++)
- {
- BYTE * pPixel = GetBits (y) ;
- for (int x=0 ; x < Width() ; x++, pPixel += nSpan)
- {
- pPixel[0] = ~pPixel[0] ;
- pPixel[1] = ~pPixel[1] ;
- pPixel[2] = ~pPixel[2] ;
- }
- }
- }
- void FCObjImage::AdjustRGB (int R, int G, int B)
- {
- if ((GetHandle() == NULL) || (ColorBits() < 24))
- return ;
- int nSpan = ColorBits() / 8 ; // 每象素字節(jié)數(shù)3, 4
- for (int y=0 ; y < Height() ; y++)
- {
- BYTE * pPixel = GetBits (y) ;
- for (int x=0 ; x < Width() ; x++, pPixel += nSpan)
- {
- pPixel[0] = FClamp0255 (pPixel[0] + B) ;
- pPixel[1] = FClamp0255 (pPixel[1] + G) ;
- pPixel[2] = FClamp0255 (pPixel[2] + R) ;
- }
- }
- }
這里舉了兩個例子(分別實現(xiàn)反色,調(diào)節(jié)RGB值功能),現(xiàn)實中會有大量的此類操作:亮度、對比度、飽和度......現(xiàn)在回想一下,你添加這些方法的步驟是什么,Ooooooooo,RCP(我同事的發(fā)明,全稱:rapid copy paste^-^),第一步一定是從上面復(fù)制一塊代碼下來,然后改掉其中的接口和處理部分。雖然這里的示范代碼很短小,不會連同bug一起復(fù)制,但,定時炸彈卻又多了一個。有天,你的boss告訴你:我不能忍受長時間的等待,請給我加個進度條.....。你也許會加個全局變量,也許會給每個函數(shù)加個參數(shù),但不變的是:你必須修改所有這些處理函數(shù)的代碼,內(nèi)心的咒罵并不會使你少改其中的任何一個。而此時,bug已經(jīng)在旁邊伺機而動了...然而苦日子遠沒熬到頭,一個月后,你心血來潮的老板會讓你在其中加上區(qū)域處理的功能,再一個月后......
回頭重新看看代碼?沒錯,除了紅色的代碼外,其他地方一摸一樣,那能不能把這些算法分離抽出來呢?可能我們馬上會想到標(biāo)準(zhǔn)庫中qsort和windows中常用的回調(diào)方法。好,讓我們實作一下:
- void Pixel_Invert (BYTE * pPixel)
- {
- pPixel[0] = ~pPixel[0] ;
- pPixel[1] = ~pPixel[1] ;
- pPixel[2] = ~pPixel[2] ;
- }
- void FCObjImage::PixelProcess (void(__cdecl*PixelProc)(BYTE * pPixel))
- {
- if ((GetHandle() == NULL) || (ColorBits() < 24))
- return ;
- int nSpan = ColorBits() / 8 ; // 每象素字節(jié)數(shù)3, 4
- for (int y=0 ; y < Height() ; y++)
- {
- BYTE * pPixel = GetBits (y) ;
- for (int x=0 ; x < Width() ; x++, pPixel += nSpan)
- {
- PixelProc (pPixel) ;
- }
- }
- }
- void FCObjImage::Invert ()
- {
- PixelProcess (Pixel_Invert) ;
- }
嗯,看樣子不錯,算法被剝離到一個單一函數(shù)中,我們似乎已經(jīng)解決問題了。處理Invert它完成的非常好,但處理AdjustRGB時遇到了麻煩,RGB那三個調(diào)節(jié)參數(shù)怎么傳進去呢?我們的接口參數(shù)只有一個,通過添加全局變量/成員變量?這是一個辦法,但隨著類方法的增加,程序的可讀性和維護性會急劇的下降,反而倒不如改之前的效果好。
那么如何實現(xiàn)高度的抽象和良好的接口呢?我們現(xiàn)場請來OO(object orient),請它來講一下它的實現(xiàn)。設(shè)計如下派生關(guān)系:
- class FCSinglePixelProcessBase
- {
- public :
- virtual void ProcessPixel (int x, int y, BYTE * pPixel) PURE ;
- } ;
- class FCPixelInvert : public FCSinglePixelProcessBase
- {
- public :
- virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;
- } ;
- void FCPixelInvert::ProcessPixel (int x, int y, BYTE * pPixel)
- {
- pPixel[0] = ~pPixel[0] ; pPixel[1] = ~pPixel[1] ; pPixel[2] = ~pPixel[2] ;
- }
- class FCPixelAdjustRGB : public FCSinglePixelProcessBase
- {
- public :
- FCPixelAdjustRGB (int DeltaR, int DeltaG, int DeltaB) ;
- virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;
- protected :
- int m_iDeltaR, m_iDeltaG, m_iDeltaB ;
- } ;
- void FCPixelAdjustRGB::ProcessPixel (int x, int y, BYTE * pPixel)
- {
- pPixel[0] = FClamp0255 (pPixel[0] + m_iDeltaB) ;
- pPixel[1] = FClamp0255 (pPixel[1] + m_iDeltaG) ;
- pPixel[2] = FClamp0255 (pPixel[2] + m_iDeltaR) ;
- }
然后我們修改image類如下:
- #include "PixelProcessor.h"
- class FCObjImage
- {
- public :
- void PixelHandler (FCSinglePixelProcessBase & PixelProcessor, FCObjProgress * progress = NULL) ;
- } ;
- void FCObjImage::PixelHandler (FCSinglePixelProcessBase & PixelProcessor, FCObjProgress * progress)
- {
- if (GetHandle() == NULL)
- return ;
- int nSpan = ColorBits() / 8 ; // 每象素字節(jié)數(shù)3, 4
- for (int y=0 ; y < Height() ; y++)
- {
- BYTE * pPixel = GetBits (y) ;
- for (int x=0 ; x < Width() ; x++, pPixel += nSpan)
- {
- PixelProcessor.ProcessPixel (x, y, pPixel) ;
- }
- if (progress != NULL)
- progress->SetProgress (y * 100 / Height()) ;
- }
- }
- void FCObjImage::Invert (FCObjProgress * progress)
- {
- PixelHandler (FCPixelInvert(), progress) ;
- }
- void FCObjImage::AdjustRGB (int R, int G, int B, FCObjProgress * progress)
- {
- PixelHandler (FCPixelAdjustRGB (R,G,B), progress) ;
- }
(以上只是一個基本框架,你可以很輕易的把區(qū)域處理的參數(shù)添加進去-通過構(gòu)造時傳遞一個RECT參數(shù)。)
對象真的是一個很奇妙的東西,它可以對外提供一個簡單的接口,而自身又可以封裝上很多附加信息。
好,現(xiàn)在讓我們來檢驗一下剛才的成果:添加一個給圖像奇數(shù)行置黑,給偶數(shù)行置白的操作。
- class FCPixelTest : public FCSinglePixelProcessBase
- {
- public :
- virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;
- } ;
- void FCPixelTest::ProcessPixel (int x, int y, BYTE * pPixel)
- {
- if (y % 2) pPixel[0]=pPixel[1]=pPixel[2] = 0 ;
- // 奇數(shù)行
- else
- pPixel[0]=pPixel[1]=pPixel[2] = 0xFF ;
- // 偶數(shù)行
- }
然后進行如下調(diào)用:
- PixelHandler (FCPixelTest(), progress) ;
多么的和諧美妙,設(shè)計算法的人員只需寫出自己的算法,而不用去考慮怎么讓它支持進度條和區(qū)域這些問題。感覺這就象一把設(shè)計優(yōu)良的AK,你可以不斷的往里添加子彈(對象)^-^
至此,我們應(yīng)該已經(jīng)大功告成了。還有問題嗎?
等等,別忙,有些地方不太對,我添加這個算法后,怎么編譯這么久啊。
問題就出在那個不起眼的:
- #include "PixelProcessor.h"
image是圖像處理的最底層對象,工程中的所有文件都直接或間接地包含它,因此,任何對image.h本身及它所包含的.h的修改都會引起幾乎整個工程的build,這當(dāng)然是無法忍受的,解決的辦法是使用“前置聲明”,因為在PixelHandler接口中我們只需要它的引用(也即是說:我(接口)并不需要知道傳給我的類的內(nèi)部結(jié)構(gòu),給我一個32(64)的內(nèi)存地址就OK了)。
因此我們把
- #include "PixelProcessor.h"
替換成:
- class FCSinglePixelProcessBase ; // external class 前置聲明
然后在.cpp文件中再包含PixelProcessor.h,這樣,對PixelProcessor.h的改變僅僅會導(dǎo)致.cpp文件的重新編譯,大大節(jié)約了編譯時間。
總結(jié):
1)可能的話,在編程中永遠也別去想“拷貝代碼”這個字眼。畢竟,OO就是為了抽象和代碼重用才誕生的。
2)除非必要,否則類的成員變量和函數(shù)的參數(shù)盡量用指針或引用代替,這樣做可以在.h中盡可能地少包含其他.h文件,而用前置聲明來替代,以此來減少編譯時間和以后可能會產(chǎn)生的交叉包含。
3)最后說一下效率問題:有些朋友可能會說每個像素都調(diào)用虛函數(shù)會影響性能,這的確,但實際的損失遠沒有想象的大。我實測了一下:對1024*768的圖片進行反片處理,速度只有5%左右的損失,進行復(fù)雜處理(亮度/對比度/gamma)時損失可完全忽略,畢竟多出來的那部分代碼只是進出棧和查表,而不是浮點除這樣耗時的指令。