# 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|

不同的資料型態,會佔用不同大小的記憶體空間。例如一個整數`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`的記憶體位址。值得注意的是,如果指向的變數變數佔用了不只一塊記憶體空間,指標變數會指向其==起始位址==。

## 取址符號`&`
使用取址符號`&`,可以用來獲得變數的記憶體位址。我們可以將其直接輸出,或是指派給指標變數。我們再來看一段程式碼:
```cpp=
int age = 10;
float average = 1.234;
int *p = &age;
float *q = &average;
```
執行完以上程式,在記憶體堆疊將會依序出現`age`、`average`、`p`、`q`四個變數,裡面存放的值如圖所示:

其中`0022FF4C`、`0022FF48`代表的是變數`age`和`average`的起始記憶體位址,也分別是指標變數`p`和`q`的值。其值只是舉例,在每次的執行結果,記憶體位址可能都不相同,我們甚至無法保證在執行後,變數會被儲存在相鄰的位址。
由於實際的記憶體位址無從知曉,我們可以使用箭頭,來表示變數與指標之間的關係:

## 提領運算子`*`
提領運算子同樣為`*`符號,但他的意思並不是乘法,也不是用來定義變數為指標型態,而是用來==提領變數的記憶體位址==,目的是==存取指標所指向的變數==。
```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. 這個詞彙的意思是「記憶體區段錯誤」。在操作陣列、指標的時候,當存取到不存在的記憶體位址,就會發生此錯誤。