C程序員如何使用D編程(二)

設置和結構成員對齊方式

C 的方式

這是使用命令行選項完成的,而且該效果會影響整個程序,並且如果有模塊或者庫沒有重新編譯,結果會是悲劇性的。爲了解決這個問題,需要用到 #pragma :
            
            ABC 
           { 
               ... 
           }; 
           
但是,無論在理論上還是實際上,#pragma 在編譯器之間都是不可移植的。

D 的方式

很顯然,設置對齊的主要目的是使數據可移植,因此需要一種表述結構的可移植的方式。
            ABC 
           { 
                z;                     

              (1)  x;             
              (4) 
             { 
               ...                        
             } 
              (2):                   

                y;                     
           } 

匿名結構和聯合

有時,有必要控制嵌套在結構或聯合內部的結構的分佈。

C 的方式

C 不允許出現匿名的結構或聯合,這意味着需要使用傀儡標記名和傀儡成員:
            Foo 
           {    i; 
                Bar 
               { 
                    Abc {  x;  y; } _abc; 
                    *p; 
               } _bar; 
           }; 

           
           
           
            Foo f; 

           f.i; 
           f.x; 
           f.y; 
           f.p; 
這樣做不僅笨拙,由於使用了宏,還使符號調試器無法理解程序究竟做了什麼,並且宏還佔據了全局作用域而不是結構作用域。

D 的方式

匿名結構和聯合用來以一種更自然的方式控制分佈:
            Foo 
           {    i; 
                
               { 
                    {  x;  y; } 
                   * p; 
               } 
           } 

           Foo f; 

           f.i; 
           f.x; 
           f.y; 
           f.p; 

聲明結構類型和變量

C 的方式

可以在一條以分號結尾的語句中完成:
            Foo {  x;  y; } foo; 
或者分爲兩條語句:
            Foo {  x;  y; };         
            Foo foo; 

D 的方式

結構的定義和聲明不能在一條語句中完成:
            Foo {  x;  y; }         
           Foo foo; 
這意味着結尾的‘;’可以去掉,免得還要區分 struct {} 和函數及語句塊的 {} 之間在分號用法上的不同。

獲得結構成員的偏移量

C 的方式

很自然,又用了一個宏:
           
            Foo {  x;  y; }; 

           off = offsetof(Foo, y); 

D 的方式

偏移量只是另一個屬性:
            Foo {  x;  y; } 

           off = Foo.y.offset; 

聯合的初始化

C 的方式

聯合的初始化採用“首個成員”規則:
            U {  a;  b; }; 
            U x = { 5 };               
爲聯合添加成員或者重新排列成員的結果對任何的初始化語句來說都是災難性的。

D 的方式

在 D 中,初始化那個成員是顯式地指定的:
            U {  a;  b; } 
           U x = { a:5 } 
還避免了誤解和維護問題。

結構的初始化

C 的方式

成員按照它們在 {} 內的順序初始化:
            S {  a;  b; }; 
            S x = { 5, 3 }; 
對於小結構來說,這不是什麼問題,但當成員的個數變得很大時,小心地排列初始值以同聲明它們的順序對應變得很繁瑣。而且,如果新加了或者重新排列了成員的話,所有的初始化語句都需要進行適當地修改。這可是 bug 的雷區。

D 的方式

可以顯式地初始化成員:
            S {  a;  b; } 
           S x = { b:3, a:5 } 
這樣意義明確,並且不依賴於位置。

數組的初始化

C 的方式

C 初始化數組時依賴於位置:
            a[3] = { 3,2,2 }; 
潛逃的數組可以使用 {} ,也可以不使用 {}:
            b[3][2] = { 2,3, {6,5}, 3,4 }; 

D 的方式

D 也依賴於位置,但是還可以使用索引,下面的語句都產生同樣的結果:
    [3] a = [ 3, 2, 0 ]; 
    [3] a = [ 3, 2 ];              
    [3] a = [ 2:0, 0:3, 1:2 ]; 
    [3] a = [ 2:0, 0:3, 2 ];       
如果數組的下標爲枚舉的話,這會很方便。而且枚舉的順序可以變更,也可以加入新的枚舉值:
     color { black, red, green }
    [3] c = [ black:3, green:2, red:5 ]; 
必須顯式地初始化嵌套數組:
    [2][3] b = [ [2,3], [6,5], [3,4] ]; 

    [2][3] b = [[2,6,3],[3,5,4]];            

轉義字符串文字量

C 的方式

C 在 DOS 文件系統中會遇到問題,因爲字符串中的‘/’是轉義符。如果要使用文件 c:/root/file.c :
     file[] = ; 
如果使用這則表達式的話,會讓人很難高興起來。考慮匹配引號字符串的轉義序列:
    //

在 C 中,令人恐怖的表示如下:

     quoteString[] = ;

D 的方式

字符串本身是 WYSIWYG(所見即所得)的。轉義字符位於另外的字符串中。所以:
    [] file = ; 
    [] quoteString =   r  ;
著名的 hello world 字符串變爲:
    [] hello =  ; 

Ascii 字符 vs 寬字符

現代的程序設計工作需要語言以一種簡單的方法支持 wchar 字符串,這樣你的程序就可以實現國際化。

C 的方式

C 使用 wchar_t 並在字符串前添加 L 前綴:
    
     foo_ascii[] = ; 
     foo_wchar[] = L; 
如果代碼需要同時兼容 ascii 和 wchar 的化,情況會變得更糟。需要使用宏來屏蔽 ascii 和 wchar 字符串的差別:
    
     string[] = TEXT(); 

D 的方式

字符串的類型由語義分析決定,所以沒有必要用宏調用將字符串包裹起來:
    [] foo_ascii = ;		
    [] foo_wchar = ;		

同枚舉相應的數組

C 的方式

考慮:
        COLORS { red, blue, green, max }; 
        *cstring[max] = {, , }; 
當項的數目較小時,很容易保證其正確。但是如果數目很大,當加入新的項時就會很難保證其正確性。

D 的方式

    COLORS { red, blue, green }

    [][COLORS.max + 1] cstring = 
    [
	COLORS.red   : ,
	COLORS.blue  : , 
	COLORS.green : ,
    ]; 
雖不完美,但卻更好。

創建一個新的 typedef 類型

C 的方式

C 中的 typedef 是弱的,也就是說,他們並不真正引入一個類型。編譯器並不區分 typedef 類型和它底層的類型。
	  *Handle;
	 foo( *);
	 bar(Handle);

	Handle h;
	foo(h);			
	bar(h);			
	
C 的解決方案是創建一個傀儡結構,目的是獲得新類型纔有的類型檢查和重載能力。(譯註:這裏捎帶的涉及了 C++ 中的重載問題)
	 Handle__ {  *value; }
	 Handle__ *Handle;
	 foo( *);
	 bar(Handle);

	Handle h;
	foo(h);			
	bar(h);			
	
如果要給這個類型定一個默認值,需要定義一個宏,一個命名規範,然後時刻遵守這個規範:
	

	Handle h = HANDLE_INIT;
	h = func();
	 (h != HANDLE_INIT)
	    ...
	
對於採用結構的那種解決方案,事情甚至變得更復雜:
	 Handle__ HANDLE_INIT;

	 init_handle()		
	{
	    HANDLE_INIT.value = ( *)-1;
	}

	Handle h = HANDLE_INIT;
	h = func();
	 (memcmp(&h,&HANDLE_INIT,(Handle)) != 0)
	    ...
	
需要記住四個名字:Handle、HANDLE_INIT、struct Handle__ value

D 的方式

不需要上面那樣的習慣構造。只需要寫:
	* Handle;
	 foo(*);
	 bar(Handle);

	Handle h;
	foo(h);
	bar(h);
	
爲了處理默認值,可以給 typedef 添加一個初始值,可以使用 .init 屬性訪問這個初始值:
	* Handle = (*)(-1);
	Handle h;
	h = func();
	if (h != Handle.init)
	    ...
	
之需要記住一個名字:Handle

比較結構

C 的方式

儘管 C 爲結構賦值定義了一種簡單、便捷的方法:
	 A x, y;
	...
	x = y;
	
卻不支持結構之間的比較。因此,如果要比較兩個結構實例之間的相等性:
	

	 A x, y;
	...
	if(memcmp(&x, &y, ( A)) == 0)
	    ...
	
請注意這種方法的笨拙,而且在類型檢查上得不到語言的任何支持。

memcmp() 中有一個潛伏的 bug 。結構的分佈中,由於對齊的原因,可能會有“空洞”。C 不保證這些空洞中爲何值,所以兩個不同的結構實例可能擁有所有成員的值都對應相等,但比較的結果卻由於空洞中的垃圾的存在而爲“不等”。

D 的方式

D 的方式直接而顯然:
	A x, y;
	...
	 (x == y)
	    ...
	

比較字符串

C 的方式

庫函數 strcmp() 用於這個目的:
	 string[] = ;

	 (strcmp(string, ) == 0)	
	    ...
	
C 的字符串以‘/0’結尾,所以由於需要不停地檢測結尾的‘/0’,C 的方式在效率上先天不足。

D 的方式

爲什麼不用 == 運算符呢?
	[] string = ;

	 (string == )
	    ...
	
D 的字符串另外保存有長度。因此,字符串比較的實現可以比 C 的版本快得多(它們之間的差異就如同 C 的 memcmp() 同 strcmp() 之間的差異一樣)。

D 還支持字符串的比較運算符:

	[] string = ;

	 (string < )
	    ...
	
這對於排序/查找是很有用的。

數組的排序

C 的方式

儘管許多的 C 程序員不厭其煩地一遍一遍實現着冒泡排序,C 中正確的方法卻是使用 qsort() :
	 compare(*p1,  *p2)
	{
	    type *t1 = (type *)p1;
	    type *t1 = (type *)p2;

	     *t1 - *t2;
	}

	type array[10];
	...
	qsort(array, (array)/(array[0]), (array[0]), compare);
	
必須爲每種類型編寫一個 compare() 函數,而這些工作極易出錯。

D 的方式

這恐怕是最容易的排序方式了:
	type[] array;
	...
	array.sort;		
	

訪問易失性內存

C 的方式

如果要訪問易失性內存,如共享內存或者內存映射 I/O ,需要一個易失性的指針:
	 *p = address;

	i = *p;
	

D 的方式

D 有一種易失性語句,而不是一種類型修飾符:
	* p = address;

	 { i = *p; }
	

字符串文字量

C 的方式

C 的字符串文字量不能跨越多行,所以需要用‘/’將文本塊分割爲多行:
	
	
如果有很多的文本的話,這種做法是很繁瑣的。

D 的方式

字符串文字量可以跨越多行,如下所示:
	
	
所以可以簡單用剪切/粘貼將成塊的文字插入到 D 源碼中。

遍歷數據結構

C 的方式

考慮一個遍歷遞歸數據結構的函數。在這個例子中,有一個簡單的字符串符號表。數據結構爲一個二叉樹數組。代碼需要窮舉這個結構以找到其中的特定的字符串,並檢測它是否是唯一的實例。

爲了完成這項工作,需要一個輔助函數 membersearchx 遞歸地遍歷整棵樹。該輔助函數需要讀寫樹外部的一些上下文,所以創建了一個 struct Paramblock ,用指針指向它的以提高效率。

     Symbol
    {	 *id;
	 Symbol *left;
	 Symbol *right;
    };

     Paramblock
    {    *id;
	 Symbol *sm;
    };

     membersearchx( Paramblock *p,  Symbol *s)
    {
	 (s)
	{
	     (strcmp(p->id,s->id) == 0)
	    {
		 (p->sm)
		    error(,p->id);
		p->sm = s;
	    }

	     (s->left)
		membersearchx(p,s->left);
	    s = s->right;
	}
    }

     Symbol *symbol_membersearch(Symbol *table[],  tablemax,  *id)
    {
	 Paramblock pb;
	 i;

	pb.id = id;
	pb.sm = NULL;
	 (i = 0; i < tablemax; i++)
	{
	    membersearchx(pb, table[i]);
	}
	 pb.sm;
    }
    

D 的方式

這是同一個算法的 D 版本,代碼量大大少於上一個版本。因爲嵌套函數可以訪問外圍函數的變量,所以就不需要 Paramblock 或者處理它的簿記工作的細節了。嵌套的輔助函數完全處於使用它的函數的內部,提高了局部性和可維護性。

這兩個版本的性能沒什麼差別。

     Symbol
    {	[] id;
	Symbol left;
	Symbol right;
    }

    Symbol symbol_membersearch(Symbol[] table, [] id)
    {   Symbol sm;

	 membersearchx(Symbol s)
	{
	     (s)
	    {
		 (id == s.id)
		{
		     (sm)
			error(, id);
		    sm = s;
		}

		 (s.left)
		    membersearchx(s.left);
		s = s.right;
	    }
	}

	 ( i = 0; i < table.length; i++)
	{
	    membersearchx(table[i]);
	}
	 sm;
    }
    

無符號右移

C 的方式

如果左操作數是有符號整數類型,右移運算符 >> 和 >>= 表示有符號右移;如果左操作數是無符號整數類型,右移運算符 >> 和 >>= 表示無符號右移。如果要對 int 施行無符號右移,必須使用類型轉換:
	 i, j;
	...
	j = ()i >> 3;
	
如果 iint ,這種方法會工作得很好。但是如果 i 是一個 typedef 類型,
	myint i, j;
	...
	j = ()i >> 3;
	
並且 myint 恰巧是 long int ,則這個類型轉換會悄無聲息地丟掉最重要的那些 bit ,給出一個不正確的結果。

D 的方式

D 的右移運算符 >> 和 >>= 的行爲同它們在 C 中的行爲相同。但是 D 還支持顯式右移運算符 >>> 和 >>>= ,無論左操作數是否有符號,都會執行無符號右移。因此,
	myint i, j;
	...
	j = i >>> 3;
	
避免了不安全的類型轉換並且對於任何整數類型都能如你所願的工作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章