1. 程式人生 > >找工作筆試面試那些事兒(2)---函式那些事

找工作筆試面試那些事兒(2)---函式那些事

作者:寒小陽
時間:2013年8月。
出處:http://blog.csdn.net/han_xiaoyang/article/details/10539723
宣告:版權所有,轉載請註明出處,謝謝。

 

六、函式那些事

       函式是C++/C 程式的基本功能單元,當然是筆試面試重點考察內容。函式介面的兩個要素是引數和返回值。C 語言中,函式的引數和返回值的傳遞方式有兩種:值傳遞(pass by value)和指標傳遞(pass by pointer)。C++語言中多了引用傳遞(passby reference)。函式的設計必須兼顧功能正確和細節合理。這裡對函式的介面設計和內部實現提供了一些參考的規則,同時後面對指標傳遞和引用傳遞的差別進行了討論。

6.1 關於函式引數

      函式在設計時其引數的設計有以下一些參考規則:

      1)引數的書寫要完整,不要貪圖省事只寫引數的型別而省略引數名字。如果函式沒有引數,則用void填充。

      例如:

            void SetValue(int width, int height); //  良好的風格 

            void SetValue(int, int);     // 不良的風格 

            float GetValue(void);    //  良好的風格 

            float GetValue();    // 不良的風格

 

      2)引數命名要恰當,順序要合理。

      例如 

            void StringCopy(char *str1, char *str2); 

      就很難搞清楚究竟是把str1 拷貝到str2 中,還是剛好倒過來。

      但是寫成:

            void StringCopy(char *strDestination,char *strSource);

      就明瞭多了。

 

      3)如果引數是指標,且僅作輸入用,則應在型別前加cons t ,以防止該指標在函式體內被意外修改。

      例如:

            void StringCopy(char *strDestination,const char *strSource);

 

      4)如果輸入引數以值傳遞的方式傳遞物件,則宜改用“cons t & ”方式來傳遞,這樣可以省去臨時物件的構造和析構過程,從而提高效率。

 

      5)避免函式有太多的引數,引數個數儘量控制在5 個以內。如果引數太多,在使用時容易將引數型別或順序搞錯。

      6)儘量不要使用型別和數目不確定的引數。

            所以建議大家不要使用C中int printf(const chat *format[, argument] …)形式的定義。

 

6.2  關於函式返回值

      1)不要省略返回值的型別。如果函式沒有返回值,那麼應宣告為void 型別。

 

      2)函式名字與返回值型別在語義上不可衝突。

      例如C中getchar原型是int getchar(void),返回值是int 型別而不是char型別,但經常有人寫出如下程式碼:

            char c; 

            c = getchar(); 

            if (c == EOF) 

            … 

 

      3)不要將正常值和錯誤標誌混在一起返回。正常值用輸出引數獲得,而錯誤標誌用return 語句返回。

我們在實際工作中,為了避免出現誤解,我們應該將正常值和錯誤標誌分開。即:正常值用輸出引數獲得,而錯誤標誌用return 語句返回。例如上例中的getchar就是很好的例子。

 

      4)有時候函式原本不需要返回值,但為了增加靈活性如支援鏈式表達,可以附加返回值。

      例如字串拷貝函式strcpy 的原型: 

            char *strcpy(char *strDest ,const char *strSrc); 

      strcpy 函式將strSrc 拷貝至輸出引數strDest中,同時函式的返回值又是strDest。這樣做並非多此一舉,可以獲得如下靈活性: 

            char str[20]; 

            int  length = strlen( strcpy(str, “Hello World ”) ); 

 

      5)如果函式的返回值是一個物件,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合只能用“值傳遞”而不能用“引用傳遞”,否則會出錯。這是一個筆試面試會遇到的知識點。具體舉例說來如下:

 

 
  1. class String

  2. { …

  3. // 賦值函式

  4. String & operate=(const String &other);

  5. // 相加函式,如果沒有friend修飾則只許有一個右側引數

  6. friend String operate+( const String &s1, const String &s2);

  7. private:

  8. char *m_data;

  9. }

 

 

String 的賦值函式operate =  的實現如下: 

 

 

 
  1. String & String::operate=(const String &other)

  2. {

  3. if (this == &other)

  4. return *this;

  5. delete m_data;

  6. m_data = new char[strlen(other.data)+1];

  7. strcpy(m_data, other.data);

  8. return *this; // 返回的是 *this的引用,無需拷貝過程

  9. }

 

        對於賦值函式,應當用“引用傳遞”的方式返回St r i n g 物件。如果用“值傳遞”的方式,雖然功能仍然正確,但由於return 語句要把 *this拷貝到儲存返回值的外部儲存單元之中,增加了不必要的開銷,降低了賦值函式的效率。例如:

 

 
  1. String a,b,c;

  2. a = b; // 如果用“值傳遞”,將產生一次 *this 拷貝

  3. a = b = c; // 如果用“值傳遞”,將產生兩次 *this 拷貝

 

 

        String 的相加函式operate + 的實現如下: 

 

 

 
  1. String operate+(const String &s1, const String &s2)

  2. {

  3. String temp;

  4. delete temp.data; // temp.data 是僅含‘\0 ’的字串

  5. temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];

  6. strcpy(temp.data, s1.data);

  7. strcat(temp.data, s2.data);

  8. return temp;

  9. }

 

        對於相加函式,應當用“值傳遞”的方式返回St r i n g 物件。如果改用“引用傳遞”,那麼函式返回值是一個指向區域性物件te m p 的“引用”。由 於te m p 在函式結束時被自動銷燬,將導致返回的“引用”無效。例如: 

                  c = a + b;  

        此時 a + b  並不返回期望值,c 什麼也得不到,流下了隱患。

 

6.3 關於函式內部實現

        事實上,因為函式的功能不同,其內部實現一定是不同的,也無法指定一個統一的標準。這裡所謂的內部實現的原則,主要是指的我們可以在函式體的“入口處”和“出口處”嚴格規範,提高函式的質量。

        1)在函式體的“入口處”,對引數的有效性進行檢查。我們應該充分理解並正確使用“斷言”(assert)來防止非法輸入引數。關於斷言,後面會單獨拿出一小節來說。

        2)在函式體的“出口處”,對return 語句的正確性和效率進行檢查。

        出口處的return語句很容易導致函數出錯或者效率低下。有以下幾個注意點:

        1.return 語句不可返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時被自動銷燬。例:

 

 

 
  1. char * Func(void)

  2. {

  3. char str[] = “ hello world ” ; // str 的記憶體位於棧上

  4. return str; // 將導致錯誤

  5. }

 

 

        2.要搞清楚返回的究竟是“值”、“指標”還是“引用”。

 

        3.如果函式返回值是一個物件,要考慮 return 語句的效率。例如:

                return String(s1 + s2); 

        這是臨時物件的語法,表示“建立一個臨時物件並返回它”。不要以為它與“先創 建一個區域性物件tem p 並返回它的結果”是等價的,如 

                String temp(s1 + s2); 

                return temp; 

        實質不然,上述程式碼將發生三件事。首先,temp物件被建立,同時完成初始化; 然後拷貝建構函式把te mp拷貝到儲存返回值的外部儲存單元中;最後,te m p 在 函式結束時被銷燬(呼叫解構函式)。然而“建立一個臨時物件並返回它”的過程 是不同的,編譯器直接把臨時物件建立並初始化在外部儲存單元中,省去了拷貝和 析構的化費,提高了效率。 

        類似地,我們不要將   

                return int(x + y); //  建立一個臨時變數並返回它 

        寫成 

                int temp = x + y; 

                return temp; 

        由於內部資料型別如int , float,double 的變數不存在建構函式與解構函式,雖然該 “臨時變數的語法”不會提高多少效率,但是程式更加簡潔易讀。

6.4 其他關於函式的建議

        1)函式的功能要單一,不要設計多用途的函式。

        2)儘量避免函式帶有“記憶”功能。相同的輸入應當產生相同的輸出。再通俗一點說,儘量少使用static變數。

        3)僅要檢查輸入引數的有效性,還要檢查通過其它途徑進入函式體內的變數的有效性,例如全域性變數、檔案控制代碼等。

        4)用於出錯處理的返回值一定要清楚,讓使用者不容易忽視或誤解錯誤情況。

 

6.5 關於斷言

        前面提到了在函式的入口處,要使用斷言判斷輸入引數的合法性,那這裡專門拿出一節來談談斷言。斷言assert 是僅在Debug 版本起作用的巨集,它用於檢查“不應該”發生的情況。在執行過程中,如果assert 的引數為假,那麼程式就會中止(一般地還會出現提示對話,說明在什麼地方引發了assert)。

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

        關於斷言,也有以下一些建議:

        1)使用斷言捕捉不應該發生的非法情況。不要混淆非法情況與錯誤情況之間的區別,後者是必然存在的並且是一定要作出處理的。

        2)在函式的入口處,使用斷言檢查引數的有效性(合法性)。

        3)在編寫函式時,要進行反覆的考查,並且自問:“我打算做哪些假定?”一旦確定了的假定,就要使用斷言對假定進行檢查。

        4)一般教科書都鼓勵程式設計師們進行防錯設計,但要記住這種程式設計風格可能會隱瞞錯誤。當進行防錯設計時,如果“不可能發生”的事情的確發生了,則要使用斷言進行報警。

 

6.5 關於引用和指標

        這個絕對是筆試面試愛考察的重點之一。尤其是應聘C++的職位,多少會被問到這個問題。引用是C++ 中的概念,初學者容易把引用和指標混淆一起。例如定義n 為m 的一個引用(reference)。 

                 int m; 

                 int &n = m; 

        n 相當於m 的別名(綽號),對 n 的任何操作就是對m 的操作。所以n 既不是m 的拷貝,也不是指向m 的指標,其實n 就是m 它自己。

        關於引用有一些非常重要的規則,如下:

        1)引用被建立的同時必須被初始化(指標則可以在任何時候被初始化)。 

        2)不能有NULL引用,引用必須與合法的儲存單元關聯(指標則可以是NULL)。 

        3)一旦引用被初始化,就不能改變引用的關係(指標則可以隨時改變所指的物件)。

 

        C++ 語言中,函式的引數和返回值的傳遞方式有三種:值傳遞、指標傳遞和引用傳遞。這裡再提一下這三種方式吧。

        下面是“值傳遞”的一個例子,由於Func1函式體內的x 是外部變數n 的一份拷貝,改變x 的值不會影響n, 所以n的值仍然是0。

 

 
  1. void Func1(int x)

  2. {

  3. x = x + 10;

  4. }

  5. int n = 0;

  6. Func1(n);

  7. cout << “n = ” << n << endl; // n = 0

 

 

       以下是“指標傳遞”的一個例子。由於Func2函式體內的x 是指向外部變數n 的指標,改變該指標的內容將導致n 的值改變,所以n 的值成為10。

 

 
  1. void Func2(int *x)

  2. {

  3. (* x) = (* x) + 10;

  4. }

  5. int n = 0;

  6. Func2(&n);

  7. cout << “n = ” << n << endl; // n = 10

 

 

        以下是“引用傳遞”的示例程式。由於Func3函式體內的x 是外部變數n 的引用,x和n 是同一個東西,改變x 等於改變n ,所以n 的值成為10。 

 

 
  1. void Func3(int &x)

  2. {

  3. x = x + 10;

  4. }

  5. int n = 0;

  6. Func3(n);

  7. cout << “n = ” << n << endl; // n = 10

 

 

        在上述例子中,你可能會產生一種感覺,“引用”可以做的任何事情“指標”也都能夠做,為什麼還要“引用”這東西?

        答案是“用適當的工具做恰如其分的工作”。 

        指標能夠毫無約束地操作記憶體中的如何東西,儘管指標功能強大,但是非常危險。就象一把刀,它可以用來砍樹、裁紙、修指甲、理髮等等,誰敢這樣用? 如果的確只需要借用一下某個物件的“別名”,那麼就用“引用”,而不要用“指標”,以免發生意外。比如說,人需要一份證明,本來在檔案上蓋上公章的印子就行了,如果把取公章的鑰匙交給他,那麼他就獲得了不該有的權利。

--------------------- 本文來自 寒小陽 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/han_xiaoyang/article/details/10539723?utm_source=copy