如何写好的程式
如何寫好的程式
南台科大電子系黎靖
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 範例: