# 精誠中學資訊社社課講義 [TOC] ## 迴圈 ### for #### 語法 ```cpp for (初始值; 條件判斷; 每次迴圈後做什麼事) ``` - 初始值: 可以在此宣告變數,也可使用外部的變數,如果宣告的話則變數的scope會限制在迴圈裡 - 注意: **初始值裡的變數不一定要跟迴圈的內容有任何關係,可以使用任何變數** - 條件判斷: 當這裡面的條件成立時才會進行迴圈,**會在迴圈每次即將開始時檢查**,不成立則跳出迴圈 - 「成立」的定義: 只要條件回傳的數值不是0就算成立,如果條件是大於小於等於這類的比較,則如果比較成立,該比較式就會回傳`true`(也就是1),否則就是`false`(也就是0) - 每次迴圈後做什麼事: 如同名字所說的 - 注意: **只有在迴圈完整跑完一次之後才會做這件事** 例如: ```cpp for (int i = 0; i < 10; i++) { std::cout << i << '\n'; } ``` - 輸出: ```cpp 0 1 2 3 4 5 6 7 8 9 ``` #### 特殊用法 - 省略參數 三個參數各自都可以省略 例如: ```cpp for (int i = 0; ;i++) { std::cout << i << '\n'; } ``` - 輸出: ```cpp 0 1 2 3 4 5 6 7 8 9 ...(無限迴圈) ``` 或是: ```cpp for (int i = 0; ; ) { std::cout << i << '\n'; } ``` - 輸出: ```cpp 0 0 0 0 0 0 0 0 0 0 ...(無限迴圈) ``` 甚至: ```cpp for ( ; ; ) { std::cout << "Hi" << '\n'; } ``` - 輸出: ```cpp Hi Hi Hi Hi Hi Hi Hi Hi Hi Hi ...(無限迴圈) ``` - 多個初始值、條件、要做的事 例如: ```cpp for (int i = 0, j = 14; i < 10 && j < 15; i++, j--) { std::cout << i << ", " << j << '\n'; } ``` - 輸出: ```cpp 0, 14 1, 13 2, 12 3, 11 4, 10 5, 9 6, 8 7, 7 8, 6 9, 5 ``` - 註: - &&: 同時符合(Python的and) - ||: 其中一個符合(Python的or) --- ### break, continue 在繼續看下面兩種迴圈之前,先了解break和continue的作用是很重要的 #### break 立刻跳出迴圈 例如: ```cpp for (int i = 0; i < 10; i++) { std::cout << i << '\n'; if (i == 5) { break; } } ``` - 輸出: ```cpp 0 1 2 3 4 5 ``` --- #### continue 立刻重新開始迴圈 - 注意: **「每次迴圈後做什麼事」會在使用continue後執行** 例如: ```cpp for (int i = 0; i < 10; i++) { if (i == 5) { continue; } std::cout << i << '\n'; } ``` - 輸出: ```cpp 0 1 2 3 4 6 7 8 9 ``` --- ### while #### 語法 ```cpp while (條件) ``` - 條件: 當條件成立時繼續迴圈,**會在每次迴圈即將開始時檢查** 例如: ```cpp int x = 1024; while (x > 1) { std::cout << x << '\n'; x /= 2; } ``` - 輸出: ```cpp 1024 512 256 128 64 32 16 8 4 2 ``` --- ### do-while #### 語法 ```cpp do { ... } while (條件) ``` 跟`while`的功能一樣,只是不管條件成不成立都一定會跑一次,跑完第一次後再看符不符合條件,跟其他迴圈都一樣,**條件會在每次迴圈即將開始時檢查** ## 字串 ### 什麼是字串 字串顧名思義就是一串字元,在我們生活中幾乎到處都是字串,而有很多事情如果要用程式解決也需要用到字串,C++有內建`std::string`這個型別來幫你處理字串,裡面有很多的功能可以讓你用很少的程式碼就做到強大的功能 ### 使用方法 1. `#include <string>` 2. 就像宣告普通的變數一樣,宣告一個`std::string` - 宣告的語法(僅列出常用,欲查看所有建構子請至[官方紀錄](http://www.cplusplus.com/reference/string/string/string/)): ```cpp std::string str; // 宣告一個空字串 //等同於 std::string str(); ``` ```cpp std::string str("這是字串內容"); // 等同於 std::string str = "這是字串內容"; ``` ```cpp std::string str1("字串一"); std::string str2(str1); // 複製str1的內容到str2 // 等同於 std::string str2 = str1; ``` ```cpp std::string str(10, 'A'); // str 是 "AAAAAAAAAA" ``` 3. 使用它,以下是範例(欲查看所有功能請移駕至[官方紀錄](http://www.cplusplus.com/reference/string/string/)) - 印出它 ```cpp std::string str("Hello World!\n") std::cout << str; ``` --- - 轉成大寫 ```cpp std::string str("i am palapapa"); for (std::size_t i = 0; i < str.size(); i++) { str[i] = std::toupper(str[i]); } std::cout << str; // 等同於 std::string str("i am palapapa"); for (auto it = str.begin(); it != str.end(); it++) { *it = std::toupper(*it); } std::cout << str; ``` - 輸出: ```cpp I AM PALAPAPA ``` - 其中: - `std::size_t`會宣告一個非負整數(相當於`unsigned int`),為什麼不直接用`int`呢?因為: 1. 一個字串不可能有「第負數」個字元 2. 這個型別裡有"size"字眼,可以讓程式碼閱讀者更清楚你的意圖 3. 使用`int`與`str.size()`(也就是`std::size_t`,也就是`unsigned int`)比較時會使編譯器發出警告 - `str.size()`會回傳字串的字元總數(相當於`str.length()`) (如果不知道什麼是「回傳」,別擔心,會在教函數時解釋的) - `str[i]`代表取第i個字元(第一個字元是第0個,第二個是第1個...) - `std::toupper`會將字元轉成大寫 - 至於第二個版本是什麼意思,講到指標時你就會明白了,它是一個比較正確的寫法 --- - 清除字串內容 ```cpp std::string str("Something"); str.clear(); std::cout << str; ``` - 輸出 ```cpp ``` (什麼都沒有) --- - 連接字串 ```cpp std::string str1("I am"); std::string str2("palapapa"); std::string cat = str1 + str2; std::cout << cat; ``` - 輸出 ```cpp I ampalapapa ``` --- - 刪掉其中一段 ```cpp std::string str ("This is an example sentence."); str.erase(10,8); std::cout << str << '\n'; ``` - 輸出: ```cpp This is an sentence. ``` - 其中: - `str.erase(開始位置, 要刪多少字元)` --- - 插入一段 ```cpp std::string str("I am"); str.insert(str.size(), std::string(" palapapa")); std::cout << str; ``` - 輸出: ```cpp I am palapapa ``` - 其中: - `str.insert(開始位置, 插入什麼)` --- - 尋找 ```cpp std::string haystack("There are two needles in this haystack with needles."); std::string needle("needle"); std::size_t location = haystack.find(needle); if (location != std::string::npos) { std::cout << "first needle found at: " << location << '\n'; } location = haystack.find(needle, location + 1); if (location != std::string::npos) { std::cout << "second needle found at: " << location << '\n'; } ``` - 輸出: ```cpp first needle found at: 14 second needle found at: 44 ``` - 其中: - `haystack`和`needle`是字串尋找使用的術語,`haystack`表示要尋找的地方,`needle`代表要找的東西 - 當`find`找不到`needle`時會回傳`std::string::npos`,這裡的`if`便是在判斷有沒有找到(`std::string::npos`的本意是字串的最大可能長度) - 語法: ```cpp std::string::find(要找的東西) // 或 std::string::find(要找的東西, 開始找的地方) ``` --- - 取代 ```cpp std::string str("This is a test string."); str.replace(9, 5, "n example"); std::cout << str; ``` - 輸出: ```cpp This is an example string. ``` - 其中: - 語法: ```cpp std::string::replace(從哪裡開始取代, 取代幾個字元, 要取代成的字串) ``` --- - 尋找與取代 ```cpp std::string haystack("There are two needles in this haystack with needles."); std::string needle("needle"); std::size_t location = haystack.find(needle); while (location != std::string::npos) { haystack.replace(location, needle.size(), "#"); location = haystack.find(needle); } std::cout << haystack; ``` - 輸出: ```cpp There are two #s in this haystack with #s. ``` --- - 把數字轉成字串 ```cpp int num = 0; std::cin >> num; std::string numStr = std::to_string(num) + '0'; std::cout << numStr; ``` - 輸入: ```cpp 123 ``` - 輸出: ```cpp 1230 ``` 當然,沒有必要把這些用法都背起來,當有需要時Google一下便可 ## 函數 ### 什麼是函數 試想在算數學時,我們都很常用到$f(x)$之類的函數吧?在程式語言中,函數也是同樣的概念,只是與數學的函數比較起來,數學的函數通常都是一條算式,而程式語言的函數可以包含很多條複雜的步驟。 試想有一個函數$f(x) = x + 1$,要把它用C++改寫很簡單: ```cpp int f(int x) { return x + 1; } ``` - 語法: ```cpp 回傳值型別 函數名稱(參數型別 參數名稱, ...) ``` - 注意: **參數名稱不一定要跟傳給函數的變數名稱一樣,它只是一個代號** 最好理解函數的方法就是用數學的方法去理解它,`回傳值型別`其實就是函數計算完後的「答案」的型別,而`return`就是用來「回傳」答案的,當你在函數裡`return`後,函數會立刻結束,並回傳一個值,這個值就可以被理解為這個函數的答案。而參數就像數學裡的$x$一樣,它就是一個丟給函數讓它計算的東西。如果不想要函數回傳任何東西,就在`回傳值型別`裡打`void`。 函數在程式語言的應用在於,如果你有一件工作要以程式碼完成,而且這個工作在你的程式中有多個地方都會使用到,如果不用函數的話,就只能複製貼上了吧?但複製貼上不但會讓你的程式顯得格外冗長,也會讓除錯變成一場惡夢,試想一個情況,你把一段程式碼複製貼上了100次,之後你發現你的那段程式碼有bug,為了修好這個bug,你不就要修100次了嗎?如果你一開始就把這項工作打包成一個函數,然後把這個函數用在100個地方,這樣將來如果要修改那段程式碼,只要修改函數的定義,那100個地方也就會跟著被修改了。 ## 練習1 1. 綜合今天所講的迴圈、字串、函數,請寫出一個接受三個參數的函數,名為`FindAndReplace`,三個參數都要接受字串,第一個參數是一段文字,第二個參數是要取代的字串,第三個參數代表要取代成什麼,而這個函數要回傳把第一個字串裡出現第二個字串的位置都替換成第三個字串。(其實答案上面就有,只是要修改成函數的形式而已) - 使用範例: ```cpp FindAndReplace("My name is palapapa", "p", "K"); // 回傳"My name is KalaKaKa" ``` 2. 寫出一個函數,名為`IsArmstrong`,參數是一個`int`,並回傳一個`bool`,如果參數是阿姆斯壯數的話,就回傳`true`,否則就回傳`false`。(提示: 跟數字轉成字串,字元轉成數字及`while`迴圈有關) - 阿姆斯壯數定義: 給一正整數$x$,該正整數有$y$位數,且$x$的每個位數分別是$d_1, d_2, d_3, \cdots, d_{y - 1}, d_y$,即 $$ x = 10^{y - 1}d_y + 10^{y - 2}d_{y - 1} + 10^{y - 3}d_{y - 2} +\cdots + 10^1d_2 + 10^0d_1 $$ 如果$x$是一個阿姆斯壯數,則 $$ x = d_y^y + d_{y - 1}^y + d_{y - 2}^y + \cdots + d_2^y + d_1^y $$ 例如$1634$就是一個阿姆斯壯數,因為 $$ 1634 = 1^4 + 6^4 + 3^4 + 4^4 $$ - 使用範例 ```cpp int i = 0; while (std::cin >> i) { if (IsArmstrong(i)) { std::cout << i << " is an Armstrong Number\n"; } else { std::cout << i << " is not an Armstrong Number\n"; } } ``` ## 指標 ### 什麼是指標 在電腦裡面,每一個變數都是存在記憶體裡面,所以每個變數在記憶體裡都有一個位址,用來標記它們存在哪裡,而這個位址就是**指標**,指標就只是一個32位元(跟`int`一樣大)(或64位元,取決與你的處理器)的數字。 ### 為什麼要學指標 在C++裡,在你傳變數給函數的時候,預設都是「傳值」,而不是「傳址」,也就是說,如果你寫: ```cpp void f(int x) { x++; } int x = 0; f(x); std::cout << x ``` 輸出的會是0而不是1,也就是`x`的值沒有被修改。 這是因為當你把`x`傳給`f(int x)`的時候,你傳的並不是`x`本身,而是`x`的複製品,這就是「傳值」的意義,真正進入函數只是參數的複製品,而`x++`修改的是那個複製品,在函數外面的`x`還是維持原樣。因此,如果你想要讓參數**本身**進入函數,讓參數可以在函數裡被修改,你需要的就是指標。在這一部份的最後,你就會知道如何做到這件事。 ### 如何宣告指標 在型別後面加上星號: ```cpp int *ptr; ``` 這樣一來,`ptr`的型別就是`int*`而不是`int`,也就是說,ptr裡面存放的是一個`int`在記憶體上的位址,而不是`int`的值。 - 因為指標只是記憶體的位址,所以不管是`char*`、`double*`、還是`int*`的大小都是4 byte(或8 byte) - 如果指標只是記憶體的位址,也就是一串數字,那為什麼指標還有型別呢? 這是因為對電腦來說,所有的資料都只是0和1,而且不管是什麼型別的變數都是用0和1儲存,因此指標需要型別來告訴電腦應該要怎麼解讀這串0和1,否則對電腦來說,這筆0和1只是無意義的資料 - 如果你寫`int *x, y;`,那麼`x`是一個`int*`,而`y`只是普通的`int`,如果要`x`和`y`都是`int*`則要寫`int *x, *y;` - 再次強調: 指標**就只是數字**,它並沒有什麼特別 如果我們有一個變數`x`,並且我們想要取得它被放在記憶體的哪邊,我們可以使用`&`: ```cpp int x = 0; std::cout << &x; ``` 這個動作叫做「取址」 如果我們想要把`x`的位址存到一個變數裡,這時剛剛的`ptr`就派上用場了: ```cpp int x = 0; int *ptr = &x; ``` 這樣`ptr`存的就是`x`的位址。 ### 參考與解參考 因為`ptr`存放了`x`的位址,我們稱`ptr`有`x`的「**參考**」(reference),而藉由`ptr`來取得`x`的值的動作就稱為「**解參考**」(dereference)。你可以把`ptr`想像成是一張畫著一棟房子(象徵`int`)的地圖(象徵`int*`),當你有地圖時,你就有了那棟房子的參考,而跟著地圖走到那棟房子的這個動作就叫解參考。簡單來說,解參考就是叫電腦去某個記憶體位址看看有什麼資料並取回那個資料,在我們的例子裡,那個記憶體位址被存在`ptr`裡,而那個資料就是`x`。只要在指標前面加上一個星號就代表解參考。 範例: ```cpp int x = 1337; int *ptr = &x; std::cout << *ptr; ``` 會輸出1337。 ### 陣列就是指標 當你寫: ```cpp int nums[2]; ``` 的時候,nums其實是一個指標(`int*`),它指向nums的第一個元素,當你加上下標(方括號)時,電腦會先去nums所指向的那個`int`,然後往前跳幾個位元組,位元組數取決於指標的型別(稍後解釋),之後找到你要的那個元素的位址,然後**自動解參考**(這就是為什麼雖然陣列就是指標卻不用手動加星號),並給你一個`int`。 在記憶體裡面,`nums`長得像這樣: | 位址 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | |:----:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| | 值 | 10101001 | 01010011 | 11101110 | 00101001 | 11101000 | 00001101 | 11010100 | 00001001 | 從1000到1003是第一個`int`,1004到1007是第二個`int`(一個`int`占了四個位元組),而`nums`本身的值就是1000,當你寫`nums[1]`的時候,電腦會先去`nums`所指的位址,也就是1000,並往前跳$4 \times 1$個位元組,其中,4是因為`nums`的型別是`int*`,因為每種型別占的記憶體大小不同,當你在下標寫1的時候,電腦要知道這個陣列裡的每一個元素到底占了多少空間,這樣它才知道「往前一個元素」實際上是要往前多少位元組,這也是前面提到的「為什麼指標有型別」的另一個原因。而1指的就是「第一個元素」。 ### 傳址給函數 在了解指標到底是什麼以後,我們就可以回到最初的問題,如何讓`x`在函數中被更改: ```cpp void f(int *x) { (*x)++; } int x = 0; f(&x); std::cout << x; ``` 會輸出1。 首先,我們把`f(int x)`改成`f(int *x)`,這代表現在這個函數接受的是一個`int*`而不是`int`。接著,在函數裡面,`x++`被改成了`(*x)++`,這是因為傳進去函數的是一個`int*`,所以我們要先把它解參考,才能把它的值加1(記得: 對位址解參考會得到它指向的值),如果不加星號,我們的`++`會把`x`的指標加1,而不是把`x`的值加1。最後,在呼叫這個函數的時候,我們把`f(x)`改成了`f(&x)`,這是因為現在`f`要求你傳給它一個`int*`,因此,我們需要在`x`前面加上一個`&`來取得它的位址(參考)。由於我們傳的是位址,現在這個函數會**直接去函數外面的那個`x`的位址並修改它的值**。 ### 指標運算 指標是可以相加減的,允許的計算有: 1. 指標加數字 2. 數字加指標 3. 指標減數字 4. 指標減指標 當你把數字跟指標加起來的時候,結果會是「**指標的位址向前移動(指標所指的型別的大小 * 加的數字)**」,例如,假設`ptr`是一個`int*`並且它的值是1000,則`ptr + 1`會是1004,會這樣設計是因為當你加1的時候,指標如果會自己移動到下一個`int`的所在位址會比較方便,如果它不這樣設計的話,每次就還要手動乘4。 同理,當把指標減掉一個數字的時候,結果會是「**指標的位址向後移動(指標所指的型別的大小 * 減的數字)**」,例如,假設`ptr`是一個`int*`並且它的值是1000,則`ptr - 1`會是996。 如果把兩個指標相減,則結果是「**在這兩個指標間可以放多少個指標所指的型別的變數**」,例如,假設`ptr1`是一個`int*`並且它的值是1000,`ptr2`是一個`int*`並且它的值是2000,則`ptr2 - ptr1`會是250(如果`ptr1 - ptr2`則會是-250)。