|
為16、32、64位架構編寫可移植代碼
與16位相比,32位意味著程序更快、可直接尋址訪問更多的內存和更好的處理器架構。鑒于此,越來越多的程序員已經開始考慮利用64位處理器所帶來的巨大優勢了。 克雷研究(Cray Research 應為品牌名)計算機已經開始使用64位字,并可訪問更大的內存地址。然而,作為正在向開發標準軟件和與其他操作系統相互努力的一部分,我們已經停止了移植那些原本基于32位處理器的代碼。事實上,我們不斷遇到我們稱之為“32位主義”的代碼,這些代碼都是在假定機器字長為32位的情況下編寫的,因而很難移植這種代碼,所以必須確立一些簡單的指導方針,以便助于編寫跨16、32、64位處理器平臺的代碼。 由于有一些遺留問題, C語言在數據類型和數據構造方面,顯得有點過剩了。可以使用的不僅僅是char、short、int和long類型,還有相應的unsigned(無符號)類型,當然你可在結構(structure)和聯合(union)中混合使用,可以在聯合中包含結構,再在結構中包含聯合,如果還嫌數據類型不夠復雜,還可以轉換成比特位,當然也可以把一種數據 類型轉換成另一種你想要的數據類型。正是因為這些工具太強大,如果不安全地使用它們,就有可能會傷到自己了。 高級代碼的高級結構 在Kernighan和Plauger經典的《The Elements of Programming Style》一書中,他們的建議是“選擇使程序看上去簡單的數據表示法”。對個人而言,這意味著為高級編程使用高級 數據結構,而低級編程使用低級數據結構。 在我們移植的程序中,有一個常見的32位主義bug,不如拿它來做個例子,科內爾大學編寫的用于網間互訪的路由協議引擎,在伯克利網絡環境下(指TCP/IP),自然是使用inet_addr( )來把表示Internet地址的 字符串轉換成二進制形式。Internet地址碰巧也是32位的,就如運行伯克利網絡系統的其他計算機一樣,都是有著同樣的字寬。 但也存在著有關Internet地址的高級定義:in_ addr結構。這個結構定義包括了子域s_ addr,它是一個包含了Internet地址的unsigned long標量。inet_addr()接受一個指向字符的指針,并且返回一個unsigned long,在轉換地址字符串過程中如果發生錯誤,inet_addr將返回-1。 程序Gated讀取以文本格式存放Internet地址的配置文件,并把它們放入sockaddr_in(這是一個包含了結構in_addr的高級結構)。例1(a)中的代碼可以在32位電腦上正常運行,但移植到克雷研究計算機上,卻無法運行,為什么呢? 例1:高級代碼的高級結構 (a) struct sockaddr_in saddrin char *str;
if ((saddrin.sin_addr.s_addr = inet_addr(str)) == (unsigned long)-1) { do_some_error_handling; } |
(b) struct sockaddr_in saddrin char *str;
if (inet_aton(str, &saddrin.sin_addr) ! = OK) { do_some_error_handling; } |
因為只要inet_addr能夠正確地解析字符串,那么一切OK。當inet_addr在64位計算機上返回一個錯誤時,這段代碼卻未能捕捉到。你必須要考慮比較語句中的數據位寬,來確定到底是哪出了錯。 首先,inet_addr返回錯誤值——unsigned long -1,在64位中表示為比特位全為1,這個值被存儲在結構in_addr下的子域s_addr中,而in_addr必須是32位來匹配Internet地址,所以它是一個32比特位的unsigned int(在我們的編譯器上,int是64位)。現在我們存儲進32個1,存儲進的值將與unsigned long -1比較。當我們存儲32個1于unsigned int時,編譯器自動把32位提升為64位;這樣,數值0x00000000 ffffffff與0xffffffff ffffffff的比較當然就失敗了。這是一個很難發現的bug,特別是在這種因為32位到64位的隱式提升上。 那我們對這個bug怎么辦呢?一個解決方法是在語句中比較0xffffffff,而不是-1,但這又會使代碼更加依賴于特定大小的對象。另一個方法是,使用一個中間的unsigned long變量,從而在把結果存入sockaddr_in前,執行比較,但這會讓程序代碼更復雜。 真正的問題所在是,我們期望一個unsigned long值與一個32位量(如Internet地址)相等。Internet地址必須以32位形式進行存儲,但有些時候用一個標量,來訪問這個地址的一部分,是非常方便的。在32位字長的電腦中,用一個long數值(常被當作32位)來訪問這個地址,看上去沒什么問題。讓我們暫時不想一個低級的數據項(32位Internet地址)是否與一個機器字相等,那么高級數據類型結構in_addr就應該被一直使用。因為in_addr中沒有無效值,那么應有一個單獨的狀態用作返回值。 解決方案是定義一個新的函數,就像inet_addr那樣,但返回一個狀態值,而且接受一個結構in_addr作為參數,參見例1(b)。因為高級的數據元素可以一直使用,而返回的值是沒有溢出的,所以這個代碼是可以跨架構移植的,而不管字長是多少。雖然伯克利發布了NET2,其中的確定義了一個新的函數inet_aton(),但如果試著改變inet_addr()中的代碼,將會損壞許多程序。 低級代碼的低級結構 低級編程意味著直接操縱物理設備或者特定協議的通訊格式,例如,設備驅動程序經常使用非常精確的位模式來操縱控制寄存器。此外, 網絡協議通過特定的比特位模式傳輸數據項時,也必須適當地轉譯。 為了操縱物理數據項,此處的數據結構必須準確地反映被操縱的內容。比特位是一個不錯的選擇,因為它們正好指定了比特的位數及排列。事實上,正是這種精確度,使比特位相對于short、int、long,更好地映像了物理結構(short、int、long會因為電腦的不同而改變,而比特位不會)。 當映像一個物理結構時,是通過定義格式來使比特位達到這種精度的,這就使你必須一直使用一種編碼風格來訪問結構。此時的每個位域都是命名的,你寫出的代碼可直接訪問這些位域。當訪問物理結構時,有件事可能你并不想去做,那就是使用標量數組(short、int、or long),訪問這些數組的代碼都假定存在一個特定的比特位寬,當移植這些代碼到一臺使用不同字寬的電腦上時,就可能不正確了。 在我們移植PEX圖像庫時遇到的一個問題,就涉及到其映像的協議消息結構。在某臺電腦上,如果int整型的長度與消息中的元素一樣,那例2(a)中的代碼就會工作得很正常。32位的數據元素在32字長的電腦上沒有問題,拿到64位的克雷計算機上,它就出錯了。對例2(b)而言,不單要改變結構定義,還要改變所有涉及到coord數組的代碼。這樣,擺在我們面前就有兩個選擇,要么重寫涉及此消息的所有代碼,要么定義一個低級的結構和一個高級的結構,然后用一段特殊的代碼把數據從一個拷貝到另一個當中,不過我也不期望可以找出每個對zcoord = draw_ msg.coord[2]的引用,而且,當現在需要移植到一個新架構上時,把所有的代碼都改寫成如例2(c)所示無疑是一項艱苦的工作。這個特殊的問題是由于忽視字長的不同而帶來的,所以不能假設在可移植的代碼中機器字長、short、int、long都是具有同樣的大小。 例2:低級代碼的低級結構 (a) struct draw_msg { int objectid; int coord[3]; } |
(b) struct draw_msg { int objectid:32; int coord1:32; int coord2:32; int coord3:32; } |
(c) int *iptr, *optr, *limit; int xyz[3];
iptr = draw_msg.coord; limit = draw_msg.coord + sizeof(draw_msg.coord);
optr = xyz; while (iptr < limit) *optr++ = *iptr++; | 結構打包和字對齊
正是因為編譯器會對結構進行打包,所以不同計算機上字長的變化,還導致了另一個問題。C編譯器在字(word)的邊界上對齊字長,當具有一個字長的數據后面緊接著一個較小的數據時,這種方法會產生內存空缺(不過也有例外,比如說當有足夠多的小數據剛好可以填充一個字時)。 一些聰明的程序員在聲明聯合時,往往在其中會帶有兩個或更多的結構,其中一個結構剛好填充聯合,另一個則可以用來從不同的角度來看待這個聯合,參見例3(a)。假設這段代碼是為16位字長的計算機所寫,int為16位,long為32位,那么存取這個結構的代碼將會得到正常的映射關系(如圖1),而例3(b)中的代碼也會按預期的那樣工作。可是,如果這段代碼一旦移植到另一臺具有32位字長的計算機上時,映射關系就改變了。如果新計算機上的編譯器允許你使用16位的int,那么字的對齊就會像圖2所示了,或者如果編譯器遵循K&R約定,那么int將會和一個字(32比特)一樣長,對齊就如圖3所示,在任一情況下,這都將導致問題。 例3:結構打包和字對齊 (a) union parse_hdr { struct hdr { char data1; char data2; int data3; int data4; } hdr; struct tkn { int class; long tag; } tkn; } parse_item; |
(b) char *ptr = msgbuf;
parse_item.hdr.data1 = *ptr++; parse_item.hdr.data2 = *ptr++; parse_item.hdr.data3 = (*ptr++ << 8 | *ptr++); parse_item.hdr.data4 = (*ptr++ << 8 | *ptr++);
if (parse.tkn.class >= MIN_TOKEN_CLASS && parse.tkn.class <= MAX_TOKEN_CLASS) { interpret_tag(parse.tkn.tag); } |
在第一個情況中(圖2),tag域不是像期望的那樣線性拉長,而被填充了一些垃圾。而在第二個情況中(圖3),無論是class還是tag域,都不再有意義,兩個char值因為被打包成一個int,所以也都不再正確。再次強調,首先不要假設標準數據類型大小一樣,其次還要了解它們是怎樣被映射成其他數據類型的,這才是書寫可移植代碼的最好方法。 機器尋址特性 幾乎所有的處理器都在字邊界上以字為單位進行尋址,而且通常都為此作了一些優化。另有一些的處理器允許其他類型的尋址,如以字節為單位尋址、或在半個字邊界上以半字為單位尋址,甚至還有一些處理器有輔助硬件允許在奇數邊界上同時以字和半字進行尋址。 尋址機制在不同計算機上會有所變化,最快的尋址模式是在字邊界上以字為單位進行尋址。其他方式的尋址需要輔助硬件,通常都會對內存訪問增加了一些時鐘周期。而這些過多的模式和特殊硬件的支持,是與RISC處理器的設計初衷背道而馳的,就拿克雷計算機來說,就只支持在字邊界上以字為單位進行尋址。 在那些不提供多種數據類型尋址方式的計算機上,編譯器可以提供一些模擬。例如:編譯器可以生成一些指令,當讀取一個字時,通過移位和屏蔽,來找到所想要的位置,以此來模擬在字中的半字尋址,但這會需要額外的時鐘周期,并且代碼體積會更大。 從這點上來說,位域的效率是非常低的,在以位域來取出一個字時,它們產生的代碼體積最大。當你存取同一個字中的其他位域時,又需要對包含這個位域字的內存,重新進行一遍尋址,這就是典型的以空間換時間。 當在設計數據結構時,我們總是想用可以保存數據的最小數據類型,來達到節省空間的目的。我們有時小氣得經常使用char和short,來存取位特域,這就像是為了節省一角錢,而花了一元錢。儲存空間上的高效,會付出在程序速度和體積上隱藏的代價。 試想你只為一個緊湊結構分配了一小點的空間,但卻產生了大量的代碼來存取結構中的域,而且這段代碼還是經常執行的,那么,會因為非字尋址,而導致代碼運行緩慢,而且充斥了大量用于提取域的代碼,程序體積也因此增大。這些額外代碼所占的空間,會讓你前面為節省空間所做的努力付之東流。 在高級數據結構中,特定的比特定位已不是必須的了,應在所有的域中都使用字(word),而不要操心它們所占用的空間。特別是在程序某些依賴于機器的部分,應該為字準備一個typedef,如下: /*在這臺計算機上,int是一個字長*/ typedef word int; |
在高級結構中,對所有域都使用字有如下好處: a.. 對其他計算機架構的可移植性 b.. 編譯器可能生成最快的代碼 c.. 處理器可能最快地訪問到所需內存 d.. 絕對沒有結構對齊的意外發生 必須也承認,在某些時候,是不能做到全部使用字的。例如,有一個很大的結構,但不會被經常存取,如果使用了數千個字的話,體積將會增大25%,但使用字通常會節省空間、提高執行速度,而且更具移植性。 以下是我們的結論: 書寫跨平臺移植的代碼,其實是件簡單的事情。最基本的規則是,盡可能地隱藏機器字長的細節,用非常精確的數據元素位大小來映射物理數據結構。或者像前面所建議的,為高級編程使用高級數據結構,而低級編程使用低級數據結構,當闡明高級數據結構時,對標準C的標量類型,不要作任何假設
使用宏定義
在C語言中,宏是產生內嵌代碼的唯一方法。對于嵌入式系統而言,為了能達到性能要求,宏是一種很好的代替函數的方法。 寫一個"標準"宏MIN ,這個宏輸入兩個參數并返回較小的一個: 錯誤做法: #define MIN(A,B) ( A <= B ? A : B ) |
正確做法: #define MIN(A,B) ((A)<= (B) ? (A) : (B) ) |
對于宏,我們需要知道三點: (1)宏定義"像"函數; (2)宏定義不是函數,因而需要括上所有"參數"; (3)宏定義可能產生副作用。 下面的代碼: 將被替換為: ( (*p++) <= (b) ?(*p++):(b) ) |
發生的事情無法預料。 因而不要給宏定義傳入有副作用的"參數"。 使用寄存器變量 當對一個變量頻繁被讀寫時,需要反復訪問內存,從而花費大量的存取時間。為此,C語言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使用時,不需要訪問內存,而直接從寄存器中讀寫,從而提高效率。寄存器變量的說明符是register。對于循環次數較多的循環控制變量及循環體內反復使用的變量均可定義為寄存器變量,而循環計數是應用寄存器變量的最好候選者。 (1) 只有局部自動變量和形參才可以定義為寄存器變量。因為寄存器變量屬于動態存儲方式,凡需要采用靜態存儲方式的量都不能定義為寄存器變量,包括:模塊間全局變量、模塊內全局變量、局部static變量; (2) register是一個"建議"型關鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能因為條件不滿足并未成為寄存器變量,而是被放在了存儲器中,但編譯器中并不報錯(在C++語言中有另一個"建議"型關鍵字:inline)。 下面是一個采用寄存器變量的例子: /* 求1+2+3+….+n的值 */ WORD Addition(BYTE n) { register i,s=0; for(i=1;i<=n;i++) { s=s+i; } return s; } |
本程序循環n次,i和s都被頻繁使用,因此可定義為寄存器變量。 內嵌匯編 程序中對時間要求苛刻的部分可以用內嵌匯編來重寫,以帶來速度上的顯著提高。但是,開發和測試匯編代碼是一件辛苦的工作,它將花費更長的時間,因而要慎重選擇要用匯編的部分。 在程序中,存在一個80-20原則,即20%的程序消耗了80%的運行時間,因而我們要改進效率,最主要是考慮改進那20%的代碼。 嵌入式C程序中主要使用在線匯編,即在C程序中直接插入_asm{ }內嵌匯編語句: /* 把兩個輸入參數的值相加,結果存放到另外一個全局變量中 */ int result; void Add(long a, long *b) { _asm { MOV AX, a MOV BX, b ADD AX, [BX] MOV result, AX } } | 利用硬件特性 首先要明白CPU對各種存儲器的訪問速度,基本上是: CPU內部RAM > 外部同步RAM > 外部異步RAM > FLASH/ROM 對于程序代碼,已經被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取代碼執行,但通常這不是一個好辦法,我們最好在系統啟動后將FLASH或ROM中的目標代碼拷貝入RAM中后再執行以提高取指令速度; 對于UART等設備,其內部有一定容量的接收BUFFER,我們應盡量在BUFFER被占滿后再向CPU提出中斷。例如計算機終端在向目標機通過RS-232傳遞數據時,不宜設置UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間; 如果對某設備能采取DMA方式讀取,就采用DMA讀取,DMA讀取方式在讀取目標中包含的存儲信息較大時效率較高,其數據傳輸的基本單位是塊,而所傳輸的數據是從設備直接送入內存的(或者相反)。DMA方式較之中斷驅動方式,減少了CPU 對外設的干預,進一步提高了CPU與外設的并行操作程度。 活用位操作 使用C語言的位操作可以減少除法和取模的運算。在計算機程序中數據的位是可以操作的最小數據單位,理論上可以用"位運算"來完成所有的運算和操作,因而,靈活的位操作可以有效地提高程序運行的效率。舉例如下: /* 方法1 */ int i,j; i = 879 / 16; j = 562 % 32; /* 方法2 */ int i,j; i = 879 >> 4; j = 562 - (562 >> 5 << 5); |
對于以2的指數次方為"*"、"/"或"%"因子的數學運算,轉化為移位運算"<< >>"通常可以提高算法效率。因為乘除運算指令周期通常比移位運算大。 C語言位運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統的編程特點有很大關系。我們通常要對硬件寄存器進行位設置,譬如,我們通過將AM186ER型80186處理器的中斷屏蔽控制寄存器的第低6位設置為0(開中斷2),最通用的做法是: #define INT_I2_MASK 0x0040 wTemp = inword(INT_MASK); outword(INT_MASK, wTemp &~INT_I2_MASK); |
而將該位設置為1的做法是: #define INT_I2_MASK 0x0040 wTemp = inword(INT_MASK); outword(INT_MASK, wTemp | INT_I2_MASK); |
判斷該位是否為1的做法是: #define INT_I2_MASK 0x0040 wTemp = inword(INT_MASK); if(wTemp & INT_I2_MASK) { … /* 該位為1 */ } |
上述方法在嵌入式系統的編程中是非常常見的,我們需要牢固掌握。 總結 在性能優化方面永遠注意80-20準備,不要優化程序中開銷不大的那80%,這是勞而無功的。 宏定義是C語言中實現類似函數功能而又不具函數調用和返回開銷的較好方法,但宏在本質上不是函數,因而要防止宏展開后出現不可預料的結果,對宏的定義和使用要慎而處之。很遺憾,標準C至今沒有包括C++中inline函數的功能,inline函數兼具無調用開銷和安全的優點。 使用寄存器變量、內嵌匯編和活用位操作也是提高程序效率的有效方法。 除了編程上的技巧外,為提高系統的運行效率,我們通常也需要最大可能地利用各種硬件設備自身的特點來減小其運轉開銷,例如減小中斷次數、利用DMA傳輸方式等。
Posted on 2006-06-24 16:55 Gin 閱讀(125) 評論(0)? 編輯 收藏 摘自網上的,呵,這幾個是自己比較少用到的
F2 ??? 當你選中一個文件的話,這意味著“重命名” ALT+ ENTER或 ALT+雙擊????? 查看項目的屬性 F10或ALT?????????????????????????????????? 激活當前程序的菜單欄 CTRL+ ESC??????????????????????????????? 顯示“開始”菜單 ALT+空格鍵?????????????????????????????? 顯示當前窗口的系統菜單 CTRL+F5 ????? 強行刷新 ALT+RIGHT ARROW 顯示前一頁(前進鍵) ALT+LEFT ARROW 顯示后一頁(后退鍵) ALT+ESC 切換當前程序 CTRL+N 新建一個新的文件 CTRL+O 打開“打開文件”對話框 CTRL+P 打開“打印”對話框
SHIFT+F10??????????????????????????? 顯示某個鏈接的快捷菜單 CTRL+W?????????????????????????????? 關閉當前窗口 ALT+D????????????????????????????????? 選擇地址欄中的文字 CTRL+ENTER???????????????????? 在地址欄中將"www."添加到鍵入的文本的前面,將".com"添加到文本的后面 CTRL+D??????????????????????????????? 將當前Web 頁添加到收藏夾中 CTRL+B?????????????????????????????? 打開"整理收藏夾"對話框 CTRL+I?????????????????????????????? 在瀏覽欄中打開收藏夾 ALT+ENTER 將 Windows 下運行的命令行窗口在窗口和全屏幕狀態間切換;查看選定的文件的屬性;選定任務欄時打開"任務欄和開始菜單"屬性 Alt+空格→X?????????????????????? 最大化當前窗口 Alt+空格→N?????????????????????? 最小化當前窗口
在用C++寫要導出類的庫時,我們經常只想暴露接口,而隱藏類的實現細節。也就是說我們提供的頭文件里只提供要暴露的公共成員函數的聲明,類的其他所有信息都不會在這個頭文件里面顯示出來。這個時候就要用到接口與實現分離的技術。 下面用一個最簡單的例子來說明。 類ClxExp是我們要導出的類,其中有一個私有成員變量是ClxTest類的對象,各個文件內容如下: lxTest.h文件內容: class ClxTest { public: ClxTest(); virtual ~ClxTest(); void DoSomething(); }; |
lxTest.cpp文件內容: #include "lxTest.h"
#include <iostream> using namespace std;
ClxTest::ClxTest() {}
ClxTest::~ClxTest() {}
void ClxTest::DoSomething() { cout << "Do something in class ClxTest!" << endl; }
//////////////////////////////////////////////////////////////////////////// |
lxExp.h文件內容: #include "lxTest.h"
class ClxExp { public: ClxExp(); virtual ~ClxExp(); void DoSomething(); private: ClxTest m_lxTest; void lxTest(); }; |
lxExp.cpp文件內容: #include "lxExp.h"
ClxExp::ClxExp() {}
ClxExp::~ClxExp() {}
// 其實該方法在這里并沒有必要,我這樣只是為了說明調用關系 void ClxExp::lxTest() { m_lxTest.DoSomething(); }
void ClxExp::DoSomething() { lxTest(); } |
為了讓用戶能使用我們的類ClxExp,我們必須提供lxExp.h文件,這樣類ClxExp的私有成員也暴露給用戶了。而且,僅僅提供lxExp.h文件是不夠的,因為lxExp.h文件include了lxTest.h文件,在這種情況下,我們還要提供lxTest.h文件。那樣ClxExp類的實現細節就全暴露給用戶了。另外,當我們對類ClxTest做了修改(如添加或刪除一些成員變量或方法)時,我們還要給用戶更新lxTest.h文件,而這個文件是跟接口無關的。如果類ClxExp里面有很多像m_lxTest那樣的對象的話,我們就要給用戶提供N個像lxTest.h那樣的頭文件,而且其中任何一個類有改動,我們都要給用戶更新頭文件。還有一點就是用戶在這種情況下必須進行重新編譯! 上面是非常小的一個例子,重新編譯的時間可以忽略不計。但是,如果類ClxExp被用戶大量使用的話,那么在一個大項目中,重新編譯的時候我們就有時間可以去喝杯咖啡什么的了。當然上面的種種情況不是我們想看到的!你也可以想像一下用戶在自己程序不用改動的情況下要不停的更新頭文件和編譯時,他們心里會罵些什么。其實對用戶來說,他們只關心類ClxExp的接口DoSomething()方法。那我們怎么才能只暴露類ClxExp的DoSomething()方法而不又產生上面所說的那些問題呢?答案就是--接口與實現的分離。我可以讓類ClxExp定義接口,而把實現放在另外一個類里面。下面是具體的方法: 首先,添加一個實現類ClxImplement來實現ClxExp的所有功能。注意:類ClxImplement有著跟類ClxExp一樣的公有成員函數,因為他們的接口要完全一致。 lxImplement.h文件內容: #include "lxTest.h"
class ClxImplement { public: ClxImplement(); virtual ~ClxImplement();
void DoSomething(); private: ClxTest m_lxTest; void lxTest(); }; |
lxImplement.cpp文件內容: #include "lxImplement.h"
ClxImplement::ClxImplement() {}
ClxImplement::~ClxImplement() {}
void ClxImplement::lxTest() { m_lxTest.DoSomething(); }
void ClxImplement::DoSomething() { lxTest(); } |
然后,修改類ClxExp。 修改后的lxExp.h文件內容: // 前置聲明 class ClxImplement;
class ClxExp { public: ClxExp(); virtual ~ClxExp(); void DoSomething(); private: // 聲明一個類ClxImplement的指針,不需要知道類ClxImplement的定義 ClxImplement *m_pImpl; }; |
修改后的lxExp.cpp文件內容: // 在這里包含類ClxImplement的定義頭文件 #include "lxImplement.h"
ClxExp::ClxExp() { m_pImpl = new ClxImplement; }
ClxExp::~ClxExp() { delete m_pImpl; }
void ClxExp::DoSomething() { m_pImpl->DoSomething(); } |
通過上面的方法就實現了類ClxExp的接口與實現的分離。請注意兩個文件中的注釋。類ClxExp里面聲明的只是接口而已,而真正的實現細節被隱藏到了類ClxImplement里面。為了能在類ClxExp中使用類ClxImplement而不include頭文件lxImplement.h,就必須有前置聲明class ClxImplement,而且只能使用指向類ClxImplement對象的指針,否則就不能通過編譯。 在發布庫文件的時候,我們只需給用戶提供一個頭文件lxExp.h就行了,不會暴露類ClxExp的任何實現細節。而且我們對類ClxTest的任何改動,都不需要再給用戶更新頭文件(當然,庫文件是要更新的,但是這種情況下用戶也不用重新編譯!)。這樣做還有一個好處就是,可以在分析階段由系統分析員或者高級程序員來先把類的接口定義好,甚至可以把接口代碼寫好(例如上面修改后的lxExp.h文件和lxExp.cpp文件),而把類的具體實現交給其他程序員開發。
前些日子開始看《C++ Primer》,順便做一些筆記,既有書上的,也有自己理解的。 因為剛學C++不久,筆下難免有謬誤之處,行文更是凌亂; 所幸不是用來顯配的東西,發在linuxsir只是為了方便自己閱讀記憶,以防只顧上網忘了正事。 書看了不到一半,所以大約才寫了一半,慢慢補充。 ========================================= ========================================== 轉載務必注明原作者 neplusultra 2005.2.3 ========================================== const要注意的問題 1、下面是一個幾乎所有人剛開始都會搞錯的問題: 已知:typedef char *cstring; 在以下聲明中,cstr的類型是什么? extern const cstring cstr; 錯誤答案:const char *cstr; 正確答案:char *const cstr; 錯誤在于將typedef當作宏擴展。const 修飾cstr的類型。cstr是一個指針,因此,這個定義聲明了cstr是一個指向字符的const指針。 2、指針是const還是data為const? 辨別方法很簡單,如下: 代碼: char *p="hello"; //non-const pointer, non-const data;
const char *p="hello"; // non-const pointer, const data;
char * const p="hello"; // const pointer , non-const data;
const char * const p="hello"; // const pointer, const data;
要注意的是,"hello"的類型是const char * ,按C++standard規則,char *p="hello" 是非法的(右式的const char* 不能轉換為左式的char *),違反了常量性。但是這種行為在C中實在太頻繁,因此C++standard對于這種初始化動作給予豁免。盡管如此,還是盡量避免這種用法。 3、const初始化的一些問題 const 對象必須被初始化: 代碼: const int *pi=new int; // 錯誤,沒有初始化
const int *pi=new int(100); //正確
const int *pci=new const int[100]; //編譯錯誤,無法初始化用new表達式創建的內置類型數組元素。
什么時候需要copy constructor,copy assignment operator,destructor 注意,若class需要三者之一,那么它往往需要三者。 當class的copy constructor內分配有一塊指向hcap的內存,需要由destructor釋放,那么它也往往需要三者。 為什么需要protected 訪問級別 有人認為,protected訪問級別允許派生類直接訪問基類成員,這破壞了封裝的概念,因此所有基類的實現細節都應該是private的;另外一些人認為,如果派生類不能直接訪問基類的成員,那么派生類的實現將無法有足夠的效率供用戶使用,如果沒有protected,類的設計者將被迫把基類成員設置為public。 事實上,protected正是在高純度的封裝與效率之間做出的一個良好折衷方案。 為什么需要virtual member function又不能濫用virtual 若基類設計者把本應設計成virtual的成員函數設計成非virtual,則繼承類將無法實現改寫(overridden),給繼承類的實現帶來不便; 另一方面,一旦成員函數被設計成virtual,則該類的對象將額外增加虛擬指針(vptr)和虛擬表格(vtbl),所以倘若出于方便繼承類overridden的目的而使所有成員函數都為virtual,可能會影響效率,因為每個virtual成員函數都需付出動態分派的成本。而且virtual成員函數不能內聯(inline),我們知道,內聯發生在編譯時刻,而虛擬函數在運行時刻才處理。對于那些小巧而被頻繁調用、與類型無關的函數,顯然不應該被設置成virtual。 關于引用的一些注意點 1、把函數參數聲明為數組的引用:當函數參數是一個數組類型的引用時,數組長度成為參數和實參類型的一部分,編譯器檢查數組實參的長度和與在函數參數類型中指定的長度是否匹配。 代碼: //參數為10個int數組
void showarr(int (&arr)[10]);
void func()
{
int i,j[2],k[10];
showarr(i); //錯誤!實參必須是10個int的數組
showarr(j); //錯誤!實參必須是10個int的數組
showarr(k); //正確!
}
//更靈活的實現,借助函數模板。下面是一個顯示數組內容的函數。
template <typename Type , int size>
void printarr(const Type (& r_array)[size])
{
for(int i=0;i<size;i++) std::cout<< r_array[i] <<' ';
std::cout << std::endl;
}
void caller()
{
int ar[5]={1,2,5,3,4}; //數組可以任意大小。
printarr(ar); //正確!自動正確調用printarr()
}
2、 3、 goto語句的一些要注意的地方 1、label語句只能用作goto的目標,且label語句只能用冒號結束,且label語句后面不能緊接右花括號'}',如 辦法是在冒號后面加一個空語句(一個';'即可),如 2、goto語句不能向前跳過如下聲明語句: 代碼: goto label6;
int x=1; //錯誤,不能跳過該聲明!
cout<<x<<endl; //使用x
label6:
//其他語句
但是,把int x=1; 改為int x; 則正確了。另外一種方法是: 代碼: goto label6;
{
int x=1; //正確,使用了語句快
cout<<x<<endl;
}
label6:
//其他語句
3、goto語句可以向后(向程序開頭的方向)跳過聲明定義語句。 代碼: begin:
int i=22;
cout<< i <<endl;
goto begin; //非常蹩腳,但它是正確的
變量作用域 1、花括號可以用來指明局部作用域。 2、在for、if、switch、while語句的條件/循環條件中可以聲明變量,該變量僅在相應語句塊內有效。 3、extern為聲明但不定義一個對象提供了一種方法;它類似于函數聲明,指明該對象會在其他地方被定義:或者在此文本的其他地方,或者在程序的其他文本文件中。例如extern int i; 表示在其他地方存在聲明 int i; extern 聲明不會引起內存分配,他可以在同一個文件或同一個程序中出現多次。因此在全局作用域中,以下語句是正確的: 代碼: extern int c;
int c=1; //沒錯
extern int c; //沒錯
但是,extern聲明若指定了一個顯式初始值的全局對象,將被視為對該對象的定義,編譯器將為其分配存儲區;對該對象的后續定義將出錯。如下: 代碼: extern int i=1;
int i=2; //出錯!重復定義
auto_ptr若干注意點 1、auto_ptr的主要目的是支持普通指針類型相同的語法,并為auto_ptr所指對象的釋放提供自動管理,而且auto_ptr的安全性幾乎不會帶來額外的代價(因為其操作支持都是內聯的)。定義形式有三種: 代碼: auto_ptr<type_pointed_to>identifier(ptr_allocated_by_new);
auto_ptr<type_pointed_to>identifier(auto_ptr_of_same_type);
auto_ptr<type_pointed_to>identifier;
2、所有權概念。auto_ptr_p1=auto_ptr_p2的后果是,auto_ptr_p2喪失了其原指向對象的所有權,并且auto_ptr_p2.get()==0。不要讓兩個auto_ptr對象擁有空閑存儲區內同一對象的所有權。注意以下兩種種初始化方式的區別: 代碼: auto_ptr<string>auto_ptr_str1(auto_ptr_str2.get()); //注意!用str2指針初始化str1, 兩者同時擁有所有權,后果未定義。
auto_ptr<string>auto_ptr_str1(auto_ptr_str2.release());//OK!str2釋放了所有權。
3、不能用一個指向“內存不是通過應用new表達式分配的”指針來初始化或者賦值auto_ptr。如果這樣做了,delete表達式會被應用在不是動態分配的指針上,這將導致未定義的程序行為。 C風格字符串結尾空字符問題代碼: char *str="hello world!"; //str末尾自動加上一個結尾空字符,但strlen不計該空字符。
char *str2=new char[strlen(str)+1] // +1用來存放結尾空字符。
定位new表達式 頭文件:<new> 形式:new (place_address) type-specifier 該語句可以允許程序員將對象創建在已經分配好的內存中,允許程序員預分配大量的內存供以后通過這種形式的new表達式創建對象。其中place_address必須是一個指針。例如: 代碼: char *buf=new char[sizeof(myclass-type)*16];
myclass-type *pb=new (buf) myclass-type; //使用預分配空間來創建對象
// ...
delete [] buf; // 無須 delete pb。
名字空間namespace
1、namespace的定義可以是不連續的(即namespace的定義是可以積累的),即,同一個namespace可以在不同的文件中定義,分散在不同文件中的同一個namespace中的內容彼此可見。這對生成一個庫很有幫助,可以使我們更容易將庫的源代碼組織成接口和實現部分。如:在頭文件(.h文件)的名字空間部分定義庫接口;在實現文件(如.c或.cpp文件)的名字空間部分定義庫實現。名字空間定義可積累的特性是“向用戶隱藏實現細節”必需的,它允許把不同的實現文件(如.c或.cpp文件)編譯鏈接到一個程序中,而不會有編譯錯誤和鏈接錯誤。 2、全局名字空間成員,可以用“::member_name”的方式引用。當全局名字空間的成員被嵌套的局部域中聲明的名字隱藏時,就可以采用這種方法引用全局名字空間成員。 3、名字空間成員可以被定義在名字空間之外。但是,只有包圍該成員聲明的名字空間(也就是該成員聲明所在的名字空間及其 外圍名字空間)才可以包含它的定義。 尤其要注意的是#include語句的次序。假定名字空間成員mynamespace::member_i的聲明在文件dec.h中,且#include "dec.h"語句置于 全局名字空間,那么在include語句之后定義的其他名字空間內,mynamespace::member_i的聲明均可見。即,mynamespace::member_i可以在#include "dec.h"之后的任何地方任何名字空間內定義。 4、未命名的名字空間。我們可以用未命名的名字空間聲明一個局部于某一文件的實體。未命名的名字空間可以namespace開頭,其后不需名字,而用一對花括號包含名字空間聲明塊。如: 代碼: // 其他代碼略
namespace
{
void mesg()
{
cout<<"**********\n";
}
}
int main()
{
mesg(); //正確
//...
return 0;
}
由于未命名名字空間的成員是程序實體,所以mesg()可以在程序整個執行期間被調用。但是,未命名名字空間成員只在特定的文件中可見,在構成程序的其他文件中是不可以見的。未命名名字空間的成員與被聲明為static的全局實體具有類似的特性。在C中,被聲明為static的全局實體在聲明它的文件之外是不可見的。 using關鍵字 1、using聲明與using指示符:前者是聲明某名字空間內的一個成員,后者是使用整個名字空間。例如: 代碼: using cpp_primer::matrix; // ok,using聲明
using namespace cpp_primer; //ok,using指示符
2、 該using指示符語句可以加在程序文件的幾乎任何地方,包括文件開頭(#include語句之前)、函數內部。不過用using指定的名字空間作用域(生命周期)受using語句所在位置的生命周期約束。如,函數內部使用“using namespace myspacename;”則 myspacename僅在該函數內部可見。 3、可以用using語句指定多個名字空間,使得多個名字空間同時可見。但這增加了名字污染的可能性,而且只有在 使用各名字空間相同成員時由多個using指示符引起的二義性錯誤才能被檢測到,這將給程序的檢測、擴展、移植帶來很大的隱患。因此,因該盡量使用using聲明而不是濫用using指示符。 重載函數 1、如果兩個函數的參數表中參數的個數或者類型不同,則認為這兩個函數是重載的。 如果兩個函數的返回類型和參數表精確匹配,則第二個聲明被視為第一個的重復聲明,與參數名無關。如 void print(string& str)與void print(string&)是一樣的。 如果兩個函數的參數表相同,但是返回類型不同,則第二個聲明被視為第一個的錯誤重復聲明,會標記為編譯錯誤。 如果在兩個函數的參數表中,只有缺省實參不同,則第二個聲明被視為第一個的重復聲明。如int max(int *ia,int sz)與int max(int *, int=10)。 參數名類型如果是由typedef提供的,并不算作新類型,而應該當作typedef的原類型。 當參數類型是const或者volatile時,分兩種情況:對于實參按值傳遞時,const、volatile修飾符可以忽略;對于把const、volatile應用在指針或者引用參數指向的類型時,const、volatile修飾符對于重載函數的聲明是有作用的。例如: 代碼: //OK,以下兩個聲明其實一樣
void func(int i);
void func(const int i);
//Error,無法通過編譯,因為func函數被定義了兩次。
void func(int i){}
void func(const int i){}
//OK,聲明了不同的函數
void func2(int *);
void func2(const int *);
//OK,聲明了不同的函數
void func3(int&);
void func3(const int&);
2、鏈接指示符extern "C"只能指定重載函數集中的一個函數。原因與內部名編碼有關,在大多數編譯器內部,每個函數明及其相關參數表都被作為一個惟一的內部名編碼,一般的做法是把參數的個數和類型都進行編碼,然后將其附在函數名后面。但是這種編碼不使用于用鏈接指示符extern "C"聲明的函數,這就是為什么在重載函數集合中只有一個函數可以被聲明為extern "C"的原因,具有不同的參數表的兩個extern "C"的函數會被鏈接編輯器視為同一函數。例如,包含以下兩個聲明的程序是非法的。 代碼: //error:一個重載函數集中有兩個extern "C"函數
extern "C" void print(const char*);
extern "C" void print(int);
函數模板 1、定義函數模板: 代碼: template <typename/class identifier, ...>
[inline/extern]
ReturnType FunctionName(FuncParameters...)
{
//definition of a funciton template...
}
?
條款28: 劃分全局名字空間全局空間最大的問題在于它本身僅有一個。在大的軟件項目中,經常會有不少人把他們定義的名字都放在這個單一的空間中,從而不可避免地導致名字沖突。例如,假設library1.h定義了一些常量,其中包括: const double lib_version = 1.204; 類似的,library2.h也定義了: const int lib_version = 3; 很顯然,如果某個程序想同時包含library1.h和library2.h就會有問題。對于這類問題,你除了嘴里罵幾句,或給作者發報復性郵件,或自己編輯頭文件來消除名字沖突外,也沒其它什么辦法。 但是,作為程序員,你可以盡力使自己寫的程序庫不給別人帶來這些問題。例如,可以預先想一些不大可能造成沖突的某種前綴,加在每個全局符號前。當然得承認,這樣組合起來的標識符看起來不是那么令人舒服。 另一個比較好的方法是使用c++ namespace。namespace本質上和使用前綴的方法一樣,只不過避免了別人總是看到前綴而已。所以,不要這么做: const double sdmbook_version = 2.0;????? // 在這個程序庫中, ???????????????????????????????????????? // 每個符號以"sdm"開頭 class sdmhandle { ... };???????????????? sdmhandle& sdmgethandle();???????????? // 為什么函數要這樣聲明? ?????????????????????????????????????? // 參見條款47 而要這么做: namespace sdm { ? const double book_version = 2.0; ? class handle { ... }; ? handle& gethandle(); } 用戶于是可以通過三種方法來訪問這一名字空間里的符號:將名字空間中的所有符號全部引入到某一用戶空間;將部分符號引入到某一用戶空間;或通過修飾符顯式地一次性使用某個符號: void f1() { ? using namespace sdm;?????????? // 使得sdm中的所有符號不用加 ???????????????????????????????? // 修飾符就可以使用 ? cout << book_version;????????? // 解釋為sdm::book_version ? ... ? handle h = gethandle();??????? // handle解釋為sdm::handle, ???????????????????????????????? // gethandle解釋為sdm::gethandle ? ...??????????????????????????? } void f2() { ? using sdm::book_version;??????? // 使得僅book_version不用加 ???????????????????????????????? // 修飾符就可以使用 ? cout << book_version;?????????? // 解釋為 ????????????????????????????????? // sdm::book_version ? ... ? handle h = gethandle();???????? // 錯誤! handle和gethandle ????????????????????????????????? // 都沒有引入到本空間 ? ...???????????????????????????? } void f3() { ? cout << sdm::book_version;????? // 使得book_version ????????????????????????????????? // 在本語句有效 ? ...???????????????????????????? ? double d = book_version;??????? // 錯誤! book_version ????????????????????????????????? // 不在本空間 ? handle h = gethandle();???????? // 錯誤! handle和gethandle ????????????????????????????????? // 都沒有引入到本空間 ? ...??????????????????????????? } (有些名字空間沒有名字。這種沒命名的名字空間一般用于限制名字空間內部元素的可見性。詳見條款m31。) 名字空間帶來的最大的好處之一在于:潛在的二義不會造成錯誤(參見條款26)。所以,從多個不同的名字空間引入同一個符號名不會造成沖突(假如確實真的從不使用這個符號的話)。例如,除了名字空間sdm外,假如還要用到下面這個名字空間: namespace acmewindowsystem { ? ... ? typedef int handle; ? ... } 只要不引用符號handle,使用sdm和acmewindowsystem時就不會有沖突。假如真的要引用,可以明確地指明是哪個名字空間的handle: void f() { ? using namespace sdm;???????????????? // 引入sdm里的所有符號 ? using namespace acmewindowsystem;??? // 引入acme里的所有符號 ? ...????????????????????????????????? // 自由地引用sdm ?????????????????????????????????????? // 和acme里除handle之外 ?????????????????????????????????????? // 的其它符號 ? handle h;??????????????????????????? // 錯誤! 哪個handle? ? sdm::handle h1;????????????????????? // 正確, 沒有二義 ? acmewindowsystem::handle h2;???????? // 也沒有二義 ? ... } 假如用常規的基于頭文件的方法來做,只是簡單地包含sdm.h和acme.h,這樣的話,由于handle有多個定義,編譯將不能通過。 名字空間的概念加入到c++標準的時間相對較晚,所以有些人會認為它不太重要,可有可無。但這種想法是錯誤的,因為c++標準庫(參見條款49)里幾乎所有的東西都存在于名字空間std之中。這可能令你不以為然,但它卻以一種直接的方式影響到你:這就是為什么c++提供了那些看起來很有趣的、沒有擴展名的頭文件,如<iostream>, <string>等。詳細介紹參見條款49。 由于名字空間的概念引入的時間相對較晚,有些編譯器可能不支持。就算是這樣,那也沒理由污染全局名字空間,因為可以用struct來近似實現namespace。可以這樣做:先創建一個結構用以保存全局符號名,然后將這些全局符號名作為靜態成員放入結構中: // 用于模擬名字空間的一個結構的定義 struct sdm { ? static const double book_version; ? class handle { ... }; ? static handle& gethandle(); }; const double sdm::book_version = 2.0;????? // 靜態成員的定義 現在,如果有人想訪問這些全局符號名,只用簡單地在它們前面加上結構名作為前綴: void f() { ? cout << sdm::book_version; ? ... ? sdm::handle h = sdm::gethandle(); ? ... } 但是,如果全局范圍內實際上沒有名字沖突,用戶就會覺得加修飾符麻煩而多余。幸運的是,還是有辦法來讓用戶選擇使用它們或忽略它們。 對于類型名,可以用類型定義(typedef)來顯式地去掉空間引用。例如,假設結構s(模擬的名字空間)內有個類型名t,可以這樣用typedef來使得t成為s::t的同義詞: typedef sdm::handle handle; 對于結構中的每個(靜態)對象x,可以提供一個(全局)引用x,并初始化為s::x: const double& book_version = sdm::book_version; 老實說,如果讀了條款47,你就會不喜歡定義一個象book_version這樣的非局部靜態對象。(你就會用條款47中所介紹的函數來取代這樣的對象) 處理函數的方法和處理對象一樣,但要注意,即使定義函數的引用是合法的,但代碼的維護者會更喜歡你使用函數指針: sdm::handle& (* const gethandle)() =????? // gethandle是指向sdm::gethandle ? sdm::gethandle;???????????????????????? // 的const 指針 (見條款21) 注意gethandle是一個常指針。因為你當然不想讓你的用戶將它指向別的什么東西,而不是sdm::gethandle,對不對? (如果真想知道怎么定義一個函數的引用,看看下面: sdm::handle& (&gethandle)() =????? // gethandle是指向 ? sdm::gethandle;????????????????? // sdm::gethandle的引用 我個人認為這樣的做法也很好,但你可能以前從沒見到過。除了初始化的方式外,函數的引用和函數的常指針在行為上完全相同,只是函數指針更易于理解。) 有了上面的類型定義和引用,那些不會遭遇全局名字沖突的用戶就會使用沒有修飾符的類型和對象名;相反,那些有全局名字沖突的用戶就會忽略類型和引用的定義,代之以帶修飾符的符號名。還要注意的是,不是所有用戶都想使用這種簡寫名,所以要把類型定義和引用放在一個單獨的頭文件中,不要把它和(模擬namespace的)結構的定義混在一起。 struct是namespace的很好的近似,但實際上還是相差很遠。它在很多方面很欠缺,其中很明顯的一點是對運算符的處理。如果運算符被定義為結構的靜態成員,它就只能通過函數調用來使用,而不能象常規的運算符所設計的那樣,可以通過自然的中綴語法來使用: // 定義一個模擬名字空間的結構,結構內部包含widgets的類型 // 和函數。widgets對象支持operator+進行加法運算 struct widgets { ? class widget { ... }; ? // 參見條款21:為什么返回const ? static const widget operator+(const widget& lhs, ??????????????????????????????? const widget& rhs);
? ... }; // 為上面所述的widge和operator+ // 建立全局(無修飾符的)名稱 typedef widgets::widget widget; const widget (* const operator+)(const widget&,??????? // 錯誤! ???????????????????????????????? const widget&);?????? // operator+不能是指針名 ?
widget w1, w2, sum; sum = w1 + w2;?????????????????????????? // 錯誤! 本空間沒有聲明 ???????????????????????????????????????? // 參數為widgets 的operator+ sum = widgets::operator+(w1, w2);??????? // 合法, 但不是 ???????????????????????????????????????? // "自然"的語法 正因為這些限制,所以一旦編譯器支持,就要盡早使用真正的名字空間。
編寫高效簡潔的C語言代碼,是許多軟件工程師追求的目標。本文就工作中的一些體會和經驗做相關的闡述,不對的地方請各位指教。
第1招:以空間換時間
計算機程序中最大的矛盾是空間和時間的矛盾,那么,從這個角度出發逆向思維來考慮程序的效率問題,我們就有了解決問題的第1招——以空間換時間。 例如:字符串的賦值。 方法A,通常的辦法: #define LEN 32 char string1 [LEN]; memset (string1,0,LEN); strcpy (string1,“This is a example!!”); 方法B: const char string2[LEN] =“This is a example!”; char * cp; cp = string2 ; (使用的時候可以直接用指針來操作。)
從上面的例子可以看出,A和B的效率是不能比的。在同樣的存儲空間下,B直接使用指針就可以操作了,而A需要調用兩個字符函數才能完成。B的缺點在于靈活性沒有A好。在需要頻繁更改一個字符串內容的時候,A具有更好的靈活性;如果采用方法B,則需要預存許多字符串,雖然占用了大量的內存,但是獲得了程序執行的高效率。
如果系統的實時性要求很高,內存還有一些,那我推薦你使用該招數。
該招數的變招——使用宏函數而不是函數。舉例如下: 方法C: #define bwMCDR2_ADDRESS 4 #define bsMCDR2_ADDRESS 17 int BIT_MASK(int __bf) { return ((1U << (bw ## __bf)) - 1) << (bs ## __bf); } void SET_BITS(int __dst, int __bf, int __val) { __dst = ((__dst) & ~(BIT_MASK(__bf))) | \ (((__val) << (bs ## __bf)) & (BIT_MASK(__bf)))) }
SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber); 方法D: #define bwMCDR2_ADDRESS 4 #define bsMCDR2_ADDRESS 17 #define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS) #define BIT_MASK(__bf) (((1U << (bw ## __bf)) - 1) << (bs ## __bf)) #define SET_BITS(__dst, __bf, __val) \ ((__dst) = ((__dst) & ~(BIT_MASK(__bf))) | \ (((__val) << (bs ## __bf)) & (BIT_MASK(__bf))))
SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber);
函數和宏函數的區別就在于,宏函數占用了大量的空間,而函數占用了時間。大家要知道的是,函數調用是要使用系統的棧來保存數據的,如果編譯器里有棧檢查選項,一般在函數的頭會嵌入一些匯編語句對當前棧進行檢查;同時,CPU也要在函數調用時保存和恢復當前的現場,進行壓棧和彈棧操作,所以,函數調用需要一些CPU時間。而宏函數不存在這個問題。宏函數僅僅作為預先寫好的代碼嵌入到當前程序,不會產生函數調用,所以僅僅是占用了空間,在頻繁調用同一個宏函數的時候,該現象尤其突出。
D方法是我看到的最好的置位操作函數,是ARM公司源碼的一部分,在短短的三行內實現了很多功能,幾乎涵蓋了所有的位操作功能。C方法是其變體,其中滋味還需大家仔細體會。
第2招:數學方法解決問題
現在我們演繹高效C語言編寫的第二招——采用數學方法來解決問題。
數學是計算機之母,沒有數學的依據和基礎,就沒有計算機的發展,所以在編寫程序的時候,采用一些數學方法會對程序的執行效率有數量級的提高。 舉例如下,求 1~100的和。 方法E int I , j; for (I = 1 ;I<=100; I ++){ j += I; } 方法F int I; I = (100 * (1+100)) / 2
這個例子是我印象最深的一個數學用例,是我的計算機啟蒙老師考我的。當時我只有小學三年級,可惜我當時不知道用公式 N×(N+1)/ 2 來解決這個問題。方法E循環了100次才解決問題,也就是說最少用了100個賦值,100個判斷,200個加法(I和j);而方法F僅僅用了1個加法,1次乘法,1次除法。效果自然不言而喻。所以,現在我在編程序的時候,更多的是動腦筋找規律,最大限度地發揮數學的威力來提高程序運行的效率。
第3招:使用位操作
實現高效的C語言編寫的第三招——使用位操作,減少除法和取模的運算。
在計算機程序中,數據的位是可以操作的最小數據單位,理論上可以用“位運算”來完成所有的運算和操作。一般的位操作是用來控制硬件的,或者做數據變換使用,但是,靈活的位操作可以有效地提高程序運行的效率。舉例如下: 方法G int I,J; I = 257 /8; J = 456 % 32; 方法H int I,J; I = 257 >>3; J = 456 - (456 >> 4 << 4);
在字面上好像H比G麻煩了好多,但是,仔細查看產生的匯編代碼就會明白,方法G調用了基本的取模函數和除法函數,既有函數調用,還有很多匯編代碼和寄存器參與運算;而方法H則僅僅是幾句相關的匯編,代碼更簡潔,效率更高。當然,由于編譯器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 來看,效率的差距還是不小。相關匯編代碼就不在這里列舉了。 運用這招需要注意的是,因為CPU的不同而產生的問題。比如說,在PC上用這招編寫的程序,并在PC上調試通過,在移植到一個16位機平臺上的時候,可能會產生代碼隱患。所以只有在一定技術進階的基礎下才可以使用這招。
第4招:匯編嵌入
高效C語言編程的必殺技,第四招——嵌入匯編。
“在熟悉匯編語言的人眼里,C語言編寫的程序都是垃圾”。這種說法雖然偏激了一些,但是卻有它的道理。匯編語言是效率最高的計算機語言,但是,不可能靠著它來寫一個操作系統吧?所以,為了獲得程序的高效率,我們只好采用變通的方法 ——嵌入匯編,混合編程。
舉例如下,將數組一賦值給數組二,要求每一字節都相符。 char string1[1024],string2[1024]; 方法I int I; for (I =0 ;I<1024;I++) *(string2 + I) = *(string1 + I) 方法J #ifdef _PC_ int I; for (I =0 ;I<1024;I++) *(string2 + I) = *(string1 + I); #else #ifdef _ARM_ __asm { MOV R0,string1 MOV R1,string2 MOV R2,#0 loop: LDMIA R0!, [R3-R11] STMIA R1!, [R3-R11] ADD R2,R2,#8 CMP R2, #400 BNE loop } #endif
方法I是最常見的方法,使用了1024次循環;方法J則根據平臺不同做了區分,在ARM平臺下,用嵌入匯編僅用128次循環就完成了同樣的操作。這里有朋友會說,為什么不用標準的內存拷貝函數呢?這是因為在源數據里可能含有數據為0的字節,這樣的話,標準庫函數會提前結束而不會完成我們要求的操作。這個例程典型應用于LCD數據的拷貝過程。根據不同的CPU,熟練使用相應的嵌入匯編,可以大大提高程序執行的效率。
雖然是必殺技,但是如果輕易使用會付出慘重的代價。這是因為,使用了嵌入匯編,便限制了程序的可移植性,使程序在不同平臺移植的過程中,臥虎藏龍,險象環生!同時該招數也與現代軟件工程的思想相違背,只有在迫不得已的情況下才可以采用。切記,切記。
使用C語言進行高效率編程,我的體會僅此而已。在此以本文拋磚引玉,還請各位高手共同切磋。希望各位能給出更好的方法,大家一起提高我們的編程技巧。
一.系統環境 2 二.gSOAP的簡要使用例子 2 三.圖示說明 6 四.要注意的問題 6 五.參考文檔 7 六.備注 7
一.系統環境 linux操作系統kernel2.4.2,安裝gsoap2.6到目錄/usr/local/gsoap 二.gSOAP的簡要使用例子 下面是一個簡單的例子,實現一個加法運算的WebService,具體功能是cli端輸入num1和num2,server端返回一個num1和num2相加的結果sum。
1. 首先,我們需要做的是寫一個函數聲明文件,來定義接口函數ns__add,文件名字為add.h,內容如下:
//gsoap ns service name: add //gsoap ns service namespace: http://mail.263.net/add.wsdl //gsoap ns service location: http://mail.263.net //gsoap ns service executable: add.cgi //gsoap ns service encoding: encoded //gsoap ns schema namespace: urn:add
int ns__add( int num1, int num2, int* sum );
2. 然后我們需要創建文件Makefile,從而利用gsoapcpp2工具由add.h生成一些.xml文件、.c文件和.h文件,這些文件均為自動生成,Makefile的內容如下:
GSOAP_ROOT=/usr/local/gsoap WSNAME=add CC=g++ -g -DWITH_NONAMESPACES INCLUDE=-I $(GSOAP_ROOT) SERVER_OBJS=$(WSNAME)C.o $(WSNAME)Server.o stdsoap2.o CLIENT_OBJS=$(GSOAP_ROOT)/env/envC.o $(WSNAME)ClientLib.o stdsoap2.o ALL_OBJS=${WSNAME}server.o $(WSNAME)C.o $(WSNAME)Server.o ${WSNAME}test.o ${WSNAME}client.o $(WSNAME)ClientLib.o
#總的目標 all:server
${WSNAME}.wsdl:${WSNAME}.h $(GSOAP_ROOT)/soapcpp2 -p$(WSNAME) -i -n -c ${WSNAME}.h
stdsoap2.o:$(GSOAP_ROOT)/stdsoap2.c $(CC) -c $?
#編譯一樣生成規則的.o文件 $(ALL_OBJS):%.o:%.c $(CC) -c $? $(INCLUDE)
#編譯服務器端 server:Makefile ${WSNAME}.wsdl ${WSNAME}server.o $(SERVER_OBJS) $(CC) ${WSNAME}server.o $(SERVER_OBJS) -o ${WSNAME}server
#編譯客戶端 client:Makefile ${WSNAME}.wsdl ${WSNAME}client.c ${WSNAME}test.c $(ALL_OBJS) stdsoap2.o $(CC) ${WSNAME}test.o ${WSNAME}client.o $(CLIENT_OBJS) -o ${WSNAME}test
cl: rm -f *.o *.xml *.a *.wsdl *.nsmap $(WSNAME)H.h $(WSNAME)C.c $(WSNAME)Server.c $(WSNAME)Client.c $(WSNAME)Stub.* $(WSNAME)$(WSNAME)Proxy.* $(WSNAME)$(WSNAME)Object.* $(WSNAME)ServerLib.c $(WSNAME)ClientLib.c $(WSNAME)server ns.xsd $(WSNAME)test
3.我們先來做一個server端,創建文件addserver.c文件,內容如下:
#include "addH.h" #include "add.nsmap"
int main(int argc, char **argv) { int m, s; /* master and slave sockets */ struct soap add_soap; soap_init(&add_soap); soap_set_namespaces(&add_soap, add_namespaces); if (argc < 2) { printf("usage: %s <server_port> \n", argv[0]); exit(1); } else { m = soap_bind(&add_soap, NULL, atoi(argv[1]), 100); if (m < 0) { soap_print_fault(&add_soap, stderr); exit(-1); } fprintf(stderr, "Socket connection successful: master socket = %d\n", m); for ( ; ; ) { s = soap_accept(&add_soap); if (s < 0) { soap_print_fault(&add_soap, stderr); exit(-1); } fprintf(stderr, "Socket connection successful: slave socket = %d\n", s); add_serve(&add_soap);//該句說明該server的服務 soap_end(&add_soap); } } return 0; } //server端的實現函數與add.h中聲明的函數相同,但是多了一個當前的soap連接的參數 int ns__add(struct soap *add_soap, int num1, int num2, int *sum) { *sum = num1 + num2; return 0; }
4.讓我們的server跑起來吧: shell>make shell>./addserver 8888 如果終端打印出“Socket connection successful: master socket = 3”,那么你的server已經在前臺run起來了,應該是值得高興的。 打開IE,鍵入http://本機IP:8888,顯示XML,服務已經啟動,終端打印出“Socket connection successful: slave socket = 4”,表示服務接收到了一次soap的連接。
5.讓我們再來寫個客戶端(這個只是將soap的客戶端函數封裝一下,具體的調用參見下面的addtest.c),創建文件addclient.c,內容如下:
#include "addStub.h" #include "add.nsmap" /** * 傳入參數:server:server的地址 * num1,num2:需要相加的數 * 傳出參數:sum:num1和num2相加的結果 * 返回值:0為成功,其他為失敗 */ int add( const char* server, int num1, int num2, int *sum ) { struct soap add_soap; int result = 0; soap_init(&add_soap); soap_set_namespaces(&add_soap, add_namespaces);
//該函數是客戶端調用的主要函數,后面幾個參數和add.h中聲明的一樣,前面多了3個參數,函數名是接口函數名ns__add前面加上soap_call_ soap_call_ns__add( &add_soap, server, "", num1, num2, sum ); if(add_soap.error) { printf("soap error:%d,%s,%s\n", add_soap.error, *soap_faultcode(&add_soap), *soap_faultstring(&add_soap) ); result = add_soap.error; } soap_end(&add_soap); soap_done(&add_soap); return result; }
6.我們最終寫一個可以運行的客戶端調用程序,創建文件addtest.c,內容如下:
#include <stdio.h> #include <stdlib.h>
int add(const char* server, int num1, int num2, int *sum);
int main(int argc, char **argv) { int result = -1; char* server="http://localhost:8888"; int num1 = 0; int num2 = 0; int sum = 0; if( argc < 3 ) { printf("usage: %s num1 num2 \n", argv[0]); exit(0); }
num1 = atoi(argv[1]); num2 = atoi(argv[2]);
result = add(server, num1, num2, &sum); if (result != 0) { printf("soap err,errcode = %d\n", result); } else { printf("%d+%d=%d\n", num1, num2, sum ); } return 0; }
7.讓我們的client端和server端通訊 shell>make client shell>./addtest 7 8 當然,你的server應該還在run,這樣得到輸出結果7+8=15,好了,你成功完成了你的第一個C寫的WebService,恭喜。 三.圖示說明
四.要注意的問題 1. add.h文件前面的幾句注釋不能刪除,為soapcpp2需要識別的標志 2. 接口函數的返回值只能是int,是soap調用的結果,一般通過soap.error來判斷soap的連接情況,這個返回值沒有用到。 3. 接口函數的最后一個參數為傳出參數,如果需要傳出多個參數,需要自己定義一個結構將返回項封裝。 4. 在.h文件中不能include別的.h文件,可能不能生效,需要用到某些結構的時候需要在該文件中直接聲明。 5. 如果客戶端的調用不需要返回值,那么最后一個參數 五.參考文檔 1.gsoap主頁 http://gsoap2.sourceforge.net
2.跟我一起寫Makefile http://dev.csdn.net/develop/article/20/20025.shtm
3.Web Services: A Technical Introduction(機械工業出版社) 六.備注 192.168.18.233和192.168.18.234的/usr/local/gsoap目錄下的3個需要的文件及一個env目錄,不是編譯安裝的,是在別的地方編譯好了直接copy過來的(實際編譯結果中還有wsdl2h工具及其他一些文件,但是我們的實際開發中只是用到了這3個文件及env目錄)。因為時間倉促,本人還沒有時間研究編譯的問題,相關細節可以查看參考文檔1。 在192.168.18.233的/home/weiqiong/soap/sample目錄下及192.168.18.234的/tmp/soap/sample目錄下有本文講到的加法運算的例子。
|
全文結束 |
#ifndef MSG_H #define MSG_H //msgid #define LISTEN_THREAD??7 #define CENTER_THREAD??0 #define SEND_THREAD???2 #define REV_THREAD???3 #define TIME_THREAD???4 //lp #define EXIT????0 #define SEND_SGIP_SUBMIT?1 #define SEND_SGIP_BIND #define SEND_SGIP_R #define SEND_SGIP_UNBIND #define SEND_SGIP_UNBIND_R #define REV_SGIP_SOCKET //wp #define SEND_SUCCESS #define PACK_FAIL #define SEND_FAIL enum mgnt_cmd_type { ?event_login???????? = 0, ??event_logout, ??event_sip_init_para, ??event_log_init_para, ??event_sip_clean, ??event_set_dtmf_mode, ??event_set_dhcp, ??event_set_pppoe, ?? ??event_pstn_call_out, ??event_sip_call_out, ??event_answer_sipcall, ??event_release_sipcall, ??event_loadBMP_init, ?? ?? ??event_pstn_call_in=20, ??event_sip_call_in, ??event_remote_release_call, ??event_remote_establish_call, ??event_remote_cancelcall, ??event_login_return, ??event_remote_ignore,
??event_set_pstn_ring, ??event_set_sip_ring, ??event_set_alarm_ring, ??event_set_ring_volume ?? };
typedef struct msgbuf { ?long???msgtype; ?unsigned long?msgid; ?unsigned long?lp; ?unsigned long?wp; }MSGBuf, *pMSGBuf;
int vvMSGSend(long thread_id, unsigned long msgid, unsigned long lp, unsigned long wp); int vvMSGRecv(long thread_id, struct msgbuf *msg, int is_wait);
#ifndef _WINDOWS
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h>
#define?MSG_FILE_NAME??? "/rw/"????? //"/mnt/" #define MSG_FLAG???(IPC_CREAT? | 00666) //| IPC_EXCL ?
typedef struct sendMsg { ?int sd; ?void *content; }SendMsg, *pSendMsg;
#endif
#endif //MSG_H
#include "vvmsg.h" #include <ps_log.h>
#ifndef _WINDOWS #include <phone_Interface.h> #include <pthread.h> #include <basegdi.h> #include <keyboard.h> //#include "hash.h" extern? pthread_t g_incomingthread; //extern? hash_table table; #endif
?
int vvMSGSend(long thread_id, unsigned long msgid, unsigned long lp, unsigned long wp) {? ?struct msgbuf bmsg; #ifndef _WINDOWS ?key_t key;
?int msg_id; ?bmsg.msgtype = thread_id; ?bmsg.msgid = msgid; ?bmsg.lp = lp; ?bmsg.wp = wp;
?if((key = ftok(MSG_FILE_NAME,'a')) == -1) ?{ ??return -1; ?}
?if((msg_id = msgget(key,MSG_FLAG)) == -1) ?{ ??return -1; ?}
?if (msgsnd(msg_id, &bmsg, sizeof(struct msgbuf), IPC_NOWAIT) == -1) ?{ ??return -1; ?} #endif ?return 1;
}
int vvMSGRecv(long thread_id, struct msgbuf *msg, int is_wait) { ?#ifndef _WINDOWS ?key_t key;?? ?int msg_id; ?if((key = ftok(MSG_FILE_NAME,'a')) == -1) ?{ ??printf("Recv msg error 1!\n"); ??return -1; ?} ?if((msg_id = msgget(key,MSG_FLAG)) == -1) ?{ ??printf("Recv msg error 2!\n"); ??return -1; ?} ?if (is_wait != 1) ?{ ??if (msgrcv(msg_id, msg, sizeof(struct msgbuf), thread_id, IPC_NOWAIT) == -1) ??{ ???printf("Recv msg error 3!\n"); ???return -1; ??}? ?} ?else ?{ ??if (msgrcv(msg_id, msg, sizeof(struct msgbuf), thread_id, 0) == -1) ??{ ???//printf("Recv msg error 4!\n"); ???return -1; ??} ?} ??#endif ?return 1;
}
void *skype_thread_start(void *arg) { ?#ifndef _WINDOWS ?MSGBuf msg; ?pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,0);//設置線程屬性 ?pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,0); ?for (;;) ?{ ??pthread_testcancel();//設置取消點 ??if (vvMSGRecv((long)g_incomingthread, &msg, 1) == -1) ???continue; ?? ??// analysis the message ??switch (msg.msgid) ??{ ???ps_show_str(log_DEBUG, "vvmsg event!!!!!!!!!!!!!!!%d\r\n", msg.msgid); ??case event_login: ???{ ????userLogin(); ???? ???} ???break; ??case event_logout: ???{ ????userLogout(); ???} ??case event_sip_clean: ???{ ????SipClean(); ???} ???break; ??case event_sip_init_para: ???{ ????ps_show_str(log_DEBUG, "event before################UpdateSipInitPara\r\n"); ????UpdateSipInitPara(); ????ps_show_str(log_DEBUG, "event after##################UpdateSipInitPara\r\n"); ???} ???break; ??case event_log_init_para: ???{ ????UpdateLogInitPara(); ???} ???break; ??case event_set_dtmf_mode: ???{ ????int i = (int)msg.lp; ????ps_show_str(log_DEBUG, "event_set_dtmf_mode########################%d\r\n", i); ????SetDTMFMode(i); ???} ???break; ??case event_set_dhcp: ???{ ????SetDHCP(); ???} ???break; ??case event_set_pppoe: ???{ ????SetPPPOE(); ???} ???break;
??case event_pstn_call_out: ???{ ????pstncall((char*)msg.lp); ???} ???break; ??case event_sip_call_out: ???{ ???? ????sipcall((char*)msg.lp); ???} ???break;
??case event_answer_sipcall: ???{ ????callmgr_answercall((LINE_ID_T *)msg.lp); ???} ???break; ??????? case event_release_sipcall: ???{ ????callmgr_releasecall((LINE_ID_T *)msg.lp); ???} ???break; ??case event_loadBMP_init: ???{ ????CreateSysBmp(); ???? ???} ???break; ???
?
?
?
?
?
?
?
??case event_pstn_call_in: ???{ ????LINE_ID_T *line = (LINE_ID_T *)msg.wp; ????sipcome_create(line); ???} ???break; ??case event_sip_call_in: ???{ ????LINE_ID_T *line = (LINE_ID_T *)msg.wp; ????sipcome_create(line); ???} ???break; ? ??case event_remote_establish_call: ???{ ???? ????LINE_ID_T *line = (LINE_ID_T *)msg.wp; ????pstnchat_create(line); ????if(g_Hwnd[HWND_CALLOUT]!=0) ????calling_destroy(g_Hwnd[HWND_CALLOUT]); ???? ???} ???break; ??case event_remote_cancelcall: ???{ ????if(g_Hwnd[HWND_CALLIN]!=0) ???SendMessage(g_Hwnd[HWND_CALLIN],MSG_KEYDOWN,KEY_SW_RSK,0); ???} ???break; ??case event_remote_release_call: ???{ ????if(g_Hwnd[HWND_CHAT]!=0) ????SendMessage(g_Hwnd[HWND_CHAT],MSG_KEYDOWN,KEY_SW_RSK,0); ???} ???break; ??case event_login_return: ???{ ????printf("sfds0000000000000000000000000000000dssssssss^^^^^^^^^^^^^^^^^\r\n"); ????if(g_Hwnd[HWND_MAINSCREEN]!=0) ????{ ?????UpdateWindow(g_Hwnd[HWND_MAINSCREEN],1); ????//?SetFocusChild(g_Hwnd[HWND_MAINSCREEN]); ????//?ShowWindow(g_Hwnd[HWND_MAINSCREEN], SW_SHOW); ????} ???} ???break; ??case event_remote_ignore: ???{ ????if(g_Hwnd[HWND_CALLOUT]!=0) ????SendMessage(g_Hwnd[HWND_CALLOUT],MSG_KEYDOWN,KEY_SW_RSK,0);? ???} ???break; ??case event_set_pstn_ring: ???{ ????SetPstnRing((int)msg.lp); ???} ???break; ??case event_set_sip_ring: ???{ ????SetSipRing((int)msg.lp); ???} ???break; ??case event_set_ring_volume: ???{ ????SetRingVolume((int)msg.lp); ???} ???break; ??}
?} ?#endif }
附(創建線程):if (pthread_create(&g_incomingthread, NULL, skype_thread_start, NULL)) ??return -1;
一個簡化的問題示例 鏈表的難點在于必須復制鏈表處理函數來處理不同的對象,即便邏輯是完全相同的。例如兩個結構類似的鏈表: struct Struct_Object_A
{
int a;
int b;
Struct_Object_A *next;
} OBJECT_A;
typedef struct Struct_Object_B
{
int a;
int b;
int c;
Struct_Object_B *next;
} OBJECT_B; |
上面定義的兩個結構只有很小的一點差別。OBJECT_B 和 OBJECT_A 之間只差一個整型變量。但是,在編譯器看來,它們仍然是非常不同的。必須為存儲在鏈表中的每個對象復制用來添加、刪除和搜索鏈表的函數。為了解決這個問題,可以使用具有全部三個變量的一個聯合或結構,其中整數 c 并不是在所有的情況下都要使用。這可能變得非常復雜,并會形成不良的編程風格。
C 代碼解決方案:虛擬鏈表
此問題更好的解決方案之一是虛擬鏈表。虛擬鏈表是只包含鏈表指針的鏈表。對象存儲在鏈表結構背后。這一點是這樣實現的,首先為鏈表節點分配內存,接著為對象分配內存,然后將這塊內存分配給鏈表節點指針,如下所示:
虛擬鏈表結構的一種實現
typedef struct liststruct
{
liststruct *next;
} LIST, *pLIST;
pLIST Head = NULL;
pLIST AddToList( pLIST Head,
void * data, size_t datasize )
{
pLIST newlist=NULL;
void *p;
// 分配節點內存和數據內存
newlist = (pLIST) malloc
( datasize + sizeof( LIST ) );
// 為這塊數據緩沖區指定一個指針
p = (void *)( newlist + 1 );
// 復制數據
memcpy( p, data, datasize );
// 將這個節點指定給鏈表的表頭
if( Head )
{
newlist->next = Head;
}
else
newlist->next = NULL;
Head = newlist;
return Head;
} |
鏈表節點現在建立在數據值副本的基本之上。這個版本能很好地處理標量值,但不能處理帶有用 malloc 或 new 分配的元素的對象。要處理這些對象,LIST 結構需要包含一個一般的解除函數指針,這個指針可用來在將節點從鏈表中刪除并解除它之前釋放內存(或者關閉文件,或者調用關閉方法)。
一個帶有解除函數的鏈表
typedef void (*ListNodeDestructor)( void * );
typedef struct liststruct
{
ListNodeDestructor DestructFunc;
liststruct *next;
} LIST, *pLIST;
pLIST AddToList( pLIST Head, void * data,
size_t datasize,
ListNodeDestructor Destructor )
{
pLIST newlist=NULL;
void *p;
// 分配節點內存和數據內存
newlist = (pLIST) malloc
( datasize + sizeof( LIST ) );
// 為這塊數據緩沖區指定一個指針
p = (void *)( newlist + 1 );
// 復制數據
memcpy( p, data, datasize );
newlist->DestructFunc = Destructor;
// 將這個節點指定給鏈表的表頭
if( Head )
{
newlist->next = Head;
}
else
newlist->next = NULL;
Head = newlist;
return Head;
}
void DeleteList( pLIST Head )
{
pLIST Next;
while( Head )
{
Next = Head->next;
Head->DestructFunc(
(void *) Head );
free( Head );
Head = Next;
}
}
typedef struct ListDataStruct
{
LPSTR p;
} LIST_DATA, *pLIST_DATA;
void ListDataDestructor( void *p )
{
// 對節點指針進行類型轉換
pLIST pl = (pLIST)p;
// 對數據指針進行類型轉換
pLIST_DATA pLD = (pLIST_DATA)
( pl + 1 );
delete pLD->p;
}
pLIST Head = NULL;
void TestList()
{
pLIST_DATA d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "Hello" );
Head = AddToList( Head, (void *) d,
sizeof( pLIST_DATA ),
ListDataDestructor );
// 該對象已被復制,現在刪除原來的對象
delete d;
d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "World" );
Head = AddToList( Head, (void *) d,
sizeof( pLIST_DATA ),
ListDataDestructor );
delete d;
// 釋放鏈表
DeleteList( Head );
} |
在每個鏈表節點中包含同一個解除函數的同一個指針似乎是浪費內存空間。確實如此,但只有鏈表始終包含相同的對象才屬于這種情況。按這種方式編寫鏈表允許您將任何對象放在鏈表中的任何位置。大多數鏈表函數要求對象總是相同的類型或類。
虛擬鏈表則無此要求。它所需要的只是將對象彼此區分開的一種方法。要實現這一點,您既可以檢測解除函數指針的值,也可以在鏈表中所用的全部結構前添加一個類型值并對它進行檢測。
當然,如果要將鏈表編寫為一個 C++ 類,則對指向解除函數的指針的設置和存儲只能進行一次。
C++ 解決方案:類鏈表
本解決方案將 CList 類定義為從 LIST 結構導出的一個類,它通過存儲解除函數的單個值來處理單個存儲類型。請注意添加的 GetCurrentData() 函數,該函數完成從鏈表節點指針到數據偏移指針的數學轉換。一個虛擬鏈表對象
// 定義解除函數指針
typedef void (*ListNodeDestructor)
( void * );
// 未添加解除函數指針的鏈表
typedef struct ndliststruct
{
ndliststruct *next;
} ND_LIST, *pND_LIST;
// 定義處理一種數據類型的鏈表類
class CList : public ND_LIST
{
public:
CList(ListNodeDestructor);
~CList();
pND_LIST AddToList
( void * data, size_t datasize );
void *GetCurrentData();
void DeleteList( pND_LIST Head );
private:
pND_LIST m_HeadOfList;
pND_LIST m_CurrentNode;
ListNodeDestructor
m_DestructFunc;
};
// 用正確的起始值構造這個鏈表對象
CList::CList(ListNodeDestructor Destructor)
: m_HeadOfList(NULL),
m_CurrentNode(NULL)
{
m_DestructFunc = Destructor;
}
// 在解除對象以后刪除鏈表
CList::~CList()
{
DeleteList(m_HeadOfList);
}
// 向鏈表中添加一個新節點
pND_LIST CList::AddToList
( void * data, size_t datasize )
{
pND_LIST newlist=NULL;
void *p;
// 分配節點內存和數據內存
newlist = (pND_LIST) malloc
( datasize + sizeof( ND_LIST ) );
// 為這塊數據緩沖區指定一個指針
p = (void *)( newlist + 1 );
// 復制數據
memcpy( p, data, datasize );
// 將這個節點指定給鏈表的表頭
if( m_HeadOfList )
{
newlist->next = m_HeadOfList;
}
else
newlist->next = NULL;
m_HeadOfList = newlist;
return m_HeadOfList;
}
// 將當前的節點數據作為 void 類型返回,
以便調用函數能夠將它轉換為任何類型
void * CList::GetCurrentData()
{
return (void *)(m_CurrentNode+1);
}
// 刪除已分配的鏈表
void CList::DeleteList( pND_LIST Head )
{
pND_LIST Next;
while( Head )
{
Next = Head->next;
m_DestructFunc( (void *) Head );
free( Head );
Head = Next;
}
}
// 創建一個要在鏈表中創建和存儲的結構
typedef struct ListDataStruct
{
LPSTR p;
} LIST_DATA, *pND_LIST_DATA;
// 定義標準解除函數
void ClassListDataDestructor( void *p )
{
// 對節點指針進行類型轉換
pND_LIST pl = (pND_LIST)p;
// 對數據指針進行類型轉換
pND_LIST_DATA pLD = (pND_LIST_DATA)
( pl + 1 );
delete pLD->p;
}
// 測試上面的代碼
void MyCListClassTest()
{
// 創建鏈表類
CList* pA_List_of_Data =
new CList(ClassListDataDestructor);
// 創建數據對象
pND_LIST_DATA d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "Hello" );
// 創建指向鏈表頂部的局部指針
pND_LIST Head = NULL;
//向鏈表中添加一些數據
Head = pA_List_of_Data->AddToList
( (void *) d,
sizeof( pND_LIST_DATA ) );
// 該對象已被復制,現在刪除原來的對象
delete d;
// 確認它已被存儲
char * p = ((pND_LIST_DATA) pA_List_of_Data->GetCurrentData())->p;
d = new LIST_DATA;
d->p = new char[24];
strcpy( d->p, "World" );
Head = pA_List_of_Data->AddToList
( (void *) d, sizeof( pND_LIST_DATA ) );
// 該對象已被復制,現在刪除原來的對象
delete d;
// 確認它已被存儲
p = ((pND_LIST_DATA)
pA_List_of_Data->GetCurrentData())->p;
// 刪除鏈表類,析構函數將刪除鏈表
delete pA_List_of_Data;
} |
小結
從前面的討論來看,似乎僅編寫一個簡單的鏈表就要做大量的工作,但這只須進行一次。很容易將這段代碼擴充為一個處理排序、搜索以及各種其他任務的 C++ 類,并且這個類可以處理任何數據對象或類(在一個項目中,它處理大約二十個不同的對象)。您永遠不必重新編寫這段代碼。
|