如何写好的程式

如何写好的程式
如何写好的程式

如何寫好的程式

南台科大電子系黎靖

1.程式除了必須正確外,還要重視可讀性高、速度快及記憶體少。

2.要提高程式的可讀性必須注意下列原則:

(1)將程式盡量函式化,有助於產生可讀性高的程式,但卻必須付出執行速度緩慢的代價。

(2)程式必須結構化,而且要縮排,每個程式的第一行必須加上註解,說明程式的輸入及

輸出變數、程式的目的、程式的版本、撰寫人及撰寫日期等。

(3)函式的參數不要太多,參數太多代表函式可能執行太多工作,應考慮將此函式分割成

數個較小的函式。

(4)使用遞迴函式化,有助於快速產生正確且可讀性高的程式,但卻必須付出執行速度緩

慢的代價。

(5)盡量保持程式的單純化,即以簡單直接的方法寫程式。

(6)使用正確的資料型態,能減少程式錯誤且加快程式執行速度。

(7)使用有意義的函式與變數名稱,有助於程式自我說明。

(8)變數的使用範圍越小越不容易誤用,因此應該避免使用整體參數,區域變數的宣告盡

量延後到給初值的地方再宣告。

(9)結構:儘量使用結構以減少變數的個數。

// For 3D geometry

typedef struct Point3Struct { double x, y, z; } Point3;

typedef Point3 Vector3;

typedef struct Matrix4Struct { double element[4][4]; } Matrix4;

typedef struct Box3dStruct { Point3 min, max; } Box3;

(10)盡量使用函式或巨集減少程式的行數,函式或巨集命名要有意義。巨集命名採用全大

寫以便區別。

(11)使用const和inline取代#define。

(12)利用自訂資料型態,資料型態的名稱要有意義,以幫助程式自我說明,資料型態的名

稱採用首字大寫以便區別。

typedef unsigned Index;

typedef long Number;

typedef float Force;

typedef float Torque;

typedef float Size;

typedef float Thickness;

typedef float Temperature;

typedef float Power;

typedef float Angle;

(13)使用命名有意義的符號常數或常數變數代替不變的常數,有助於閱讀。命名採用全大

寫以便區別

(14)識別字的命名要有意義,但不要以底線或雙底線開頭,以免與C++內定的識別字衝

突。在程式中應避免對不同的目的使用相同的變數名稱,以避免混淆。

(15)使用全域變數容易發生與區域變數發生重複命名的問題,因此命名時應特別規定以

便與其他變數區別,或根本避免採用。

(16)藉由列舉型態的宣告,可以用有意義的名稱取代整數常數,從而提高程式的可讀性。

(17)在二元運算子的前後都置空白字元,有助於閱讀;但單元運算子與運算元間不可以

有空白字元。

(18)逗號後面加入空白字元,有助於閱讀。

(19)運算式中加入冗餘括弧,有助於閱讀。

(20)作“==” 運算時,將常數置於運算子左邊比置於右邊好。這樣的好處是萬一誤將

“==” 寫成“=”,編譯器就會指出此項錯誤。例如if ( x == 5) 應寫成if ( 5 == x)。

(21)使用最小開放權限原則可以使程式比較容易修改及維護。

(22)如果傳入函式的數值在函式內為定值,則該參數應宣告為const,以確保他不會無意

間被修改。修飾詞const可加強最小開放權限原則。

(23)由於捨入誤差的影響,算數式的運算順序會影響計算的誤差,應仔細考慮下列情況以

避免誤差產生:

3.有效率的程式:

簡單而直覺的程式往往也是效率最差的程式,但好處是容易撰寫、測試和除錯;此外,也可以用來驗證其他寫法之程式的正確性與效率。因此寫一個簡單而直覺的程式是寫程式的一個不錯的起點。

完成簡單而直覺的程式後,思考是否有更好的資料結構與演算法。

如果可能盡量使用陣列而不要使用鏈結串列,因為使用陣列的演算法通常較有效率。

如果使用到stack或queue,則應進一步思考改用heap是否更好。

如果是靜態的資料即應使用陣列,事先對陣列排序通常有助於發展較簡單而且快速的方法。

加(+)、減(-)、乘法(*)、位元運算(&、|)需要的時間差不多;rand()約2倍加/減法的時間;除法(/)及模數(%)運算約為5倍加/減法的時間;開根號(sqrt)約10倍加/減法的時間;簡單三角運算(sin)約20倍加/減法的時間;反三角運算(asin)約50倍加/減法的時間;進階三角運算(sinh)約100倍加/減法的時間,記憶體的配置(malloc、free) 約200倍加/減法的時間。所以除法應該考慮改為乘法,模數(%)可考慮用減法取代,避免使用三角函數:sin(a)用y/r、cos(a) 用x/r取代。記憶體的配置非常浪費時間,應盡量避免。如果記憶體不是問題,就不需要使用動態記憶體。

範例:使用減法求餘數

int mod(unsigned m, unsigned n)

{ while(m >= n) m -= n; return m; }

#define MOD(m, n) { while(m >= n) m -= n; return m; }

對於恆正的變數應宣告unsigned,宣告unsigned可以使編譯器最佳化的編譯此程式,從而加速執行速度。針對下列程式:

int middle(int a, int b)

{ return (a+b)/2; }

若middle(即回傳值)恆正,應修改為

int middle(int a, int b)

{ return unsigned (a+b)/2; }

迴圈內的程序往往對執行時間有決定性的影響,因此應該仔細撰寫,不需要在迴圈內的步驟,就應該移出廻圈。

求f =1 + t 0 + m 0t 1 + m 0m 1t 2 + … + m 0m 1…m i-1t i + … = ∑∏=-=???

?

??++n

i i j j i m t t 11001

// 直覺的程式

f = 1 + t[0]; mprod =1;

for (i = 1; i <= n; i++) {

mprod = 1;

for (j=0; j <= i-1; j++) mprod *= m[j]; f += t[i] * mprod; }

// 較佳之程式

f = 1 + t[0]; mprod =1;

for (i = 1; i <= n; i++) { mprod *= m[i-1]; f += t[i] * mprod; }

利用矩陣儲存相同的資料,去除資料的重複計算

for (j = 0; j < r; j++)

for (i = 0; i < c; i++) { CL[j][i].x = bx + (i+0.5)*px; CL[j][i].y = by + (j+0.5)*py; } 較快的程式:

float tempx[c], tempy[r];

for (i = 0; i < c; i++) tempx[i] = bx + (i+0.5)*px; for (j = 0; j < r; j++) tempy[j] = by + (j+0.5)*py; for (j = 0; j < r; j++) for (i = 0; i < c; i++) { CL[j][i].x = tempx[i]; CL[j][i].y = tempy[j]; } 上述程式可以進一步改進:

float tempx[c], tempy;

for (i = 0; i < c; i++) tempx[i] = bx + (i+0.5)*px; for (j = 0; j < r; j++) {

tempy = by + (j+0.5)*py; for (i = 0; i < c; i++) { CL[j][i].x = tempx[i]; CL[j][i].y = tempy; }

} 此程式將重複的步驟合併,不但加快速度也減少記憶體的使用。

迴路裡面避免使用函數

C 是堆疊導向的語言,函式的局部變數及參數均使用堆疊做為暫時的儲存所。當函

式被呼叫時,返回位址也被放入堆疊以備程式執行完後可以返回,把這些資料壓入堆疊的過程稱為calling sequence,而將這些資料自堆疊中取出的過程稱為returning sequence,這些過程都會耗用許多時間。

Example:

void exchangesort (int n, keytype S[ ])

{

for (index i = 1; i <= n-1; i++)

for (index j = i+1; j <= n; j++) if (S[j] < S[i]) exchange(S[i], S[j]);

}

去除函數:

void exchangesort (int n, keytype S[ ])

{

keytype temp;

for (index i = 1; i <= n-1; i++)

for (index j = i+1; j <= n; j++)

if (S[j] < S[i]) { temp = S[i]; S[i] = S[j]; S[j] = temp: }

}

使用條件運算子取代if else結構

針對下列程式:

if (S[i] < S[j]) { U[k] = S[i]; i++; }

else { U[k] = S[j]; j++; }

k++;

可改為下列較快的程式:

U[k++] = (S[i] < S[j]? S[i++]: S[j++]);

去除不必要的if else結構

Example: 考慮下列程式

void f(int a, int b)

{

bool b1;

if (a == b) b1 = true;

else b1 = false;

}

上述程式實際上可簡化為:

void f(int a, int b) {

bool b1 = a == b; }

Example:

)

())(()()

())(()()(110110,n k k k k k k n k k k n x x x x x x x x x x x x x x x x x L --------=

+-+-

將上式表為連乘形式

∏≠≤≤--=

k

i n i i

k i

k n x x x x x L 0,)( // 直覺的程式

float x[n], y, L;

for (unsign i=0; i <= n; i++) if (i != k ) L *= (y-x[i])/(x[k]-x[i]); 較快的程式:

float x[n], y, L;

for (int i=0, L=1; i < k; i++) L *= (y-x[i])/(x[k]-x[i]); for (i=k+1; i <= n; i++) L *= (y-x[i])/(x[k]-x[i]); 此程式少了i 與k 的比較,所以速度較快。 Example:

求∑∑===n i n j ij f F 11, 其中???????>=>====otherwise

j i c i j if i b j i if j a j i if f ij ),,(1,1),(1

,1),(1

,1,0

// 直覺的程式

for (i = 1, sum=0; i <= max; i++) for (j = 1; j <= max; j++) {

if (i==1 && j==1) continue; else if (i==1) sum += A[j]; else if (j==1) sum += B[i]; else sum += C[i][j]; }

改進:(去除不必要的判斷)

sum = 0;

for (j = 2; j <= max; j++) sum += A[j];

for (i = 2; i <= max; i++) sum += B[i];

for (i = 2; i <= max; i++)

for (j = 2; j <= max; j++) sum += C[i][j];

巢狀的if/else結構,要將為true機率較大判斷式置於前面

範例:二元搜尋法Binary search

(1) 常見的寫法:

index location (index low, index high)

{ index mid;

if (low > high) return 0;

else {

mid = (unsigned)(low+high)/2;

if (x == S[mid]) return mid; // Find x.

else if (x < S[mid]) return location(low, mid-1); //Search left sublist

else return location(mid+1, high); // Search right sublist.

}

}

(2) 改進:

index location (index low, index high)

{

index mid;

if (low > high) return 0; // x S

else {

mid = (unsigned)(low+high)/2;

if (x < S[mid]) return location(low, mid-1); //Search left sublist

else if (x > S[mid]) return location(mid+1, high); // Search right sublist.

else return mid; // Find x.

}

}

去除遞迴:遞迴演算法比起非遞廻程式耗費更多的執行時間與記憶體。

雖然遞迴函式功能很強,但使用上要小心。採用遞迴方式的常式會比使用迭代的常式執行速度較慢,需要更多的資源負擔。遞迴呼叫某個函式可能造成堆疊發生溢位。因為函式的參數和局部變數是存放在堆疊區,而且每次函式呼叫都會把這些變數複製一份,堆疊空間將會耗盡。如堆疊空間耗盡,將會發生堆疊溢位。如果發生在已經除錯的遞迴函式,可以嘗試配置更多的堆疊空間給程式,當撰寫遞迴函式時,必須在所有的遞迴呼叫還沒有執行完畢之前,增加條件敘述句使函式強制返回。假如沒有加入條件敘述句,函式將自我呼叫直到堆疊耗盡。發展遞迴函式時,這是常見的錯誤。當發展遞迴式時,使用大量的輸出敘述句,可觀

察函式在執行期間的行為,發現錯誤時,就可以略過執行程序。

求費氏數列之遞迴演算法:

unsigned long fib(unsigned n)

{ return (n <= 1? n : fib(n-1) + fib(n-2)) ; }

去除遞迴之費氏數列演算法:

unsigned long fib2(unsigned n)

{

Index i; unsigned long f[n];

for (f[0] = 0, f[1] = 1, i = 2; i <= n; i++) f[i] = f[i-1] + f[i-2];

return f[n];

}

使用環狀移動取代一系列的對調

Example: Insertion sort

void insertionsort (unsigned n, keytype S[])

{

index i, j; keytype x, temp;

for (i = 2; i <= n; i++)

{

x = S[i]; j = i– 1;

while (S[j] > x && j > 0)

{

temp = S[j+1]; S[j+1] = S[j]; S[j] = temp; // Swap S[j] and S[j+1]

j--;

}

}

}

改進:

void insertionsort (unsigned n, keytype S[])

{

index i, j; keytype x;

for (i = 2; i <= n; i++)

{

x = S[i]; j = i– 1;

while (S[j] > x && j > 0) S[j+1] = S[j--];

S[j+1] = x;

}

}

使用sentinel減少比較次數

Example: Insertion sort

void insertionsort (unsigned n, keytype S[])

{

index i, j; keytype x;

S[0] = -∞// Sentinal

for (i = 2; i <= n; i++)

{

x = S[i]; j = i– 1;

while (S[j] > x) S[j+1] = S[j--];

S[j+1] = x;

}

}

Example: Binary search

void seqsearch1 (index n, const keytype S[ ], keytype x, index& location)

{ // index& denote location is an output parameter.

location = 0;

while (++location <= n && S[location]!= x) ;

if (location > n) location = 0;

}

加入sentinel

void seqsearch2 (index n, const keytype S[ ], keytype x, index& location)

{

S[n+1] = x; // Set a sentinel at the end of S.

location = 0;

while (S[++location] != x); // Neglect the checking of list end

if (location > n) location = 0;

}

將迴圈展開,降低迴圈的執行次數,可以避免pipeline阻塞、減少分支、增進指令的平行運算。

void seqsearch3 (int n, const keytype S[ ], keytype x, index& location)

{

S[n+1] = x;// Set a sentinel at the end of S.

location = 1;

while (1)

{

if (S[location] ==x)break;

if (S[location+1] == x){location += 1;break;}

if (S[location+2] == x){location += 2; break;}

if (S[location+3] ==x){location += 3;break;}

if (S[location+4] ==x){location += 4;break;}

if (S[location+5] ==x) {location += 5;break;}

if (S[location+6] ==x){location += 6; break;}

if (S[location+7] ==x){location += 7;break;}

location += 8;

}

if (location > n) location = 0;

}

3.18 善用static

在函式中宣告區域陣列時,最好宣告為static ,如此就不需要每次呼叫此函式時就必須將此陣列建立及設定初值,而且每次程式離開此函式時,也不會將此陣列清除,因此可以增進程式執行效率。

3.19 使用horner 公式求多項式的值

()()0

1230

12233)(a t a t a t a a t a t a t a t f +++=+++=

#define Horner(t, a3, a2, a1, a0) ((((a3)*(t)+(a2))*(t)+(a1))*(t)+(a0))

3.20 用指標算數取代陣列索引

使用指標方法時,當 array 的位址放到A 之後,此位址會存在一個註標暫存器中(如8086處理器之SI),當每一次重複迴圈時,只要執行一次遞增運算即可;然而陣列索引的型式則強迫程式在每一次迴圈中,根據t 的數值來計算陣列的註標。指標算數及陣列索引之差異隨著多重註標的使用而增加,因為指標算數只需使用簡單的加法,然而求得每一個註標則需要一連串的指令。 Example:

sum = 0;

for (j = 2; j <= max; j++) sum += A[j]; for (i = 2; i <= max; i++) sum += B[i]; for (i = 2; i <= max; i++)

for (j = 2; j <= max; j++) sum += C[i][j];

改進:用指標存取

int k;

sum = 0;

for (j = 2; j <= max; j++) sum += *(A+j);

for (i = 2; i <= max; i++) sum += *(B+i);

for (i = 2; i <= max; i++)

for (j = 2; j <= max; j++)

{

k = i*(max+1)+j;

sum += *(C[0]+k);

}

3.21以空間換取時間:儲存預先計算的結果,已備後續程式使用。

以下為求費氏數的遞廻程式

unsigned long Fib(unsigned i)

{ return(i < 1? i : Fib(i-1) + Fib(i-2); )

上面的程式非常浪費時間,我們可以利用陣列儲存較小的費氏數,求較大的費氏數時就可以用查表的方式直接利用這些已知的費氏數,而不需要一再的重算這些較小的費氏數。

unsigned long F(unsigned i)

{

unsigned long t=i;

if (knownF[i] != 0) return knownF[i];

if (i > 1) t = F(i-1) + F(i-2);

return knownF[i] = t; }

3.22 使用位元運算取代一般運算

範例1:#define ODD(m)((m) & 1)

範例2:#define SWAPInt(a, b){ (a) ^= (b); (b) ^= (a); (a) ^= (b); }

範例3:#define Div2(n)( (n) >> 1)

3.23使用inline 或marco取代三行內之函式,幾乎所有的C++的書本都反對使用巨集,但小心使用巨集可以加快一倍的函式執行速度。

#define ODD(m)((m) & 1)

#define EVEN(m)(!ODD(m))

#define MAX(a,b)( (a)>(b)? (a): (b))

#define MIN(a,b)( (a)<(b)? (a): (b))

#define MAX3(x, y, z)((x)>(y)? MAX((x),(z)): MAX((y),(z)))

#define MIN3(x,y,z) ((x)<(y)? MIN((x),(z)): MIN((y),(z)))

#define TOGGLE(x)((x)? FALSE : TRUE)

#define ABSV AL(val) ((val >= 0)? val: -val) /* Absolute Value Function */

#define RINGCHANGE(a, b, c) { a=b; b=c; c=a;}

#define SGN(x)((x) > 0? 1: (x == 0? 0 : -1)))

#define SIGN(x)((x) > 1e-5 ? 1: (x) < -1e-5 ? -1: 0)

#define SWAPInt(a, b){ a ^= b; b ^= a; a ^= b; }

#define SWAPfloat( a, b){ float aux = (a); (a) = (b); (b) = aux; }

#define SWAP3(aux, a, b, c){ (aux) = (a); (a) = (b); (b) = (c); (c) = (aux); }

#define SIN(x)(sin((x)*RD))

#define COS(x) (cos((x)*RD))

#define ROUND(a)((a)>0? (int)(a+0.5): -(int)(0.5-a))

#define SQUARE(x)((x) * (x))

#define CUBE(x)((x) * (x) * (x))

結論:撰寫程式的步驟如下:

1.決定演算法

2.決定資料結構、確定全域變數

3.寫出資料型態及基本運算,使用有意義的名稱

4.用函數寫程式

5.驗證程式

6.檢討if-else 結構

7.檢討資料型態

8.去除函數:三行以內的函數改用巨集或inline函數、有重複性的函數予以合併,簡單的函

數自己定義而不要使用內定的函數

9.增加輸入資料檢查步驟及自動更正程式。

10.寫上註解。註解應該增加一些從程式碼無法立即看出來的東西。

2.4 範例:

相关主题
相关文档
最新文档