日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

[转载]高质量c/c++编程指南读书笔记

發布時間:2023/12/20 c/c++ 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [转载]高质量c/c++编程指南读书笔记 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一個strcpy函數的代碼 能考查三個方面 (1) 編程風格 (2) 出錯處理 (3) 算法復雜度分析(用于提供性能)

定義編程老手和編程高手 定義1:能長期穩定地編寫出高質量程序的程序員稱為編程老手 定義2:能長期穩定地編寫出高難度、高質量的程序與稱為編程高手

?

第一章 文件結構

1.1 版權和版本的聲明 版權和版本的聲明位于頭文件和定義文件的開頭,主要內容有: (1) 版權信息 (2) 文件名稱、標識符、摘要 (3) 當前版本號、作者/修改者、完成日期 (4) 版本歷史信息

/* * Copyright (c) 2001,上海貝爾有限公司網絡應用事業部 * All rights reserved. *? * 文件名稱:filename.h * 文件標識:見配置管理計劃書 * 摘??? 要:簡要描述本文件的內容 *? * 當前版本:1.1? * 作??? 者:輸入作者(或修改者)名字 * 完成日期:2001年7 月20日 * * 取代版本:1.0?? * 原作者? :輸入原作者(或修改者)名字 * 完成日期:2001年5 月10日 */

1.2 頭文件結構 ??? 頭文件由三部分內容組成 (1) 頭文件開頭處的版權和版本聲明 (2) 預處理塊 (3) 函數和類結構聲明等 ??? 為了防止頭文件被重復用in用,應當用ifndef/define/endif結構產生預處理塊 ??? 頭文件中只存放"聲明"而不存放"定義" ? 在C++語法中,類的成員函數可以在聲明的同時被定義,并且自動成為內聯函數。這雖然會帶來書寫上的方便,但卻造成了風格不一致,弊大于利。建議將成員函數的定義與聲明分開,不論該函數體有多么小。 ??? 不提倡使用全局變量,盡量不要再頭文件中出現像extern int value這類聲明

1.3 頭文件的作用 (1) 通過頭文件來調用庫功能。在很多場合,源代碼不便(或不準)向用戶公布,只要向用戶提供頭文件和二進制庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功能,而不必關系接口怎么實現的。編譯器會從庫中提取相應的代碼。

(2) 頭文件能加強類型安全監察。如果某個接口被實現或被使用時,其方式與頭文件中的聲明不一致,編譯器就會指出錯誤,這一見到的規則能大大減輕程序員調試、改錯的負擔。

1.4 目錄結構 ??? 如果一個軟件的頭文件數目比較多(如超過十個),通常應將頭文件和定義文件分別保存于不同的目錄,以便于維護。 ??? 例如可將頭文件保存于include目錄,將定義文件保存于source目錄(可以是多級目錄)。 ??? 如果某些頭文件是私有的,他不會被用戶的程序直接引用,則沒有必要公開其"聲明"。為了加強信息隱藏,這些私有的頭文件可以和定義文件存放在同一目錄。

?

第二章 程序的版式 ??? 版式雖然不會影響程序的功能,但會影響可讀性。程序的版式追求清晰、美觀,是程序風格的重要構成因素。

2.1 空行 ??? 空行起著分隔程序段落的作用??招械皿w(不過多也不過少)將使程序的布局更加清晰。空行不會浪費內存,雖然打印含有空行的程序是會多消耗一些紙張,但是值得。 (1) 在每個類聲明之后、每個函數定義結束之后都要加空行 void Function1(...) {}

vodi Function2(...) {}

(2) 在一個函數體內,邏輯上密切相關的語句之間不加空行,其他地方應加空行分隔 while( flag ) { ??? statement1; ??? // 空行 ??? if( condition ) ??? { ?????? statement2; ??? } ??? else ??? { ?????? statement3; ??? } ??? // 空行 ??? statement4; }

2.2 代碼行 (1) 一行代碼只做一件事情,如之定義一個變量,或只寫一條語句。這樣的代碼容易閱讀,并且方便于寫注釋 int width; int height;

(2) if、for、while、do等語句自占一行,執行語句不得緊跟其后。不論執行語句有多少都要加{}。這樣可以防止書寫失誤

2.3 代碼行內的空格 (1) 關鍵字之后要留空格。像const、virtual、inline、case等關鍵字之后至少要留一個空格,否則無法辨析關鍵字。想if、for、while等關鍵字之后應留一個空格再跟左括號'(',以突出關鍵字

(2) 代碼行最大長度宜控制在70至80個字符以內。代碼行不要過長,否則眼睛看不過來,也不便于打印

(3) 長表達式要在低優先級操作符處拆分成新航,操作符放在新行之首(以便突出操作符)。拆分出的新行要進行適當的縮進,使排版整齊,語句刻度 if (( very_longer_variable1 >= very_longer_variable2 ) ?? && (very_longer_variable3 <= very_longer_variable4 ) ?? && (very_longer_variable5 <= very_longer_variable6 )) {...}

2.4 修飾符的位置 ??? 應當將修飾符*和&緊靠變量名

2.5 注釋 ??? C語言的注釋符為"/*...*/"。C++語言中,程序塊的注釋常采用"/*...*/",行注釋一般采用"//..."。注釋通常用于: 版本、版權聲明 函數接口說明 重要的代碼行或段落提示 ??? 雖然注釋有助于理解代碼,但注意不可過多地使用注釋 (1) 注釋是對代碼的“提示”,而不是文檔。程序中的注釋不可喧賓奪主,注釋太多了會讓人眼花繚亂。注釋的花樣要少

(2) 如果代碼本來就是清楚的,則不必加注釋。否則多此一舉,令人厭煩例如 ??? i++; // i加1,多余的注釋

(3) 邊寫代碼邊注釋,修改代碼同時修改相應的注釋,以保證注釋與代碼的一致性。不再有用的注釋要刪除 /* *? 函數介紹: *? 輸入參數: *? 輸出參數: *? 返回值? : */ void Function(float x, float y, float z) { ? … }

2.6 類的版式 ??? 類可以將數據和函數封裝在一起,其中函數表示了類的行為(或稱服務)。類提供關鍵字public、protected和private。這樣可以達到信息隱藏的目的,即讓類僅僅公開必須要讓外界知道的內容,而隱藏其他一切內容。不可以濫用類的封裝功能,不要把它當成火鍋,什么東西都往里扔。 ??? 將public類型的函數寫在前面,而將private類型的數據寫在后面,采用這種版式的程序員主張類的設計“以行為為中心”,重點關注的是類應該提供什么樣的接口(或服務) class A { ? public: ? void Func1(void); ? void Func2(void); ? … private: ? int??? i, j; ? float? x, y; ? … }

?

第三章 命名規則 ??? 比較著名的命名規則當推MS公式的“匈牙利”法,該命名規則的主要思想是“在變量和函數名中加入前綴以增進人們對程序的理解”。例如所有的字符變量均以ch為前綴,若是指針變量則追加前綴p。如果一個變量有ppch開頭,則表明它是指向字符指針的指針。 ??? 匈牙利法最大缺點是繁瑣

3.1 共性規則 (1) 標識符應當直觀且可以品讀,可望文知意,不必進行“解碼” 標識符最好采用英文單詞或其組合,便于記憶和閱讀。切忌使用漢語拼音來命名。程序中的英文檔次一般不會太復雜,用詞應當準確。例如不要把CrurentValue寫成NowValue

(2) 標識符的長度應當符合“min-length” && “max-information”原則。 幾十年前老ANSI C規定名字不準超過6個字符,現今的C++/C不再有此限制。一般來說,長名字能更好地表達含義,所以函數名、變量名、類名長達十幾個字符不足為奇。那么名字是否越長越好?不見得!例如變量名maxVal就比maxValueUntilOverflow好用。大字符的名字也是有用的,常見的如i,jk等,它們通??梢杂米骱瘮祪鹊木植孔兞?/span>

(3) 命名規則盡量與所采用的操作系統或開發工具的風格保持一致。例如windows應用程序的標識符通常采用“大小寫”混排的方式,如AddChild,而Unix應用程序的標識符通常采用“小寫加下劃線”的方式,如add_child。別把這兩類風格混在一起用。

(4) 程序中不要出現緊靠大小寫區分的相似的標識符。例如 int x, X; // 變量x與X容易混淆

(5) 變量的名字應當使用“名詞”或者“形容詞+名詞”。例如 float value; float oldValue; float newValue;

(6) 全局函數的名字應當使用“動詞”或者“動詞+名詞”(動賓詞組)。類的成員函數應當只使用“動詞”,被省略掉的名詞就是對象本身。例如 DrawBox();?? // 全局函數 bos->Draw(); // 類的成員函數

3.2 簡單的windows應用程序命名規則 (1) 類名和函數名用大寫字母開頭的單詞組合而成,例如: class Node;???? // 類名 class LeadNode; // 類名 void Draw( void );????????? // 函數名 void SetValue( int value ); // 函數名

(2) 變量和參數用小寫字母開頭的單詞組合而成 BOOL flag; int drawNode;

(3) 常量全用大寫的字母,用下劃線分隔單詞,例如: const int MAX = 100; const int MAX_LENGTH = 100;

(4) 靜態變量加前綴s_(表示static),例如: vodi Init(...) { ??? static int s_initValue; // 靜態變量 }

(5) 如果不得已需要全局變量,則使全局變量加前綴g_(表示global),例如: int g_howManyPeople; // 全局變量 int g_howMuchMoney;? // 全局變量

(6) 類的數據成員加前綴m_(表示member),這樣可以避免數據成員與成員函數的參數同名。例如: void Object:L:SetValue( int width, int height ) { ??? m_width = width; ??? m_height = height; }

?

第四章 表達式和基本語句 4.1 if語句 a、布爾變量與零值比較 ??? 不可將不二變量直接與TRUE、FALSE或者1、0進行比較 ??? 根據布爾類型的語義,零值為“假”(記為FALSE),任何非零值都是“真”(記為TRUE)。TRUE的值究竟是什么病沒有統一的標準。例如Virtual C++將TRUE定義為1,而VB則就愛那個TRUE定義為-1 假設布爾變量名字為flag,它與零值比較的標準if語句如下: if ( flag )? // 表示flag為真 if ( !flag ) // 表示flag為假 其他的用法都屬于不良風格,例如: if ( TRUE == flag ) if ( 1 == flag )

b、整型變量與零值比較 if ( 0 == value ) if ( 0 != value )

c、浮點變量與零值比較 ??? 不可將浮點變量用"=="或"!="與任何數字比較 ??? 千萬要留意,無論是float還是double類型的表露,都有精度限制。所以一定要避免將浮點變量用"=="或"!="與數字比較,應該設法轉化成">="或"<="形式 假設浮點變量的名字為x,應當將 if ( 0.0 == x ) // 隱含錯誤的比較 轉化為 if ( ( x >= -ESPINON) && ( x <= EPSINON) ) 其中EPSINON是允許的誤差(即精度)

d、指針變量與零值比較 if ( NULL == p ) if ( NULL != p )

4.2 循環語句的效率 C/C++循環語句中,for語句使用頻率最高,while語句其次,do語句很少用。

a、在多重循環中,如果有可能,應當將最長的循環放在最內層,最短的循環放在最外層,以減少CPU跨切循環層的次數。例如 for ( row = 0; row < 100; row ++) { ??? for ( col = 0; col < 5; col++ ) ??? { ??????? sum = sum + a[ row ][ col ]; ??? } } 這里的效率就比較低:長循環在最外層,應改成: for ( col = 0; col < 5; col++ ) { ??? for ( row = 0; row < 100; row ++) ??? { ??????? sum = sum + a[ row ][ col ]; ??? } }

b、如果循環體內存在邏輯判斷,并且循環次數很大,宜將邏輯判斷移到循環體的外面。比如(1)中的程序比(2)的程序多執行了N-1次邏輯判斷。并且由于前者老妖進行邏輯判斷,打斷了循環“流水線”作業,是的編譯器不能對循環進行優化處理,降低了小路。如果N非常大,最好采用(2)的寫法,可以提高小路。如果N非常小,兩者效率差別并不明顯,但采用(1)的寫法比較好,因為程序更加簡潔 (1) for ( i = 0; i < N; i++ ) { ??? if ( condition ) ??? { ??????? DoSomething(); ??? } ??? else ??? { ??????? DoOtherthing(); ??? } } // 效率低但程序簡潔 (2) if ( condition ) { ??? for ( i = 0; i < N; i++ ) ??? { ??????? DoSomething(); ??? } } else { ??? for ( i = 0; i < N; i++ ) ??? { ??????? DoOtherthing(); ??? } } // 效率高但程序簡潔

4.3 for語句的循環控制變量 建議for語句的循環控制變量的取值采用“半開半閉區間”寫法 (1) for ( int i = 0; i < N; i++ ) {...} x值屬于半開半閉區間“0 =< x < N”,起點到終點的間隔為N,循環次數為N (2) for ( int i = 0 ; i <= N -1; i++ ) {...} x值屬于閉區間“0 =< x <= N-1”,起點到終點的間隔為N-1,循環次數為N 相比之下(1)的寫法更加直觀,盡管兩者的功能是相同的

?

第五章 常量 ??? 常量是一種標識符,它的值在運行期間恒定不變。C語言用#define來定義常量(成為宏常量)。C++語言除了#define外還可以用const來定義常量(成為const常量)

5.1 為什么需要常量 ??? 如果不適用常量,直接在程序中填寫數字或字符串,將會有什么麻煩? (1) 程序的可讀性(可理解性)變差。程序員自己會放假那些數字或字符串是什么意思,用戶則更加不知他們從何而來、表示什么 (2) 在程序的很多地方輸入同樣的數字或字符串,難保不發生書寫錯誤 (3) 如果要修改數字或字符串,則會在很多地方改動,既麻煩又容易出錯

5.2 const與#define的比較 ??? C++語言可以用const來定義常量,也可以用#define來定義常量。但是前者比后者有更多的有點: (1) const常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全監察。而對后者只進行字符替換,沒有類型安全監察,并且在字符替換可能會產生意料不到的錯誤(邊界效應) (2) 有些集成化調試工具可以對const常量進行調試,但是不能對宏常量進行調試

5.3 類的常量 ??? 不能在類聲明中初始化const數據成員。以下用法是錯誤的,因為累的對象未被創建時,編譯器不知道SIZE的值是什么 class Test_A { ??? ... private: ??? const int SIZE = 100; // 錯誤,企圖在類聲明中初始化const數據成員 ??? int array[ SIZE ];??? // 錯誤,未知而SIZE };

const數據成員的初始化只能在類構造函數的初始化表中進行,例如: class Test_A { public: ??? Test_A( int size );?? // 構造函數 ??? const int SIZE; };

Test_A::Test_A( int size) : SIZE( size ) // 構造函數的初始化表 {}

怎樣才能簡歷在整個類中都恒定的常量呢?別指望const數據成員了,應該用類中的枚舉常量來實現。例如: class Test_A { ??? ... public: ??? enum { SIZE1 = 100, SIZE2 = 200 }; // 枚舉常量 ??? int array1[ SIZE]; ??? int array2[ SIZE];?? } 枚舉常量不會占用對象的存儲空間,他們在編譯時被全部求職。枚舉常量的缺點是:他的隱含數據類型是整數,其最大值優先,且不能表示浮點數(如PI = 3.14159)

?

第六章 函數設計 6.1 參數的規則 a、參數額書寫要完整,不要貪圖省事致謝參數的類型而省略參數名字,如果函數沒有參數,則用void填充,例如: void GetValue(void);

b、如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體內被意外修改

c、如果輸入參數以值傳遞的方式傳遞對象,則宜改用“const &”方式來傳遞,這樣可以省去臨時對象的構造和析構過程,從而提高效率

6.2 返回值的規則 a、函數名字與返回值類型在語義上不可沖突 違反這條規則的典型帶便是C標準庫函數getchar,例如: char c; c = getchar(); if ( EOF == c ) {...} 按照getchar名字的意思,將變量c聲明為char類型是很自然的事情。但不幸的是getchar的確不是char類型,而是int類型,其原型如下: int getchar( void ); 由于c是char類型,取值范圍是[ -128, 127 ],如果宏EOF的值在char的取值范圍之外,那么if語句將總是失敗,這種“危險”人們一般不會料到,導致本例錯誤的責任是函數getchar誤導了使用者

b、不要將正常值和錯誤標識混在一起返回。正常值用輸出參數獲得,而錯誤標志用return語句返回 ??? 回顧上例,C標準庫函數的設計者為什么要將getchar聲明為令人迷糊的int類型? ??? 在正常情況下,getchar的確返回單個字符。但如果getchar碰到文件結束標志或發生讀錯誤,它必須返回一個標志EOF。為了區別于正常的字符,志豪將EOF定義為負數(通常為-1)。因此函數getchar就成了int類型。

c、有時候函數原本不需要返回值,但為了增加靈活性如支持鏈式表達,可以附加返回值 例如字符串拷貝函數strcpy的原型: char *strcpy( char *strDest, const char *strSrc ); strcpy函數將strSrc拷貝至輸出參數strDest中,同時函數的返回值又是strDest。這樣做并非多次一舉,可以獲得如下靈活性: char str[ 20 ]; int length = strlen( strcpy( str, "hello world" ));

6.3 函數內部實現的規則 ??? 不同功能的函數其內部實現各不相同,看起來似乎無法就“內部實現”達成一致的觀點。但根據經驗,可以在函數體的“入口處”和“出口處”從嚴把關,從而提高函數的質量 a、在函數體的“入口處”,對參數的有效性進行檢查 看6.5

b、在函數體的“出口處”,對return語句的正確性和效率進行檢查 如果函數有返回值,那么函數的“出口處”是return語句。如果return語句寫得不好,函數要么出錯,要么效率低下。 注意事項如下: (1) return語句不可反悔指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀,例如: char *Func( void ) { ??? char str[] = "hello world"; // str的內存位于棧上 ??? return str;??? // 將導致錯誤 } (2) 如果函數返回值是一個對象,要考慮return語句的效率。例如: return String( s1 + s2 ); ??? 這是臨時對象的語法,表示“創建一個臨時對象并返回他”。不要以為他與“先創建一個局部對象temp并返回他的結果”是等價的,如: String temp( s1 + s2 ); return temp; 是指不然,上述代碼發生三件事。首先,temp對象被創建,同時完成初始化;然后拷貝夠咱函數吧temp拷貝到保存返回值存儲單元中;最后,temp在函數結束時被銷毀(調用析構函數)。然后“創建一個臨時對象并返回他”的過程是不同的,編譯器直接把臨時對象創建并初始化在外部存儲單元中,省去了拷貝和析構的花費,提高了效率

6.4 其他建議 (1) 函數的功能要單一,不要涉及多用途的函數 (2) 函數體的規模要小,盡量控制在50行代碼之內 (3) 函數中盡量少用static局部變量,除非必要 (4) 不僅要檢查輸入蠶食的有效性,還要檢查通過其他途徑進入函數體內的變量的有效性,例如全局變量、文件句柄等 (5) 用于出錯處理的返回值一定要清楚,讓使用者不容易忽視或誤解錯誤情況

6.5 使用斷言 ??? 程序一般分為Debug版本和Release版本。 ??? 斷言assert是盡在Debug版本其作用的宏,他用于檢測“不應該”發生的情況。 void *memory( void *pvTo, const void *pvFrom, size_t size ) { ??? assert( ( NULL != pvTo) && ( NULL != pvFrom ) ); // 使用斷言 ??? byte *pbTo = ( byte * )pvTo;???? // 防止改變pvTo的地址 ??? byte *pbFrom = ( byte * )pvFrom; // 防止改變pvFrom的地址 ??? while( size-- > 0 ) ??? { ??????? *pbTo++ = *pbFrom++; ??? } ??? return pvTo; } ??? 在函數的入口處,使用斷言檢查參數的有效性(合法性) ??? 在編寫函數時,要進行反復的考查,并且自問:“我打算做哪些假定?”,一旦確定了假定,就要使用斷言對假定進行檢查

6.6 引用于指針的比較 引用的一些規則 (1) 引用被創建的同事必須被初始化(指針則可以在任何時候被初始化) (2) 不能有NULL引用,引用必須與合法的存儲單元關聯(指針則可以是NULL) (3) 一旦引用被初始化,就不能改變引用的關系(指針則可以隨時改變所指的對象) ??? 指針能夠毫無約束地操作內存中的任何東西,盡管指針功能強大,但是非常危險。就像一把刀,他可以用來砍樹、裁紙、修指甲、理發等等,誰敢這樣用? ??? 如果的確只需要借用一下某個對象的“別名”,那么就用“引用”,而不要用“指針”,以免發生意外。比如說,某人需要一份證明,本來在文件上蓋上公章的印子就行了,如果把取公章的鑰匙交給他,那么他就獲得了不該有的權利。

?

第七章 內存管理 7.1 內存分配方式 內存的分配方式有三種: (1) 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量 (2) 在棧上創建。在執行函數時,函數內局部變量存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的執行集中,效率很高,但是分配的內存容量有限 (3) 從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多

7.2 常見的內存錯誤及其對策 (1) 內存分配未成功,卻使用了它 ??? 常用的解決辦法是,在使用內存之前檢查指針是否為NULL。如果指針p是函數的參數,那么在函數的入口處用assert(NULL != p)進行檢查。如果是malloc或者new來申請內存,應該用if ( NULL == p )或if ( NULL != p )進行放錯處理

(2) 內存分配雖然成功,但是尚未初始化就引用它 ??? 犯這種錯誤主要有兩個原因:一時沒有初始化的觀念;二是誤以為內存的缺省初值全為零,導致引用初值錯誤(例如數組) ??? 內存的缺省初值究竟是什么并沒有統一的標準,盡管有些時候全為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩

(3) 釋放了內存卻繼續使用它 ??? 有三種情況: a、程序中的對象調用關系過于復雜,是在難以搞清楚某個對象究竟是否已經釋放了內存。此時應該重新設計數據結構,從根本上解決對象管理的混亂局面 b、函數的return語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀 c、使用free或delete釋放了內存后,沒有將指針設置為NULL。導致產生“野指針”

(4) 用free或delete釋放了內存之后,立即將指針設置為NULL,防止產生“野指針”

7.3 指針與數組的對比 (1) 例 char *p = "hello world"; p[ 0 ] = 'X'; cout << p << endl; 指針p指向常量字符串"hello world"(位于靜態存儲區,內容為hello world\0),常量字符串的內容是不可以被修改的。從語法上看,編譯器并不覺得語句p[ 0 ] = 'X'有什么不妥,但是該語句企圖修改常量字符串的內容而導致運行錯誤

(2) 把數組中的數據賦給指針p int Length = strlen( a ); char *p = ( char *)malloc( sizeof( char ) * ( len + 1 ) ); strcpy( p, a ); if( 0 == strcmp( p, a ) ) 語句p = a并不能把a的內容復制到指針p,而是把a的地址賦給了p。要像復制a的內容,可以先用庫函數malloc為p申請一塊容量為strlen( a ) + 1個字符的內容,再用strcpy進行字符串復制。同理,語句if( p == a )比較的不是內容而是地址,應該用庫函數strcmp來比較 注意:strcpy自動會給字符串的末尾加上\0;memcpy卻不會

一個例子: char str[ 6 ] = "world"; cout << strlen( str ) << endl; char *p = new char[ strlen( str ) + 1 ]; memcpy( p, str, strlen( str ) + 1 ); cout << p << endl; cout << strlen( p ) << endl;

另一個例子: char str[ 6 ] = "world"; cout << strlen( str ) << endl; char *p = new char[ strlen( str ) + 1 ]; strcpy( p, str ); cout << p << endl; cout << strlen( p ) << endl;

(3) 計算內存容量 ??? 用運算符sizeof可以計算出數組的容量(字節數)。但是sizeof( p )的值卻是4.這是因為sizeof( p )得到的是一個指針變量的字節數,相當于sizeof( char * ),而不是p所指向的內存容量。C/C++語言沒有辦法知道指針所指的內存容量,除非在申請內存時記住它。 ??? 注意當數組作為函數的參數繼續參數傳遞時,該數組自動退化為同類型的指針,例如 void Func( char a[ 100 ]) { ??? cout << sizeof(a ) << endl; // 4字節而不是100字節} 不論數組a的容量是多少,sizeof( a )始終是等于sizeof( char *)

(4) 指針參數是如何傳遞內存的 ??? 如果函數的參數是一個指針,不要指望用該指針去申請動態內存 void GetMemory( char *p, int num ) { ??? p = ( char * )malloc( sizeof( char ) * num ); } void Test( void ) { ??? char *str = { 0 }; ??? GetMemory( str, 100 );? // str仍然為NULL ??? strcpy( str, "hello" ); // 運行錯誤 } 毛病出在函數GetMemory中。編譯器總是要為函數的每個參數制作臨時副本,指針參數p的副本是_p,編譯器使_p = p。如果函數體內的程序修改了_p的內容,就導致參數p的內容作相應的修改。這就是指針可以用作輸出參數的原因。在上面的例子中,_p申請了新的內存,只是_p所指的內存地址改變了,但是p絲毫未變。所以函數GetMemory并不能輸出任何東西。事實上,每執行一次GetMemory就會泄露一塊內存,因為沒有用free釋放掉。

??? 如果非得要用指針參數去申請內存,那么應該改用“指向指針的指針” void GetMemory( char **p, int num ) { ??? *p = ( char * )malloc( sizeof( char ) * num ); } void Test( void ) { ??? char *str = { 0 }; ??? GetMemory( &str, 100 ); ??? strcpy( str, "hello" ); ??? free( str ); }

??? 由于“指向指針的指針”這個概念不容易理解,我們可以用函數返回值i來傳遞動態內存。這種方法更加簡單: char *GetMemory( int num ) { ??? char *p = ( char * )malloc( sizeof( char ) * num ); ??? return p; } void Test( void ) { ??? char *str = { 0 }; ??? str = GetMemory( 100 ); ??? strcpy( str, "hello" ); ??? free( str ); }

??? 用函數返回值來傳遞動態內存這種方法雖然好用,但是常常有人把return語句用錯了。這里強調不要用return語句返回指向“棧內存”的指針,因為該內存在函數結束時自動消亡: char *GetString( void ) { ??? char p[] = "hello world"; ??? return p; // 編譯器將發出警告 } void Test( void ) { ??? char *str = NULL; ??? str = GetString(); // str的內容是垃圾 ??? cout << str << endl; } 用調試器逐步跟蹤Test,發現執行str = GetString語句后str不再是NULL指針,但是str的內容不是"hello world"而是垃圾。

??? 如果改成如下: char *GetString( void ) { ??? char *p = "hello world"; ??? return p; // 編譯器將發出警告 } void Test( void ) { ??? char *str = NULL; ??? str = GetString(); // str的內容是垃圾 ??? cout << str << endl; } 函數Test運行雖然不會出錯但是函數GetString的設計概念卻是錯誤的。因為GetString內的"hello world"是常量字符串,位于靜態存儲區,他在程序生命期內恒定不變。無論什么時候調用GetString,他返回的始終是同一個"只讀"的內存塊

(5) free和delete把指針怎么啦? ??? 別看free和delete的名字惡狠狠的(尤其是delete),他們只是把指針所指的內存給釋放掉,但并沒有把指針本身干掉 char *p = new char[ 5 ]; strcpy( p, "zeng" ); cout << p << endl; delete p; if ( NULL !=? p ) { cout << "p is not NULL, p value is:" << p << endl; } 可以看到在delete之后if ( NULL !=? p )進行放錯處理并不起作用,因為即便p不是NULL指針,他也不指向合法的內存塊

(6) 動態內存會被自動釋放嗎? void Func( void ) { ??? char *p = ( char * )malloc( 100 ); // 動態內存會自動釋放嗎? } ??? 當函數Func結束時,指針消亡了,并不表示它所指向的內存會被自動釋放;內存被釋放了,并不表示指針會消亡或者成了NULL指針。

(7) 杜絕“野指針” ??? “野指針”不是NULL指針,是指向“垃圾”內存的指針?!耙爸羔槨钡某梢蛑饕袃煞N: a、指針變量沒有被初始化。任何賀子珍變量剛被創建時不會自動成為NULL指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同事應當被初始化,要么將指針設置為NULL,要么讓他指向合法的內存。例如: char *p = NULL; char *str = ( char * )malloc( 100 );

b、指針p被free或者delete之后,沒有被置為NULL,讓人誤以為p是個合法的指針

c、指針操作超越了變量的作用范圍。這種情況讓人防不勝防: class TestA { public: ??? void Func( void ){ cout << "Func of class TestA" << endl; } }; void Test( void ) { ??? A *p; ??? { ??????? A a; ??????? p = &a; // 注意a的生命期 ??? } ??? p->Func();? // p是“野指針” } 函數Test在執行語句p->Func()時面對像a已經消失,而p是指向a的,所以p就成了“野指針”。但是要論編譯器而定,留意指針的生命期即可

?

第八章 C++函數的高級特性 8.1 如果C++程序要調用已經被編譯后的C函數,該怎么辦? 假設某個C函數的聲明如下: void foo( int x, int y ); 該函數被C編譯器編譯后在庫中的名字為_foo,而C++編譯器則會產生像_foo_int_int之類的名字用來指出函數重載和類型安全連接。由于編譯后的名字不同,C++程序不能直接調用C函數。C++提供了一個C連接交換指定符號extern "C"來解決這個問題。例如: extern "C" { ??? void foo( int x, int y ); ??? ... // 其他函數 } 或者寫成 extern "C" { ??? #include "myheader.h" ??? ... // 其他C頭文件 } 這就告訴C++編譯器,函數foo是個C連接,應該到庫中找名字_foo而不是找_foo_int_int。C++編譯器開發商 依舊對C標準庫頭文件做了extern "C"處理,所以可以用#include直接引用這些頭文件

8.2 成員函數的重載、覆蓋與隱藏 (1) 重載與覆蓋 ??? 成員函數被重載的特征 a、相同的范圍(在同一個類中) b、函數名字相同 c、參數不同 d、virtual關鍵字可有可無

??? 覆蓋是指派生類函數覆蓋基類函數,特征是: a、不同的范圍(分別位于派生類與基類) b、函數名字相同 c、參數相同 d、積累函數必須有virtual關鍵字

(2) 令人迷惑的隱藏規則 ??? 本來僅僅區別重載與覆蓋并不算困難,但是C++的隱藏規則使問題復雜性突然增加。這里“隱藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下: a、如果派生類的函數與基類函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被影藏 b、如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏 #include <iostream> class Base { public: ??? virtual void f( float x ){ cout << "Base::f( float ) " << endl; } ??????????? void g( float x ){ cout << "Base::g( float ) " << endl; } ??????????? void h( float x ){ cout << "Base::h( float ) " << endl; } }; class Derived : public Base { public: ??? virtual void f( float x ){ cout << "Derived::f( float ) " << endl; } ??????????? void g( int x ){ cout << "Derived::g( float ) " << endl; } ??????????? void h( float x ){ cout << "Derived::h( float ) " << endl; } }; void main( void ) { ??? Derived d; ??? Base *pb = &d; ??? Derived? *pd = &d;

??? pb->f( 3.14f ); // Derived::f( float ) 3.14 ??? pd->f( 3.14f ); // Derived::f( float ) 3.14

??? pb->g( 3.14f ); // Base::g( float ) 3.14 ??? pd->g( 3.14f ); // Derived::g( int ) 3

??? pb->h( 3.14f ); // Base::h( float ) 3.14 ??? pd->h( 3.14f ); // Derived::h( int ) 3 }

函數Derived::f( float )覆蓋了Base::f( float ) 函數Derived::g( float )隱藏了Base::g( float ),而不是重載 函數Derived::h( float )隱藏了Base::h( float ),而不是覆蓋 根據類的virtual很容易就能判斷出來

(3) 擺脫隱藏 ??? 隱藏規則引起來不少麻煩 class Base { public: ??? void f( int x ); }; class Derived : public Base { public: ??? void f( char *str ); }; void Test( void ) { ??? Derived *pd = new Derived; ??? pd->f( 10 ); // error } 語句pd->f( 10 )的本意是想調用函數Base::f( int ),但是Base::f( int )不行被Derived::f( char * )隱藏了。由于數字10不能被隱式地轉化為字符串,所以在編譯時出錯。 ??? 隱藏規則至少有兩個存在的理由: a、寫語句pd->f( 10 )的人可能真的想調用Derived::f( char * )函數,只是他誤將函數參數寫錯了。有了隱藏規則,編譯器就可以明確指出錯誤,這未必不是好事。否則,編譯器會靜悄悄地將錯就錯,程序員將很難發現這個錯誤,留下禍根。 b、加入類Derived有多個基類(多重繼承),有時搞不清楚哪些基類定義了函數f。如果沒有隱藏規則,那么pd->f( 10 )可能會調用一個出乎意料的基類函數f,盡管隱藏規則看起來不怎么有道理,但它的確能消滅這些意外

如果要調用基類的函數,需要使用域操作符Base::f( int );

8.3 參數的缺省值 ??? 參數缺省值的使用規則: (1) 參數缺省值只能出現在函數的聲明中,而不能出現在定義體重。例如: void Foo( int x = 0, int y = 0 ); // 正確,缺省值出現在函數的聲明中 void Foo( int x = 0, int y = 0 )? // 錯誤,缺省值出現在函數的定義體重 { ??? ... } 可能的原因: 一、函數的實現(定義)本來就與參數是否有缺省值無關,所以沒有必要讓缺省值出現在函數的定義體重 二、參數的缺省值可能會改動,顯然修改函數的聲明比修改函數的定義要方便

8.4 不能被重載的運算符 ??? 在C++運算符集合中,有一些運算符是不允許被重載的。這種限制是出于安全方面的考慮,可防止錯誤和混亂 (1) 不能改變C++內部數據類型(如int、float等)的運算符 (2) 不能重載'.',因為'.'在類中對任何成員都有意義,已成成為標準用法 (3) 不能重載目前C++運算符集合中沒有的符號,如#、@、$等。原因有兩點,一時難以理解,而是難以確定優先級 (4) 對已存在的運算符進行重載時,不能改變優先級規則,否則將引起混亂

8.5 內聯函數 ??? 對于任何內聯函數,編譯器在符號表里放入函數的聲明(包括名字、參數類型、返回值)。如果編譯器沒有發現內聯函數存在錯誤,那么該函數的代碼也被放入符號表里。在調用一個內聯函數時,編譯器首先檢查調用是否正確(進行類型安全檢查,或者進行自動類型串行,當然對所有的函數都一樣)。如果正確,內聯函數的代碼就會直接替換函數調用,于是省去了函數調用的開銷。這個過程與預處理有顯著的不同,因為預處理器不能進行類型安全檢查,或者進行自動類型轉換。加入內聯函數是成員函數,對象的地址(this)會被放在合適的地方,這也是預處理器辦不到

(1) inline是一種“用于實現的關鍵字”,而不是一種“用于聲明的關鍵字”。 (1) 慎用內聯內聯能提供函數的執行效率,但是內聯是以代碼膨脹(復制)為代價,僅僅省去了函數調用的開銷,從而提高函數的執行效率。如果執行函數體內代碼的時間,相比于函數調用的開銷較大,那么效率的收獲會很少。另一方面,每一次內聯函數的的調用都要復制代碼,將使程序的總代碼量增大,消耗更多的內存空間。以下情況不宜使用內聯: a、如果函數體內的代碼比較長,使用內聯將導致內存消耗代價較高 b、如果函數體內出現循環,那么執行函數體內代碼的時機要比函數調用的開銷大

?

第九章 類的構造函數、析構函數與賦值函數 9.1 示例:類String的構造函數 String::String( const char *str ) { ??? if( NULL == str ) ??? { ??????? m_data = new char[ 1 ]; ??????? *m_date = '\0'; ??? } ??? else ??? { ??????? int length = strlen( str ); ??????? m_data = new char[ length + 1 ]; ??????? strcpy( m_data, str ); ??? } }

?

9.2 不要輕視拷貝構造函數與賦值函數 ??? 由于并非所有的對象都會使用拷貝構造函數和賦值函數,程序員可能對這兩個函數有些輕視。 (1) 如果不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”的方式自動生成缺省值的函數。倘若類中含有指針變量,那么這兩個缺省的函數就隱含了錯誤。 (2) 拷貝構造函數和賦值函數非常容易混淆,常導致錯寫、錯用??截悩嬙旌瘮凳窃趯ο蟊粍摻〞r調用的,而賦值函數只能被已經存在了的對象調用。 String a( "hello" ); String b( "world" ); String c = a; // 調用了拷貝構造函數,最好寫成c( a ); ?????? c = b; // 調用了賦值函數 第三個語句的風格較差,宜改寫成String c( a )以區別于第四個語句

?

9.3 類String的拷貝構造函數與賦值函數 // 拷貝構造函數 String::String( const String &other ) { ??? int length = strlen( other.m_data ); ??? m_data = new char[ length + 1]; ??? strcpy( m_data, other.m_data ); } // 賦值函數 String & String::operator=( const String &other ) { ??? // 檢查自賦值 ??? if( this == &other ) ??? { ??????? return *this; ??? }

?

??? // 釋放原有的內存資源 ??? delete[] m_data;

?

??? // 分配新的內存資源,并復制內容 ??? int length = strlen( other.m_data ); ??? m_data = new char[ length + 1]; ??? strcpy( m_data, other.m_data );

?

??? // 返回本對象的引用 ??? return *this; } 類String拷貝構造函數與普通構造函數的區別是:在函數入口處無需與NULL進行比較,這是因為“引用”不可能為NULL,而“指針”可以為NULL ??? 注意函數strlen返回的是有效字符串長度,不保護結束符'\0'。函數strcpy則連'\0'一起復制 ??? 返回本對象的引用,目的是為了實現像a = b = c注意的鏈式表達。不能寫成return other,因為不知道參數other的生命期。有可能other是個臨時對象,在賦值結束后它馬上消失,那么return other返回的將是垃圾

?

9.4 偷懶的辦法處理拷貝構造函數與賦值函數 ??? 只需將拷貝構造函數與賦值函數聲明為私有函數,不用編寫代碼,例如: class A { private: ??? A( const A &a );????????????? // 私有的拷貝構造函數 ??? A & operator =( const A &a ); // 私有的賦值函數 }; 如果試圖編寫: A b( a ); // 調用了私有的拷貝構造函數 b = a;??? // 調用了私有的賦值函數 編譯器將指出錯誤,因為外界不可以操作A的私有函數

?

9.5 如何在派生類中實現類的基本函數 ??? 基類的構造函數、析構函數、賦值函數都不能被派生類繼承。如果類之間存在繼承關系,在編寫上述基本函數時應注意: a、派生類的構造函數應在其初始化表里調用基類的構造函數 b、基類與派生類的析構函數應該為虛(即加virtual關鍵字) c、在編寫派生類的賦值函數時,不要忘記對基類的數據成員重新賦值 class Base { public: ??? ... ??? Base & operator =( const Base &other ); // 類Base的賦值函數 private: ??? int m_i, m_j, m_k; }; class Derived : public Base { public: ??? ... ??? Derived & operator = ( const Derived &other ); // 類Derived的賦值函數 private: ??? int m_x, m_y, m_z; };

?

Derived & Derived::operator = ( const Derived &other ) { ??? // 檢查自賦值 ??? if( this == &other ) ??? { ??????? return *this; ??? }

?

??? // 對基類的數據成員重新賦值 ??? Base::operator = ( other ); // 因為不能直接操作私有數據成員

?

??? // 對派生類的數據成員賦值 ??? m_x = other.m_x; ??? m_y = other.m_y; ??? m_z = other.m_z;

?

??? // 返回本對象的引用 ??? return *this; }

?

?

?

第十章 類的繼承與組合 10.1 繼承 (1) 如果類A和類B毫不相關,不可以為了使B的功能更多些而讓B繼承A的功能和屬性 (2) 若在邏輯上B市A的“一種”( a kind of ),則允許B繼承A的功能和屬性。例如男人是人的一種,男孩是男人的一種。那么累Man可以從類Human派生,類Boy可以從類Man派生

?

10.2 組合 若在邏輯上A是B的“一部分”( a part of ),則不允許B從A派生,而是要用A和其他東西組合出B,例如眼睛(Eye)、鼻子(Nose)、口(Mouth)、耳朵(Ear)是頭(Head)的一部分,所以類Head應該有類Eye、Nose、Mouth、Ear組合而成,不是派生而成 class Eye { public: ??? void Look( void ); }; class Nose { public: ??? void Smell( void ); }; class Mouth { public: ??? void Eat( void ); }; class Ear { public: ??? void Listen( void ); };

?

// 正確的Head設計,雖然代碼冗長 class Head { public: ??? void Look( void )? { m_eye.Look(); } ??? void Smell( void ) { m_nose.Smell(); } ??? void Eat( void )?? { m_mouth.Eat(); } ??? void Listen( void ){ m_ear.Listen(); } private: ??? Eye m_eye; ??? Nose m_nose; ??? Mouth m_mouth; ??? Ear m_ear; };

?

?

?

第十一章 其他編程經驗 11.1 使用const提高函數的健壯性 const更大的魅力是它可以修飾函數的參數、返回值,甚至函數的定義提 (1) 用const修飾函數的參數 ??? 如果參數做輸出用,不論它是什么數據類型,也不論他采用“指針傳遞”還是“引用傳遞”,都不能加const修飾,否則該參數將失去輸出功能 ??? const只能修飾輸入參數: a、如果輸出參數采用“指針傳遞”,那么假const修飾可以防止意外地改動該指針,起到保護作用 b、如果輸入參數采用“值傳遞”,由于函數將自動產生臨時變量用于復制該參數,該輸入參數本來就無須保護,所以不要加const修飾,例如不要將函數void Func1( int x )寫成void Func1( const int x ),同理不要將函數void Func2( A a )寫成void Func2( const A a )。其中A為用戶自定義數據類型 c、void Func2( A a )效率比較低可以使用A &a,但是注意要加上const;但如果將void Func1( int x )寫成void Func1( const int &x ),完全沒有必要,因為內部數據類型的參數不存在構造、析構的過程,而復制也非???#xff0c;“值傳遞”和“引用傳遞”的效率幾乎相當

?

(2) 用const修飾函數的返回值 a、如果給以“指針傳遞”方式的函數返回值加const修飾,那么函數返回值(即指針)的內容不能被修改,該返回值只能被賦給const修飾的同類型指針 例如函數:const char *GetString( void ); 如下語句將會出現編譯錯誤 char *str = GetString(); 正確的用法是 const char *str = GetString(); b、如果函數返回值采用“值傳遞方式”,由于函數會把返回值復制到外部臨時的存儲單元中,加const修飾沒有任何價值 例如不要把函數int GetInt( void )寫成const int GetInt( void ) 同理不要把函數A GetA( void )寫成const A GetA( void ),其中A為用戶自定義數據類型 c、如果返回值不是內部數據類型,將函數A GetA( void )改寫成const A & GetA( void )的確能提高效率。但此時千萬要小心,一定要搞清楚函數究竟是想返回一個對象的“拷貝”還是僅返回“別名”就可以了,否則程序會出錯

?

(3) const成員函數 ??? 任何不會修改數據成員的函數都應該聲明為const類型。如果在編寫const成員函數時,不慎修改了數據成員,或者調用了其他非const成員函數,編譯器將指出錯誤,這無疑會提高成員的健壯性 class Stack { public: ??? void Push( int elem ); ??? int Pop( void ); ??? int GetCount( void ) const; // const成員函數 private: ??? int m_num; ??? int m_data[ 100 ]; };

?

int Stack::GetCount( void ) const { ??? ++m_num; // 編譯錯誤,企圖修改數據成員m_num ??? Pop();?? // 編譯錯誤,企圖調用非const函數 ??? return m_num; } const成員函數的聲明看起來很怪;const關鍵字只能放在函數聲明的尾部,大概是因為其他地方都已經被占用了

?

11.2 提高程序的效率 ??? 程序的時間效率是指運行速度,空間效率是指程序占用內存或者外存的狀況 (1) 不要一味地追求程序的效率,應當在滿足正確性、可靠性、健壯性、可讀性等質量因素的前提下,設法提高程序的效率 (2) 在優化程序的效率時,應當先找出限制效率的“瓶頸”,不要再無關緊要之處優化 (3) 先優化數據結構和算法,再優化執行代碼 (4) 不要追求緊湊的代碼,因為緊湊的代碼并不能產生高效的機器碼

?

11.3 一些有益的建議 (1) 當心數據類型轉換發生錯誤。盡量使用顯示的數據結構類型轉換(讓人們知道發生了什么事),避免讓編譯器悄悄地進行隱式的數據類型轉換 (2) 避免編寫技術性很高的代碼 (3) 不要設計面面俱到、非常靈活的數據結構 (4) 如果原有的代碼直來那個比較好,盡量復用他。但是不要修補很差勁的代碼,應當重新編寫 (5) 盡量使用標準庫函數,不要“發明”已經存在的庫函數 (6) 盡量不要使用與具體硬件或軟件環境關系密切的變量 (7) 把編譯器的選項設置為最嚴格狀態 (8) 如果可能的話,使用PC-Lint、LogiScope等工具進行代碼審查

?

?

?

第十二章 補充 12.1 仔細設計結構中玉薩怒的布局與排列順序,使結構容易理解、節省占用空間,并減少引起誤用現象 示例:如下結構中的位域排列,將占較大空間,可讀性也稍差 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;

?

12.2 結構的設計要盡量考慮向前兼容和以后的版本升級,并未某些未來可能的應用保留余地(如預留一些空間等) ???? 說明:軟件向前兼容的特性,是軟件產品是否成功的重要標志之一。如果要想使產品具有較好的前向兼容,那么在產品設計之初就應為以后版本升級保留一定余地,并且在產品升級時必須考慮前一版本的各種特性。

?

12.3 對自定義數據類型進行恰當命名,使它成為自描述性的,以提高代碼可讀性。注意其命名方式在同一產品中的統一。 ???? 說明:使用自定義類型,可以彌補編程語言提供類型少、信息量不足的缺點,并能使程序清晰、簡潔。 下面的聲明可使數據類型的使用簡潔、明了 typedef unsigned char? BYTE; typedef unsigned short WORD; typedef unsigned int?? DWORD; 下面的聲明可使數據類型具有更豐富的含義: typedef float DISTANCE; typedef float SCORE;

?

12.4 函數、過程 (1) 對所調用函數的錯誤返回碼要仔細、全面地處理

?

(2) 編寫可重入函數時,若使用全局變量,則應通過關中斷、信號量(即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賦值并計算其 ??? Exam = para; // (**)? // 平方過程中(即正在使用此信號),本進程必須等待其釋放信號后,才 ??? temp = Square_Exam(); // 可繼續執行,但其他進程必須等待本進程釋放信號量后,才能在使用 ??? [釋放信號量操作]????? // 本信號 ?? ??? return temp; }

?

(3) 在同一項目組應明確規定對接口函數參數的合法性檢查應有函數逇調用者負責還是接口函數本身負責,缺省是由函數調用者負責 ??? 說明:對于模塊接口函數的參數逇合法性檢查這一問題,往往有兩個極端現象,即:要么是調用者和被調用者對參數不作合法性檢查,結果就遺漏了合法性檢查這一必要的處理過程,造成問題隱患;要么就是調用者和被調用者均對參數進行合法性檢查,這種情況雖不會造成問題,但產生了冗余代碼,降低了效率。

?

(4) 防止就愛那個函數的參數作為工作便利 ??? 說明:將函數逇參數作為工作便利,有可能錯誤地改變參數內容,所以很危險。對必須改變的參數,最好先用局部變量代之,最后再講該局部變量的內容賦給該參數 示例:下函數的實現不太好 void sum_date( unsigned int num, int *data, int *sum ) { ??? unsigned int count; ??? ??? *sum = 0; ??? for ( count = 0; count < num; count++ ) ??? { ??????? *sum += data[ count ]; // sum成了工作變量,很不好 ??? } } 若改為如下,則更好些: void sum_date( 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 = sum_temp; }

?

(5) 函數的規模盡量限制在200行以內 ??? 說明:不包括注釋和空格行 (6) 一個那還俗僅完成一個功能 (7) 不要設計多用途面面俱到的函數 ??? 說明:多功能集于一身的函數,很可能使函數的理解、測試、維護等變得困難

?

(8) 函數的功能應該是可以預測的,也就是只要輸入數據相同就應產生同樣的輸出 ??? 說明:帶有內部“存儲器”的函數功能可能是不可測的,因為他的輸出可能取決于內部存儲器(如某標志)的狀態。這樣的函數既不易于理解有不利用測試和維護。在C/C++語言中,函數的static局部變量是函數的內部存儲器,有可能使函數的功能不可預測,然而,當某函數的返回值i為指針類型是,則必須是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; }

?

(9) 避免設計多參數函數,不使用的參數從接口中去掉 ??? 說明:目的減少函數間接口的復雜度

?

(10) 非調度函數應減少或防止控制參數,盡量只是用數據參數 ???? 說明:本建議目的是防止函數間的控制耦合。調度函數是指根據輸入的消息類型或控制命令,來啟動相應的功能實體(即函數或過程),而本身并不完成具體功能??刂茀凳侵父淖兒瘮倒δ苄袨榈膮?#xff0c;即函數要根據此參數來決定具體怎樣工作。非調度函數的控制參數增加了函數間的控制耦合,很可能使函數間的耦合度增大,并使函數的功能不唯一。 示例:如下函數構造不太合理 int add_sub( int a, int b, unsigned char add_sub_flg ) { ??? if ( INTEGER_ADD == add_sub_flg ) ??? { ??????? return ( a + b ); ??? } ??? else ??? { ??????? return ( a - b ); ??? } } 不如分為如下兩個函數清晰: int add( int a, int b ) { ??? return ( a + b ); } int sub( int a, int b ) { ??? return ( a - b ); }

?

(11) 檢查函數所有參數輸入的有效性 (12) 檢查函數所有非參數輸入的有效性,如數據文件、公共變量等 ???? 說明:函數的輸入主要有兩種:一種是參數輸入;另一種是全局變量、數據文件的輸入,即非參數輸入。函數在使用輸入之前,應進行必要的檢查。 (13) 使用動賓詞組為執行某操作的函數命名。如果是OOP方法,可以只有動詞(名詞是對象本身) 示例:參照如下方式命名函數 void print_record( unsigned int rec_ind ); int input_record( void );

?

(14) 函數的返回值清楚、明了,讓使用者不容易忽視錯誤情況 ???? 說明:函數的美中出錯返回值的意義要清晰、明了、準確,防止使用者誤用、理解錯誤或忽視錯誤返回碼

?

(15) 防止把沒有關聯的語句放到一個函數中 ???? 說明:防止函數或過程內出現隨機內聚。隨機內聚是指將沒有關聯或關聯很弱的語句放到同一個函數或過程中。隨機內聚給函數火鍋城的維護、測試及以后的升級造成了不變,同事也使函數或過程的功能不明確。使用隨機內聚函數,常常容易出現在一種應用場合需要改進此函數,而另一種應用場合又不允許這種改進,從而陷入困境。 ???? 在編程時,經常遇到在不同韓式中使用相同的代碼,許多開發人員都愿把這些代碼提出來,并構成一個新函數。若這些代碼關聯較大并且是完成一個功能的,那么這種構造是合理的,否則這種構造將產生隨機內聚的函數 示例: 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; }

?

(16) 如果多段代碼重復做同一件事情,那么在函數的劃分上可能存在問題 ???? 說明:若此段代碼個語句之間有實質性關聯并且是完成同一件功能的,那么可考慮把此段代碼構成一個新的函數 (15) 功能不明確較小的函數, 特別是僅有一個上級函數調用它時,應考慮把它合并到商機函數中,而不必單獨存在 ???? 說明:模塊中函數劃分的過多,一般會使函數間的接口變得復雜。所以過小的函數,特別是閃入很低或功能不明確的函數,不值得單獨存在 (16) 仔細分析模塊的功能及性能需求,并進一步細分,同時若有必要畫出有關數據流圖,據此來進行木的函數劃分與組織 ???? 說明:函數的劃分與組織是模塊的實現過程中很關鍵的步驟,如何劃分出合理的函數結構,關系到模塊的最終效率和可維護性、可測性等。格局模塊的功能圖或/數據流圖映射出函數結構是常用的方法之一

?

(17) 改進模塊中的函數結構,降低函數間的耦合度,并提供函數的獨立性以及代碼可讀性、效率和可維護性。優化函數結構時,要遵守以下原則: a、不能影響模塊功能的實現 b、仔細考察模塊或函數出錯處理及模塊的性能要求并進行完善 c、通過分級或合并函數來改進軟件結構 d、考察函數的規模,過大的要進行分解 e、降低函數間接口的復雜度 f、不同層次的函數調用要有較合理的扇入、扇出 g、函數功能應可預測 h、提高函數內聚(單一功能的函數內聚最高) 說明:對初步劃分后的函數結構應進行改進、優化,使之更為合理

?

(18) 避免使用BOOL參數 ???? 說明:原因有而,其一是BOOL參數值無意義,TRUE/FALSE的含義是非常模糊的,在調用時很難知道該蠶食到底傳達的是什么意思;其二是BOOL參數值不利于擴充。還有NULL夜市一個無意義的單詞。

?

(19) 當一個過程(函數)中對較長變量(一般是結構的成員)有較多引用時,可以用一個意義相當的宏代替 ???? 說明:這樣可以增加編程效率和程序的可讀性 示例:在某過程中較多引用TheReceiveBuffer[ FirstSocker ].buDataPtr 則可以通過以下宏定義來替換: #define pSOCKDATA TheReceiveBuffer[ FirstSocker ].buDataPtr

?

12.5 可測性 (1) 在同一項目組或產品組中,要有一套同一的為集成測試與系統聯調準備的調測開關及相應的打印函數,并且要有詳細的說明 (2) 編程的同時要為單元測試選擇恰當的測試點,并仔細構造測試代碼、測試用例,同時給出明確的注釋說明。測試代碼部分應作為(模塊中的)一個子模塊,以網編測試代碼在模塊中的安裝于卸載(通過調測開關) (3) 在進行集成測試/系統聯調之前,要構造好測試環境、測試項目及測試用例,同時仔細分析并優化測試用例,以提高測試效率 ??? 說明:好的測試用例應盡可能模擬出程序所遇到的邊界值、各種復雜環境及一些極端情況等 (4) 使用斷言來發現軟件問題,提高代碼可測性 ??? 說明: (5) 使用斷言來發現軟件問題,提高代碼可測性 ??? 說明:斷言是對某種假設條件進行檢查(可理解為若條件成立則無動作,否則應報告),他可以快速發現并定位軟件問題,同時對系統錯誤進行自動報警。斷言可以對在系統中隱藏很深,用其他手段極難發現的問題進行定位,從而縮短軟件問題定位時間,提高系統的可測性。實際應用時,可根據具體情況靈活地設計斷言。 示例:下面是C語言中的一個斷言,用宏來設計的(其中NULL為0L) #ifdef _EXAM_ASSERT_TEST_ // 若使用斷言測試

?

void exam_assert( char *file_numa, unsigned int line_no ) { ??? printf( "\n[EXAM]Assert failed:%s, line %u\n", file_name, line_no ); ??? abort(); }

?

#define EXAM_ASSERT( condition ) { ??? if ( condition ) // 若條件成立,則無動作 ??? { ??????? NULL; ??? } ??? else ??? { ??????? exam_assert( __FILE__, __LINE__) ??? } } #else // 若不使用斷言測試

?

#define EXAM_ASSERT( condition ) NULL

?

#endif /* end of ASSERT */ (6) 不能用斷言來檢查最終產品肯定會出現且必須處理的錯誤情況 ??? 說明:斷言是用來處理不應該發生的錯誤情況的,對于可能會發生的且必須處理的情況要寫防錯程序,而不是斷言。如某模塊收到其他模塊或鏈路上的消息后,要對消息的合理性進行檢查,此過程為正常的錯誤檢查,不能用斷言來實現

?

(7) 對較復雜的斷言加上明確的注釋 ??? 說明:為復雜的斷言加注釋,可澄清斷言含義并減少不必要的誤用

?

(8) 用斷言保證沒有定義的特性或功能不被使用 示例:假設某通信模塊在設計時,準備提供“無連接”和“連接”這兩種業務。但當前的版本中僅實現了“無連接”業務,且在此版本的正式發行版中,用戶(上層模塊)不用產生“連接”業務的請求,那么在測試時可用斷言檢查用戶是否使用“連接”業務。如下: #define EXAM_CONNECTIONLESS 0 // 無連接業務 #define EXAM_CONNECTION???? 1 // 連接業務

?

int msg_process( EXAM_MESSAGE *msg ) { ??? unsigned char service;

?

??? EXAM_ASSERT( NULL != msg );

?

??? service = get_msg_service_class( msg );

?

??? EXAM_ASSERT( service != EXAM_CONNECTION); // 假設不使用連接業務 .... }

?

(8) 正式軟件產品中應把斷言及其他調測代碼去掉(即把有關的調測開關關掉) ??? 說明:加快軟件運行速度

?

(9) 在編寫代碼之前,應先設計好程序調試與測試的方法和手段,并設計好各種調測開關及相應測試代碼如打印函數等 ??? 說明:程序的調試與測試是軟件生命周期中很重要的一個階段,如何對軟件進行較全面、高效的測試并盡可能低找出軟件中的錯誤就成為很關鍵的問題。因此在編寫源代碼之前,除了要有一套比較完善的測試計劃外,還應設計出一系列代碼測試手段,為單元測試、集成測試及系統聯調提供方便

?

(10) 調測開關應分為不同級別和類型 ???? 說明:調測開關的設置及分類應從以下幾方面考慮:針對模塊或系統某部分代碼的調測;針對模塊或系統某功能的調測;處于某種其他目的,如對性能、容量等的測試、這樣做便于軟件功能的調測,并且便于模塊的單元測試、系統聯調等

?

12.6 程序效率 (1) 編程時要經常注意代碼的效率 ??? 說明:代碼效率分為全局效率、局部效率、時間效率及空間效率。

?

(2) 循環體內工作量最小化 ??? 說明:應仔細考慮循環體內的語句是否可以放在循環體之外,是循環體內工作量最小,從而提高程序的時間效率

?

(3) 對模塊中函數的劃分及組織方式進行分析、優化,改進模塊中函數的組織結構,提高程序效率 ??? 說明:軟件系統的效率主要與算法、處理任務方式、系統功能及函數結構有很大關系,盡在代碼上下功夫一般不能解決根本問題 (4) 編程時,要隨時留心代碼效率;優化代碼時,要考慮周全

?

(5) 要仔細的構造或直接用匯編編寫調用頻繁或性能要求極高的函數 ??? 說明:只有對編譯系統產生機器碼的方式以及硬件系統較為熟悉時,才可以使用匯編嵌入方式。嵌入匯編可提高時間及空間效率,但也存在一定風險

?

(6) 在多重循環中,應將最忙的循環放在最外層 ??? 說明:減少cpu切入循環層的次數

?

(7) 避免循環體內含判斷語句,應將循環語句至于判斷語句的代碼塊之中 ??? 說明:目的是減少判斷次數。循環體重的判斷語句是否可以移到循環體外,要視程序的具體情況而言,一般情況,與循環變量無關的判斷語句可以移到循環體外,而有關的則不可以。

?

(8) 盡量用乘法或其他方法代替除法,特別是浮點運算中的觸發 ??? 說明:浮點運算觸發要占用較多cpu資源 示例:如下表達式運算可能要占較多cpu資源 #define PAI 3.1416 radius = circle_length / ( 2 * PAI );

?

應如下把浮點除法改為浮點乘法 #define PAI_RECIPROCAL ( 1 / 3.1416 ); // 編譯器編譯時,將發生具體浮點數 radius = circle_length * PAI_RECIPROCAL / 2;

?

12.7 代碼編輯、編譯、審查 (1) 在產品軟件(項目組)中,要統一編譯開關選項 (2) 通過代碼走讀及審查方式對代碼進行檢查 ??? 說明:代碼走讀主要是對程序的編譯風格如注釋、命名等以及編程時易出錯的內容進行檢查,可由開發人員自己或開發人員交叉的方式進行;代碼審查主要是對程序實現的功能及程序的穩定性、安全性、可靠性等進行檢查及評審,可通過自審、交叉審核或指定部門抽查等方式進行

?

12.8 代碼測試、維護 (1) 單元測試要求至少達到語句覆蓋 (2) 單元測試開始要跟蹤每一條語句,并觀察數據流及變量的變化 (3) 清理、整理或優化后的代碼要經過審查及測試 (4) 代碼版本升級要經過嚴格測試 (5) 使用工具軟件對代碼版本進行維護 (6) 正式版本上軟件的任何修改都應有詳細的文檔記錄 (7) 關鍵的代碼在匯編級跟蹤 (8) 仔細設計并分析測試用例,使測試用例覆蓋盡可能多的情況,以提高測試用例的效率 (9) 盡可能模擬出程序的各種出錯情況,對出錯處理代碼進行充分的測試 (10) 仔細測試代碼處理數據、變量的邊界情況 (11) 保留測試信息,以便分析、總結經驗及進行更充分的測試 (12) 不應通過“試”來解決問題,應尋找問題的根本原因 (13) 對自動消失的錯誤進行分析,搞清楚錯誤時如何消失的 (14) 修改錯誤不僅要治標,更要治本 (15) 測試時應設法使很少發生的事件經常發生 (16) 明確模塊或函數處理哪些事件,并使他們經常發生 (17) 堅持在編碼階段就對代碼進行徹底的單元測試,不要等以后的測試工作來發現問題 (18) 去除代碼運行的隨機性(如去掉誤用的數據、代碼及盡可能防止并注意函數中的“內部寄存器”等),讓函數運行的結果可預測,并使出現的錯誤可再現

?

轉載于:https://www.cnblogs.com/studynote/articles/3448859.html

總結

以上是生活随笔為你收集整理的[转载]高质量c/c++编程指南读书笔记的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。