找工作筆試面試那些事兒(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)如果函式的返回值是一個物件,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合只能用“值傳遞”而不能用“引用傳遞”,否則會出錯。這是一個筆試面試會遇到的知識點。具體舉例說來如下:
-
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的引用,無需拷貝過程
-
}
對於賦值函式,應當用“引用傳遞”的方式返回St r i n g 物件。如果用“值傳遞”的方式,雖然功能仍然正確,但由於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 是僅含‘\0 ’的字串
-
temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
-
strcpy(temp.data, s1.data);
-
strcat(temp.data, s2.data);
-
return temp;
-
}
對於相加函式,應當用“值傳遞”的方式返回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 語句不可返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時被自動銷燬。例:
-
char * Func(void)
-
{
-
char str[] = “ hello world ” ; // str 的記憶體位於棧上
-
…
-
return str; // 將導致錯誤
-
}
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。
-
void Func1(int x)
-
{
-
x = x + 10;
-
}
-
…
-
int n = 0;
-
Func1(n);
-
cout << “n = ” << n << endl; // n = 0
以下是“指標傳遞”的一個例子。由於Func2函式體內的x 是指向外部變數n 的指標,改變該指標的內容將導致n 的值改變,所以n 的值成為10。
-
void Func2(int *x)
-
{
-
(* x) = (* x) + 10;
-
}
-
…
-
int n = 0;
-
Func2(&n);
-
cout << “n = ” << n << endl; // n = 10
以下是“引用傳遞”的示例程式。由於Func3函式體內的x 是外部變數n 的引用,x和n 是同一個東西,改變x 等於改變n ,所以n 的值成為10。
-
void Func3(int &x)
-
{
-
x = x + 10;
-
}
-
…
-
int n = 0;
-
Func3(n);
-
cout << “n = ” << n << endl; // n = 10
在上述例子中,你可能會產生一種感覺,“引用”可以做的任何事情“指標”也都能夠做,為什麼還要“引用”這東西?
答案是“用適當的工具做恰如其分的工作”。
指標能夠毫無約束地操作記憶體中的如何東西,儘管指標功能強大,但是非常危險。就象一把刀,它可以用來砍樹、裁紙、修指甲、理髮等等,誰敢這樣用? 如果的確只需要借用一下某個物件的“別名”,那麼就用“引用”,而不要用“指標”,以免發生意外。比如說,人需要一份證明,本來在檔案上蓋上公章的印子就行了,如果把取公章的鑰匙交給他,那麼他就獲得了不該有的權利。
--------------------- 本文來自 寒小陽 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/han_xiaoyang/article/details/10539723?utm_source=copy