“安全第一”的C語言編程規范
編者按:C語言是開發嵌入式應用的主要工具,然而C語言并非是專門為嵌入式系統設計,相當多的嵌入式系統較一般計算機系統對軟件安全性有更苛刻的要求。1998年,MISRA指出,一些在C看來可以接受,卻存在安全隱患的地方有127處之多。2004年,MISRA對C的限制增加到141條。嵌入式系統應用工程師借用計算機專家創建的C語言,使嵌入式系統應用得以飛速發展,而MISRAC是嵌入式系統應用工程師對C語言嵌入式應用做出的貢獻。如今MISRA C已經被越來越多的企業接受,成為用于嵌入式系統的C語言標準,特別是對安全性要求極高的嵌入式系統,軟件應符合MISRA標準。
從本期開始,本刊將分6期,與讀者共同學習MISRAC。
第一講:“‘安全第一’的C語言編程規范”,簡述MISRAC的概況。
第二講:“跨越數據類型的重重陷阱”,介紹規范的數據定義和操作方式,重點在隱式數據類型轉換中的問題。
第三講:“指針、結構體、聯合體的安全規范”,解析如何安全而高效地應用指針、結構體和聯合體。
第四講:“防范表達式的失控”,剖析MISRAC中關于表達式、函數聲明和定義等的不良使用習慣,最大限度地減小各類潛在錯誤。
第五講:“準確的程序流控制”,表述C語言中控制表達式和程序流控制的規范做法。
第六講:“構建安全的編譯環境”,講解與編譯器相關的規范編寫方式,避免來自編譯器的隱患。
C/C++語言無疑是當今嵌入式開發中最為常見的語言。早期的嵌入式程序大都是用匯編語言開發的,但人們很快就意識到匯編語言所帶來的問題——難移植、難復用、難維護和可讀性極差。很多程序會因為當初開發人員的離開而必須重新編寫,許多程序員甚至連他們自己幾個月前寫成的代碼都看不懂。C/C++語言恰恰可以解決這些問題。作為一種相對“低級”的高級語言,C/C++語言能夠讓嵌入式程序員更自由地控制底層硬件,同時享受高級語言帶來的便利。對于C語言和C++語言,很多的程序員會選擇C語言,而避開龐大復雜的C++語言。這是很容易理解的——C語言寫成的代碼量比C++語言的更小些,執行效率也更高。
對于程序員來說,能工作的代碼并不等于“好”的代碼。“好”代碼的指標很多,包括易讀、易維護、易移植和可靠等。其中,可靠性對嵌入式系統非常重要,尤其是在那些對安全性要求很高的系統中,如飛行器、汽車和工業控制中。這些系統的特點是:只要工作稍有偏差,就有可能造成重大損失或者人員傷亡。一個不容易出錯的系統,除了要有很好的硬件設計(如電磁兼容性),還要有很健壯或者說“安全”的程序。
然而,很少有程序員知道什么樣的程序是安全的程序。很多程序只是表面上可以干活,還存在著大量的隱患。當然,這其中也有C語言自身的原因。因為C語言是一門難以掌握的語言,其靈活的編程方式和語法規則對于一個新手來說很可能會成為機關重重的陷阱。同時,C語言的定義還并不完全,即使是國際通用的C語言標準,也還存在著很多未完全定義的地方。要求所有的嵌入式程序員都成為C語言專家,避開所有可能帶來危險的編程方式,是不現實的。最好的方法是有一個針對安全性的C語言編程規范,告訴程序員該如何做。
1 MISRAC規范
1994年,在英國成立了一個叫做汽車工業軟件可靠性聯合會(The Motor Industry Software Reliability Association,以下簡稱MISRA)的組織。它是致力于協助汽車廠商開發安全可靠的軟件的跨國協會,其成員包括:AB汽車電子、羅孚汽車、賓利汽車、福特汽車、捷豹汽車、路虎公司、Lotus公司、MIRA公司、Ricardo公司、TRW汽車電子、利茲大學和福特VISTEON汽車系統公司。經過了四年的研究和準備,MISRA于1998年發布了一個針對汽車工業軟件安全性的C語言編程規范——《汽車專用軟件的C語言編程指南》(Guidelines for the Use of the C Language in Vehicle Based Software),共有127條規則,稱為MISRAC:1998。[Page]
C語言并不乏國際標準。國際標準化組織(International Organization of Standardization,簡稱ISO)的“標準C語言”經歷了從C90、C96到C99的變動。但是,嵌入式程序員很難將ISO標準當作編寫安全代碼的規范。一是因為標準C語言并不是針對代碼安全的,也并不是專門為嵌入式應用設計的;二是因為“標準C語言”太龐大了,很難操作。MISRAC:1998規范的產生恰恰彌補了這方面的空白。
隨著很多汽車廠商開始接受MISRAC編程規范,MISRAC:1998也成為汽車工業中最為著名的有關安全性的C語言規范。2004年,MISRA出版了該規范的新版本——MISRAC:2004。在新版本中,還將面向的對象由汽車工業擴大到所有的高安全性要求(Critical)系統。在MISRAC:2004中,共有強制規則121條,推薦規則20條,并刪除了15條舊規則。任何符合MISRAC:2004編程規范的代碼都應該嚴格的遵循121條強制規則的要求,并應該在條件允許的情況下盡可能符合20條推薦規則。
MISRAC:2004將其141條規則分為21個類別,每一條規則對應一條編程準則。詳細情況如表1所列。

最初,MISRAC:1998編程規范的建立是為了增強汽車工業軟件的安全性。可能造成汽車事故的原因有很多,如圖1所示,設計和制造時埋下的隱患約占總數的15%,其中也包括軟件的設計和制造。MISRAC:1998就是為了減小這部分隱患而制定的。
MISRAC編程規范的推出迎合了很多汽車廠商的需要,因為一旦廠商在程序設計上出現了問題,用來補救的費用將相當可觀。1999年7月22日,通用汽車公司(General Motors)就曾經因為其軟件設計上的一個問題,被迫召回350萬輛已經出廠的汽車,損失之大可想而知。
MISRAC規范不僅在汽車工業開始普及,也同時影響到了嵌入式開發的其他方向。嵌入式實時操作系統μC/OSII的2.52版本雖然已經于2000年通過了美國航空管理局(FAA)的安全認證,但2003年作者就根據MISRAC:1998規范又對源碼做了相應的修改,如將
if ((pevent->OSEventTbl[y] &= ~bitx) == 0) {
/*… */
}
的寫法,改寫成
pevent->OSEventTbl[y] &= ~bitx;
if (pevent->OSEventTbl[y] == 0) {
/*… */
}
發布了2.62的新版本,并宣稱其源代碼99%符合MISRAC:1998規范。
一個程序能夠符合MISRAC編程規范,不僅需要程序員按照規范編程,編譯器也需要對所編譯的代碼進行規則檢查。現在,很多編譯器開發商都對MISRAC規范有了支持,比如IAR的編譯器就提供了對MISRAC:1998規范127條規則的檢查功能。
2 MISRAC對安全性的理解
MISRAC:2004的專家們大都來自于軟件工業或者汽車工業的知名公司,規范的制定不僅僅像過去一樣局限于汽車工業的C語言編程,同時還涵蓋了其他高安全性系統。

MISRAC:2004認為C程序設計中存在的風險可能由5個方面造成:程序員的失誤、程序員對語言的誤解、程序員對編譯器的誤解、編譯器的錯誤和運行出錯(runtime errors)。
程序員的失誤是司空見慣的。程序員是人,難免會犯錯誤。很多由程序員犯下的錯誤可以被編譯器及時地糾正(如鍵入錯誤的變量名等),但也有很多會逃過編譯器的檢查。相信任何一個程序員都曾經犯過將“= =”誤寫成“=”的錯誤,編譯器可能不會認為
if(x=y)
是一個程序員的失誤。
再舉個例子,大家都知道++運算符。假如有下面的指令:
i=3;
printf(“%d”,++i);
輸出應該是多少?如果是:
printf(“%d”,i++);
呢?如果改成-i++呢?i+++i呢?i+++++i呢?絕大多數程序員恐怕已經糊涂了。在MISRAC:2004中,會明確指出++或--運算符不得和其他運算符混合使用。
C語言非常靈活,它給了程序員非常大的自由。但事情有好有壞,自由越大,犯錯誤的機會也就越多。[Page]
如果說有些錯誤是程序員無心之失的話,那么因為程序員對C語言本身或是編譯器特性的誤解而造成的錯誤就是“明”知故犯了。C語言有一些概念很難掌握,非常容易造成誤解,如表達式的計算。請看下面這條語句:
if ( ishigh && (x == i++))
很多程序員認為執行了這條指令后,i變量的值就會自動加1。但真正的情況如何呢?MISRA中有一條規則:邏輯運算符&&或||的右操作數不得帶有副作用(side effect)*,就是為了避免這種情況下可能出現的問題。
*所謂帶有副作用,就是指執行某條語句時會改變運行環境,如執行x=i++之后,i的值會發生變化。
另外,不同編譯器對同一語句的處理可能是不一樣的。例如整型變量的長度,不同編譯器的規定就不同。這就要求程序員不僅要清楚C語言本身的特性,還要了解所用的編譯器,難度很大。
還有些錯誤是由編譯器(或者說是編寫編譯器的程序員)本身造成的。這些錯誤往往較難發現,有可能會一直存留在最后的程序中。
運行錯誤指的是那些在運行時出現的錯誤,如除數等于零、指針地址無效等問題。運行錯誤在語法檢查時一般無法發現,但一旦發生很可能導致系統崩潰。例如:
#define NULL 0
……
char* p;
p=NULL;
printf(“Location of 0 is %d\n”, *p);
語法上沒有任何問題,但在某些系統上卻可能運行出錯。
C語言可以產生非常緊湊、高效的代碼,一個原因就是C語言提供的運行錯誤檢查功能很少,雖然運行效率得以提高,但也降低了系統的安全性。
有句話說得好,“正確的觀念重于一切”。MISRAC規范對于嵌入式程序員來講,一個很重要的意義就是提供給他們一些建議,讓他們逐漸樹立一些好的編程習慣和編程思路,慢慢摒棄那些可能存在風險的編程行為,編寫出更為安全、健壯的代碼。比如,很多嵌入式程序員都會忽略注釋的重要性,但這樣的做法會降低程序的可讀性,也會給將來的維護和移植帶來風險。嵌入式程序員經常要接觸到各種的編譯器,而很多C程序在不同編譯器下的處理是不一樣的。MISRAC:2004有一條強制規則,要求程序員把所有和編譯器特性相關的C語言行為記錄下來。這樣在程序員做移植工作時,風險就降低了。
3 MISRAC的負面效應
程序員可能會擔心采用MISRAC:2004規范會對他們的程序有負面影響,比如可能會影響代碼量、執行效率和程序可讀性等。應該說,這種擔心不無道理。縱觀141條MISRAC:2004編程規范,大多數的規則并不會對程序的代碼量、執行效率和可讀性造成什么大的影響;一部分規則可能會以增加存儲器的占用空間為代價來增加執行效率,或者增加代碼的可讀性;但是,也確實存在著一些規則可能會降低程序的執行效率。一個典型的例子就是關于聯合體的使用。MISRAC:2004有一條規則明確指出:不得使用聯合體。這是因為,在聯合體的存儲方式(如位填充、對齊方式、位順序等)上,各種編譯器的處理可能不同。比如,經常會有程序員這樣做:一邊將采集得到的數據按照某種類型存入一個聯合體,而同時又采用另外一種數據類型將該數據讀出。如下面這段程序:
typedef union{
uint32_t word;
uint8_t bytes[4];
}word_msg_t;
unit32_t read_word_big_endian (void) {
word_msg_t tmp;
tmp.bytes[0] = read_byte();
tmp.bytes[1] = read_byte();
tmp.bytes[2] = read_byte();
tmp.bytes[3] = read_byte();
return (tmp.word);
}
原理上,這種聯合體很像是一個硬件上的雙口RAM存儲器。但程序員必須清楚,這種做法是有風險的。MISRAC:2004推薦用下面這種方法來做:
uint32_t read_word_big_endian (void) {
uint32_t word;
word=((unit32_t)read_byte())<<24;[Page]
word=word|(((unit32_t)read_byte())<<16);
word=word|(((unit32_t)read_byte())<<8);
word=word| ((unit32_t)read_byte());
return(word);
}
先不論為什么這樣做會更安全,只談執行效率,這種采用二進制數移位的方法遠遠不如使用聯合體。到底是使用更安全的做法,還是采用效率更高的做法,需要程序員權衡。對于一些要求執行效率很高的系統,使用聯合體仍然是可以接受的方法。當然,這是建立在程序員充分了解所用編譯器的基礎上的,而且程序員必須對這種做法配有相應的注釋。
4 發展中的MISRAC
MISRAC并非完美,它自身的發展也印證了這一點。MISRAC:2004就去掉了MISRAC:1998中的15條規則。今后的發展,MISRAC仍然要解決很多問題。比如,MISRAC:2004是基于C90標準的,但最新的國際C標準是C99,而C99中沒有確切定義的C語言特性幾乎比C90多了一倍,MISRAC如何適應新的標準還需要進一步探討。另外,C++在嵌入式應用中也越來越受到重視,MISRA正在著手制定MISRAC++編程規范。讀者可以通過訪問網站http://www.misra.org.uk了解MISRAC的發展動向。
5 對MISRAC的思考
嵌入式系統并不算是一個獨立的學科,但作為一個發展中的行業,它確實需要有一些自己的創新之處。嵌入式工程師們不應僅僅局限于從計算機專家那里學習相關理論知識,并運用于自己的項目,還應該共同努力去完善自己行業的標準和規范,為嵌入式系統的發展做出貢獻。MISRAC編程規范就是一個很好的典范。它始于汽車工程師和軟件工程師經驗的總結,然后逐漸發展成為一種對整個嵌入式行業都有指導意義的規范。對于推動整個嵌入式行業的正規化發展,MISRAC無疑有著重要意義。從另一個角度講,MISRAC規范也可以看成是嵌入式工程師對軟件業的一種完善。嵌入式工程師雖然不是計算機專家,但卻對嵌入式應用有著最深刻的了解,將自己在嵌入式應用中的經驗和體會貢獻給其他行業,也是他們應該肩負的責任。
參考文獻
1 MISRAC:2004, Guidelines for the use of the C language in critical systems. The Motor Industry Software Reliability Association, 2004
2 Harbison III. Samuel P, Steele Jr. Guy L. C語言參考手冊. 邱仲潘,等譯. 第5版. 北京:機械工業出版社,2003
3 Kernighan. Brian W, Ritchie. Dennis M. C程序設計語言. 徐寶文,等譯. 第2版. 北京:機械工業出版社,2001
4 Koenig Andrew. C陷阱與缺陷. 高巍譯. 北京:人民郵電出版社,2002
5 McCall Gavin. Introduction to MISRAC:2004, Visteon UK, http://www.MISRAC2.com/
6 Hennell Mike. MISRA CIts role in the bigger picture of critical software development, LDRA. http://www.MISRAC2.com/
7 Hatton Les. The MISRA C Compliance Suite—The next step, Oakwood Computing. http://www.MISRAC2.com/
8 Montgomery Steve. The role of MISRA C in developing automotive software, Ricardo Tarragon. http://www.MISRAC2.com/
非計算機專業C語言初學者編程規范(學生用)—概述
注:以下信息來源于成都信息工程學院對于程序員來說,能工作的代碼并不等于“好”的代碼。“好”代碼的指標很多,包括易讀、易維護、易移植和可靠等。其中,可靠性對嵌入式系統非常重要,尤其是在那些對安全性要求很高的系統中,如飛行器、汽車和工業控制中。這些系統的特點是:只要工作稍有偏差,就有可能造成重大損失或者人員傷亡。一個不容易出錯的系統,除了要有很好的硬件設計(如電磁兼容性),還要有很健壯或者說“安全”的程序。
然而,很少有程序員知道什么樣的程序是安全的程序。很多程序只是表面上可以干活,還存在著大量的隱患。當然,這其中也有C語言自身的原因。因為C語言是一門難以掌握的語言,其靈活的編程方式和語法規則對于一個新手來說很可能會成為機關重重的陷阱。同時,C語言的定義還并不完全,即使是國際通用的C語言標準,也還存在著很多未完全定義的地方。要求所有的嵌入式程序員都成為C語言專家,避開所有可能帶來危險的編程方式,是不現實的。最好的方法是有一個針對安全性的C語言編程規范,告訴程序員該如何做。
本規范在制定過程中,主要參考了業界比較推崇的《華為軟件編程規范和范例》和《MISRA 2004規則》,適合于非計算機專業的C語言初學者使用,目的在于在教學中培養學生良好的編程規范和意識、素質,促進所設計程序安全、健壯、可靠、可讀與可維護(程序簡單、清晰)。考慮到面向的是初學者,為便于教學和課程考核操作,本規范中的要求比較基本。事實上,很多公司都有自己規定的代碼風格,包括命名規則、縮進規則等,學生參加工作后,應再進一步學習和應用公司的規范。
建議學生在學習本規范的同時,花點時間閱讀本規范的參考文獻原文,特別是熟讀本規范的參考文獻之一的《“安全第一”的C語言編程規范》,深刻理解編程規范與程序安全、健壯、可靠、可讀、可維護間的關系和作用,在學習和工作中養成良好的編程風格。
非計算機專業C語言初學者編程規范(學生用)—排版
1.1 嚴格采用階梯層次組織程序代碼
函數或過程的開始、結構的定義及循環、判斷等語句中的代碼都要采用縮進風格,case 語句下的情況處理語句也要遵從語句縮進要求。程序塊的分界符(如C/C++ 語言的大括號‘{’ 和‘}’)應各獨占一行并且位于同一列,同時與引用它們的語句左對齊。在函數體的開始、類的定義、結構的定義、枚舉的定義以及if 、for 、do 、while 、switch 、case 語句中的程序都要采用如上的縮進方式。
各層次縮進的風格采用TAB縮進(TAB寬度原則上使用系統默認值,TC使用8空格寬度,VC使用4空格寬度)。示例:
if (x is true)
{
we do y
}
else
{
if (a > b)
{
? ...
}
else
{
? ...
}
}
和:
if (x == y)
{
...
}
else if (x > y)
{
...
}
else
{
....
}
注意,右括號所在的行不應當有其它東西,除非跟隨著一個條件判斷。也就是do-while語句中的“while”,象這樣:
do
{
body of do-loop
} while (condition);
說明:代碼離不開縮進,縮進背后的思想是:清楚地定義一個控制塊從哪里開始,到哪里結束。尤其是在你連續不斷的盯了20個小時的屏幕后,如果你有大尺寸的縮進。你將更容易發現縮進的好處。
關于縮進主要有兩個爭論,一個是該用 空格(Space)還是用 制表符(Tab),另外一個是該用4格縮進還是8格縮進甚至都不是。建議總是使用Tab縮進,因為幾乎所有的代碼(不僅僅是C代碼)都在使用Tab縮進。
現在,有些人說8個字符大小的縮進導致代碼太偏右了,并且在一個80字符寬的終端屏幕上看著很不舒服。對這個問題的回答是:如果你有超過3個級別的縮進,你就有點犯糊涂了,應當修改你的程序。簡而言之,8個字符的縮進使程序更易讀,而且當你把功能隱藏的太深時,多層次的縮進還會對此很直觀的給出警告。要留心這種警告信息。
例外:對于由開發工具自動生成的代碼可以有不一致。
1.2 及時折行
較長的語句(>80 字符)要分成多行書寫,長表達式要在低優先級操作符處劃分新行,操作符放在新行之首,劃分出的新行要進行適當的縮進(至少1個TAB位置),使排版整齊,語句可讀。示例:report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)
?? && (n7stat_stat_item_valid (stat_item))
?? && (act_task_table[taskno].result_data != 0));
循環、判斷等語句中若有較長的表達式或語句,則要進行適應的劃分,長表達式要在低優先級操作符處劃分新行,操作符放在新行之首。示例:
if ((taskno < max_act_task_number)
? && (n7stat_stat_item_valid (stat_item)))
{
... // program code
}
for (i = 0, j = 0; (i < BufferKeyword[word_index].word_length)
???? && (j < NewKeyword.word_length); i++, j++)
{
... // program code
}
for (i = 0, j = 0;
(i < first_word_length) && (j < second_word_length);
? i++, j++)
{
... // program code
}
若函數或過程中的參數較長,則要進行適當的劃分。示例:
n7stat_str_compare((BYTE *) & stat_object,
???? (BYTE *) & (act_task_table[taskno].stat_object),
?? sizeof (_STAT_OBJECT));
n7stat_flash_act_duration( stat_item, frame_id *STAT_TASK_CHECK_NUMBER
?????? + index, stat_object );
1.3 一行只寫一條語句
不允許把多個短語句寫在一行中,即一行只寫一條語句。示例,如下例子不符合規范:rect.length = 0;? rect.width = 0;
應如下書寫
rect.length = 0;
rect.width? = 0;
1.4 if、for、do、while等語句格式規定
if 、for 、do 、while 、case 、switch 、default 等語句自占一行,且if 、for 、do 、while 等語句的執行語句部分無論多少都要加花括號{}。
1.5 空行
(1)變量說明之后必須加空行。(2)相對獨立的程序塊之間應加空行。
1.6 空格
在兩個以上的關鍵字、變量、常量進行對等操作時,它們之間的操作符之前、之后或者前后要加空格;進行非對等操作時,如果是關系密切的立即操作符(如-> ),后不應加空格。采用這種松散方式編寫代碼的目的是使代碼更加清晰。由于留空格所產生的清晰性是相對的,所以,在已經非常清晰的語句中沒有必要再留空格,如果語句已足夠清晰則括號內側(即左括號后面和右括號前面)不需要加空格,多重括號間不必加空格,因為在C/C++語言中括號已經是最清晰的標志了。
在長語句中,如果需要加的空格非常多,那么應該保持整體清晰,而在局部不加空格。給操作符留空格時不要連續留兩個以上空格。
(1)逗號、分號只在后面加空格。
int a, b, c;
(2)比較操作符, 賦值操作符"="、 "+=",算術操作符"+"、"%",邏輯操作符"&&"、"&",位域操作符"<<"、"^"等雙目操作符的前后加空格。
if (current_time >= MAX_TIME_VALUE)
{
a = b + c;
}
a *= 2;
a = b ^ 2;
(3)"!"、"~"、"++"、"--"、"&"(地址運算符)等單目操作符前后不加空格。
*p = 'a';??????? // 內容操作"*"與內容之間flag = !isEmpty; // 非操作"!"與內容之間
p = &mem;??????? // 地址操作"&" 與內容之間
i++;???????????? // "++","--"與內容之間
(4)"->"、"."前后不加空格。
p->id = pid;???? // "->"指針前后不加空格
(5) if、for、while、switch等與后面的括號間應加空格,使if等關鍵字更為突出、明顯。
if (a >= b && c > d)
1.7 對變量的定義,盡量位于函數的開始位置
(1)應避免分散定義變量。(2)同一行內不要定義過多變量。
(3)同一類的變量在同一行內定義,或者在相鄰行定義。
(4)數組、指針等復雜類型的定義放在定義區的最后。
(5)變量定義區不做較復雜的變量賦值。
1.8 程序各部分的放置順序
在較小的項目中,按如下順序組織安排程序各部分:(1)#include <C的標準頭文件>。
(2)#include 〞用戶自定義的文件〞。
(3)#define 宏定義。
(4)全局變量定義。
(5)函數原型聲明。
(6)main函數定義。
(7)用戶自定義函數。
以上各部分之間、用戶自定義的函數之間應加空行。注意,函數原型聲明統一集中放在main函數之前,不放在某個函數內部。
非計算機專業C語言初學者編程規范(學生用)—注釋
2.1 注釋的原則和目的
注釋的原則是有助于對程序的閱讀理解,在該加的地方都加了,注釋不宜太多也不能太少,注釋語言必須準確、易懂、簡潔。通過對函數或過程、變量、結構等正確的命名以及合理地組織代碼的結構,使代碼成為自注釋的——清晰準確的函數、變量等的命名,可增加代碼可讀性,并減少不必要的注釋——過量的注釋則是有害的。注釋的目的是解釋代碼的目的、功能和采用的方法,提供代碼以外的信息,幫助讀者理解代碼,防止沒必要的重復注釋信息。 示例:如下注釋意義不大。
/* if receive_flag is TRUE */
if (receive_flag)
而如下的注釋則給出了額外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)
2.2 函數頭部應進行注釋
函數頭部應進行注釋,列出:函數的目的/ 功能、輸入參數、輸出參數、返回值、調用關系(函數、表)等。示例1:下面這段函數的注釋比較標準,當然,并不局限于此格式,但上述信息建議要包含在內。
/*************************************************
? Function:?????? // 函數名稱
? Description:??? // 函數功能、性能等的描述
? Calls:????????? // 被本函數調用的函數清單
? Called By:????? // 調用本函數的函數清單
? Input:????????? // 輸入參數說明,包括每個參數的作
????????????????? // 用、取值說明及參數間關系。
? Output:???????? // 對輸出參數的說明。
? Return:???????? // 函數返回值的說明
? Others:???????? // 其它說明
*************************************************/
對于某些函數,其部分參數為傳入值,而部分參數為傳出值,所以對參數要詳細說明該參數是入口參數,還是出口參數,對于某些意義不明確的參數還要做詳細說明(例如:以角度作為參數時,要說明該角度參數是以弧度(PI),還是以度為單位),對既是入口又是出口的變量應該在入口和出口處同時標明。等等。
在注釋中詳細注明函數的適當調用方法,對于返回值的處理方法等。在注釋中要強調調用時的危險方面,可能出錯的地方。
2.3 進行注釋時的注意事項
(1)建議邊寫代碼邊注釋,修改代碼同時修改相應的注釋,以保證注釋與代碼的一致性。不再有用的注釋要刪除。(2)注釋的內容要清楚、明了,含義準確,防止注釋二義性。說明:錯誤的注釋不但無益反而有害。
(3)避免在注釋中使用縮寫,特別是非常用縮寫。在使用縮寫時或之前,應對縮寫進行必要的說明。
(4)注釋應與其描述的代碼相近,對代碼的注釋應放在其上方或右方(對單條語句的注釋)相鄰位置,不可放在下面。除非必要,不應在代碼或表達中間插入注釋,否則容易使代碼可理解性變差。
示例:如下例子不符合規范。
例1:
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
例2:
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
/* get replicate sub system index and net indicator */
應如下書寫
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
(5)對于所有有物理含義的變量、常量,如果其命名不是充分自注釋的,在聲明時都必須加以注釋,說明其物理含義。變量、常量、宏的注釋應放在其上方相鄰位置或右方。
示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
(6)數據結構聲明( 包括數組、結構、類、枚舉等) ,如果其命名不是充分自注釋的,必須加以注釋。對數據結構的注釋應放在其上方相鄰位置,不可放在下面;對結構中的每個域的注釋放在此域的右方。
示例:可按如下形式說明枚舉/數據/聯合結構。
/* sccp interface with sccp user primitive message name */
enum? SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND,?? /* sccp notify user the No.7 network can not */
???? /* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
(7)全局變量要有較詳細的注釋,包括對其功能、取值范圍、哪些函數或過程存取它以及存取時注意事項等的說明。
示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */????? // 變量作用、含義
/* 0 - SUCCESS?? 1 - GT Table error */
/* 2 - GT error? Others - no use? */?????? // 變量取值范圍
/* only? function? SCCPTranslate() in */
/* this modual can modify it,? and? other */
/* module can visit it through call */
/* the? function GetGTTransErrorCode() */??? // 使用方法
BYTE g_GTTranErrorCode;
(8)注釋與所描述內容進行同樣的縮排,讓程序排版整齊,并方便注釋的閱讀與理解。
示例:如下例子,排版不整齊,閱讀稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One
? /* code two comments */
??? CodeBlock Two
}
應改為如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
(9)將注釋與其上面的代碼用空行隔開。
示例:如下例子,顯得代碼過于緊湊。
/* code one comments */
program code one
/* code two comments */
program code two
應如下書寫
/* code one comments */
program code one
/* code two comments */
program code two
(10)對變量的定義和分支語句(條件分支、循環語句等)必須編寫注釋。這些語句往往是程序實現某一特定功能的關鍵,對于維護人員來說,良好的注釋幫助更好的理解程序,有時甚至優于看設計文檔。
(11)對于switch 語句下的case 語句,如果因為特殊情況需要處理完一個case 后進入下一個case 處理(即上一個case后無break),必須在該case 語句處理完、下一個case 語句前加上明確的注釋,以清楚表達程序編寫者的意圖,有效防止無故遺漏break語句(可避免后期維護人員對此感到迷惑:原程序員是遺漏了break語句還是本來就不應該有)。示例:
case CMD_DOWN:
ProcessDown();
break;
case CMD_FWD:
ProcessFwd();
if (...)
{
? ...
? break;
} else
{
? ProcessCFW_B();?? // now jump into case CMD_A
}
case CMD_A:
ProcessA();
break;
...
(12)在程序塊的結束行右方加注釋標記,以表明某程序塊的結束。當代碼段較長,特別是多重嵌套時,這樣做可以使代碼更清晰,更便于閱讀。示例:參見如下例子。
if (...)
{
program code
while (index < MAX_INDEX)
{
? program code
} /* end of while (index < MAX_INDEX) */ // 指明該條while語句結束
} /* end of? if (...)*/ // 指明是哪條if語句結束
(13)在順序執行的程序中,每隔3—5行語句,應當加一個注釋,注明這一段語句所組成的小模塊的作用。對于自己的一些比較獨特的思想要求在注釋中標明。
(14)注釋格式盡量統一,建議使用“/* …… */”。
(15)注釋應考慮程序易讀及外觀排版的因素,使用的語言若是中、英兼有的,建議多使用中文,除非能用非常流利準確的英文表達——注釋語言不統一,影響程序易讀性和外觀排版,出于對維護人員的考慮,建議使用中文。
非計算機專業C語言初學者編程規范(學生用)—命名規則
C是一門樸素的語言,你使用的命名也應該這樣。與Modula-2和Pascal程序員不同,C程序員不使用諸如“ThisVariableIsATemporaryCounter”這樣“聰明”的名字。C程序員應該叫它“tmp”,這寫起來更簡單,也不會更難懂。然而,當面對復雜情況時就有些棘手,給全局變量取一個描述性的名字是必要的。把一個全局函數叫做“foo”是一種目光短淺的行為。全局函數也一樣,如果你有一個統計當前用戶個數的函數,應當把它命名為“count_active_user()”或者簡單點些的類似名稱,不應該命名為“cntusr()”。
3.1 三種流行的命名法則
目前,業界共有四種命名法則:駝峰命名法、匈牙利命名法、帕斯卡命名法和下劃線命名法,其中前三種是較為流行的命名法。(1)駝峰命令法。正如它的名稱所表示的那樣,是指混合使用大小寫字母來構成變量和函數的名字。例如,下面是分別用駱駝式命名法和下劃線法命名的同一個函數:
printEmployeePaychecks();
print_employee_paychecks();
第一個函數名使用了駝峰命名法,函數名中的每一個邏輯斷點都有一個大寫字母來標記。第二個函數名使用了下劃線法,函數名中的每一個邏輯斷點都有一個下劃線來標記。
駝峰命名法近年來越來越流行了,在許多新的函數庫和Microsoft Windows這樣的環境中,它使用得當相多。另一方面,下劃線法是C出現后開始流行起來的,在許多舊的程序和UNIX這樣的環境中,它的使用非常普遍。
(2)匈牙利命名法。廣泛應用于象Microsoft Windows這樣的環境中。Windows 編程中用到的變量(還包括宏)的命名規則為匈牙利命名法,這種命名技術是由一位能干的 Microsoft 程序員查爾斯-西蒙尼(Charles Simonyi) 提出的。
匈牙利命名法通過在變量名前面加上相應的小寫字母的符號標識作為前綴,標識出變量的作用域、類型等。這些符號可以多個同時使用,順序是先m_(成員變量)、再指針、再簡單數據類型、再其它。這樣做的好處在于能增加程序的可讀性,便于對程序的理解和維護。
例如:m_lpszStr, 表示指向一個以0字符結尾的字符串的長指針成員變量。
匈牙利命名法關鍵是:標識符的名字以一個或者多個小寫字母開頭作為前綴;前綴之后的是首字母大寫的一個單詞或多個單詞組合,該單詞要指明變量的用途。
(3)帕斯卡(pascal)命名法。與駝峰命名法類似,二者的區別在于:駝峰命名法是首字母小寫,而帕斯卡命名法是首字母大寫,如:
DisplayInfo();
string UserName;
二者都是采用了帕斯卡命名法。
(4)三種命名規則的小結:MyData就是一個帕斯卡命名的示例;myData是一個駝峰命名法,它第一個單詞的第一個字母小寫,后面的單詞首字母大寫,看起來像一個駱駝;iMyData是一個匈牙利命名法,它的小寫的i說明了它的型態,后面的和帕斯卡命名相同,指示了該變量的用途。
3.2 命名的基本原則
(1)標識符的命名要清晰、明了,有明確含義,同時使用完整的單詞或大家基本可以理解的縮寫,避免使人產生誤解——盡量采用采用英文單詞或全部中文全拼表示,若出現英文單詞和中文混合定義時,使用連字符“_”將英文與中文割開。較短的單詞可通過去掉“元音”形成縮寫;較長的單詞可取單詞的頭幾個字母形成縮寫;一些單詞有大家公認的縮寫。例如:temp->tmp、flag->flg、statistic->stat、increment->inc、message->msg等縮寫能夠被大家基本認可。(2)命名中若使用特殊約定或縮寫,則要有注釋說明。應該在源文件的開始之處,對文件中所使用的縮寫或約定,特別是特殊的縮寫,進行必要的注釋說明。
(3)自己特有的命名風格,要自始至終保持一致,不可來回變化。個人的命名風格,在符合所在項目組或產品組的命名規則的前提下,才可使用。(即命名規則中沒有規定到的地方才可有個人命名風格)。
(4)對于變量命名,禁止取單個字符(如i 、j 、k... ),建議除了要有具體含義外,還能表明其變量類型、數據類型等,但i 、j 、k 作局部循環變量是允許的。變量,尤其是局部變量,如果用單個字符表示,很容易敲錯(如i寫成j),而編譯時又檢查不出來,有可能為了這個小小的錯誤而花費大量的查錯時間。
(5)除非必要,不要用數字或較奇怪的字符來定義標識符。
(6)命名規范必須與所使用的系統風格保持一致,并在同一項目中統一。
(7)在同一軟件產品內,應規劃好接口部分標識符(變量、結構、函數及常量)的命名,防止編譯、鏈接時產生沖突。對接口部分的標識符應該有更嚴格限制,防止沖突。如可規定接口部分的變量與常量之前加上“模塊”標識等。
(8)用正確的反義詞組命名具有互斥意義的變量或相反動作的函數等。
下面是一些在軟件中常用的反義詞組。
add / remove?????? begin / end??????? create / destroy
insert / delete?????? first / last???????? g et / release
increment / decrement???????????????? put / get
add / delete???????? lock / unlock????? open / close
min / max????????? old / new???????? start / stop
next / previous????? source / target???? show / hide
send / receive?????? source / destination
cut / paste????????? up / down
示例:
int? min_sum;
int? max_sum;
int? add_user( BYTE *user_name );
int? delete_user( BYTE *user_name );
(9)除了編譯開關/ 頭文件等特殊應用,應避免使用_EXAMPLE_TEST_ 之類以下劃線開始和結尾的定義。
3.3 變量名的命名規則
(1)變量的命名規則要求用“匈牙利法則”。即開頭字母用變量的類型,其余部分用變量的英文意思、英文的縮寫、中文全拼或中文全拼的縮寫,要求單詞的第一個字母應大寫。
即: 變量名=變量類型+變量的英文意思(或英文縮寫、中文全拼、中文全拼縮寫)
對非通用的變量,在定義時加入注釋說明,變量定義盡量可能放在函數的開始處。
見下表:
bool 用b開頭 bFlg
int 用i開頭 iCount
short int 用n開頭 nStepCount
long int 用l開頭 lSum
char? 用c開頭 cCount
unsigned char 用by開頭
float 用f開頭 fAvg
double 用d開頭 dDeta
unsigned int(WORD) 用w開頭 wCount
unsigned long int(DWORD) 用dw開頭 dwBroad
字符串 用s開頭 sFileName
用0結尾的字符串 用sz開頭 szFileName
(2)指針變量命名的基本原則為:
對一重指針變量的基本原則為:“p”+變量類型前綴+命名,如一個float*型應該表示為pfStat。對二重指針變量的基本規則為:“pp”+變量類型前綴+命名。對三重指針變量的基本規則為:“ppp”+變量類型前綴+命名。
(3)全局變量用g_開頭,如一個全局的長型變量定義為g_lFailCount。即:變量名=g_+變量類型+變量的英文意思(或縮寫)。此規則還可避免局部變量和全局變量同名而引起的問題。
(4)靜態變量用s_開頭,如一個靜態的指針變量定義為s_plPerv_Inst。即: 變量名=s_+變量類型+變量的英文意思(或縮寫)
(5)對枚舉類型(enum)中的變量,要求用枚舉變量或其縮寫做前綴。并且要求用大寫。如:
enum cmEMDAYS
{
EMDAYS_MONDAY;
EMDAYS_TUESDAY;
……
};
(6)對struct、union變量的命名要求定義的類型用大寫。并要加上前綴,其內部變量的命名規則與變量命名規則一致。
結構一般用S開頭,如:
struct ScmNPoint
{
int nX;//點的X位置
int nY; //點的Y位置
};
聯合體一般用U開頭,如:
union UcmLPoint
{
LONG lX;
LONG lY;
}
(7)對常量(包括錯誤的編碼)命名,要求常量名用大寫,常量名用英文表達其意思。當需要由多個單詞表示時,單詞與單詞之間必須采用連字符“_”連接。
如:#define CM_FILE_NOT_FOUND CMMAKEHR(0X20B) 其中CM表示類別。
(8)對const 的變量要求在變量的命名規則前加入c_。即:c_+變量命名規則;示例:const char* c_szFileName;
3.4 函數的命名規范
(1)函數的命名應該盡量用英文(或英文縮寫、中文全拼、中文全拼縮寫)表達出函數完成的功能——函數名應準確描述函數的功能。遵循動賓結構的命名法則,函數名中動詞在前,并在命名前加入函數的前綴,函數名的長度不得少于8個字母。函數名首字大寫,若包含有兩個單詞的每個單詞首字母大寫。如果是OOP 方法,可以只有動詞(名詞是對象本身)。示例:LONG GetDeviceCount(……);
void print_record( unsigned int rec_ind ) ;
int? input_record( void ) ;
unsigned char get_current_color( void ) ;
(2)避免使用無意義或含義不清的動詞為函數命名。如使用process、handle等為函數命名,因為這些動詞并沒有說明要具體做什么。
(3)必須使用函數原型聲明。函數原型聲明包括:引用外來函數及內部函數,外部引用必須在右側注明函數來源: 模塊名及文件名;內部函數,只要注釋其定義文件名——和調用者在同一文件中(簡單程序)時不需要注釋。
應確保每個函數聲明中的參數的名稱、類型和定義中的名稱、類型一致。
3.5 函數參數命名規范
(1)參數名稱的命名參照變量命名規范。(2)為了提高程序的運行效率,減少參數占用的堆棧,傳遞大結構的參數,一律采用指針或引用方式傳遞。
(3)為了便于其他程序員識別某個指針參數是入口參數還是出口參數,同時便于編譯器檢查錯誤,應該在入口參數前加入const標志。
如:……cmCopyString(const CHAR * c_szSource, CHAR * szDest)
3.6 文件名(包括動態庫、組件、控件、工程文件等)的命名規范
文件名的命名要求表達出文件的內容,要求文件名的長度不得少于5個字母,嚴禁使用象file1,myfile之類的文件名。非計算機專業C語言初學者編程規范(學生用)—可讀性
4.1 避免使用默認的運算優先級
注意運算符的優先級,并用括號明確表達式的操作順序,避免使用默認優先級,可防止閱讀程序時產生誤解,防止因默認的優先級與設計思想不符而導致程序出錯。示例:下列語句中的表達式
word = (high << 8) | low???? (1)
if ((a | b) && (a & c))????? (2)
if ((a | b) < (c & d))?????? (3)
如果書寫為:
high << 8 | low
a | b && a & c
a | b < c & d
由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不會出錯,但語句不易理解;a | b < c & d = a | (b < c) & d,(3)造成了判斷條件出錯。
4.2 使用有意義的標識,避免直接使用數字
避免使用不易理解的數字,用有意義的標識來替代。涉及物理狀態或者含有物理意義的常量,不應直接使用數字,必須用有意義的枚舉或宏來代替。示例:如下的程序可讀性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 1;
...? // program code
}
應改為如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 1
if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
...? // program code
}
4.3 源程序中關系較為緊密的代碼應盡可能相鄰
這樣做的好處是便于程序閱讀和查找。示例:以下代碼布局不太合理。rect.length = 10;
char_poi = str;
rect.width = 5;
若按如下形式書寫,可能更清晰一些。
rect.length = 10;
rect.width = 5; // 矩形的長與寬關系較密切,放在一起。
char_poi = str;
4.4 不要使用難懂的技巧性很高的語句、復雜的表達式
除非很有必要時,原則上不要使用難懂的技巧性很高的語句和復雜的表達式——高技巧語句不等于高效率的程序,源程序占用空間的節約并不等于目標程序占用空間的節約,實際上程序的效率關鍵在于算法。(1)如下表達式,考慮不周就可能出問題,也較難理解。
* stat_poi ++ += 1;
* ++ stat_poi += 1;
應分別改為如下:
*stat_poi += 1;
stat_poi++;???? // 此二語句功能相當于“ * stat_poi ++ += 1; ”
++ stat_poi;
*stat_poi += 1; // 此二語句功能相當于“ * ++ stat_poi += 1; ”
(2)如下表達式,不同的編譯器給出的結果不一樣,b[i]是否先執行?
x=b[i] + i++;
應改為:
x = b[i] + i;
i++;
非計算機專業C語言初學者編程規范(學生用)—變量與結構
5.1 謹慎使用全局(公共)變量
(1)去掉沒必要的公共變量。公共變量是增大模塊間耦合的原因之一,故應減少沒必要的公共變量以降低模塊間的耦合度。(2)仔細定義并明確公共變量的含義、作用、取值范圍及公共變量間的關系。在對變量聲明的同時,應對其含義、作用及取值范圍進行注釋說明,同時若有必要還應說明與其它變量的關系。
(3)防止局部變量與公共變量同名——通過使用較好的命名規則來消除此問題。
5.2 數據類型間的轉換
(1)編程時,要注意數據類型的強制轉換。當進行數據類型強制轉換時,其數據的意義、轉換后的取值等都有可能發生變化,而這些細節若考慮不周,就很有可能留下隱患。(2)對編譯系統默認的數據類型轉換,也要有充分的認識。
示例:如下賦值,多數編譯器不產生告警,但值的含義還是稍有變化。
char chr;
unsigned short int exam;
chr = -1;
exam = chr; // 編譯器不產生告警,此時exam為0xFFFF。
(3)盡量減少沒有必要的數據類型默認轉換與強制轉換。例如,所有的 unsigned類型都應該有后綴“U”以明確其類型。
(4)合理地設計數據并使用自定義數據類型,避免數據間進行不必要的類型轉換。
(5)對自定義數據類型進行恰當命名,使它成為自描述性的,以提高代碼可讀性。注意其命名方式在同一產品中的統一,并且保證沒有多重定義。使用自定義類型,可以彌補編程語言提供類型少、信息量不足的缺點,并能使程序清晰、簡潔。
示例:可參考如下方式聲明自定義數據類型。下面的聲明可使數據類型的使用簡潔、明了。
typedef unsigned char? BYTE;
typedef unsigned short WORD;
typedef unsigned int?? DWORD;
下面的聲明可使數據類型具有更豐富的含義。
typedef float DISTANCE;
typedef float SCORE;
(6)不要用八進制數——整型常數以”0“開始會被認為是8進制。示例:
code[1]=109
code[2]=100
code[3]=052
code[4]=071
如果是對總線消息初始化,會有危險。
非計算機專業C語言初學者編程規范(學生用)—函數與過程
6.1 函數的功能與規模設計
(1)函數應當短而精美,而且只做一件事。不要設計多用途面面俱到的函數,多功能集于一身的函數,很可能使函數的理解、測試、維護等變得困難。 一個函數應最多占滿1或2個屏幕(就象我們知道的那樣,ISO/ANSI的屏幕大小是80X24),只做一件事并且把它做好。
一個函數的最大長度與它的復雜度和縮進級別成反比。所以,如果如果你有一個概念上簡單(案,“簡單”是simple而不是easy)的函數,它恰恰包含著一個很長的case語句,這樣你不得不為不同的情況準備不懂的處理,那么這樣的長函數是沒問題的。
然而,如果你有一個復雜的函數,你猜想一個并非天才的高一學生可能看不懂得這個函數,你就應當努力把它減縮得更接近前面提到的最大函數長度限制。可以使用一些輔助函數,給它們取描述性的名字(如果你認為這些輔助函數的調用是性能關鍵的,可以讓編譯器把它們內聯進來,這比在單個函數內完成所有的事情通常要好些)。
對函數還存在另一個測量標準:局部變量的數目。這不該超過5到10個,否則你可能會弄錯。應當重新考慮這個函數,把它分解成小片。人類的大腦一般能同時記住7個不同的東西,超過這個數目就會犯糊涂。或許你認為自己很聰明,那么請你理解一下從現在開始的2周時間你都做什么了。
(2)為簡單功能編寫函數。
雖然為僅用一兩行就可完成的功能去編函數好象沒有必要,但用函數可使功能明確化,增加程序可讀性,亦可方便維護、測試。 示例:如下語句的功能不很明顯。
value = ( a > b ) ? a : b ;改為如下就很清晰了。
int max (int a, int b)
{
return ((a > b) ? a : b);
}
value = max (a, b);或改為如下。
#define MAX (a, b) (((a) > (b)) ? (a) : (b))
value = MAX (a, b);
當一個過程(函數)中對較長變量(一般是結構的成員)有較多引用時,可以用一個意義相當的宏代替——這樣可以增加編程效率和程序的可讀性。 示例:在某過程中較多引用TheReceiveBuffer[FirstSocket].byDataPtr,則可以通過以下宏定義來代替:# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr
(3)防止把沒有關聯的語句放到一個函數中,防止函數或過程內出現隨機內聚。
隨機內聚是指將沒有關聯或關聯很弱的語句放到同一個函數或過程中。隨機內聚給函數或過程的維護、測試及以后的升級等造成了不便,同時也使函數或過程的功能不明確。使用隨機內聚函數,常常容易出現在一種應用場合需要改進此函數,而另一種應用場合又不允許這種改進,從而陷入困境。
在編程時,經常遇到在不同函數中使用相同的代碼,許多開發人員都愿把這些代碼提出來,并構成一個新函數。若這些代碼關聯較大并且是完成一個功能的,那么這種構造是合理的,否則這種構造將產生隨機內聚的函數。
示例:如下函數就是一種隨機內聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的長與寬 */
Point.x = 10;
Point.y = 10;?? /* 初始化“點”的坐標 */
}
矩形的長、寬與點的坐標基本沒有任何關系,故以上函數是隨機內聚。應如下分為兩個函數:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的長與寬 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10;?? /* 初始化“點”的坐標 */
}
(4)如果多段代碼重復做同一件事情,那么在函數的劃分上可能存在問題。若此段代碼各語句之間有實質性關聯并且是完成同一件功能的,那么可考慮把此段代碼構造成一個新的函數。
(5)減少函數本身或函數間的遞歸調用。遞歸調用特別是函數間的遞歸調用(如A->B->C->A),影響程序的可理解性;遞歸調用一般都占用較多的系統資源(如棧空間);遞歸調用對程序的測試有一定影響。故除非為某些算法或功能的實現方便,應減少沒必要的遞歸調用,對于safe-related 系統不能用遞歸,因為超出堆棧空間很危險。
6.2 函數的返回值
(1)對于函數的返回位置,盡量保持單一性,即一個函數盡量做到只有一個返回位置。(單入口單出口)。要求大家統一函數的返回值,所有的函數的返回值都將以編碼的方式返回。
例如編碼定義如下:
#define CM_POINT_IS_NULL CMMAKEHR(0X200)
:
:
建議函數實現如下:
LONG 函數名(參數,……)
{
LONG lResult; //保持錯誤號
lResult=CM_OK;
//如果參數有錯誤則返回錯誤號
if(參數==NULL)
{
lResult=CM_POINT_IS_NULL;
goto END;
}
……
END:
return lResult;
}
(2)除非必要,最好不要把與函數返回值類型不同的變量,以編譯系統默認的轉換方式或強制的轉換方式作為返回值返回。
(3)函數的返回值要清楚、明了,讓使用者不容易忽視錯誤情況。函數的每種出錯返回值的意義要清晰、明了、準確,防止使用者誤用、理解錯誤或忽視錯誤返回碼。
(4)函數的功能應該是可以預測的,也就是只要輸入數據相同就應產生同樣的輸出。帶有內部“存儲器”的函數的功能可能是不可預測的,因為它的輸出可能取決于內部存儲器(如某標記)的狀態。這樣的函數既不易于理解又不利于測試和維護。在C/C++語言中,函數的static局部變量是函數的內部存儲器,有可能使函數的功能不可預測,然而,當某函數的返回值為指針類型時,則必須是STATIC的局部變量的地址作為返回值,若為AUTO類,則返回為錯針。
示例:如下函數,其返回值(即功能)是不可預測的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static類型的。
????????????????????????????? // 若改為auto類型,則函數即變為可預測。
for (index = 1; index <= base; index++)
{
? sum += index;
}
return sum;
}
6.3 函數參數
(1)只當你確實需要時才用全局變量,函數間應盡可能使用參數、返回值傳遞消息。(2)防止將函數的參數作為工作變量。將函數的參數作為工作變量,有可能錯誤地改變參數內容,所以很危險。對必須改變的參數,最好先用局部變量代之,最后再將該局部變量的內容賦給該參數。
示例:下函數的實現不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count;
*sum = 0;
for (count = 0; count < num; count++)
{
? *sum? += data[count]; // sum成了工作變量,不太好。
}
}
若改為如下,則更好些。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count ;
int sum_temp;
sum_temp = 0;
for (count = 0; count < num; count ++)
{
? sum_temp? += data[count];
}
*sum = sum_temp;
}
非計算機專業C語言初學者編程規范(學生用)—效率
(1)編程時要經常注意代碼的效率。
代碼效率分為全局效率、局部效率、時間效率及空間效率。全局效率是站在整個系統的角度上的系統效率;局部效率是站在模塊或函數角度上的效率;時間效率是程序處理輸入任務所需的時間長短;空間效率是程序所需內存空間,如機器代碼空間大小、數據空間大小、棧空間大小等。(2)在保證軟件系統的正確性、穩定性、可讀性及可測性的前提下,提高代碼效率。不能一味地追求代碼效率,而對軟件的正確性、穩定性、可讀性及可測性造成影響。
(3)局部效率應為全局效率服務,不能因為提高局部效率而對全局效率造成影響。
(4)循環體內工作量最小化。
應仔細考慮循環體內的語句是否可以放在循環體之外,使循環體內工作量最小,從而提高程序的時間效率。示例:如下代碼效率不高。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
back_sum = sum; /* backup sum */
}
語句“back_sum = sum;”完全可以放在for語句之后,如下。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
??? sum += ind;
}
back_sum? = sum; /* backup sum */
(5)不應花過多的時間拼命地提高調用不很頻繁的函數代碼效率。
對代碼優化可提高效率,但若考慮不周很有可能引起嚴重后果。(6)在多重循環中,應將最忙的循環放在最內層,以減少CPU切入循環層的次數。
示例:如下代碼效率不高。for (row = 0; row < 100; row++)
{
for (col = 0; col < 5; col++)
{
? sum += a[row][col];
}
}
可以改為如下方式,以提高效率。
for (col = 0; col < 5; col++)
{
for (row = 0; row < 100; row++)
{
? sum += a[row][col];
}
}
(7)盡量減少循環嵌套層次。
(8)避免循環體內含判斷語句,應將循環語句置于判斷語句的代碼塊之中。
目的是減少判斷次數。循環體中的判斷語句是否可以移到循環體外,要視程序的具體情況而言,一般情況,與循環變量無關的判斷語句可以移到循環體外,而有關的則不可以。示例:如下代碼效率稍低。
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
if (data_type == RECT_AREA)
{
? area_sum += rect_area[ind];
??? } else
??? {
? rect_length_sum += rect[ind].length;
? rect_width_sum += rect[ind].width;
}
}
因為判斷語句與循環變量無關,故可如下改進,以減少判斷次數。
if (data_type == RECT_AREA)
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
? area_sum += rect_area[ind];
}
} else
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
? rect_length_sum += rect[ind].length;
? rect_width_sum? += rect[ind].width;
}
}
(9)盡量用乘法或其它方法代替除法,特別是浮點運算中的除法——浮點運算除法要占用較多CPU資源。
示例:如下表達式運算可能要占較多CPU資源。#define PAI 3.1416
radius = circle_length / (2 * PAI);
應如下把浮點除法改為浮點乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 編譯器編譯時,將生成具體浮點數
radius = circle_length * PAI_RECIPROCAL / 2;
(10)不要一味追求緊湊的代碼,因為緊湊的代碼并不代表高效的機器碼。
非計算機專業C語言初學者編程規范(學生用)—質量保證
(1)代碼質量保證優先原則:
①正確性,指程序要實現設計要求的功能。②穩定性、安全性,指程序穩定、可靠、安全。
③可測試性,指程序要具有良好的可測試性。
④規范/可讀性,指程序書寫風格、命名規則等要符合規范。
⑤全局效率,指軟件系統的整體效率。
⑥局部效率,指某個模塊/子模塊/函數的本身效率。
⑦個人表達方式/個人方便性,指個人編程習慣。
(2)過程/ 函數中分配的內存,在過程/ 函數退出之前要釋放,過程/ 函數中申請的(為打開文件而使用的)文件句柄,在過程/ 函數退出之前要關閉。
分配的內存不釋放以及文件句柄不關閉,是較常見的錯誤,而且稍不注意就有可能發生。這類錯誤往往會引起很嚴重后果,且難以定位。示例:下函數在退出之前,沒有把分配的內存釋放。
typedef unsigned char BYTE;
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE *) malloc (MAX_GT_LENGTH);
...? //program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
? return GT_LENGTH_ERROR; // 忘了釋放gt_buf
}
...? // other program code
free( gt_buf? );
}
(3)防止內存操作越界。
內存操作主要是指對數組、指針、內存地址等的操作。內存操作越界是軟件系統主要錯誤之一,后果往往非常嚴重,所以當我們進行這些操作時一定要仔細小心。(4)編程時,要防止差1 錯誤。
此類錯誤一般是由于把“<=”誤寫成“<”或“>=”誤寫成“>”等造成的,由此引起的后果,很多情況下是很嚴重的,所以編程時,一定要在這些地方小心。當編完程序后,應對這些操作符進行徹底檢查。(5)要時刻注意易混淆的操作符。
當編完程序后,應從頭至尾檢查一遍這些操作符,以防止拼寫錯誤。形式相近的操作符最容易引起誤用,如C/C++中的“=”與“==”、“|”與“||”、“&”與“&&”等,若拼寫錯了,編譯器不一定能夠檢查出來。(6)有可能的話,if 語句盡量加上else 分支,對沒有else 分支的語句要小心對待;switch 語句必須有default 分支。
(7)不要濫用goto 語句。
goto語句會破壞程序的結構性,所以除非確實需要,最好不使用goto語句。(8)sizeof操作符不能用在包含邊界作用(side effect)的表達式上。
例:Int32_t = i;
Int32_t = j;
j = sizeof(i = 1234);
表達式i = 1234并沒有執行,只是得到表達式類型int的size。
(9)邏輯操作符&&或者||右邊不能包含邊界作用(side effect)。
例:if (ishight) && (x == i++))
如果ishight=0那么i++不會被執行。
(10)++和--不能和其他表達式用在一個表達式中。
(11)賦值語句不能用在一個產生布爾值的表達式中。
例:if ((x = y) != 0)…
更差的用法:
if (x = y)…
(12)浮點表達式不應該測試其是否相等或者不相等,for控制表達式中不要包含任何浮點類型。
(13)數字變量作為for循環的循環計數不要在循環體內部被修改。
例:flag=1;
for (i=0;(i<5)&&(flag==1);i++)
{
flag=0;
i=i+3;
}
(14)non-void類型函數的所有出口路徑都應該有一個明確的return語句表達式。
(15)不要用2級以上指針。
(16)不要輕易使用用Union。
確需使用Union時,一定要注意和清楚在聯合體的存儲方式(如位填充、對齊方式、位順序等)上,所使用編譯器的處理方法。(17)標準庫中的保留標識符,宏和函數不能定義、重定義和undefined。
(18)時刻注意表達式是否會上溢、下溢。
示例:如下程序將造成變量下溢。unsigned char size ;
while (size-- >= 0) // 將出現下溢
{
... // program code
}
當size等于0時,再減1不會小于0,而是0xFF,故程序是一個死循環。應如下修改。
char size; // 從unsigned char 改為char
while (size-- >= 0)
{
... // program code
}
(19)使用變量時要注意其邊界值的情況。
示例:如C語言中字符型變量,有效值范圍為-128到127。故以下表達式的計算存在一定風險。char chr = 127;
int sum = 200;
chr += 1; // 127為chr的邊界值,再加1將使chr上溢到-128,而不是128。
sum += chr; // 故sum的結果不是328,而是72。
若chr與sum為同一種類型,或表達式按如下方式書寫,可能會好些。
sum = sum + chr + 1;
(20)系統應具有一定的容錯能力,對一些錯誤事件(如用戶誤操作等)能進行自動補救。
非計算機專業C語言初學者編程規范(學生用)—宏
(1)用宏定義表達式時,要使用完備的括號。
示例:如下定義的宏都存在一定的風險。#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)
正確的定義應為:
#define RECTANGLE_AREA( a, b ) ((a) * (b))
(2)將宏所定義的多條表達式放在大括號中。
示例:下面的語句只有宏的第一條表達式被執行。為了說明問題,for語句的書寫稍不符規范。#define INTI_RECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );
正確的用法應為:
#define INTI_RECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}
(3)使用宏時,不允許參數發生變化。
示例:如下用法可能導致錯誤。#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 結果:a = 7,即執行了兩次增1。
正確的用法是:
b = SQUARE( a );
a++; // 結果:a = 6,即只執行了一次增1。
華為C語言編程規范(1)—總則
編程規范總則
1 排版
2 注釋
3 標識符命名
4 可讀性
5 變量、結構
6 函數、過程
7 程序效率
8 質量保證1-1:程序塊要采用縮進風格編寫,縮進的空格數為4 個。
說明:對于由開發工具自動生成的代碼可以有不一致。
1-2:相對獨立的程序塊之間、變量說明之后必須加空行。
示例:如下例子不符合規范。
if (!valid_ni(ni))
{
... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
應如下書寫
if (!valid_ni(ni))
{
... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
Generated by Foxit PDF Creator ? Foxit Software
http://www.foxitsoftware.com For evaluation only.
1-3:較長的語句(>80 字符)要分成多行書寫,長表達式要在低優先級操作符處劃分新行,操作符放在新行之首,劃分出的新行要進行適當的縮進,使排版整齊,語句可讀。
示例:
perm_count_msg.head.len = NO7_TO_STAT_PERM_COUNT_LEN+ STAT_SIZE_PER_FRAM * sizeof( _UL );
act_task_table[frame_id * STAT_TASK_CHECK_NUMBER + index].occupied= stat_poi[index].occupied;
act_task_table[taskno].duration_true_or_false= SYS_get_sccp_statistic_state( stat_item );
report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)&& (n7stat_stat_item_valid (stat_item))&& (act_task_table[taskno].result_data != 0));
1-4:不允許把多個短語句寫在一行中,即一行只寫一條語句。
示例:如下例子不符合規范。
rect.length = 0; rect.width = 0;
應如下書寫
rect.length = 0;
rect.width = 0;
1-5:if、for、do、while、case、switch、default 等語句自占一行,且if、for、do、while
等語句的執行語句部分無論多少都要加括號{}。
示例:如下例子不符合規范。
if (pUserCR == NULL) return;
應如下書寫:
if (pUserCR == NULL)
{
return;
}
1-6:對齊只使用空格鍵,不使用TAB 鍵。
說明:以免用不同的編輯器閱讀程序時,因TAB 鍵所設置的空格數目不同而造成程序布局不整齊,不要使用BC 作為編輯器合版本,因為BC 會自動將8 個空格變為一個TAB 鍵,因此使用BC 合入的版本大多會將縮進變亂。
1-7:函數或過程的開始、結構的定義及循環、判斷等語句中的代碼都要采用縮進風格,case
語句下的情況處理語句也要遵從語句縮進要求。
1-8:程序塊的分界符(如C/C++語言的大括號‘{’和‘}’)應各獨占一行并且位于同一列,同時與引用它們的語句左對齊。在函數體的開始、類的定義、結構的定義、枚舉的定義以及if、for、do、while、switch、case 語句中的程序都要采用如上的縮進方式。
示例:如下例子不符合規范。
for (...) {
... // program code
}
if (...)
{
... // program code
}
void example_fun( void )
{
... // program code
}
應如下書寫。
for (...)
{
... // program code
}
if (...)
{
... // program code
}
void example_fun( void )
{
... // program code
}
1-9:一行程序以小于80 字符為宜,不要寫得過長。
華為C語言編程規范(3)—注釋
2-1:一般情況下,源程序有效注釋量必須在20%以上。說明:注釋的原則是有助于對程序的閱讀理解,在該加的地方都加了,注釋不宜太多也不能太少,注釋語言必須準確、易懂、簡潔。
2-2:文件頭部應進行注釋,注釋必須列出:版權說明、版本號、生成日期、作者、內容、功能、修改日志等。
示例:下面這段頭文件的頭注釋比較標準,當然,并不局限于此格式,但上述信息建議要包含在內。
/*****************************************************************************
Copyright: 1988-1999, Huawei Tech. Co., Ltd.
File name: 文件名
Description: 用于詳細說明此程序文件完成的主要功能,與其他模塊或函數的接口,輸出值、取值范圍、含義及參數間的控制、順序、獨立或依賴等關系
Author: 作者
Version: 版本
Date: 完成日期
History: 修改歷史記錄列表, 每條修改記錄應包括修改日期、修改者及修改內容簡述。
*****************************************************************************/
2-3:函數頭部應進行注釋,列出:函數的目的/功能、輸入參數、輸出參數、返回值、調用關系(函數、表)等。
示例:下面這段函數的注釋比較標準,當然,并不局限于此格式,但上述信息建議要包含在內。
/*************************************************
Function: // 函數名稱
Description: // 函數功能、性能等的描述
Calls: // 被本函數調用的函數清單
Called By: // 調用本函數的函數清單
Table Accessed: // 被訪問的表(此項僅對于牽扯到數據庫操作的程序)
Table Updated: // 被修改的表(此項僅對于牽扯到數據庫操作的程序)
Input: // 輸入參數說明,包括每個參數的作// 用、取值說明及參數間關系。
Output: // 對輸出參數的說明。
Return: // 函數返回值的說明
Others: // 其它說明
*************************************************/
2-4:邊寫代碼邊注釋,修改代碼同時修改相應的注釋,以保證注釋與代碼的一致性。不再有用的注釋要刪除。
2-5:注釋的內容要清楚、明了,含義準確,防止注釋二義性。說明:錯誤的注釋不但無益反而有害。
2-6:注釋應與其描述的代碼相近,對代碼的注釋應放在其上方或右方(對單條語句的注釋)相鄰位置,不可放在下面,如放于上方則需與其上面的代碼用空行隔開。
示例:如下例子不符合規范。
例1:
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
例2:
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
/* get replicate sub system index and net indicator */
應如下書寫
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
2-7:對于所有有物理含義的變量、常量,如果其命名不是充分自注釋的,在聲明時都必須加以注釋,說明其物理含義。變量、常量、宏的注釋應放在其上方相鄰位置或右方。
示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
2-8:數據結構聲明(包括數組、結構、類、枚舉等),如果其命名不是充分自注釋的,必須加以注釋。對數據結構的注釋應放在其上方相鄰位置,不可放在下面;對結構中的每個域的注釋放在此域的右方。
示例:可按如下形式說明枚舉/數據/聯合結構。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not */
/* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
2-9:全局變量要有較詳細的注釋,包括對其功能、取值范圍、哪些函數或過程存取它以及存取時注意事項等的說明。
示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */ // 變量作用、含義
/* 0 - SUCCESS 1 - GT Table error */
/* 2 - GT error Others - no use */ // 變量取值范圍
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
/* the function GetGTTransErrorCode() */ // 使用方法
BYTE g_GTTranErrorCode;
2-10:注釋與所描述內容進行同樣的縮排。
說明:可使程序排版整齊,并方便注釋的閱讀與理解。示例:如下例子,排版不整齊,閱讀稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
應改為如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
2-11:避免在一行代碼或表達式的中間插入注釋。
說明:除非必要,不應在代碼或表達中間插入注釋,否則容易使代碼可理解性變差。
2-12:通過對函數或過程、變量、結構等正確的命名以及合理地組織代碼的結構,使代碼成為自注釋的。
說明:清晰準確的函數、變量等的命名,可增加代碼可讀性,并減少不必要的注釋。
2-13:在代碼的功能、意圖層次上進行注釋,提供有用、額外的信息。
說明:注釋的目的是解釋代碼的目的、功能和采用的方法,提供代碼以外的信息,幫助讀者理解代碼,防止沒必要的重復注釋信息。
示例:如下注釋意義不大。
/* if receive_flag is TRUE */
if (receive_flag)
而如下的注釋則給出了額外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)
2-14:在程序塊的結束行右方加注釋標記,以表明某程序塊的結束。
說明:當代碼段較長,特別是多重嵌套時,這樣做可以使代碼更清晰,更便于閱讀。示例:參見如下例子。
if (...)
{
// program code
while (index < MAX_INDEX)
{
// program code
} /* end of while (index < MAX_INDEX) */ // 指明該條while 語句結束
} /* end of if (...)*/ // 指明是哪條if 語句結束
2-15:注釋格式盡量統一,建議使用“/* …… */”。
2-16:注釋應考慮程序易讀及外觀排版的因素,使用的語言若是中、英兼有的,建議多使用中文,除非能用非常流利準確的英文表達。
說明:注釋語言不統一,影響程序易讀性和外觀排版,出于對維護人員的考慮,建議使用中文。
3-1:標識符的命名要清晰、明了,有明確含義,同時使用完整的單詞或大家基本可以理解的縮寫,避免使人產生誤解。
說明:較短的單詞可通過去掉“元音”形成縮寫;較長的單詞可取單詞的頭幾個字母形成縮寫;一些單詞有大家公認的縮寫。示例:如下單詞的縮寫能夠被大家基本認可。
temp 可縮寫為 tmp ;
flag 可縮寫為 flg ;
statistic 可縮寫為 stat ;
increment 可縮寫為 inc ;
message 可縮寫為 msg ;
3-2:命名中若使用特殊約定或縮寫,則要有注釋說明。
說明:應該在源文件的開始之處,對文件中所使用的縮寫或約定,特別是特殊的縮寫,進行必要的注釋說明。
3-3:自己特有的命名風格,要自始至終保持一致,不可來回變化。
說明:個人的命名風格,在符合所在項目組或產品組的命名規則的前提下,才可使用。(即命名規則中沒有規定到的地方才可有個人命名風格)。
3-4:對于變量命名,禁止取單個字符(如i、j、k...),建議除了要有具體含義外,還能表明其變量類型、數據類型等,但i、j、k 作局部循環變量是允許的。
說明:變量,尤其是局部變量,如果用單個字符表示,很容易敲錯(如i 寫成j),而編譯時又檢查不出來,有可能為了這個小小的錯誤而花費大量的查錯時間。
示例:下面所示的局部變量名的定義方法可以借鑒。
int liv_Width
其變量名解釋如下:
l 局部變量(Local) (其它:g 全局變量(Global)...)
i 數據類型(Interger)
v 變量(Variable) (其它:c 常量(Const)...)
Width 變量含義
這樣可以防止局部變量與全局變量重名。
3-5:命名規范必須與所使用的系統風格保持一致,并在同一項目中統一,比如采用UNIX的全小寫加下劃線的風格或大小寫混排的方式,不要使用大小寫與下劃線混排的方式,用作特殊標識如標識成員變量或全局變量的m_和g_,其后加上大小寫混排的方式是允許的。
示例: Add_User 不允許,add_user、AddUser、m_AddUser 允許。
3-6:除非必要,不要用數字或較奇怪的字符來定義標識符。
示例:如下命名,使人產生疑惑。
#define _EXAMPLE_0_TEST_
#define _EXAMPLE_1_TEST_
void set_sls00( BYTE sls );
應改為有意義的單詞命名
#define _EXAMPLE_UNIT_TEST_
#define _EXAMPLE_ASSERT_TEST_
void set_udt_msg_sls( BYTE sls );
3-7:在同一軟件產品內,應規劃好接口部分標識符(變量、結構、函數及常量)的命名,防止編譯、鏈接時產生沖突。
說明:對接口部分的標識符應該有更嚴格限制,防止沖突。如可規定接口部分的變量與常量之前加上“模塊”標識等。
3-8:用正確的反義詞組命名具有互斥意義的變量或相反動作的函數等。
說明:下面是一些在軟件中常用的反義詞組。
add / remove begin / end create / destroy
insert / delete first / last get / release
increment / decrement put / get
add / delete lock / unlock open / close
min / max old / new start / stop
next / previous source / target show / hide
send / receive source / destination
cut / paste up / down
示例:
int min_sum;
int max_sum;
int add_user( BYTE *user_name );
int delete_user( BYTE *user_name );
3-9:除了編譯開關/頭文件等特殊應用,應避免使用_EXAMPLE_TEST_之類以下劃線開始和結尾的定義。4-1:注意運算符的優先級,并用括號明確表達式的操作順序,避免使用默認優先級。
說明:防止閱讀程序時產生誤解,防止因默認的優先級與設計思想不符而導致程序出錯。
示例:下列語句中的表達式
word = (high << 8) | low (1)
if ((a | b) && (a & c)) (2)
if ((a | b) < (c & d)) (3)
如果書寫為
high << 8 | low
a | b && a & c
a | b < c & d
由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不會出錯,但語句不易理解;a | b < c & d = a | (b < c) & d,(3)造成了判斷條件出錯。
4-2:避免使用不易理解的數字,用有意義的標識來替代。
涉及物理狀態或者含有物理意義的常量,不應直接使用數字,必須用有意義的枚舉或宏來代替。示例:如下的程序可讀性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 1;
... // program code
}
應改為如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 1
if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
... // program code
}
4-3:源程序中關系較為緊密的代碼應盡可能相鄰。
說明:便于程序閱讀和查找。
示例:以下代碼布局不太合理。
rect.length = 10;
char_poi = str;
rect.width = 5;
若按如下形式書寫,可能更清晰一些。
rect.length = 10;
rect.width = 5; // 矩形的長與寬關系較密切,放在一起。
char_poi = str;
4-4:不要使用難懂的技巧性很高的語句,除非很有必要時。
說明:高技巧語句不等于高效率的程序,實際上程序的效率關鍵在于算法。
示例:如下表達式,考慮不周就可能出問題,也較難理解。
* stat_poi ++ += 1;
* ++ stat_poi += 1;
應分別改為如下。
*stat_poi += 1;
stat_poi++; // 此二語句功能相當于“ * stat_poi ++ += 1; ”
++ stat_poi;
*stat_poi += 1; // 此二語句功能相當于“ * ++ stat_poi += 1; ”5-1:去掉沒必要的公共變量。
說明:公共變量是增大模塊間耦合的原因之一,故應減少沒必要的公共變量以降低模塊間的耦合度。
5-2:仔細定義并明確公共變量的含義、作用、取值范圍及公共變量間的關系。
說明:在對變量聲明的同時,應對其含義、作用及取值范圍進行注釋說明,同時若有必要還應說明與其它變量的關系。
5-3:明確公共變量與操作此公共變量的函數或過程的關系,如訪問、修改及創建等。
說明:明確過程操作變量的關系后,將有利于程序的進一步優化、單元測試、系統聯調以及代碼維護等。這種關系的說明可在注釋或文檔中描述。
示例:在源文件中,可按如下注釋形式說明。
RELATION System_Init Input_Rec Print_Rec Stat_Score?Student Create Modify Access Access?Score Create Modify Access Access, Modify
注:RELATION 為操作關系;System_Init、Input_Rec、Print_Rec、Stat_Score 為四個不同的函數;Student、Score 為兩個全局變量;Create 表示創建,Modify 表示修改,Access 表示訪
問。其中,函數Input_Rec、Stat_Score 都可修改變量Score,故此變量將引起函數間較大的耦合,并可能增加代碼測試、維護的難度。
5-4:當向公共變量傳遞數據時,要十分小心,防止賦與不合理的值或越界等現象發生。
說明:對公共變量賦值時,若有必要應進行合法性檢查,以提高代碼的可靠性、穩定性。
5-5:防止局部變量與公共變量同名。
說明:若使用了較好的命名規則,那么此問題可自動消除。
5-6:嚴禁使用未經初始化的變量作為右值。
說明:特別是在C/C++中引用未經賦值的指針,經常會引起系統崩潰。
5-7:結構的功能要單一,是針對一種事務的抽象。
說明:設計結構時應力爭使結構代表一種現實事務的抽象,而不是同時代表多種。結構中的各元素應代表同一事務的不同側面,而不應把描述沒有關系或關系很弱的不同事務的元素放到同一結構中。
示例:如下結構不太清晰、合理。
typedef struct STUDENT_STRU
{
unsigned char name[8]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned char
teacher_name[8]; /* the student teacher's name */
unisgned char
teacher_sex; /* his teacher sex */
} STUDENT;
若改為如下,可能更合理些。
typedef struct TEACHER_STRU
{
unsigned char name[8]; /* teacher name */
unisgned char sex; /* teacher sex, as follows */
/* 0 - FEMALE; 1 - MALE */
} TEACHER;
typedef struct STUDENT_STRU
{
unsigned char name[8]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* his teacher index */
} STUDENT;
5-8:不要設計面面俱到、非常靈活的數據結構。
說明:面面俱到、靈活的數據結構反而容易引起誤解和操作困難。
5-9:不同結構間的關系不要過于復雜。
說明:若兩個結構間關系較復雜、密切,那么應合為一個結構。
示例:如下兩個結構的構造不合理。
typedef struct PERSON_ONE_STRU
{
unsigned char name[8];
unsigned char addr[40];
unsigned char sex;
unsigned char city[15];
} PERSON_ONE;
typedef struct PERSON_TWO_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char tel;
} PERSON_TWO;
由于兩個結構都是描述同一事物的,那么不如合成一個結構。
typedef struct PERSON_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char sex;
unsigned char addr[40];
unsigned char city[15];
unsigned char tel;
} PERSON;
5-10:結構中元素的個數應適中。若結構中元素個數過多可考慮依據某種原則把元素組成不同的子結構,以減少原結構中元素的個數。
說明:增加結構的可理解性、可操作性和可維護性。
示例:假如認為如上的_PERSON 結構元素過多,那么可如下對之劃分。
typedef struct PERSON_BASE_INFO_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char sex;
} PERSON_BASE_INFO;
typedef struct PERSON_ADDRESS_STRU
{
unsigned char addr[40];
unsigned char city[15];
unsigned char tel;
} PERSON_ADDRESS;
typedef struct PERSON_STRU
{
PERSON_BASE_INFO person_base;
PERSON_ADDRESS person_addr;
} PERSON;
5-11:仔細設計結構中元素的布局與排列順序,使結構容易理解、節省占用空間,并減少引起誤用現象。
說明:合理排列結構中元素順序,可節省空間并增加可理解性。
示例:如下結構中的位域排列,將占較大空間,可讀性也稍差。
typedef struct EXAMPLE_STRU
{
unsigned int valid: 1;
PERSON person;
unsigned int set_flg: 1;
} EXAMPLE;
若改成如下形式,不僅可節省1 字節空間,可讀性也變好了。
typedef struct EXAMPLE_STRU
{
unsigned int valid: 1;
unsigned int set_flg: 1;
PERSON person ;
} EXAMPLE;
5-12:編程時,要注意數據類型的強制轉換。
說明:當進行數據類型強制轉換時,其數據的意義、轉換后的取值等都有可能發生變化,而這些細節若考慮不周,就很有可能留下隱患。
5-13:對編譯系統默認的數據類型轉換,也要有充分的認識。
示例:如下賦值,多數編譯器不產生告警,但值的含義還是稍有變化。
char chr;
unsigned short int exam;
chr = -1;
exam = chr; // 編譯器不產生告警,此時exam 為0xFFFF。
5-14:盡量減少沒有必要的數據類型默認轉換與強制轉換。
5-15:合理地設計數據并使用自定義數據類型,避免數據間進行不必要的類型轉換。
5-16:對自定義數據類型進行恰當命名,使它成為自描述性的,以提高代碼可讀性。注意其命名方式在同一產品中的統一。
說明:使用自定義類型,可以彌補編程語言提供類型少、信息量不足的缺點,并能使程序清晰、簡潔。
示例:可參考如下方式聲明自定義數據類型。下面的聲明可使數據類型的使用簡潔、明了。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int DWORD;
下面的聲明可使數據類型具有更豐富的含義。
typedef float DISTANCE;
typedef float SCORE;6-1:對所調用函數的錯誤返回碼要仔細、全面地處理。
6-2:明確函數功能,精確(而不是近似)地實現函數設計。
6-3:編寫可重入函數時,應注意局部變量的使用(如編寫C/C++語言的可重入函數時,應使用auto 即缺省態局部變量或寄存器變量)。
說明:編寫C/C++語言的可重入函數時,不應使用static 局部變量,否則必須經過特殊處理,才能使函數具有可重入性。
6-4:編寫可重入函數時,若使用全局變量,則應通過關中斷、信號量(即P、V 操作)等手段對其加以保護。
說明:若對所使用的全局變量不加以保護,則此函數就不具有可重入性,即當多個進程調用此函數時,很有可能使有關全局變量變為不可知狀態。
示例:假設Exam 是int 型全局變量,函數Squre_Exam 返回Exam 平方值。那么如下函數不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函數若被多個進程調用的話,其結果可能是未知的,因為當(**)語句剛執行完后,另外一個使用本函數的進程可能正好被激活,那么當新激活的進程執行到此函數時,將使Exam賦與另一個不同的para 值,所以當控制重新回到“temp = Square_Exam( )”后,計算出的temp很可能不是預想中的結果。此函數應如下改進。
unsigned int example( int para )
{
unsigned int temp;
[申請信號量操作] // 若申請不到“信號量”,說明另外的進程正處于Exam = para; // 給Exam 賦值并計算其平方過程中(即正在使用此temp = Square_Exam( ); // 信號),本進程必須等待其釋放信號后,才可繼[釋放信號量操作]
// 續執行。若申請到信號,則可繼續執行,但其
// 它進程必須等待本進程釋放信號量后,才能再使
// 用本信號。
return temp;
}
6-5:在同一項目組應明確規定對接口函數參數的合法性檢查應由函數的調用者負責還是由接口函數本身負責,缺省是由函數調用者負責。
說明:對于模塊間接口函數的參數的合法性檢查這一問題,往往有兩個極端現象,即:要么是調用者和被調用者對參數均不作合法性檢查,結果就遺漏了合法性檢查這一必要的處理過程,造成問題隱患;要么就是調用者和被調用者均對參數進行合法性檢查,這種情況雖不會造成問題,但產生了冗余代碼,降低了效率。
6-6:函數的規模盡量限制在200 行以內。
說明:不包括注釋和空格行。
6-7:一個函數僅完成一件功能,不要設計多用途面面俱到的函數。
說明:多功能集于一身的函數,很可能使函數的理解、測試、維護等變得困難。
6-8:函數的功能應該是可以預測的,也就是只要輸入數據相同就應產生同樣的輸出。
說明:帶有內部“存儲器”的函數的功能可能是不可預測的,因為它的輸出可能取決于內部存儲器(如某標記)的狀態。這樣的函數既不易于理解又不利于測試和維護。在C/C++語言中,函數的static 局部變量是函數的內部存儲器,有可能使函數的功能不可預測,然而,當某函數的返回值為指針類型時,則必須是STATIC的局部變量的地址作為返回值,若為AUTO類,則返回為錯針。
示例:如下函數,其返回值(即功能)是不可預測的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static 類型的。
// 若改為auto 類型,則函數即變為可預測。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
6-9:盡量不要編寫依賴于其他函數內部實現的函數。
說明:此條為函數獨立性的基本要求。由于目前大部分高級語言都是結構化的,所以通過具體語言的語法要求與編譯器功能,基本就可以防止這種情況發生。但在匯編語言中,由于其靈活性,很可能使函數出現這種情況。
示例:如下是在DOS 下TASM 的匯編程序例子。過程Print_Msg 的實現依賴于Input_Msg的具體實現,這種程序是非結構化的,難以維護、修改。
... // 程序代碼
proc Print_Msg // 過程(函數)Print_Msg
... // 程序代碼
jmp LABEL
... // 程序代碼
endp
proc Input_Msg // 過程(函數)Input_Msg
... // 程序代碼
LABEL:
... // 程序代碼
endp
6-10:檢查函數所有參數輸入的有效性。
6-11:檢查函數所有非參數輸入的有效性,如數據文件、公共變量等。
說明:函數的輸入主要有兩種:一種是參數輸入;另一種是全局變量、數據文件的輸入,即非參數輸入。函數在使用輸入之前,應進行必要的檢查。
6-12:函數名應準確描述函數的功能。
6-13:使用動賓詞組為執行某操作的函數命名。如果是OOP 方法,可以只有動詞(名詞是對象本身)。
示例:參照如下方式命名函數。
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;
6-14:避免使用無意義或含義不清的動詞為函數命名。
說明:避免用含義不清的動詞如process、handle 等為函數命名,因為這些動詞并沒有說明要具體做什么。
6-15:函數的返回值要清楚、明了,讓使用者不容易忽視錯誤情況。
說明:函數的每種出錯返回值的意義要清晰、明了、準確,防止使用者誤用、理解錯誤或忽視錯誤返回碼。
6-16:除非必要,最好不要把與函數返回值類型不同的變量,以編譯系統默認的轉換方式或強制的轉換方式作為返回值返回。
6-17:讓函數在調用點顯得易懂、容易理解。
6-18:在調用函數填寫參數時,應盡量減少沒有必要的默認數據類型轉換或強制數據類型轉換。
說明:因為數據類型轉換或多或少存在危險。
6-19:避免函數中不必要語句,防止程序中的垃圾代碼。
說明:程序中的垃圾代碼不僅占用額外的空間,而且還常常影響程序的功能與性能,很可能給程序的測試、維護等造成不必要的麻煩。
6-20:防止把沒有關聯的語句放到一個函數中。
說明:防止函數或過程內出現隨機內聚。隨機內聚是指將沒有關聯或關聯很弱的語句放到同一個函數或過程中。隨機內聚給函數或過程的維護、測試及以后的升級等造成了不便,同時也使函數或過程的功能不明確。使用隨機內聚函數,常常容易出現在一種應用場合需要改進此函數,而另一種應用場合又不允許這種改進,從而陷入困境。在編程時,經常遇到在不同函數中使用相同的代碼,許多開發人員都愿把這些代碼提出來,并構成一個新函數。若這些代碼關聯較大并且是完成一個功能的,那么這種構造是合理的,否則這種構造將產生隨機內聚的函數。
示例:如下函數就是一種隨機內聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的長與寬 */
Point.x = 10;
Point.y = 10; /* 初始化“點”的坐標 */
}
矩形的長、寬與點的坐標基本沒有任何關系,故以上函數是隨機內聚。
應如下分為兩個函數:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的長與寬 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“點”的坐標 */
}
6-21:如果多段代碼重復做同一件事情,那么在函數的劃分上可能存在問題。
說明:若此段代碼各語句之間有實質性關聯并且是完成同一件功能的,那么可考慮把此段代碼構造成一個新的函數。
6-22:功能不明確較小的函數,特別是僅有一個上級函數調用它時,應考慮把它合并到上級函數中,而不必單獨存在。
說明:模塊中函數劃分的過多,一般會使函數間的接口變得復雜。所以過小的函數,特別是扇入很低的或功能不明確的函數,不值得單獨存在。
6-23:設計高扇入、合理扇出(小于7)的函數。
說明:扇出是指一個函數直接調用(控制)其它函數的數目,而扇入是指有多少上級函數調用它。扇出過大,表明函數過分復雜,需要控制和協調過多的下級函數;而扇出過小,如總是1,表明函數的調用層次可能過多,這樣不利程序閱讀和函數結構的分析,并且程序運行時會對系統資源如堆棧空間等造成壓力。函數較合理的扇出(調度函數除外)通常是3-5。扇出太大,一般是由于缺乏中間層次,可適當增加中間層次的函數。扇出太小,可把下級函數進一步分解多個函數,或合并到上級函數中。當然分解或合并函數時,不能改變要實現的功能,也不能違背函數間的獨立性。
扇入越大,表明使用此函數的上級函數越多,這樣的函數使用效率高,但不能違背函數間的獨立性而單純地追求高扇入。公共模塊中的函數及底層函數應該有較高的扇入。較良好的軟件結構通常是頂層函數的扇出較高,中層函數的扇出較少,而底層函數則扇入到公共模塊中。
6-24:減少函數本身或函數間的遞歸調用。
說明:遞歸調用特別是函數間的遞歸調用(如A->B->C->A),影響程序的可理解性;遞歸調用一般都占用較多的系統資源(如棧空間);遞歸調用對程序的測試有一定影響。故除非為某些算法或功能的實現方便,應減少沒必要的遞歸調用。
6-26:改進模塊中函數的結構,降低函數間的耦合度,并提高函數的獨立性以及代碼可讀性、效率和可維護性。優化函數結構時,要遵守以下原則:
(1)不能影響模塊功能的實現。
(2)仔細考查模塊或函數出錯處理及模塊的性能要求并進行完善。
(3)通過分解或合并函數來改進軟件結構。
(4)考查函數的規模,過大的要進行分解。
(5)降低函數間接口的復雜度。
(6)不同層次的函數調用要有較合理的扇入、扇出。
(7)函數功能應可預測。
(8)提高函數內聚。(單一功能的函數內聚最高)
說明:對初步劃分后的函數結構應進行改進、優化,使之更為合理。
6-27:在多任務操作系統的環境下編程,要注意函數可重入性的構造。
說明:可重入性是指函數可以被多個任務進程調用。在多任務操作系統中,函數是否具有可重入性是非常重要的,因為這是多個進程可以共用此函數的必要條件。另外,編譯器是否提供可重入函數庫,與它所服務的操作系統有關,只有操作系統是多任務時,編譯器才有可能提供可重入函數庫。如DOS 下BC 和MSC 等就不具備可重入函數庫,因為DOS 是單用戶單任務操作系統。
6-28:避免使用BOOL 參數。
說明:原因有二,其一是BOOL 參數值無意義,TURE/FALSE 的含義是非常模糊的,在調用時很難知道該參數到底傳達的是什么意思;其二是BOOL 參數值不利于擴充。還有NULL也是一個無意義的單詞。
6-29: 對于提供了返回值的函數,在引用時最好使用其返回值。
6-30:當一個過程(函數)中對較長變量(一般是結構的成員)有較多引用時,可以用一個意義相當的宏代替。
說明:這樣可以增加編程效率和程序的可讀性。
示例:在某過程中較多引用TheReceiveBuffer[FirstSocket].byDataPtr,則可以通過以下宏定義來代替:
# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr7-1:編程時要經常注意代碼的效率。
說明:代碼效率分為全局效率、局部效率、時間效率及空間效率。全局效率是站在整個系統的角度上的系統效率;局部效率是站在模塊或函數角度上的效率;時間效率是程序處理輸入任務所需的時間長短;空間效率是程序所需內存空間,如機器代碼空間大小、數據空間大小、棧空間大小等。
7-2:在保證軟件系統的正確性、穩定性、可讀性及可測性的前提下,提高代碼效率。
說明:不能一味地追求代碼效率,而對軟件的正確性、穩定性、可讀性及可測性造成影響。
7-3:局部效率應為全局效率服務,不能因為提高局部效率而對全局效率造成影響。
7-4:通過對系統數據結構的劃分與組織的改進,以及對程序算法的優化來提高空間效率。
說明:這種方式是解決軟件空間效率的根本辦法。
示例:如下記錄學生學習成績的結構不合理。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef struct STUDENT_SCORE_STRU
BYTE name[8];
BYTE age;
BYTE sex;
BYTE class;
BYTE subject;
float score;
} STUDENT_SCORE;
因為每位學生都有多科學習成績,故如上結構將占用較大空間。應如下改進(分為兩個結構),總的存貯空間將變小,操作也變得更方便。
typedef struct STUDENT_STRU
{
BYTE name[8];
BYTE age;
BYTE sex;
BYTE class;
} STUDENT;
typedef struct STUDENT_SCORE_STRU
{
WORD student_index;
BYTE subject;
float score;
} STUDENT_SCORE;
7-5:循環體內工作量最小化。
說明:應仔細考慮循環體內的語句是否可以放在循環體之外,使循環體內工作量最小,從而提高程序的時間效率。
示例:如下代碼效率不高。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
back_sum = sum; /* backup sum */
}
語句“back_sum = sum;”完全可以放在for 語句之后,如下。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
}
back_sum = sum; /* backup sum */
7-6:仔細分析有關算法,并進行優化。仔細考查、分析系統及模塊處理輸入(如事務、消息等)的方式,并加以改進。
7-7:對模塊中函數的劃分及組織方式進行分析、優化,改進模塊中函數的組織結構,提高程序效率。
說明:軟件系統的效率主要與算法、處理任務方式、系統功能及函數結構有很大關系,僅在代碼上下功夫一般不能解決根本問題。
7-8:編程時,要隨時留心代碼效率;優化代碼時,要考慮周全。
7-9:不應花過多的時間拼命地提高調用不很頻繁的函數代碼效率。
說明:對代碼優化可提高效率,但若考慮不周很有可能引起嚴重后果。
7-10:要仔細地構造或直接用匯編編寫調用頻繁或性能要求極高的函數。
說明:只有對編譯系統產生機器碼的方式以及硬件系統較為熟悉時,才可使用匯編嵌入方式。嵌入匯編可提高時間及空間效率,但也存在一定風險。
7-11:在保證程序質量的前提下,通過壓縮代碼量、去掉不必要代碼以及減少不必要的局部和全局變量,來提高空間效率。
說明:這種方式對提高空間效率可起到一定作用,但往往不能解決根本問題。
7-12:在多重循環中,應將最忙的循環放在最內層。
說明:減少CPU 切入循環層的次數。
示例:如下代碼效率不高。
for (row = 0; row < 100; row++)
{
for (col = 0; col < 5; col++)
{
sum += a[row][col];
}
}
可以改為如下方式,以提高效率。
for (col = 0; col < 5; col++)
{
for (row = 0; row < 100; row++)
{
sum += a[row][col];
}
}
7-13:盡量減少循環嵌套層次。
7-14:避免循環體內含判斷語句,應將循環語句置于判斷語句的代碼塊之中。
說明:目的是減少判斷次數。循環體中的判斷語句是否可以移到循環體外,要視程序的具體情況而言,一般情況,與循環變量無關的判斷語句可以移到循環體外,而有關的則不可以。
示例:如下代碼效率稍低。
Generated by Foxit PDF Creator ? Foxit Software
http://www.foxitsoftware.com For evaluation only.
- 22 -
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
if (data_type == RECT_AREA)
{
area_sum += rect_area[ind];
}
else
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}
因為判斷語句與循環變量無關,故可如下改進,以減少判斷次數。
if (data_type == RECT_AREA)
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
area_sum += rect_area[ind];
}
}
else
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}
7-15:盡量用乘法或其它方法代替除法,特別是浮點運算中的除法。
說明:浮點運算除法要占用較多CPU 資源。
示例:如下表達式運算可能要占較多CPU 資源。
#define PAI 3.1416
radius = circle_length / (2 * PAI);
應如下把浮點除法改為浮點乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 編譯器編譯時,將生成具體浮點數
radius = circle_length * PAI_RECIPROCAL / 2;
7-16:不要一味追求緊湊的代碼。
說明:因為緊湊的代碼并不代表高效的機器碼。8-1:在軟件設計過程中構筑軟件質量。
8-2:代碼質量保證優先原則
(1)正確性,指程序要實現設計要求的功能。
(2)穩定性、安全性,指程序穩定、可靠、安全。
(3)可測試性,指程序要具有良好的可測試性。
(4)規范/可讀性,指程序書寫風格、命名規則等要符合規范。
(5)全局效率,指軟件系統的整體效率。
(6)局部效率,指某個模塊/子模塊/函數的本身效率。
(7)個人表達方式/個人方便性,指個人編程習慣。
8-3:只引用屬于自己的存貯空間。
說明:若模塊封裝的較好,那么一般不會發生非法引用他人的空間。
8-4:防止引用已經釋放的內存空間。
說明:在實際編程過程中,稍不留心就會出現在一個模塊中釋放了某個內存塊(如C 語言指針),而另一模塊在隨后的某個時刻又使用了它。要防止這種情況發生。
8-5:過程/函數中分配的內存,在過程/函數退出之前要釋放。
8-6:過程/函數中申請的(為打開文件而使用的)文件句柄,在過程/函數退出之前要關閉。
說明:分配的內存不釋放以及文件句柄不關閉,是較常見的錯誤,而且稍不注意就有可能發生。這類錯誤往往會引起很嚴重后果,且難以定位。
示例:下函數在退出之前,沒有把分配的內存釋放。
typedef unsigned char BYTE;
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE *) malloc (MAX_GT_LENGTH);
... //program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
return GT_LENGTH_ERROR; // 忘了釋放gt_buf
}
... // other program code
}
應改為如下。
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE * ) malloc ( MAX_GT_LENGTH );
... // program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
free( gt_buf ); // 退出之前釋放gt_buf
return GT_LENGTH_ERROR;
}
... // other program code
}
8-7:防止內存操作越界。
說明:內存操作主要是指對數組、指針、內存地址等的操作。內存操作越界是軟件系統主要錯誤之一,后果往往非常嚴重,所以當我們進行這些操作時一定要仔細小心。
示例:假設某軟件系統最多可由10 個用戶同時使用,用戶號為1-10,那么如下程序存在問題。
#define MAX_USR_NUM 10
unsigned char usr_login_flg[MAX_USR_NUM]= "";
void set_usr_login_flg( unsigned char usr_no )
{
if (!usr_login_flg[usr_no])
{
usr_login_flg[usr_no]= TRUE;
}
}
當usr_no 為10 時,將使用usr_login_flg 越界。可采用如下方式解決。
void set_usr_login_flg( unsigned char usr_no )
{
if (!usr_login_flg[usr_no - 1])
{
usr_login_flg[usr_no - 1]= TRUE;
}
}
8-8:認真處理程序所能遇到的各種出錯情況。
8-9:系統運行之初,要初始化有關變量及運行環境,防止未經初始化的變量被引用。
8-10:系統運行之初,要對加載到系統中的數據進行一致性檢查。
說明:使用不一致的數據,容易使系統進入混亂狀態和不可知狀態。
8-11:嚴禁隨意更改其它模塊或系統的有關設置和配置。
說明:編程時,不能隨心所欲地更改不屬于自己模塊的有關設置如常量、數組的大小等。
8-12:不能隨意改變與其它模塊的接口。
8-13:充分了解系統的接口之后,再使用系統提供的功能。
示例:在B 型機的各模塊與操作系統的接口函數中,有一個要由各模塊負責編寫的初始化過程,此過程在軟件系統加載完成后,由操作系統發送的初始化消息來調度。因此就涉及到初始化消息的類型與消息發送的順序問題,特別是消息順序,若沒搞清楚就開始編程,很容易引起嚴重后果。以下示例引自B 型曾出現過的實際代碼,其中使用了FID_FETCH_DATA與FID_INITIAL 初始化消息類型,注意B 型機的系統是在FID_FETCH_DATA 之前發送FID_INITIAL 的。
MID alarm_module_list[MAX_ALARM_MID];
int FAR SYS_ALARM_proc( FID function_id, int handle )
{
_UI i, j;
switch ( function_id )
{
... // program code
case FID_INITAIL:
for (i = 0; i < MAX_ALARM_MID; i++)
{
if (alarm_module_list[i]== BAM_MODULE // **)
|| (alarm_module_list[i]== LOCAL_MODULE)
{
for (j = 0; j < ALARM_CLASS_SUM; j++)
{
FAR_MALLOC( ... );
}
}
}
... // program code
break;
case FID_FETCH_DATA:
... // program code
Get_Alarm_Module( ); // 初始化alarm_module_list
break;
... // program code
}
}
由于FID_INITIAL 是在FID_FETCH_DATA 之前執行的,而初始化alarm_module_list 是在FID_FETCH_DATA 中進行的,故在FID_INITIAL 中(**)處引用alarm_module_list 變量時,它還沒有被初始化。這是個嚴重錯誤。應如下改正:要么把Get_Alarm_Module 函數放在FID_INITIAL 中(**)之前;要么就必須考慮(**)處的判斷語句是否可以用(不使用alarm_module_list 變量的)其它方式替代,或者是否可以取消此判斷語句。
8-14:編程時,要防止差1 錯誤。
說明:此類錯誤一般是由于把“<=”誤寫成“<”或“>=”誤寫成“>”等造成的,由此引起的后果,很多情況下是很嚴重的,所以編程時,一定要在這些地方小心。當編完程序后,應對這些操作符進行徹底檢查。
8-15:要時刻注意易混淆的操作符。當編完程序后,應從頭至尾檢查一遍這些操作符,以防止拼寫錯誤。
說明:形式相近的操作符最容易引起誤用,如C/C++中的“=”與“==”、“|”與“||”、“&”與“&&”等,若拼寫錯了,編譯器不一定能夠檢查出來。
示例:如把“&”寫成“&&”,或反之。
ret_flg = (pmsg->ret_flg & RETURN_MASK);
被寫為:
ret_flg = (pmsg->ret_flg && RETURN_MASK);
rpt_flg = (VALID_TASK_NO( taskno ) && DATA_NOT_ZERO( stat_data ));
被寫為:
rpt_flg = (VALID_TASK_NO( taskno ) & DATA_NOT_ZERO( stat_data ));
8-16:有可能的話,if 語句盡量加上else 分支,對沒有else 分支的語句要小心對待;switch語句必須有default 分支。
8-17:Unix 下,多線程的中的子線程退出必需采用主動退出方式,即子線程應return 出口。
8-18:不要濫用goto 語句。
說明:goto 語句會破壞程序的結構性,所以除非確實需要,最好不使用goto 語句。
8-19:精心地構造、劃分子模塊,并按“接口”部分及“內核”部分合理地組織子模塊,以提高“內核”部分的可移植性和可重用性。
說明:對不同產品中的某個功能相同的模塊,若能做到其內核部分完全或基本一致,那么無論對產品的測試、維護,還是對以后產品的升級都會有很大幫助。
8-20:精心構造算法,并對其性能、效率進行測試。
8-21:對較關鍵的算法最好使用其它算法來確認。
8-22:時刻注意表達式是否會上溢、下溢。
示例:如下程序將造成變量下溢。
unsigned char size ;
while (size-- >= 0) // 將出現下溢
{
... // program code
}
當size 等于0 時,再減1 不會小于0,而是0xFF,故程序是一個死循環。應如下修改。
char size; // 從unsigned char 改為char
while (size-- >= 0)
{
... // program code
}
8-23:使用變量時要注意其邊界值的情況。
示例:如C 語言中字符型變量,有效值范圍為-128 到127。故以下表達式的計算存在一定風險。
char chr = 127;
int sum = 200;
chr += 1; // 127 為chr 的邊界值,再加1 將使chr 上溢到-128,而不是128。
sum += chr; // 故sum 的結果不是328,而是72。
若chr 與sum 為同一種類型,或表達式按如下方式書寫,可能會好些。
sum = sum + chr + 1;
8-24:留心程序機器碼大小(如指令空間大小、數據空間大小、堆棧空間大小等)是否超出系統有關限制。
8-25:為用戶提供良好的接口界面,使用戶能較充分地了解系統內部運行狀態及有關系統出錯情況。
8-26:系統應具有一定的容錯能力,對一些錯誤事件(如用戶誤操作等)能進行自動補救。
8-27:對一些具有危險性的操作代碼(如寫硬盤、刪數據等)要仔細考慮,防止對數據、硬件等的安全構成危害,以提高系統的安全性。
8-28:使用第三方提供的軟件開發工具包或控件時,要注意以下幾點:
(1)充分了解應用接口、使用環境及使用時注意事項。
(2)不能過分相信其正確性。
(3)除非必要,不要使用不熟悉的第三方工具包與控件。
說明:使用工具包與控件,可加快程序開發速度,節省時間,但使用之前一定對它有較充分的了解,同時第三方工具包與控件也有可能存在問題。
8-29:資源文件(多語言版本支持),如果資源是對語言敏感的,應讓該資源與源代碼文件脫離,具體方法有下面幾種:使用單獨的資源文件、DLL 文件或其它單獨的描述文件(如數據庫格式)9-1:打開編譯器的所有告警開關對程序進行編譯。
9-2:在產品軟件(項目組)中,要統一編譯開關選項。
9-3:通過代碼走讀及審查方式對代碼進行檢查。
說明:代碼走讀主要是對程序的編程風格如注釋、命名等以及編程時易出錯的內容進行檢查,可由開發人員自己或開發人員交叉的方式進行;代碼審查主要是對程序實現的功能及程序的穩定性、安全性、可靠性等進行檢查及評審,可通過自審、交叉審核或指定部門抽查等方式進行。
9-4:測試部測試產品之前,應對代碼進行抽查及評審。
9-5:編寫代碼時要注意隨時保存,并定期備份,防止由于斷電、硬盤損壞等原因造成代碼丟失。
9-6:同產品軟件(項目組)內,最好使用相同的編輯器,并使用相同的設置選項。
說明:同一項目組最好采用相同的智能語言編輯器,如Muiti Editor,Visual Editor 等,并設計、使用一套縮進宏及注釋宏等,將縮進等問題交由編輯器處理。
9-7:合理地設計軟件系統目錄,方便開發人員使用。
說明:方便、合理的軟件系統目錄,可提高工作效率。目錄構造的原則是方便有關源程序的存儲、查詢、編譯、鏈接等工作,同時目錄中還應具有工作目錄----所有的編譯、鏈接等工作應在此目錄中進行,工具目錄----有關文件編輯器、文件查找等工具可存放在此目錄中。
9-8:某些語句經編譯后產生告警,但如果你認為它是正確的,那么應通過某種手段去掉告警信息。
說明:在Borland C/C++中,可用“#pragma warn”來關掉或打開某些告警。
示例:
#pragma warn -rvl // 關閉告警
int examples_fun( void )
{
// 程序,但無return 語句。
}
#pragma warn +rvl // 打開告警
編譯函數examples_fun 時本應產生“函數應有返回值”告警,但由于關掉了此告警信息顯
示,所以編譯時將不會產生此告警提示。
9-9:使用代碼檢查工具(如C 語言用PC-Lint)對源程序檢查。
9-10:使用軟件工具(如 LogiSCOPE)進行代碼審查。
10-1:單元測試要求至少達到語句覆蓋。
10-2:單元測試開始要跟蹤每一條語句,并觀察數據流及變量的變化。
10-3:清理、整理或優化后的代碼要經過審查及測試。
10-4:代碼版本升級要經過嚴格測試。
10-5:使用工具軟件對代碼版本進行維護。
10-6:正式版本上軟件的任何修改都應有詳細的文檔記錄。
10-7:發現錯誤立即修改,并且要記錄下來。
10-8:關鍵的代碼在匯編級跟蹤。
10-9:仔細設計并分析測試用例,使測試用例覆蓋盡可能多的情況,以提高測試用例的效率。
10-11:盡可能模擬出程序的各種出錯情況,對出錯處理代碼進行充分的測試。
10-12:仔細測試代碼處理數據、變量的邊界情況。
10-13:保留測試信息,以便分析、總結經驗及進行更充分的測試。
10-14:不應通過“試”來解決問題,應尋找問題的根本原因。
10-15:對自動消失的錯誤進行分析,搞清楚錯誤是如何消失的。
10-16:修改錯誤不僅要治表,更要治本。
10-17:測試時應設法使很少發生的事件經常發生。
10-18:明確模塊或函數處理哪些事件,并使它們經常發生。
10-19: 堅持在編碼階段就對代碼進行徹底的單元測試,不要等以后的測試工作來發現問題。
10-20:去除代碼運行的隨機性(如去掉無用的數據、代碼及盡可能防止并注意函數中的“內部寄存器”等),讓函數運行的結果可預測,并使出現的錯誤可再現。11-1:用宏定義表達式時,要使用完備的括號。
示例:如下定義的宏都存在一定的風險。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)
正確的定義應為:
#define RECTANGLE_AREA( a, b ) ((a) * (b))
11-2:將宏所定義的多條表達式放在大括號中。
示例:下面的語句只有宏的第一條表達式被執行。為了說明問題,for 語句的書寫稍不符規范。
#define INTI_RECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );
正確的用法應為:
#define INTI_RECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}
11-3:使用宏時,不允許參數發生變化。
示例:如下用法可能導致錯誤。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 結果:a = 7,即執行了兩次增1。
正確的用法是:
b = SQUARE( a );
a++; // 結果:a = 6,即只執行了一次增1。
應該在變量名中使用下劃線嗎?
在變量名中使用下劃線是一種風格。使用或完全不使用下劃線都沒有錯誤,重要的是要保持一致性——在整個程序中使用相同的命名規則。這就是說,如果你在一個小組環境中編程,你和其它小組成員應該制定一種命名規則。并自始至終使用這種規則。如果有人使用了別的命名規則,那么集成的程序讀起來將是很費勁的。此外,你還要與程序中用到的第三方庫(如果有的話)所使用的風格保持一致。如果可能的話,你應該盡量使用與第三方庫相同的命名規則,這將加強你的程序的可讀性和一致性。許多C程序員發現在命名變量時使用下劃線是很方便的,這可能是因為加下劃線會大大加強可讀性。例如,下面的兩個函數名是相似的,但使用下劃線的函數名的可讀性更強:
??? check disk space available(selected disk drive);
??? CheckDiskSpaceAvailable (Selected Disk Drive);
上例中的第二個函數名使用了駱駝式命名法——見19.5中關于駱駝式的解釋。
可以用變量名來指示變量的數據類型嗎?
可以。在變量名中指出數據類型已經成為今天的大型復雜系統中普遍使用的一條規則。通常,變量類型由一個或兩個字符表示,并且這些字符將作為變量名的前綴。使用這一技術的一種廣為人知的命名規則就是匈牙利命名法,它的名稱來自于Microsoft公司的程序員CharlesSimonyi。表19.2列出了一些常用的前綴。???????????????? ??? 表1 9.2一些常用的匈牙利命名法前綴
---------------------------------------------------------------------------------
? 數據類型????????????? 前綴?????????????????????? 例子
---------------------------------------------------------------------------------
? char?????????????????? c?????????????????????? ? clnChar
? int??????????????????? i???????????????????????? iReturnValue
? long???????????????????l???????????????????????? lNumRecs
? string???????????????? sz????????????????????? ? szlnputString ( 以零字節結束 )
? int???? array????????? ai??????????????????????? aiErrorNumbers
? char *???????????????? psz????????????????? ???? pszInputString
---------------------------------------------------------------------------------
象Microsoft Windows這樣的環境,就大量使用了匈牙利命名法或其派生體。其它一些第四代環境,例如Visual Basic和Access,也采用了匈牙利命名法的一種變體。
在編寫程序時,你不必拘泥于一種特定的命名法——你完全可以建立自己的派生命名法,特別是在為自己的typedef命名時。例如,有一個名為SOURCEFILE的typedef,用來保存源文件名、句柄、行號、最后編譯日期和時間、錯誤號等信息。你可以引入一個類似“sf”(sourcefile)的前綴符號,這樣,當你看到一個名為sfBuffer的變量時,你就會知道該變量保存了SOURCEFILE結構中的部分內容。
不管怎樣,在命名變量或函數時,引入某種形式的命名規則是一個好主意,尤其是當你與其它程序員共同開發一個大的項目時,或者在Microsoft Windows這樣的環境下工作時。采用一種認真設計好的命名規則將有助于增強你的程序的可讀性,尤其是當你的程序非常復雜時。
使用注釋、空白符會影響程序的速度、大小或效率嗎?
使用注釋會影響程序的速度、大小或效率嗎?
不會。當你的程序被編譯時,所有的注釋都會被忽略掉,只有可執行的語句才會被分析,并且被放入最終編譯所得的程序版本中。因為注釋并不會給程序的速度、大小或效率帶來負擔,所以你應該盡量多使用注釋。你應該在每一個程序模塊的頭部都加上一段注釋,以解釋該模塊的作用和有關的特殊事項。同樣,你也要為每一個函數加上注釋,其中應包括作者姓名、編寫日期、修改日期和原因、參數使用指導、函數描述等信息。這些信息將幫助其它程序員更好地理解你的程序,也有助于你以后回憶起一些關鍵的實現思想。
在源代碼中也應該使用注釋(在語句之間)。例如,如果有一部分代碼比較復雜,或者你覺得有些地方要進一步說明,你就應該毫不猶豫地在這些代碼中加入注釋。這樣做可能會花費一點時間,但這將為你或其它人節省幾個小時的寶貴時間,因為只需看一下注釋,人們就能馬上知道你的意圖。
在19.4中有一個例子,它表明了使用注釋、空白符和下劃線命名規則是如伺使程序更清晰、更易懂的。
使用空白符會影響程序的速度、大小或效率嗎?
不會。與注釋一樣,所有的空白符都會被編譯程序忽略掉。當你的程序被編譯時,所有的空白符都會忽略掉,只有可執行的語句才會被分析,并且被放入最終編譯所得的程序版本中。在C程序中用空白符隔開可執行語句、函數和注釋等,將有助于提高程序的可讀性和清晰度。許多時候,只要在語句之間加入空行,就可提高程序的可讀性。請看下面的這段代碼:
??? / * clcpy by GBlansten * /
??? void clcpy(EMP * e,int rh,int ot)
??? { e->grspy= (e->rt * rh)+ (e->rt * ot * 1.5) ;
??? e->txamt = e->grspy * e->txrt ;
??? e->ntpy = e->grspy-e->txamt ;
??? updacctdata (e);
??? if (e->dd= =false) cutpyck(e) ;
??? else prtstb (e) ; }
??? 你可以看到,這個函數確實是一團糟。盡管這個函數顯然是正確的,但恐怕這個世界上沒有一個程序員愿意去維護這樣的代碼。如果采用本章中的一些命名規則(例如使用下劃線,少用一些短而模糊的名字),使用一些大括號技巧,加入一些空白符和注釋,那么這個函數將會是下面這個樣子:
/********************************************************************************/
Function Name: calc_pay
Parameters:???? emp???????? -EMPLOYEE pointer that points to employee data
??????????????? reg-hours?? -The number of regular hours (<=40) employee
?????????????????????????????????????????? has worked
????????????? ? ot-hours??? -The number of overtime hours (>40) employee
?????????????????????????????????????????? has worked
Author :??????? Gern Blansten
Date Written:?? 13 dec 1993
Modification:?? 04 sep 1994 by Lloyd E. Work
??????????????? -Rewrote function to make it readable by human beings.
Description:??? This function calculates an employee's gross bay ,tax
??????????????? amount, and net pay, and either prints a paycheck for the
??????????????? employee or (in the case of those who have direct deposit)
????????????????????? prints a paycheck stub.
/*********************************************************************************/
void calc_pay (EMPLOYEE * emp, int reg hours, int or_hours)
{
????? / * gross_pay = (employee rate * regular hours)+
????????????????????? (employee rate * overtime hours * 1.5) * /
????? emp->gross_pay= (emp->rate * reg_hours) +
????????????????????? (emp->rate * ot hours* 1.5);
????? / * tax amount=gross_pay * employee's tax rate * /
????? emp->tax amount=emp->gross_pay * emp->tax-rate ;
????? / * net pay=gross pay-tax amount * /
????? emp->net-pay=emp->gross pay-emp->tax_amount ;
????? / * update the accounting data * /
????? update accounting data(emp);
????? / * check for direct deposit * /
????? if (emp->direct_deposit= =false)
????????? cut_ paycheck(emp);?????????? / * print a paycheck * /
????? else
????????? print_paystub(emp);?????????? /* print a paycheck stub * /
}
你可以看到,Lloyd版本(該版本中使用了大量的注釋、空行、描述性變量名等)的可讀性比糟糕的Gern版本要強得多。你應該盡量在你認為合適的地方使用空白符(和注釋符等),這將大大提高程序的可讀性一一當然,這可能會延長你的工作時間。
什么是駱駝式命名法?
駱駝式命令法,正如它的名稱所表示的那樣,是指混合使用大小寫字母來構成變量和函數的名字。例如,下面是分別用駱駝式命名法和下劃線法命名的同一個函數:??? PrintEmployeePaychecks();
?? print employee paychecks();
第一個函數名使用了駱駝式命名法——函數名中的每一個邏輯斷點都有一個大寫字母來標記;第二個函數名使用了下劃線法----函數名中的每一個邏輯斷點都有一個下劃線來標記。
駱駝式命名法近年來越來越流行了,在許多新的函數庫和Microsoft Windows這樣的環境中,它使用得當相多。另一方面,下劃線法是c出現后開始流行起來的,在許多舊的程序和UNIX這樣的環境中,它的使用非常普遍。
較長的變量名會影響程序的速度、大小或效率嗎?
不會,當你的程序被編譯時,每一個變量名和函數名都會被轉換為一個“符號”——對原變量或原函數的一種較短的符號性的表示。因此,無論你使用下面的哪一個函數名,結果都是一樣的:??? PrintOutAllTheClientsMonthEndReports();
??? prt_rpts();
一般說來,你應該使用描述性的函數名和變量名,這樣可以加強程序的可讀性。你可以查閱編譯程序文檔,看一下允許有多少個有效字符,大多數ANSI編譯程序允許有至少31個有效字符。也就是說,只有變量名或函數名的前31個字符的唯一性會被檢查,其余的字符將被忽略掉。
一種較好的經驗是使函數名或變量名讀起來符合英語習慣,就好象你在讀一本書一樣——人們應該能讀懂你的函數名或變量名,并且能很容易地識別它們并知道它們的大概作用。
給函數命名的正確方法是什么?
函數名一般應該以一個動詞開始,以一個名詞結束,這種方法符合英語的一般規則。下面列出了幾個命名比較合適的函數:??? PrintReports();
??? SpawnUtilityProgram();
??? ExitSystem();
??? Initia|izeDisk():
請注意,在這些例子中,函數名都以一個動詞開始,以一個名詞結束。如果按英語習慣來讀這些函數名,你會發現它們其實就是:
??? print the reports(打印報告)
??? spawn the utility program(生成實用程序)
??? exit the system(退出系統)
??? initialize the disk(初始化磁盤)
使用動詞一名詞規則(特別是在英語國家)能有效地加強程序的可讀性,并且使程序看起來更熟悉。
使用大括號的正確方法是什么?
在C中,使用大括號的方法無所謂對還是錯——只要每個開括號后都有一個閉括號,你的程序中就不再會出現與大括號有關的問題。然而,有三種著名的大括號格式經常被使用:Kernighan和Ritchie,Allman,Whitesmiths。下文中將討論這三種格式。
在《C程序設計語言(The C Programming Language)》一書中,Brian Kernighan和Dennis Ritchie介紹了他們所使用的大括號格式,這種格式如下所示:
if (argc<3) {
??? printf (" Error! Not enough arguments. Correct usage is ..\n" ) ;
??? printf("c:>eopyfile? <source_file>? <destination_file>\n") ;
??? exit (1) ;
}
else {
??? open_files () ;
??? while (! feof(infile)) {
???????????? read_data ( ) ;
???????????? write_data() ;
??? }
??? close files() ;
}
注意,在Kb&R格式中,開括號總是與使用它的語句在同一行上,而閉括號總是在它所關閉的語句的下一行上,并且與該語句對齊。例如,在上例中,if語句的開括號和它在同一行上,|f語句的閉括號在它的下一行上,并且與它對齊。在與if語句對應的else條件語句以及出現在程序段后部的while語句中,情況也是這樣的。
下面是用Allman格式書寫的同一個例子:
if (argc<3)
{
??? printf("Error! Not enough arguments. Correct usage is :\n" ) ;
??? printf("C:>copyfile? <source_file>? <destination_file>\n") ;
??? exit(1);
}
else
{
??? open_files ( );
???? while (! feof(infile))
??? {
???????????? read_data ( ) ;
???????????? write data();
??? }
???? close_files() ;
}
注意,在Allman格式中,每個大括號都單獨成行,并且開括號和閉括號都與使用它們的語句對齊。
下面是用Whitesmiths格式書寫的同一個例子:
if (argc<3)
?? {
??? printf("Error! Not enough arguments, Correct usage is :\n" );
??? printf ("C :> copyfile<source_file><destination_file>\n." ) ;
??? exit(1);
}
else
??? {
??? open files () ;
???? while (! feof(infile))
????????? {
???????????? read_data() ;
???????????? write data();
????????? }
???? close files () ;
??? }
與Allman格式相同,Whitesmiths格式也要求大括號單獨成行,但是它們要和它們所包含的語句對齊。例如,在上例中,if語句的開括號是與第一個printf()函數調用對齊的。
不管你使用哪一種格式,一定要保持前后一致——這將有助于你自己或其它人更方便地讀你的程序。?
一個變量名應該使用多少個字母?ANSI標準允許有多少個有效字符?
一般說來,變量名或函數名應該足夠長,以有效地描述所命名的變量或函數。應該避免使用短而模糊的名字,因為它們在別人理解你的程序時會帶來麻煩。例如,不要使用象這樣的短而模糊的函數名:??? opndatfls();
而應該使用更長一些的函數名,象下面這樣:
??? open data_files();
或者:
??? OpenDataFiles();
這對變量名也是同樣適用的。例如,與其使用這樣一個短而模糊的變量名:
??? fmem
不如將其擴展為完整的描述:
??? free_memory_available
使用擴展了的名字會使程序更易讀,更易理解。大多數ANSI編譯程序允許有至少31個有效字符——即只有變量或函數名的前31個字符的唯一性會被檢查。一種較好的經驗是使函數名或變量名讀起來符合英語習慣,就好象你在讀一本書一樣一人們應該能讀懂你的函數名或變量名,并且能很容易地識別它們并知道它們的大概作用。
什么是匈牙利式命名法?應該使用它嗎?
匈牙利命名法是由Microsoft公司的程序員Charles Simonyi(無疑是個匈牙利人的后裔)提出的。在這種命名法中,變量名或函數名要加上一個或兩個字符的前綴,用來表示變量或函數的數據類型。這種命名法有許多好處,它被廣泛應用于象Microsoft Windows這樣的環境中。關于匈牙利命名法的詳細解釋以及其中的一些命名標準。
C語言重復處理是什么
重復處理是指反復執行相同的程序語句,但可能會在滿足某個條件時退出循環。c語言提供了一些現成的結構來實現重復處理,例如while循環,do…while循環和for循環。在這些結構中,當某個條件為真時,預先定義的一批語句將被反復執行。下面是一個重復處理的例子:while (x<lO0)
{
????? y=O;
??? do {
???????????? for(z =O;z<lOO;z++)
???????????????????? y++ ;
?????? }while (y<1000) ;
??? x++;
}
在這個例子中,包含在while循環中的語句被執行100次,在while循環中還有一個do…while循環,在do…whlie循環中的for循環被執行10次;在for循環中,變量y作100次自增運算。因此,語句
??? y++;
總共被執行100,000次(100次while×10次do…while×100次for)。然而,在while循環結束時,y并不是100,000,因為每循環1000次后y都會被置為o。
在c程序中,重復處理的應用是非常廣泛的,例如你經常要用重復處理來讀寫數組或文件。下面的程序用重復處理來讀入并向屏幕打印你的AUTOEXEC.BAT文件:
# include <stdio. h>
# include <stdlib. h>
int main(viod) ;
int main (void)
{
????? FILE * autoexec_file ;
????? char buffer[250] ;
????? if ( (autoexec_file = fopen (" C : \\ AUTOEXEC. BAT", "rt ") ) = = NULL )
???? {
???????????? {printf (stderr,"Cannot open AUTOEXEC. BAT file. \n") ;
???? exit(1) ;
????printf("Contents of AUTOEXEC. BAT file : \n\n" ) ;
????while(! feof(autoexec file))
????{
???????? fgets (buffer, 200,autoexee_file) ;
???????? printf(" %s" ,buffer) ;
????}
????felose (autoexee_file) ;
????rerun(0) ;
}
注意,上例用一條while語句來反復地調用fgets()和printf()函數,以讀入AUTOEXEC.BAT文件中的每一行,并把它們打印到屏幕上。這僅僅是如何使用重復處理的例子之一。
什么是C語言遞歸(recursion)?怎樣使用遞歸?
在c語言中,一個調用自身(不管是直接地還是間接地)的函數被稱為是遞歸的(recursive)。你可能不明白究竟為什么一個函數要調用自身,要解釋這種情況,最好先舉一個例子。一個經典的馬上可以舉出來的例子就是計算整數的階乘。為了計算一個整數的階乘值,你要用這個數(x)乘以它的前一個數(X一1),并且一直乘下去,直到到達1。例如,5的階乘可以這樣來計算:??? 5*4*3*2*1
如果X是5,你可以把這個算式轉換為一個等式:
??? X!=X*(X-1)*(X-2)*(X-3)*(X-4)*1
為了用C語言完成這項計算任務,你可以編寫一個名為calc_factorial()的函數,它反復調用自身,每次都把被計算的值減1,直到到達1。下面的例子說明怎樣編寫這個calc_factorial()函數:
??? #include<stdio.h>
??? void main(void);
??? unsigned long calc_factorial(unsigned long x);
??? void main(void)
??? {
????? int x=5;
????? printf("The factorial of %d is %ld. \n" ,x,calc_factorial(x));
}
unsigned long calc_factorial(unsigned long x)
{
????? if(! x)
????????????? return 1L ;
????? return(x * calc_factorial(x-1L)) ;
}
在上例中,calc_factorial()在調用自身前要先把x的值減去1。如果x等于O,if語句的條件將為真,calc factorial()將不再被遞歸調用。因此,當被計算的值到達O時,calc_factorial()作完最后一次遞歸調用并退出,其返回值為1。返回1是因為任何值都可以安全地和1相乘,并仍能保持其原來的值。如果程序中包含下述語句:
??? x=calc_factorial(5);
它將開展為:
??? x=5*(5-1)*(4-1)*(3-1)*(2-1)*1;
因此,x將等于5的階乘,即120。
遞歸是一個簡潔的概念,同時也是一種很有用的手段。但是,使用遞歸是要付出代價的。與直接的語句(如while循環)相比,遞歸函數會耗費更多的運行時間,并且要占用大量的棧空間。遞歸函數每次調用自身時,都需要把它的狀態存到棧中,以便在它調用完自身后,程序可以返回到它原來的狀態。未經精心設計的遞歸函數總是會帶來麻煩。
如果可能的話,你應該避免使用遞歸函數。例如,前文中的階乘函數可以寫成下面這種形式:
# include <stdio. h>
void main(void) ;
unsigned long calc factorial(unsigned long x);
void main (void)
{
????? int x=5;
????? printf("The factorial of %d is %ld. \n" ,x ,calc_factorial (x)) ;
}
unsigned long calc-factorial(unsigned long x)
{
????? unsigned long factorial;
????? factorial=x;
????? while (x>1L)
????? {
???????????? factorial * =--x;
????? }
???? return (factorial);
}
這個版本的calc_factorial()函數用一個while循環來計算一個值的階乘,它不僅比遞歸版本快得多,而且只占用很小的棧空間。
在C語言中,表示真和假的最好方法是什么?
在c語言中,任何等于零的東西都被認為是假,任何等于非零值的東西都被認為是真,因此,最常見的定義就是假為O,真為1。許多程序中都包含了具有如下定義的頭文件:??? #define FALSE O
??? #define TRUE? 1
如果你在編寫Windows程序,你應該注意頭文件windows.h中的TRUE和FALSE的確切定義。上述真和假的定義方式非常普遍,并且是完全可以接受的,然而,還有其它幾種定義方式,請看下例:
??? #define FALSE 0
??? #define TRUE? !FALSE
上例把FALSE定義為0,把TRUE定義為非零值。注意,即使是負數,如-1,也是非零值,因此也被認為是真。
另一種定義方式是建立自己的枚舉類型,如Boolean(或者BOOL),如下所示:
??? enurn BOOL{
??? FALSE,
??? TRUE
??? };
正象你所可能已經知道的那樣,在缺省情況下,枚舉類型的第一個元素被賦值為O,因此,在上述枚舉定義中,FALSE被賦值為0,TRUE被賦值為1。與使用符號常量相比,使用枚舉類型有它的一些好處,詳見5.6和5.7中的有關內容。
哪種方法最好呢?這個問題沒有一個唯一的答案。如果你在編寫一個Windows程序,那么TRUE和FALSE都是已經為定義好的,你沒有必要再建立自己的定義,在其它情況下,你可以從前文所介紹的幾種方法中選擇一種。
C語言空循環和無窮循環有的區別
空循環并不會無休止地進行下去——在重復預先指定的次數后,它就會退出循環。無窮循環會無休止地進行下去,并且永遠不會退出循環。把空循環和無窮循環對比一下,就能很好地說明它們之間的區別。下面是一個空循環的例子:
??? for(x=O;x<500000;x++);
注意,在上例中,在for循環的閉括號后直接加入了一個分號。正如你可能已經知道的那樣,c語言并不要求在for循環后加分號,通常只有包含在for循環中的語句后面才會帶分號。
在for循環后面直接加入分號(并且不使用大括號),即可建立一個空循環——實際上是一個不包含任何語句的循環。在上例中,當for循環執行時,變量x將自增500,000次,而在每一次自增運算期間,沒有進行任何處理。
那么,空循環有什么用呢?在大多數情況下,它的作用就是在程序中設置一次暫停。前面的例子將使程序“暫停”一段時間,即計算機數到500,000所需的時間。然而,空循環還有更多的用處,請看下例:
??? while(!kbhit());
這個例子用一個空循環來等待一次擊鍵操作。當程序需要顯示類似"Press Any Key ToContinue"這樣的信息時,這種方法是很有用的(假設你的用戶很聰明,不會執著地在鍵盤上尋找"Any Key"!)。
無窮循環與空循環不同,它永遠不會結束。下面是一個無窮循環的例子:
??? while(1);
在這個例子中,while語句中包含了一個非零常量,因此,while的條件永遠為真,循環永遠不會結束。注意,在閉括號后面直接加入一個分號,因此while語句中不包含任何其它語句,循環將無法終止(除非終止程序)。
C語言continue和break有的區別
continue語句用來返回循環的起始處,而break語句用來退出循環。例如,下例中就有一條典型的continue語句:??? while(!feof(infile))
??? {
?????? fread(inbuffer,80,1,infile);/*read in a line from input file*/
?????? if(!strncmpi(inbuffer,"REM",3))? /*check if it is
???????????????????????????????????????????? a comment line*/
?????? continue;??? /*it's a comment,so jump back to the while()*/
???? ? else
????? parse_line();??? /*not a comment—parse this line*/
??? }
上例讀入一個文件并對其進行分析。“REM(remark的縮寫)”用來標識正在被處理的文件中的一個注釋行。因為注釋行對程序不起任何作用,所以可以跳過它。在讀入輸入文件的每一行時,上例就把該行的前三個字母與"REM"進行比較。如果匹配,則該行就是注釋行,于是就用continue語句返回到while語句,繼續讀入輸入文件的下一行;否則,該行就是一條有效語句,于是就調用parse_line()函數對其進行分析。
break語句用來退出循環。下面是一個使用break語句的例子:
while (! feof(infile))
???? fread(inbuffer,80,1,infile) ;/* read in a line from input file * /
???? if (! strncmpi (inbuffer,"REM",3))? / * check if it is
?????????????????????????????????????????????????????????? a comment line?* /
?????????? continue;?????? /* it's a comment, so jump back to the while() * /
???? else
???? {
?????????? if (parse_line()==FATAL_ERROR)?? / * attempt to parse
??????????????????????????????????????????????????????????????????? this line?* /
????????????? break;???????????????? /* fatal error occurred,so exit the loop * /
???? }
??? 這個例子建立在使用continue語句的那個例子的基礎上。注意,在這個例子中,要檢查parse_line()函數的返回值。如果parse_line()的返回值為FATAL_ERROR,就通過break語句立即退出while循環,并將控制權交給循環后面的第一條語句。
華為C語言編程規范(4)—標識符命名
9 代碼編輯、編譯、審查
10 代碼測試、維護
11 宏