# Ch4 課堂筆記 ## 函式參數之修改 Q. 函式中參數被修改,是否影響原值? A. 有兩種可能性。 - 在==不會影響原值==的狀況,函式會將主程式輸入的引數==複製一份==,作為函式的參數,再去做接下來的操作。符合這種狀況的參數資料型態包括: - 基本資料型態:如`int`、`char`、`bool`、`float`、`double`。 例如以下程式碼,已將主程式呼叫函式的引數值1,複製一份給函式參數`a`,這個`a`是區域變數,和主程式的`a`是不一樣的。無論對函式內的`a`如何修改,都不會影響到主程式內的`a`值。 ```cpp= #include <iostream> using namespace std; void reset(int a){ a = 2; } int main(){ int a = 1; reset(a); cout << a << endl; // 輸出 1 } ``` - `string`:字串,為C++ 標準函式庫`string`所定義的資料型態。 - STL容器:如`vector`、`map`。 - ==會影響原值==的狀況: - ==陣列==。因為陣列傳入函式時,傳遞的是陣列的==起始記憶體位址==,可以讓函式直接存取。是故,當我們嘗試在函式內改變陣列元素的值,==會影響到原始傳入的陣列==。 例如以下程式碼,將陣列`a`傳入函式`reset`後,`reset`直接透過記憶體存取同一個陣列,然後將陣列`a`的所有元素都設為數字 0,函式結束後仍維持此結果: ```cpp= #include <iostream> using namespace std; void reset(int p[], int size){ for(int i = 0; i < size; i++) p[i] = 0; } int main(){ int a[] = {1, 2, 3, 4, 5}; reset(a, 5); for(int i = 0; i < 5; i++) cout << a[i] << endl; } // 輸出 0 0 0 0 0 ``` ## 記憶體位址 電腦最小的儲存單位是`bit(位元)`,存放數字 0 或 1。一個字﹙`Word`﹚會用到8個bit記憶體空間,也就是一個`byte﹙位元組﹚`,這也是目前儲存裝置常用的基本單位,並且常用大寫的「B」來簡稱。 當我們使用程式宣告一個變數,這個變數就會佔用一塊記憶體空間。記憶體空間會有數字編號的位址,一個位址指向一個 byte,用以存取所需的變數。 位址常使用十六進位制數字來表示。在十進位制,每個位元最小是 0 ,最大是 9,而在十六進位制,每個位元最小是 0 ,最大是 15,其對應的表示法如下: |十進位制|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15| |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| |十六進位制|0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F| ![image](https://hackmd.io/_uploads/r1VcMczF0.png) 不同的資料型態,會佔用不同大小的記憶體空間。例如一個整數`int`,即佔用 32 bit = 4 bytes,而一個倍精度浮點數`double`,則會佔用 8 bytes,這就是為什麼上面課本這張圖,`int i`佔了4格,`double d`佔了8格。 我們可以使用函式`sizeof()`,觀察每種資料型態佔用多少`bytes`的記憶體空間: ```cpp= #include <iostream> using namespace std; int main(){ cout << sizeof(int) << endl; // 4 cout << sizeof(double) << endl; // 8 } ``` 一開始我們知道,在呼叫函式時傳遞參數的值(`pass by value`),如果不是陣列,就沒有辦法在函式內存取主程式的變數。為了解決這個問題,我們需要一個新的工具來儲存記憶體位址,透過傳遞記憶體位址(`pass by address`),來改變該位址內存放變數的值,這個工具就是`指標`。 ## 指標 `指標(pointer)`:用來儲存資料在記憶體的==位址==(`address`)。 ### 指標的宣告 ```cpp= int *p; double *q; ``` 在此`*` 號指的是「資料型態」,代表宣告的變數是一個拿來存放位址的指標。 是故,`int *p` 代表`p` 是一個儲存整數變數記憶體位置的指標,`double *q`則代表`q`是一個儲存浮點數變數記憶體的指標。 ### 指標的初始化 假設我們在主程式宣告以下四個變數: ```cpp= int i; double d; int *p = &i; float *q = &d; ``` 指摽變數`p`、`q`被宣告的時候,同時被指定了初始值。`p`的初始值是整數變數`i`的記憶體位址,而`q`的初始值則是倍精度浮點數`d`的記憶體位址。值得注意的是,如果指向的變數變數佔用了不只一塊記憶體空間,指標變數會指向其==起始位址==。 ![image](https://hackmd.io/_uploads/ryeqjqMKR.png) ## 取址符號`&` 使用取址符號`&`,可以用來獲得變數的記憶體位址。我們可以將其直接輸出,或是指派給指標變數。我們再來看一段程式碼: ```cpp= int age = 10; float average = 1.234; int *p = &age; float *q = &average; ``` 執行完以上程式,在記憶體堆疊將會依序出現`age`、`average`、`p`、`q`四個變數,裡面存放的值如圖所示: ![image](https://hackmd.io/_uploads/rJpfiqGtC.png =350x) 其中`0022FF4C`、`0022FF48`代表的是變數`age`和`average`的起始記憶體位址,也分別是指標變數`p`和`q`的值。其值只是舉例,在每次的執行結果,記憶體位址可能都不相同,我們甚至無法保證在執行後,變數會被儲存在相鄰的位址。 由於實際的記憶體位址無從知曉,我們可以使用箭頭,來表示變數與指標之間的關係: ![image](https://hackmd.io/_uploads/SJGxzsGYA.png =x150) ## 提領運算子`*` 提領運算子同樣為`*`符號,但他的意思並不是乘法,也不是用來定義變數為指標型態,而是用來==提領變數的記憶體位址==,目的是==存取指標所指向的變數==。 ```cpp= int age = 30; int *p = &age; cout << "age = " << age << endl; // 輸出30 *p = 45; cout << "age = " << age << endl; // 輸出45 ``` 即使第 4 行程式並沒有動到變數`age`,我們也會從輸出發現,`age`的值被改變了。由於變數`p`存放的是變數`age`的記憶體位址,`*p = 45`這行程式的意思是: - 透過提領運算子`*`,存取指標`p`指向的變數,也就是變數`age`。 - 將`age`的值改成`45`。 ## 參考運算子`&` 參考運算子同樣為`&`符號,但他的意思並不是用來獲得記憶體的位址,而是==讓變數變成參考型別(reference type)==。 每個變數都有其所屬的記憶體空間,相互獨立。即使我們指定一個變數的值等於另一個變數的值,改變其中一個,另一個變數的值並不會跟著改變,例如: ```cpp= int age1 = 30; int age2 = age1; age2 = 45; cout << "age1 = " << age1 << endl; // 30 cout << "age2 = " << age2 << endl; // 45 ``` 在這個例子中,將`age2`的值修改為 45,`age1`的值仍然維持在30。 在變數前面添加參考運算子`&`,並指定其參考的變數,可以讓兩者==共用同一塊記憶體位置==。因此,修改參考型別變數的同時,同時會修改原本的變數值。例如: ```cpp= int age1 = 30; int &age2 = age1; age2 = 45; cout << "age1 = " << age1 << endl; // 45 cout << "age2 = " << age2 << endl; // 45 ``` 在這個例子中,將`age2`的值修改為 45,`age1`的值也跟著變成了 45。 了解其功用後,我們就可以在自訂函式使用 ==傳參考(`pass by reference`)== 的技巧,讓自訂函式的參數與主程式輸入的引數共用記憶體位址,直接在函式內改變其值。例如: ```cpp= #include <iostream> using namespace std; void reset(int &a){ a = 2; } int main(){ int a = 1; reset(a); cout << a << endl; // 輸出 2 } ``` 跟最一開始的例子相比,只不過把`reset(int a)`改成了`reset(int &a)`,主程式的`a`值就成功在自訂函式內被改成 2 了。 如果實在不想理解上面在幹嘛,其實還有一個方法:將自訂函式改成有回傳值的函式,如此便可回傳參數更正後的結果: ```cpp= #include <iostream> using namespace std; int reset(int &a){ a = 2; return a; } int main(){ int a = 1; a = reset(a); cout << a << endl; // 輸出 2 } ``` ## 指標與陣列 ### 陣列變數的意義 當我們在對陣列進行運算時,其實隱含了陣列指標與指標的轉換。陣列變數本身,其資料型態其實是一個指向陣列元素的指標,其值為陣列第一個元素的地址。例如執行以下程式碼: ```cpp= int a[] = {1, 2, 3}; cout << a << endl; ``` 如同指標變數的值,直接輸出陣列變數,輸出值會是一個記憶體位址,這就是陣列的==起始記憶體位址==。當我們在主程式將陣列變數傳入函式,輸入參數其實就是這個位址。由於陣列使用==連續的記憶體空間==,有了這個位址,就可以在函式內直接存取整個陣列。 ### 傳遞陣列到函式內 以一維陣列為例,常見的兩種定義方式如下: ```cpp= #include <iostream> using namespace std; void print1(int *p, int size){ for(int i = 0; i < size; i++) cout << p[i] << endl; } void print2(int p[], int size){ for(int i = 0; i < size; i++) cout << p[i] << endl; } int main(){ int arr[5] = {11, 22, 33, 44, 55}; print1(arr, 5); print2(arr, 5); return 0; } ``` `print1()`函式使用`int *p`,宣告傳入參數為指向整數的指標變數,而`print2()`函式則是使用`int p[]`,來宣告傳入參數是一個一維陣列。由於兩個方法都沒說陣列有多大,我們需要再傳入一個參數`size`,告知陣列的大小,才能在函式中完整取用整數陣列中的每個數字。 另請參考課本章節`3-4`,找出傳遞二維陣列的方法! ## 程式風格二三事 Q. ==變數會被宣告在哪裡?== A. 如果是區域變數,會被宣告在 Memory Stack;而如果是全域變數,會被宣告在 Memory Heap。兩者在記憶體存放的位置不同,後者能容納的大小上限較大。 Q. ==陣列大小開幾格?上限多少?== 就 Memory Heap來說,全域變數大概可以容納數 GB 大小的數字。而就Memory Stack來說,區域變數大概能容納 1MB ~ 8MB 大小的數字。 不同電腦的作業系統不同,記憶體容量上限也就不同。例如Memory Stack,Linux的大小上限為8MB,Windows下32位元程式預設1MB,64位元程式預設4MB。(可以在`cmd`使用指令:`ulimit -s`來查看幾B)。 實務上來說,若宣告整數變數,區域變數大概 $10^7$ 以上的量級就會遭遇`Segmentation Fault`,全域變數則可能要到 $10^9$、$10^{10}$ 或更大的量級才會遭遇`Segmentation Fault`。 Q. ==全域變數好不好?== A. 考量記憶體容量,若擔心資料太大而超過上限,就應試策略上,使用全域變數較為保險。然而就程式撰寫習慣來說,在任何地方都能存取到同一個變數,並不是一件好事,須盡量避免。 Q. ==為何發生`Segmentation Fault`?== A. 這個詞彙的意思是「記憶體區段錯誤」。在操作陣列、指標的時候,當存取到不存在的記憶體位址,就會發生此錯誤。