一、變量與值得比較
1、布爾變量與零值的比較
不可將布爾變量直接與 TRUE、 FALSE或者 1、 0進行比較 。據布爾類型的語義,零值為“ 假”(記為 FALSE),任何非零值都是“ 真”(記為TRUE)。
TRUE的值究竟是什么并沒有統一的標準。例如 Visual C++ 將 TRUE定義為 1, 而 Visual Basic則將 TRUE定義為-1 。
假設布爾變量名字為 flag,它與零值比較的標準 if語句如下:
if (flag) // 表示flag為真if (!flag) // 表示flag為假
其它的用法都屬于不良風格,例如:
if (flag == TRUE)if (flag == 1 )if (flag == FALSE)if (flag == 0)
2、整形變量與零值的比較
應當將整型變量用“ ==” 或“ !=” 直接與 0比較 。假設整型變量的名字為 value,它與零值比較的標準 if語句如下:
if (value == 0)if (value != 0)
不可模仿布爾變量的風格而寫成:
if (value) // 會讓人誤解 value是布爾變量if (!value)
3、浮點變量與零值的比較
不可將浮點變量用“ ==” 或“ !=” 與任何數字比較 。千萬要留意, 無論是 float還是 double類型的變量, 都有精度限制。
所以一定要避免將浮點變量用“ ==” 或“ !=” 與數字比較,應該設法轉化成“ >=” 或“ <=” 形式。假設浮點變量的名字為 x,應當 將:
if (x == 0.0) // 隱含錯誤的比
轉化為:
if ((x>=-EPSINON) && (x<=EPSINON))
其中 EPSINON是允許的誤差(即精度) 。
4、指針變量與零值的比較
應當將指針變量用“ ==” 或“ !=” 與 NULL比較 。指針變量的零值是“ 空”(記為 NULL)。
盡管 NULL 的值與 0相同,但是兩者意義不同。假設指針變量的名字為 p,它與零值比較的標準 if語句如下:
if (p == NULL) // p與 NULL顯式比較,強調 p是指針變量if (p != NULL)
不要寫成:
if (p == 0) // 容易讓人誤解 p是整型變量if (p != 0)
或者:
if (p) // 容易讓人誤解p是布爾變量if (!p)
二、變量及基本運算
1、整型數
如果我們確定整數非負,就應該使用unsigned int而不是int。
有些處理器處理無符號unsigned 整形數的效率遠遠高于有符號signed整形數(這是一種很好的做法,也有利于代碼具體類型的自解釋)。
因此,在一個緊密循環中,聲明一個int整形變量的最好方法是:
registerunsignedint variable_name;
記住,整形in的運算速度高浮點型float,并且可以被處理器直接完成運算,而不需要借助于FPU(浮點運算單元)或者浮點型運算庫。
盡管這不保證編譯器一定會使用到寄存器存儲變量,也不能保證處理器處理能更高效處理unsigned整型,但這對于所有的編譯器是通用的。
例如在一個計算包中,如果需要結果精確到小數點后兩位,我們可以將其乘以100,然后盡可能晚的把它轉換為浮點型數字。
2、除法和取余數
在標準處理器中,對于分子和分母,一個32位的除法需要使用20至140次循環操作。
除法函數消耗的時間包括一個常量時間加上每一位除法消耗的時間。
Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator) = C0 + C1 * (log2 (numerator) - log2 (denominator)).
對于ARM處理器,這個版本需要20+4.3N次循環。這是一個消耗很大的操作,應該盡可能的避免執行。
有時,可以通過乘法表達式來替代除法。例如,假如我們知道b是正數并且b*c是個整數,那么(a/b)>c可以改寫為a>(c*b)。
如果確定操作數是無符號unsigned的,使用無符號unsigned除法更好一些,因為它比有符號signed除法效率高。
3、取模的一種替代方法
我們使用取余數操作符來提供算數取模。但有時可以結合使用if語句進行取模操作。考慮如下兩個例子:
uint modulo_func1 (uint count){ return (++count % 60);}uint modulo_func2 (uint count){ if (++count >= 60) count = 0; return (count);}
優先使用if語句,而不是取余數運算符,因為if語句的執行速度更快。這里注意新版本函數只有在我們知道輸入的count結余0至59時在能正確的工作。
4、使用數組下標
如果你想給一個變量設置一個代表某種意思的字符值,你可能會這樣做:
switch ( queue ){case0 : letter = 'W'; break;case1 : letter = 'S'; break;case2 : letter = 'U'; break;}
或者這樣做:
if ( queue == 0 ) letter = 'W';elseif ( queue == 1 ) letter = 'S';else letter = 'U';
一種更簡潔、更快的方法是使用數組下標獲取字符數組的值。如下:
staticchar *classes="WSU";letter = classes[queue];
5、使用別名
考慮如下的例子:
void func1( int *data ){ int i; for(i=0; i<10; i++) { anyfunc( *data, i); }}
盡管*data的值可能從未被改變,但編譯器并不知道anyfunc函數不會修改它,所以程序必須在每次使用它的時候從內存中讀取它。如果我們知道變量的值不會被改變,那么就應該使用如下的編碼:
void func1( int *data ){ int i; int localdata; localdata = *data; for(i=0; i<10; i++) { anyfunc ( localdata, i); }}
這為編譯器優化代碼提供了條件。
6、局部變量的類型
我們應該盡可能的不使用char和short類型的局部變量。對于char和short類型,編譯器需要在每次賦值的時候將局部變量減少到8或者16位。
這對于有符號變量稱之為有符號擴展,對于無符號變量稱之為零擴展。這些擴展可以通過寄存器左移24或者16位,然后根據有無符號標志右移相同的位數實現,這會消耗兩次計算機指令操作(無符號char類型的零擴展僅需要消耗一次計算機指令)。
可以通過使用int和unsigned int類型的局部變量來避免這樣的移位操作。這對于先加載數據到局部變量,然后處理局部變量數據值這樣的操作非常重要。無論輸入輸出數據是8位或者16位,將它們考慮為32位是值得的。
考慮下面的三個函數:
int wordinc (int a){ return a + 1;}short shortinc (short a){ return a + 1;}char charinc (char a){ return a + 1;}
盡管結果均相同,但是第一個程序片段運行速度高于后兩者。
三、循環語句
1、多重循環
在多重循環中, 如果有可能, 應當將最長的循環放在最內層, 最短的循環放在最外層,以減少 CPU 跨切循環層的次數。例如示例 4-4(b)的效率比示例4-4(a)的高 :
2、循環體內的判斷
如果循環體內存在邏輯判斷, 并且循環次數很大, 宜將邏輯判斷移到循環體的外面。
示例 4-4(c)的程序比示例 4-4(d)多執行了 N-1次邏輯判斷。并且由于前者老要進行邏輯判斷,打斷了循環“ 流水線” 作業,使得編譯器不能對循環進行優化處理, 降低了效率。
如果N非常大, 最好采用示例 4-4(d)的寫法, 可以提高效率。如果 N非常小,兩者效率差別并不明顯,采用示例 4-4(c)的寫法比較好, 因為程序更加簡潔。
3、for 語句的循環控制變量
不可在 for 循環體內修改循環變量,防止 for 循環失去控制 。建議 for語句的循環控制變量的取值采用“ 半開半閉區間” 寫法。
示例 4-5(a)中的 x值屬于半開半閉區間“ 0 =< x < N”,起點到終點的間隔為 N,循環次數為 N。
示例 4-5(b)中的 x值屬于閉區間“ 0 =< x <= N-1”,起點到終點的間隔為 N-1,循環次數為 N。
相比之下,示例 4-5(a)的寫法更加直觀,盡管兩者的功能是相同的 。
4、更快的for()循環
這是一個簡單而高效的概念。通常,我們編寫for循環代碼如下:
for( i=0; i<10; i++){ ... }
i從0循環到9。如果我們不介意循環計數的順序,我們可以這樣寫:
for( i=10; i--; ) { ... }
這樣快的原因是因為它能更快的處理i的值–測試條件是:i是非零的嗎?如果這樣,遞減i的值。對于上面的代碼,處理器需要計算“計算i減去10,其值非負嗎?
如果非負,i遞增并繼續”。簡單的循環卻有很大的不同。這樣,i從9遞減到0,這樣的循環執行速度更快。
這里的語法有點奇怪,但確實合法的。循環中的第三條語句是可選的(無限循環可以寫為for(;;))。如下代碼擁有同樣的效果:
for(i=10; i; i--){}
或者更進一步的:
for(i=10; i!=0; i--){}
這里我們需要記住的是循環必須終止于0(因此,如果在50到80之間循環,這不會起作用),并且循環計數器是遞減的。使用遞增循環計數器的代碼不享有這種優化。
四、指針
我們應該盡可能的使用引用值的方式傳遞結構數據,也就是說使用指針,否則傳遞的數據會被拷貝到棧中,從而降低程序的性能。
函數通過參數接受結構數據的指針,如果我們確定不改變數據的值,我們需要將指針指向的內容定義為常量。例如:
void print_data_of_a_structure ( const Thestruct *data_pointer){ ...printf contents of the structure...}
這個示例告訴編譯器函數不會改變外部參數的值(使用const修飾),并且不用在每次訪問時都進行讀取。
同時,確保編譯器限制任何對只讀結構的修改操作從而給予結構數據額外的保護。
五、懶檢測開發
在if(a>10 && b=4)這樣的語句中,確保AND表達式的第一部分最可能較快的給出結果(或者最早、最快計算),這樣第二部分便有可能不需要執行。
六、用switch()函數替代if…else…
對于涉及if…else…else…這樣的多條件判斷,例如:
if( val == 1) dostuff1();elseif (val == 2) dostuff2();elseif (val == 3) dostuff3();
使用switch可能更快:
switch( val ){ case1: dostuff1(); break; case2: dostuff2(); break; case3: dostuff3(); break;}
在if()語句中,如果最后一條語句命中,之前的條件都需要被測試執行一次。switch允許我們不做額外的測試。如果必須使用if…else…語句,將最可能執行的放在最前面。
函數相關
1、參數的書寫要完整
參數的書寫要完整,不要貪圖省事只寫參數的類型而省略參數名字。如果函數沒有參數,則用void填充。例如:
voidSetValue(intwidth,intheight); // 良好的風格voidSetValue(int,int); // 不良的風格floatGetValue(void); // 良好的風格floatGetValue(); // 不良的風格
2、參數命名要恰當,順序要合理
例如編寫字符串拷貝函數StringCopy,它有兩個參數。如果把參數名字起為str1和str2,例如:
void StringCopy(char*str1,char*str2);
那么我們很難搞清楚究竟是把str1拷貝到str2中,還是剛好倒過來。
可以把參數名字起得更有意義,如叫strSource和strDestination。這樣從名字上就可以看出應該把strSource拷貝到strDestination。
還有一個問題,這兩個參數那一個該在前那一個該在后?參數的順序要遵循程序員的習慣。一般地,應將目的參數放在前面,源參數放在后面:
void StringCopy(char*strDestination,char*strSource);
3、參數是指針
如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體內被意外修改。
例如:
void StringCopy(char*strDestination,constchar*strSource);
4、不要省略返回值的類型
C語言中,凡不加類型說明的函數,一律自動按整型處理。這樣做不會有什么好處,卻容易被誤解為void類型。
5、函數名字與返回值類型在語義上不可沖突
違反這條規則的典型代表是C標準庫函數getchar。例如:
charc;c=getchar();if(c==EOF)…
按照getchar名字的意思,將變量c聲明為char類型是很自然的事情。但不幸的是getchar的確不是char類型,而是int類型,其原型如下:
int getchar(void);
由于c是char類型,取值范圍是[-128,127],如果宏EOF的值在char的取值范圍之外,那么if語句將總是失敗,這種“危險”人們一般哪里料得到!導致本例錯誤的責任并不在用戶,是函數getchar誤導了使用者。
6、不要將正常值和錯誤標志混在一起返回
正常值用輸出參數獲得,而錯誤標志用return語句返回。
回顧上例,C標準庫函數的設計者為什么要將getchar聲明為令人迷糊的int類型呢?
在正常情況下,getchar的確返回單個字符。但如果getchar碰到文件結束標志或發生讀錯誤,它必須返回一個標志EOF。為了區別于正常的字符,只好將EOF定義為負數(通常為負1)。因此函數getchar就成了int類型。
我們在實際工作中,經常會碰到上述令人為難的問題。為了避免出現誤解,我們應該將正常值和錯誤標志分開。即:正常值用輸出參數獲得,而錯誤標志用return語句返回。
函數getchar可以改寫成BOOL GetChar(char*c);。
7、附加返回值,增強函數的靈活性
有時候函數原本不需要返回值,但為了增加靈活性如支持鏈式表達,可以附加返回值。例如字符串拷貝函數strcpy的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy函數將strSrc拷貝至輸出參數strDest中,同時函數的返回值又是strDest。這樣做并非多此一舉,可以獲得如下靈活性:
char str[20];int length=strlen(strcpy(str,“HelloWorld”));
循環展開
簡單的循環可以展開以獲取更好的性能,但需要付出代碼體積增加的代價。循環展開后,循環計數應該越來越小從而執行更少的代碼分支。
如果循環迭代次數只有幾次,那么可以完全展開循環,以便消除循壞帶來的負擔。例如:
for(i=0;i<3;i++){something(i);}
展開為:
something(0);something(1);something(2);
這可以非常可觀的節省性能,原因是代碼不用每次循環需要檢查和增加i的值。
if判斷條件的順序
if的判斷條件中概率最大的情況應放在前面。例子:
if(1==condition){}elseif(2==condition){}else{}
此處,若condition為1的概率大較大則把if (1 == condition)放在前面。
若condition為2概率大較大則把if (2 == condition)放在前面,如:
if(2==condition){}elseif(1==condition){}else{}
這里有個小細節:在用if判斷某個變量與某個常量是否相等時,可以把常量寫在前面變量寫在后面,如:
if(2==condition)
2放在前面,condition放在后面。這樣的好處就是當你漏敲了一個=號時,編譯器會指出你的這個錯誤。
盡早退出循環
通常,循環并不需要全部都執行。例如,如果我們在從數組中查找一個特殊的值,一經找到,我們應該盡可能早的斷開循環。
例如:如下循環從10000個整數中查找是否存在-99。
found=FALSE;for(i=0;i<10000;i++){if(list[i]==-99){found=TRUE;}}if(found){printf("Yes,thereisa-99.Hooray!n");}
這段代碼無論我們是否查找得到,循環都會全部執行完。更好的方法是一旦找到我們查找的數字就終止繼續查詢。
把程序修改為:
found=FALSE;for(i=0;i<10000;i++){if(list[i]==-99){found=TRUE;break;}}if(found){printf("Yes,thereisa-99.Hooray!n");}
假如待查數據位于第23個位置上,程序便會執行23次,從而節省9977次循環。
使用位運算替代四則運算
在許多古老的微處理器上, 位運算比加減運算略快, 通常位運算比乘除法運算要快很多。
在現代架構中, 位運算的運算速度通常與加法運算相同,但仍然快于乘法運算。所以通常乘以或除以2n可以使用位運算來代替四則運算,如
a=a*8;a=a/8;a=a%8;
修改為:
a=a<<3;a=a>>3;a=a&7;
以空間換時間
在內存比較充足的情況下,可以使用空間來換取時間。比如使用查表法,把一些可能的結果事先保存到表中。例如求階乘通常的做法是:
longfactorial(inti){if(i==0)return1;elsereturni*factorial(i-1);}
若是空間比較足,而且所需的結果都能列舉出來,則代碼可以修改為:
staticlongfactorial_table[]={1,1,2,6,24,120,720/*etc*/};longfactorial(inti){returnfactorial_table[i];}
使用復合賦值語句
增加一個變量的值有兩種方式,如:a = a + 5和a += 5。存在兩種增加一個變量值的方法有何意義呢?
K&R C設計者認為復合賦值符可以讓程序員把代碼寫得更清楚些。另外,編譯器可以產生更為緊湊的代碼。
現在,a = a + 5和a += 5之間的差別不再那么顯著,而且現代的編譯器為這兩種表達式產生優化代碼并無多大問題。
但是,要考慮類似如下的語句:
a[2*(y-6*f(x))]=a[2*(y-6*f(x))]+5;a[2*(y-6*f(x))]+=5;
此處a為數組。在第一種形式種,由于編譯器無從知道f函數是否具有副作用,所以它必須兩次計算數組a的下標表達式的值。
而在第二種形式中,下標表達式只需計算一次,所以第二種形式效率更高。并且,從書寫的角度看,第一種形式的下標表達式需要書寫兩次,而第二種形式只需書寫一次。
盡量使循環體內的工作量達到最小化
循環中,隨著循環次數的增加,會加大對系統資源的消耗。我們應當確認一些操作是否必須放在循環體內。示例代碼:
for(i=0;i<n;i++){tmp+=i;sum=tmp;}
這是個求和操作,但是這里每循環一次,就要進行一次sum = tmp;操作,這樣的寫法很浪費資源。這一條語句完全可以移至循環體外:
for(i=0;i<n;i++){tmp+=i;}sum=tmp;
這樣,sum = tmp;語句只執行一次,不僅可以調高程序效率,也提高了可讀性。同時,我們還可以考慮類似這樣的代碼是否有必要封裝成一個函數供多個地方調用。
無限循環優先選用for(;;),而不是while(1)
在C語言中,最常用的無限循環語句主要有兩種:while(1)和for(;;)。從功能上講, 這兩種語句的效果完全一樣。那么,我們究竟該選擇哪一種呢?
其實,for(;;)語句運行速度要快一些。按照for的 語法規則,兩個分號;分開的是3個表達式。現在表達式為空,很自然地被編譯成無條件的跳轉(即無條件循環,不用判斷條件)。如代碼for(;;)在Microsoft Visual Studio 2010 集成開發環境VC++的Debug模式下將生成如下匯編代碼:
for(;;)00931451jmpmain+41h(931451h)
相比之下,while語句就不一樣了。按照while的語法規則,while()語句中必須有一個 表達式(這里是1 )判斷條件,生成的代碼用它進行條件跳轉。即while語句()屬于有條件循環,有條件就要判斷條件是否成立,所以其相對于for(;;)語句需要多幾條指令。如代碼 while (1)在Microsoft Visual Studio 2010集成開發環境VC++的Debug模式下將生成如下匯 編代碼:
while(1)011A1451moveax,1011A1456testeax,eax011A1458jemain+55h(11A1465h)011A1463jmpmain+41h(11A1451h)
根據上面的分析結果,很顯然,for(;;)語句指令少,不占用寄存器,而且沒有判斷、 跳轉指令。當然,如果從實際的編譯結果來看,兩者的效果常常是一樣的,因為大部分編譯 器都會對while (1)語句做一定的優化。
但是,這還需要取決于編譯器。因此,我們還是應該優先選用for(;;)語句。
沒有參數的函數必須用void填充
在C語言中,void的作用主要有兩個:
1、對函數返回值的限定。2、對函數參數的限定。
看一個示例函數:
intf(){return100;}
從表面看,函數f()沒有參數,也就是說,它不允許接受參數。但事實并非如此,我們來驗證一下:
#include<stdio.h>intf(){return100;}intmain(void){printf("%dn",f(666));return0;}
編譯、運行結果為:
可見,使用GCC可正常通過編譯,這說明可以向無參數的函數傳遞參數。但是,需要注意的是,在一些IDE中不能通過編譯。
所以,為了提高程序的統一性、安全性與可讀性。我們對沒有參數的函數必須使用void進行填充。我們使用void填充上面的f函數之后,編譯就不通過了,報錯如下:
盡可能為簡單功能編寫函數
有時候,我們需要用函數去封裝僅用一兩行代碼就可完成的功能。對于這樣的函數,單 從代碼最上看,好像沒有什么封裝的必要。但是,用函數可使其功能明確化、具體化,從而增加程序可讀性,并且也方便代碼的維護與測試。示例代碼如下:
intMax(intx,inty){return(x>y? x : y);}intMin(intx,inty){return(x<y?x:y);}
當然,也可以使用宏來代替上面的函數,代碼如下:
#defineMAX(x,y)(((x)>(y))?(x):(y))#defineMIN(x,y)(((x)<(y))?(x):(y))
在C程序中,我們可以適當地用宏代碼來提高執行效率。宏代碼本身不是函數,但使用起來與函數相似。預處理器用復制宏代碼的方式代替函數調用,省去了參數壓棧、生成匯編語言的CALL調用、返回參數、執行return等過程,從而提高了運行速度。但是,使用宏代碼最大的缺點就是容易出錯,預處理器在復制宏代碼時常常產生意想不到的邊際效應。關于帶參宏的筆記:【C語言筆記】學習使用帶參宏(一)、【C語言筆記】學習使用帶參宏(二)
因此, 盡管看起來宏要比函數簡單得多,但還是建議使用函數的形式來封裝這些簡單功能的代碼。
函數地抽象級別應在同一個層次
先來看下面一段示例代碼:
voidInit(void){/*本地初始化*/....../*遠程初始化*/InitRemote();}voidInitRemote(void){/*遠程初始化*/......}
上面地Init函數主要完成本地初始化與遠程初始化工作,在其功能上沒有什么不妥之處。但從設計觀點看,卻存在這一定缺陷。因為本地初始化與遠程初始化層次相當,本地初始化也應當作為獨立的函數存在。應改為:
voidInit(void){/*本地初始化*/InitLocal();/*遠程初始化*/InitRemote();}voidInitLocal(void){/*本地初始化*/......}voidInitRemote(void){/*遠程初始化*/......}
盡量避免在非調度函數中使用控制參數
在函數設計中,我們可以將函數簡單地分為兩大類:調度函數與非調度函數(非調度函數一般也稱為功能函數或實現函數)。
所謂的調度函數是指根據輸入的消息類型或控制命令來啟動相應的功能實體(即函數或過程)的函數。調度函數本身不能提供功能實現,相反,它必須委托給實現函數來完成具體的功能。也就是說,調度型函數永遠只關注“what to do”,而“how to do”則是由實現函數來關心的,調度函數不需要關心“how to do”。這種調度函數與實現函數的分離設計也滿足了單一職責的原則,即調度的不實現,實現的不調度。
對調度函數來講,控制參數是指改變函數功能行為的參數,即函數要根據此參數來決定具體怎樣工作。然而,如果在非調度函數中也使用控制參數來決定具體怎樣工作,那么這樣做無疑會增加函數間的控制耦合,很可能使函數間的耦合度增大,并使函數的功能不唯一, 違背了函數功能的單一原則。示例代碼如下:
intCalculate(inta,intb,constintcalculate_flag){intsum=0;switch(calculate_flag){case1:sum=a+b;break;case2:sum=a-b;case3:sum=a*b;break;case4:sum=a/b;break;default:printf("errorn");break;}returnsum;}
上面的函數雖然看起來很簡潔,實際上這種設計是不合理的。由于控制參數calculate_flag的原因,使函數間的耦合度增大,也違背了函數的功能單一原則。因此,不如分為如下4個函數清晰,示例代碼如下:
intAdd(inta,intb){returna+b;}intSub(inta,intb){returna-b;}intMul(inta,intb){returna*b;}intDiv(inta,intb){returna/b;}