高品質C++/C編程指南 – 第6章 函數設計

來源:互聯網
上載者:User

第6章 函數設計

函數是C++/C程式的準系統單元,其重要性不言而喻。函數設計的細微缺點很容易導致該函數被錯用,所以光使函數的功能正確是不夠的。本章重點論述函數的介面設計和內部實現的一些規則。

函數介面的兩個要素是參數和傳回值。C語言中,函數的參數和傳回值的傳遞方式有兩種:值傳遞(pass by value)和指標傳遞(pass by pointer)。C++ 語言中多了引用傳遞(pass by reference)。由於引用傳遞的性質象指標傳遞,而使用方式卻象值傳遞,初學者常常迷惑不解,容易引起混亂,請先閱讀6.6節“引用與指標的比較”。

6.1 參數的規則
【規則6-1-1】參數的書寫要完整,不要貪圖省事唯寫參數的類型而省略參數名字。如果函數沒有參數,則用void填充。
例如:
void SetValue(int width, int height);   // 良好的風格
void SetValue(int, int);            // 不良的風格
float GetValue(void);    // 良好的風格
float GetValue();       // 不良的風格
 
【規則6-1-2】參數命名要恰當,順序要合理。
例如編寫字串拷貝函數StringCopy,它有兩個參數。如果把參數名字起為str1和str2,例如void StringCopy(char *str1, char *str2);

那麼我們很難搞清楚究竟是把str1拷貝到str2中,還是剛好倒過來。
可以把參數名字起得更有意義,如叫strSource和strDestination。這樣從名字上就可以看出應該把strSource拷貝到strDestination。

還有一個問題,這兩個參數那一個該在前那一個該在後?參數的順序要遵循程式員的習慣。一般地,應將目的參數放在前面,源參數放在後面。
如果將函式宣告為:
void StringCopy(char *strSource, char *strDestination);
別人在使用時可能會不假思索地寫成如下形式:
char str[20];
StringCopy(str, “Hello World”);   // 參數順序顛倒

【規則6-1-3】如果參數是指標,且僅作輸入用,則應在類型前加const,以防止該指標在函數體內被意外修改。
例如:
void StringCopy(char *strDestination,const char *strSource);

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

【建議6-1-1】避免函數有太多的參數,參數個數盡量控制在5個以內。如果參數太多,在使用時容易將參數類型或順序搞錯。

【建議6-1-2】盡量不要使用類型和數目不確定的參數。
C標準庫函數printf是採用不確定參數的典型代表,其原型為:
int printf(const chat *format[, argument]…);
這種風格的函數在編譯時間喪失了嚴格的型別安全檢查。

6.2 傳回值的規則
【規則6-2-1】不要省略傳回值的類型。

C語言中,凡不加類型說明的函數,一律自動按整型處理。這樣做不會有什麼好處,卻容易被誤解為void類型。

C++語言有很嚴格的型別安全檢查,不允許上述情況發生。由於C++程式可以調用C函數,為了避免混亂,規定任何C++/ C函數都必須有類型。如果函數沒有傳回值,那麼應聲明為void類型。

【規則6-2-2】函數名字與傳回值類型在語義上不可衝突。
違反這條規則的典型代表是C標準庫函數getchar。
例如:
char c;
c = getchar();
if (c == EOF)

按照getchar名字的意思,將變數c聲明為char類型是很自然的事情。但不幸的是getchar的確不是char類型,而是int類型,其原型如下:
      int getchar(void);

由於c是char類型,取值範圍是[-128,127],如果宏EOF的值在char的取值範圍之外,那麼if語句將總是失敗,這種“危險”人們一般哪裡料得到!導致本例錯誤的責任並不在使用者,是函數getchar誤導了使用者。

【規則6-2-3】不要將正常值和錯誤標誌混在一起返回。正常值用輸出參數獲得,而錯誤標誌用return語句返回。

回顧上例,C標準庫函數的設計者為什麼要將getchar聲明為令人迷糊的int類型呢?他會那麼傻嗎?
在正常情況下,getchar的確返回單個字元。但如果getchar碰到檔案結束標誌或發生讀錯誤,它必須返回一個標誌EOF。為了區別於正常的字元,只好將EOF定義為負數(通常為負1)。因此函數getchar就成了int類型。

我們在實際工作中,經常會碰到上述令人為難的問題。為了避免出現誤解,我們應該將正常值和錯誤標誌分開。即:正常值用輸出參數獲得,而錯誤標誌用return語句返回。
函數getchar可以改寫成 BOOL GetChar(char *c);
雖然gechar比GetChar靈活,例如 putchar(getchar()); 但是如果getchar用錯了,它的靈活性又有什麼用呢?

【建議6-2-1】有時候函數原本不需要傳回值,但為了增加靈活性如支援鏈式表達,可以附加傳回值。

例如字串拷貝函數strcpy的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy函數將strSrc拷貝至輸出參數strDest中,同時函數的傳回值又是strDest。這樣做並非多此一舉,可以獲得如下靈活性:
    char str[20];
    int  length = strlen( strcpy(str, “Hello World”) );

【建議6-2-2】如果函數的傳回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合只能用“值傳遞”而不能用“引用傳遞”,否則會出錯。
例如:
class String
{…
    // 賦值函數
    String & operate=(const String &other);   
// 相加函數,如果沒有friend修飾則只許有一個右側參數
friend    String   operate+( const String &s1, const String &s2);
private:
    char *m_data;
}

String的賦值函數operate = 的實現如下:
String & String::operate=(const String &other)
{
    if (this == &other)
        return *this;
    delete m_data;
    m_data = new char[strlen(other.data)+1];
    strcpy(m_data, other.data);
    return *this;    // 返回的是 *this的引用,無需拷貝過程
}
 對於賦值函數,應當用“引用傳遞”的方式返回String對象。如果用“值傳遞”的方式,雖然功能仍然正確,但由於return語句要把 *this拷貝到儲存傳回值的外部儲存單元之中,增加了不必要的開銷,降低了賦值函數的效率。例如:
  String a,b,c;
  …
  a = b;     // 如果用“值傳遞”,將產生一次 *this 拷貝
  a = b = c;   // 如果用“值傳遞”,將產生兩次 *this 拷貝
 
String的相加函數operate + 的實現如下:
String  operate+(const String &s1, const String &s2) 
{
    String temp;
    delete temp.data;    // temp.data是僅含‘’的字串
        temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
        strcpy(temp.data, s1.data);
        strcat(temp.data, s2.data);
        return temp;
    }
 對於相加函數,應當用“值傳遞”的方式返回String對象。如果改用“引用傳遞”,那麼函數傳回值是一個指向局部對象temp的“引用”。由於temp在函數結束時被自動銷毀,將導致返回的“引用”無效。例如:
    c = a + b;
此時 a + b 並不返回期望值,c什麼也得不到,流下了隱患。

6.3 函數內部實現的規則
不同功能的函數其內部實現各不相同,看起來似乎無法就“內部實現”達成一致的觀點。但根據經驗,我們可以在函數體的“入口處”和“出口處”從嚴把關,從而提高函數的品質。

【規則6-3-1】在函數體的“入口處”,對參數的有效性進行檢查。
很多程式錯誤是由非法參數引起的,我們應該充分理解並正確使用“斷言”(assert)來防止此類錯
誤。詳見6.5節“使用斷言”。

【規則6-3-2】在函數體的“出口處”,對return語句的正確性和效率進行檢查。
     如果函數有傳回值,那麼函數的“出口處”是return語句。我們不要輕視return語句。如果return語句寫得不好,函數要麼出錯,要麼效率低下。
注意事項如下:
(1)return語句不可返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函數體結束時被自動銷毀。例如
    char * Func(void)
    {
        char str[] = “hello world”;    // str的記憶體位於棧上
        …
        return str;     // 將導致錯誤
    }
(2)要搞清楚返回的究竟是“值”、“指標”還是“引用”。
(3)如果函數傳回值是一個對象,要考慮return語句的效率。例如   
             return String(s1 + s2);
這是臨時對象的文法,表示“建立一個臨時對象並返回它”。不要以為它與“先建立一個局部對象temp並返回它的結果”是等價的,如
String temp(s1 + s2);
return temp;
實質不然,上述代碼將發生三件事。首先,temp對象被建立,同時完成初始化;然後拷貝建構函式把temp拷貝到儲存傳回值的外部儲存單元中;最後,temp在函數結束時被銷毀(調用解構函式)。
然而“建立一個臨時對象並返回它”的過程是不同的,編譯器直接把臨時對象建立並初始化在外部儲存單元中,省去了拷貝和析構的化費,提高了效率。

類似地,我們不要將return int(x + y); // 建立一個臨時變數並返回它寫成
int temp = x + y;
return temp;
由於內部資料類型如int,float,double的變數不存在建構函式與解構函式,雖然該“臨時變數的文法”不會提高多少效率,但是程式更加簡潔易讀。

6.4 其它建議
【建議6-4-1】函數的功能要單一,不要設計多用途的函數。
【建議6-4-2】函數體的規模要小,盡量控制在50行代碼之內。
【建議6-4-3】盡量避免函數帶有“記憶”功能。相同的輸入應當產生相同的輸出。

帶有“記憶”功能的函數,其行為可能是不可預測的,因為它的行為可能取決於某種“記憶狀態”。這樣的函數既不易理解又不利於測試和維護。在C/C++語言中,函數的static局部變數是函數的“記憶”儲存空間。建議盡量少用static局部變數,除非必需。

【建議6-4-4】不僅要檢查輸入參數的有效性,還要檢查通過其它途徑進入函數體內的變數的有效性,例如全域變數、檔案控制代碼等。

【建議6-4-5】用於出錯處理的傳回值一定要清楚,讓使用者不容易忽視或誤解錯誤情況。

6.5 使用斷言
程式一般分為Debug版本和Release版本,Debug版本用於內部調試,Release版本發行給使用者使用。

斷言assert是僅在Debug版本起作用的宏,它用於檢查“不應該”發生的情況。樣本6-5是一個記憶體複製函數。在運行過程中,如果assert的參數為假,那麼程式就會中止(一般地還會出現提示對話,說明在什麼地方引發了assert)。

         void  *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
        assert((pvTo != NULL) && (pvFrom != NULL));     // 使用斷言
        byte *pbTo = (byte *) pvTo;     // 防止改變pvTo的地址
        byte *pbFrom = (byte *) pvFrom; // 防止改變pvFrom的地址
        while(size -- > 0 )
            *pbTo ++ = *pbFrom ++ ;
        return pvTo;
}
 樣本6-5 複製不重疊的記憶體塊

assert不是一個倉促拼湊起來的宏。為了不在程式的Debug版本和Release版本引起差別,assert不應該產生任何副作用。所以assert不是函數,而是宏。程式員可以把assert看成一個在任何系統狀態下都可以安全使用的無害測試手段。如果程式在assert處終止了,並不是說含有該assert的函數有錯誤,而是調用者出了差錯,assert可以協助我們找到發生錯誤的原因。

很少有比跟蹤到程式的斷言,卻不知道該斷言的作用更讓人沮喪的事了。你化了很多時間,不是為了排除錯誤,而只是為了弄清楚這個錯誤到底是什麼。有的時候,程式員偶爾還會設計出有錯誤的斷言。所以如果搞不清楚斷言檢查的是什麼,就很難判斷錯誤是出現在程式中,還是出現在斷言中。
幸運的是這個問題很好解決,只要加上清晰的注釋即可。這本是顯而易見的

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.