# 第六章 函式 練習 ## 練習 6.1 參數和引數之間的差異為何? 解: 引數是函式呼叫的實際值,是參數的初始值 ## 練習 6.2 指出下列函式何者是錯的,並描述原因,以及你會如何進行更正 ```cpp (a)int f() { string s; // ... return s; } (b)f2(int i) { /* ... */ } (c)int calc(int v1, int v1) { /* ... */ } (d)double square(double x) return x * ``` 解: - (a)回傳型別應改為 string - (b)應加上回傳型別 void - (c)參數不可為同名 - (d)函式主體一定要加上大括號(curly braces) ## 練習 6.3 撰寫並測試你自己版本的 fact 解: ```cpp #include <iostream> using std::cerr; using std::cout; using std::endl; int fact(int i) { if (i < 0) cerr << "Input cannot be a negative number."; return i > 1 ? i * fact(i - 1) : 1; } int main() { cout << fact(5) << endl; return 0; } ``` ## 練習 6.4 編寫一個與使用者互動的函式,請求輸入一個數字並產生該數字的階乘。從 main 呼叫此函式 ```cpp #include <iostream> using std::cerr; using std::cin; using std::cout; using std::endl; int fact(int i) { return i > 1 ? i * fact(i - 1) : 1; } void userInput() { int n; cout << "Enter a number within [1, 11):" << endl; while (cin >> n) { if (n < 1 || n > 10) { cout << "Out of range, please try again." << endl; continue; } cout << fact(n) << endl; cout << "Enter a number within [1, 11):" << endl; } } int main() { userInput(); return 0; } ``` ## 練習 6.5 傳寫一個函式回傳其引數的絕對值(absolute value) ```cpp #include <iostream> using std::cout; using std::endl; int abs(int i) { return i > 0 ? i : -i; } int main() { cout << abs(-5) << endl; return 0; } ``` ## 練習 6.6 解釋參數、區域變數,以及區域 static 變數之間的差異。各自給出一個函式為例 解: 參數定義在函式的參數列表裡面,而區域變數定義在程式主體裡面,區域靜態變數在程式第一次經過它的定義述句時被初始化,並且直到程式終止時才被摧毀 ```cpp #include <iostream> using std::cout; using std::endl; int count_add(int n) { static int ctr = 0; ctr += n; return ctr; } int main() { for (int i = 0; i != 10; ++i) cout << count_add(i) << endl; return 0; } ``` ## 練習 6.7 撰寫一個會在它第一次被呼叫時回傳 0,然後之後每次被呼叫就依序產生對應數字的函式 解: ```cpp #include <iostream> using std::cout; using std::endl; int generate() { static int ctr = 0; return ctr++; } int main() { for (int i = 0; i != 10; ++i) cout << generate() << endl; return 0; } ``` ## 練習 6.8 撰寫一個名為 Chapter6.h 的標頭檔,其中含有你為 6.1 中的習題所寫的函式的宣告 解: ```cpp // 使用練習 6.4 的程式 int fact(int i); void userInput(); ``` ## 練習 6.9 寫出你自己版本的 fact.cc 和 factMain.cc 檔案。這些檔案應該引入你來自前一節習題的 Chapter6.h。使用這些檔案來這些檔案來了解你的編譯器如何支援個別編譯 解: ```cpp // fact.cpp #include "Chapter6.h" #include <iostream> using std::cerr; using std::cin; using std::cout; using std::endl; int fact(int i) { return i > 1 ? i * fact(i - 1) : 1; } void userInput() { int n; cout << "Enter a number within [1, 11) :" << endl; while (cin >> n) { if (n < 1 || n > 10) { cout << "Out of range, please try again." << endl; continue; } cout << fact(n) << endl; cout << "Enter a number within [1, 11) :" << endl; } } ``` ```cpp // factMain.cpp #include "Chapter6.h" #include <iostream> int main() { cout << "5! is:" << fact(5) << endl; userInput(); } ``` 編譯指令:g++ factMain.cpp fact.cpp -o main ## 練習 6.10 使用指標撰寫一個函式來將兩個 int 的值對調(swap)。呼叫此函式並印出對調後的值來測試之 解: ```cpp #include <iostream> #include <string> using std::cin; using std::cout; using std::endl; void swap(int *n, int *m) { int tmp = *n; *n = *m; *m = tmp; } int main() { for (int n, m; cout << "Please Enter:\n", cin >> n >> m;) { swap(&n, &m); cout << n << " " << m << endl; } return 0; } ``` ## 練習 6.11 撰寫並測試接受一個參考的你自己版本的 reset 解: ```cpp #include <iostream> using std::cout; using std::endl; void reset(int &i) { i = 0; } int main() { int i = 42; reset(i); cout << i << endl; return 0; } ``` ## 練習 6.12 改寫 6.2.1 中練習 6.10 的程式,使用參考而非指標來對調兩個 int 的值。你認為哪個版本比較容易使用,原因何在? ```cpp #include <iostream> #include <string> using std::cin; using std::cout; using std::endl; void swap(int &n, int &m) { int temp = n; n = m; m = temp; } int main() { for (int n, m; cout << "Please Enter:\n", cin >> n >> m;) { swap(n, m); cout << n << " " << m << endl; } return 0; } ``` 參考更好用,呼叫的介面也更為簡潔 ## 練習 6.13 假設 T 是一個型別的名稱,請解釋宣告 void f(T) 和宣告 void f(T&) 的函式之間的差異 解: void f(T) 的參數是透過數值複製傳遞,在函式中的 T 參數是引數的拷貝,改變 T 不會影響到原來的引數 void f(&T) 的參數是透過參考傳遞,在函式中的 T 是引數的參考,改變 T 會影響到原來的引數 ## 練習 6.14 給出例子說明何時一個參數應該是參考型別。也給出參數不應該是參考的一個例子 解: 例如交換兩個整數的函式,參數應該使用參考 ```cpp void swap(int &n, int &m) { int temp = n; n = m; m = temp; } ``` 當引數的值是右值時,參數不能為參考型別,否則導致編譯錯誤 ```cpp int add(int a, int b) { return a + b; } int main() { int i = add(1, 2); return 0; } ``` ## 練習 6.15 解釋 find_char 的每個參數之型別背後的原因。特別是,為什麼 s 是對 const 的一個參考,但 occurs 卻是一個普通的參考?為甚麼這些參數是參考,但 char 參數 c 卻不是?如果我們讓 s 是一個普通的參考,可能會發生什麼事?如果讓 occurs 是對 const 的一個參考會怎麼樣呢? 解: 1. 因為 string 很長,使用引數可以避免拷貝 2. 而在函式中我們不希望改變 s 的內容,所以令 s 為常數 3. occurs 是要傳遞到函式外部的變數,所以使用參考,occurs 的值會改變,所以是普通參考 4. 我們只需要 c 的值,這個引數可能是右值(右值引數無法用於左值參考參數),因此 c 不使用參考型別 5. 如果 s 是普通參考,也可能會意外改變原來字串的內容 6. occurs 如果是常數參考,那麼就代表不能改變它原來的值,那也就失去意義了 ## 練習 6.16 下列函式,雖然是合法的,但其用處比預期低。請找出並解除這個函式所受的限制 ```cpp bool is_empty(string& s) { return s.empty(); } ``` 解: 侷限性在於常數字串與字串字面值無法作為該函式的引數,如果下面這要使用會是不合法的: ```cpp const string str; bool flag = is_empty(str); // 非法 bool flag = is_empty("hello"); // 非法 ``` 所以要將這個函式的參數定義為常數參考 ```cpp bool is_empty(const string& s) { return s.empty(); } ``` ## 練習 6.17 寫一個函式來判斷一個 string 是否含有任何大寫字母。寫一個函式來將一個 string 全部變為小寫。你在這些函式中使用的參數有相同的型別嗎?若是,為甚麼呢?若非,為何沒有? 解: 兩個函式的參考不一樣。第一個函式使用常數參考,第二個函式使用普通參考 ```cpp // hasUpper #include <iostream> #include <string> using std::cin; using std::string; using std::cout using std::boolalpha bool hasUpper(const string &s) { bool check = false; for (const char c : s) { check = isupper(c); if (check) return check; } return check; } int main() { string input; cout << "Please enter a string:"; cin >> input; cout << boolalpha << hasUpper(input); } ``` ```cpp // strToLower #include <iostream> #include <string> using std::cin; using std::string; using std::cout using std::boolalpha std::string & strToLower(std::string &s) { for (char &c : s) c = tolower(c); return s; } int main() { std::string input; std::cout << "Please enter a string:"; std::cin >> input; std::cout << strToLower(input); } ``` ## 練習 6.18 為下列每個函式撰寫宣告。當你編寫這些宣告時,使用函式名稱來表明函式所做的事 - (a)一個名為 compare 的函式,它會回傳一個 bool 並有兩個參數,這兩個參數都是對名為 matrix 的類別的參考 - (b)一個名為 change_val 的函式,它會回傳一個 vector\<int> 迭代器,並接受兩個參數:其一是一個 int,而另一個是某個 vector<int> 的迭代器 解: ```cpp (a)bool compare(matrix &m1, matrix &m2); (b)vector<int>::iterator change_val(int num, vector<int>::iterator iter); ``` ## 練習 6.19 給定下列宣告,判斷那些呼叫是合法的,而哪些是非法的。對於那些非法的呼叫,請解釋不合法的原因 ```cpp double calc(double); int count(const string &, char); int sum(vector<int>::iterator, vector<int>::iterator, int); vector<int> vec(10); (a)calc(23.4, 55.1); (b)count("abcda",'a'); (c)calc(66); (d)sum(vec.begin(), vec.end(), 3.8); ``` 解: - (a)不合法,calc 只有一個參數 - (b)合法 - (c)合法 - (d)合法 ## 練習 6.20 什麼時候參考參數應該是對 const 的參考呢?如果我們在一個參數可以對 const 的參考之時讓它是一個普通的參考,那會發生什麼事呢? 解: 應盡量將參考參數設為常數,除非有明確的目的是為了改變這個參考參數 如果參數應該是常數參考,而我們將其設為了普通參考,那麼常數引數或字面值將無法用於普通參考參數 ## 練習 6.21 撰寫一個函式,接受一個 int 和對 int 的一個指標,並回傳所接受的 int 值和指標所指的值之中比較大的那一個。你應該為這個指標使用什麼型別呢? 解: ```cpp #include <iostream> using std::cout; int larger_one(const int i, const int *const p) { return (i > *p) ? i : *p; } int main() { int i = 6; cout << larger_one(7, &i); return 0; } ``` 應為 const int* const 型別 ## 練習 6.22 寫一個函式來對調兩個 int 指標 解: ```cpp #include <iostream> #include <string> using std::cout; using std::endl; void swap(int *&n, int *&m) { int *tmp = n; n = m; m = tmp; } int main() { int i = 30, j = 90; int *n = &i, *m = &j; swap(n, m); cout << *n << " " << *m << endl; return 0; } ``` ## 練習 6.23 為本節所呈現的那些 print 函式寫出你自己的版本。呼叫那些函式中的每一個來印出定義如下的 i 與 j: ```cpp int i = 0, j[2] = { 0, 1 }; ``` 解: ```cpp #include <iostream> using std::begin; using std::cout; using std::end; using std::endl; void print(const int *pi) { if (pi) cout << *pi << endl; } void print(const int *beg, const int *end) { while (beg != end) cout << *beg++ << endl; } void print(const int ia[], size_t size) { for (size_t i = 0; i != size; ++i) { cout << ia[i] << endl; } } void print(int (&arr)[2]) { for (auto i : arr) cout << i << endl; } int main() { int i = 0, j[2] = {0, 1}; print(&i); print(begin(j), end(j)); print(j, end(j) - begin(j)); print(j); return 0; } ``` ## 練習 6.24 解釋下列函式的行為。如果程式碼中有問題,請解釋問題為何,以及你會如何修正它們 ```cpp void print(const int ia[10]) { for (size_t i = 0; i != 10; ++i) cout << ia[i] << endl; } ``` 解: 當陣列作為引數的時候,會自動轉換為指向第一個元素的指標,因此函式的參數接受的是一個指標,實際上,以陣列作為參數都會被轉為指向元素型別的指標,而函式則會被轉為指向函式的指標 如果要讓這個程式碼可以執行(不更改也可以執行),可將參數改為陣列的參考 ```cpp void print(const int (&ia)[10]) { for (size_t i = 0; i != 10; ++i) cout << ia[i] << endl; } ``` ## 練習 6.25 撰寫接受兩個引數的一個 main 函式。串接所提供的引數,並印出所產生的 string ## 練習 6.26 寫一個程式接受在本節中呈現的那些選項。印出傳入 main 的引數之值 解: ```cpp #include <iostream> #include <string> using std::cout; using std::endl; using std::string; int main(int argc, char **argv) { string str; for (int i = 1; i != argc; ++i) str += string(argv[i]) + " "; cout << str << endl; return 0; } ``` ## 練習 6.27 撰寫一個函式,接受一個 initializer_list\<int> 並產生該串列中元素的總和 解: ```cpp #include <initializer_list> #include <iostream> using std::cout; using std::endl; using std::initializer_list; int sum(initializer_list<int> const &il) { int sum = 0; for (auto i : il) sum += i; return sum; } int main(void) { auto il = {1, 2, 3, 4, 5, 6, 7, 8, 9}; cout << sum(il) << endl; return 0; } ``` ## 練習 6.28 在具有 ErrCode 參數的那第二個版本的 error_msg 中,for 迴圈中的 elem 之型別為何? 解: elem 是 const string& 型別 ## 練習 6.29 當你在一個範圍 for 中使用一個 initializer_list,你會使用一個參考作為迴圈的控制變數嗎?若是,為甚麼呢?如果不是,為何不呢? 解: 會使用常數參考型別,因為 initializer_list 物件中的元素都是常數,我們無法修改 initilizer_list 物件中元素的值 ## 練習 6.30 編譯前面所呈現的 str_subrange 版本,看看你的編譯器會如何處理我們提過的那些錯誤 解: ``` main.cpp:4:5: error: return-statement with no value, in function returning ‘bool’ [-fpermissive] ``` ## 練習 6.31 何時回傳一個參考是有效的呢?那麼對 const 的參考呢? 解: 當回傳的參考引用的物件是區域變數時,回傳的參考會是無效的(也被稱為 dangling),當我們希望回傳的物件被修改時,回傳常數參考是無效的 ## 練習 6.32 指出下列的函式是否合法。若是,解釋為何如此;如果不是,更正所有的錯誤並解釋之 ```cpp int &get(int *array, int index) { return array[index]; } int main() { int ia[10]; for (int i = 0; i != 10; ++i) get(ia, i) = i; } ``` 解: 合法,get 函式根據索引取得陣列中的元素的參考 ## 練習 6.33 寫一個遞迴函式印出一個 vector 的內容 解: ```cpp #include <iostream> #include <vector> using std::vector; using std::cout; using Iter = vector<int>::const_iterator; void print(Iter first, Iter last) { if (first != last) { cout << *first << " "; print(++first, last); } } int main() { vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; print(vec.cbegin(), vec.cend()); return 0; } ``` ## 練習 6.34 如果 factorial 中的停止條件是 if (val != 0) 會發生什麼事? 解: 如果 val 為正數,以結果來看並無區別,只是多乘了 1 如果 val 為負數,那麼遞迴永遠都不會結束 ## 練習 6.35 對 factorial 的呼叫中,為甚麼我們傳入 val - 1 而非 val--? 解: 如果傳入的值是 val--,那麼將會永遠傳入相同的值來呼叫該函式,則遞迴永遠都不會結束,可改為 --val ## 練習 6.36 宣告一個會回傳一個參考的函式,該參考指涉有十個 string 的一個陣列,而且不使用尾端回傳、decltype 或型別別名 解: ```cpp string (&fun())[10]; ``` ## 練習 6.37 為前一個練習中的函式撰寫三個額外的宣告。其中一個應該使用型別別名,一個使用尾端回傳,而第三個則使用 decltype。你喜歡哪個形式?為甚麼呢? 解: ```cpp typedef string str_arr[10]; str_arr& fun(); auto fun()->string(&)[10]; string s[10]; decltype(s)& fun(); ``` 個人認為使用尾端回傳最好,較為簡潔且只需要一行程式碼 ## 練習 6.38 修改 arrPtr 函式,改回傳對陣列的一個參考 解: ```cpp decltype(odd) &arrPtr(int i) { return (i % 2) ? odd : even; } ``` ## 練習 6.39 解釋下列每組宣告中第二個宣告的效果。指出何者是非法的,如果有的話 ```cpp (a)int calc(int, int); int calc(const int, const int); (b)int get(); double get(); (c)int *reset(int *); double *reset(double *); ``` 解: - (a)非法,因為頂層 const 不影響傳入函式的物件,所以無法區分第一個宣告與第二個宣告 - (b)非法,對於重載的函式中,它們應該只有參數的數量和參數的型別不同,回傳值與重載無關 - (c)合法 ## 練習 6.40 下列哪個宣告是錯的?為甚麼呢? ```cpp (a)int ff(int a, int b = 0, int c = 0); (b)char *init(int ht = 24, int wd, char bckgrnd); ``` 解: - (a)正確 - (b)錯誤,一旦某個參數被賦予了默認值,那麼它之後的參數都必須要有默認值 ## 練習 6.41 下列哪個呼叫是非法的呢?為甚麼呢?如果有的話,哪個是合法的,但不太可能是程式設計師所期望的?為甚麼呢? ```cpp char *init(int ht, int wd = 80, char bckgrnd = ' '); (a)init(); (b)init(24,10); (c)init(14,'*'); ``` 解: - (a)非法,第一個參數沒有默認值,因此最少需要一個引數 - (b)合法 - (c)合法,但使用方式不正確,字元 * 被轉換成 int 傳入到了第二個參數,而不是第三個參數 ## 練習 6.42 賦予 make_plural(6.3.2)的第二個參數一個 's' 的預設引數。印出字詞 success 和 failure 的單數與複數版本來測試你的程式 解: ```cpp #include <iostream> #include <string> using std::cout; using std::endl; using std::string; string make_plural(size_t ctr, const string &word, const string &ending = "s") { return (ctr > 1) ? word + ending : word; } int main() { cout << "single: " << make_plural(1, "success", "es") << " " << make_plural(1, "failure") << endl; cout << "plural: " << make_plural(2, "success", "es") << " " << make_plural(2, "failure") << endl; return 0; } ``` ## 練習 6.43 你會將下列哪個宣告和定義放在一個標頭中?或放在源碼檔中?請解釋原因 ```cpp (a)inline bool eq(const BigInt&, const BigInt&) {...} (b)void putValues(int *arr, int size); ``` 解: 全部都放進標頭中,因為(a)是 inline 函式,(b)是函式宣告 ## 練習 6.44 將 6.2.2 的 isShorter 函式改寫為 inline 解: ```cpp inline bool is_shorter(const string &str1, const string &str2) { return str1.size() < str2.size(); } ``` ## 練習 6.45 重新檢視你為前面的練習所寫的程式,判斷它們是否應該被定義為 inline。如果是,就那麼做。若非,請解釋它們為何不應該是 inline 解: 一般來說,inline 應用於較簡短、流程直接、頻繁呼叫的函式上 ## 練習 6.46 有可能將 isShorter 定義為一個 constexpr 嗎?若是,就那麼做。如果沒辦法,請解釋原因 解: 不能,因為 string.size() 並不是 constexpr 函式,因此 s1.size() == s2.size() 並不是一個 constant expression ## 練習 6.47 修改你在 6.3.2 的練習中寫的那個以遞迴印出一個 vector 內容的程式,讓它能夠條件式地印出其執行相關的資訊。舉例來說,你可能會在每次呼叫印出 vector 的大小,編譯並執行該程式,先開啟除錯來執行,然後關閉除錯再次執行 解: ```cpp #include<iostream> #include <vector> using std::vector; using std::cout; using std::endl; void printVec(vector<int> &vec) { #ifndef NDEBUG cout << "Vector size: " << vec.size() << endl; #endif if (!vec.empty()) { auto tmp = vec.back(); vec.pop_back(); printVec(vec); cout << tmp << " "; } } int main() { vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9}; printVec(vec); cout << endl; return 0; } ``` ## 練習 6.48 解釋這個迴圈所做的事,以及這是否為 assert 的良好用法: ```cpp string s; while (cin >> s && s != sought) { } // 空的主體 assert(cin); ``` 解: 不合理,從這個程式的意圖來看,應該改為: ```cpp assert(s == sought); ``` ## 練習 6.49 候選函式是甚麼?合用函式是什麼? 解: 候選函式:與被呼叫的函式同名,並且其宣告在呼叫的地方是可見的 合用函式:參數與引數的數量相等,並且每個引數型別與對應的參數型別相同或者能轉換成參數的型別 ## 練習 6.50 給定前面的 f 宣告,為下列每個呼叫列出合用的函式。指出哪個函式是最佳匹配,哪個呼叫因為沒有匹配或歧義而是非法的 ```cpp (a)f(2.56, 42) (b)f(42) (c)f(42, 0) (d)f(2.56, 3.14) ``` 解: - (a)void f(int, int); 和 void f(double, double = 3.14); 都是合用的函式,但呼叫有歧異而不合法 - (b)void f(int); 是合用的函式,且呼叫合法 - (c)void f(int, int); 和 void f(double, double = 3.14); 都是合用的函式,而 void f(int, int); 是最佳匹配 - (d)void f(int, int); 和 void f(double, double = 3.14); 是可行函式,void f(double, double = 3.14); 是最佳匹配 ## 練習 6.51 寫出 f 的四個版本。每個函式都應該印出一個足以區別的訊息。檢查你在前一個練習的答案。如果你的答案是錯的,就研讀這一節,直到你了解為何答案是錯的為止 解: ```cpp #include <iostream> using std::cout; using std::endl; void f() { cout << "f()" << endl; } void f(int) { cout << "f(int)" << endl; } void f(int, int) { cout << "f(int, int)" << endl; } void f(double, double) { cout << "f(double, double)" << endl; } int main() { //f(2.56, 42); // error: 'f' is ambiguous. f(42); f(42, 0); f(2.56, 3.14); return 0; } ``` ## 練習 6.52 給定下列宣告 ```cpp void manip(int ,int); double dobj; ``` 下列呼叫中每個轉換的排位(6.6.1)為何? ```cpp (a)manip('a', 'z'); (b)manip(55.4, dobj); ``` 解: - (a)第三級,透過型別提升所達成的匹配 - (b)第四級,透過算術型別轉換所達成的匹配 ## 練習 6.53 說明下列宣告組合中第二個宣告的效果。如果有的話,指出哪個是非法的 ```cpp (a)int calc(int&, int&); int calc(const int&, const int&); (b)int calc(char*, char*); int calc(const char*, const char*); (c)int calc(char*, char*); int calc(char* const, char* const); ``` 解: - (a):合法,為函式多載,代表的函式是不相同的 - (a):合法,為函式多載,代表的函式是不相同的 - (c):非法,函式參數列中的頂層 const 在不影響函式匹配 ## 練習 6.54 為一個函式撰寫宣告,它有兩個 int 參數,並回傳一個 int,然後再宣告一個 vector,其元素具有這種函式指標型別 解: ```cpp int func(int, int); vector<decltype(func)*> v; ``` ## 練習 6.55 寫出四個函式,為每兩個 int 值進行加、減、乘和除。將這些函式的指標儲存到你為前一個練習所寫的 vector 解: ```cpp int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int multiply(int a, int b) { return a * b; } int divide(int a, int b) { return b != 0 ? a / b : 0; } v.push_back(add); v.push_back(subtract); v.push_back(multiply); v.push_back(divide); ``` ## 練習 6.56 呼叫那個 vector 中的每個元素,並印出它們的結果 解: ```cpp std::vector<decltype(func) *> vec{add, subtract, multiply, divide}; for (auto f : vec) std::cout << f(2, 2) << std::endl; ```