Ch4 課堂筆記

函式參數之修改

Q. 函式中參數被修改,是否影響原值?
A. 有兩種可能性。

  • 不會影響原值的狀況,函式會將主程式輸入的引數複製一份,作為函式的參數,再去做接下來的操作。符合這種狀況的參數資料型態包括:
    • 基本資料型態:如intcharboolfloatdouble
      例如以下程式碼,已將主程式呼叫函式的引數值1,複製一份給函式參數a,這個a是區域變數,和主程式的a是不一樣的。無論對函式內的a如何修改,都不會影響到主程式內的a值。
      ​​​​​​​​#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容器:如vectormap
  • 會影響原值的狀況:
    • 陣列。因為陣列傳入函式時,傳遞的是陣列的起始記憶體位址,可以讓函式直接存取。是故,當我們嘗試在函式內改變陣列元素的值,會影響到原始傳入的陣列
      例如以下程式碼,將陣列a傳入函式reset後,reset直接透過記憶體存取同一個陣列,然後將陣列a的所有元素都設為數字 0,函式結束後仍維持此結果:
      ​​​​​​​​#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 Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

不同的資料型態,會佔用不同大小的記憶體空間。例如一個整數int,即佔用 32 bit = 4 bytes,而一個倍精度浮點數double,則會佔用 8 bytes,這就是為什麼上面課本這張圖,int i佔了4格,double d佔了8格。

我們可以使用函式sizeof(),觀察每種資料型態佔用多少bytes的記憶體空間:

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

指標的宣告

int *p; double *q;

在此* 號指的是「資料型態」,代表宣告的變數是一個拿來存放位址的指標。
是故,int *p 代表p 是一個儲存整數變數記憶體位置的指標,double *q則代表q是一個儲存浮點數變數記憶體的指標。

指標的初始化

假設我們在主程式宣告以下四個變數:

int i; double d; int *p = &i; float *q = &d;

指摽變數pq被宣告的時候,同時被指定了初始值。p的初始值是整數變數i的記憶體位址,而q的初始值則是倍精度浮點數d的記憶體位址。值得注意的是,如果指向的變數變數佔用了不只一塊記憶體空間,指標變數會指向其起始位址

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

取址符號&

使用取址符號&,可以用來獲得變數的記憶體位址。我們可以將其直接輸出,或是指派給指標變數。我們再來看一段程式碼:

int age = 10; float average = 1.234; int *p = &age; float *q = &average;

執行完以上程式,在記憶體堆疊將會依序出現ageaveragepq四個變數,裡面存放的值如圖所示:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

其中0022FF4C0022FF48代表的是變數ageaverage的起始記憶體位址,也分別是指標變數pq的值。其值只是舉例,在每次的執行結果,記憶體位址可能都不相同,我們甚至無法保證在執行後,變數會被儲存在相鄰的位址。

由於實際的記憶體位址無從知曉,我們可以使用箭頭,來表示變數與指標之間的關係:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

提領運算子*

提領運算子同樣為*符號,但他的意思並不是乘法,也不是用來定義變數為指標型態,而是用來提領變數的記憶體位址,目的是存取指標所指向的變數

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)

每個變數都有其所屬的記憶體空間,相互獨立。即使我們指定一個變數的值等於另一個變數的值,改變其中一個,另一個變數的值並不會跟著改變,例如:

int age1 = 30; int age2 = age1; age2 = 45; cout << "age1 = " << age1 << endl; // 30 cout << "age2 = " << age2 << endl; // 45

在這個例子中,將age2的值修改為 45,age1的值仍然維持在30。

在變數前面添加參考運算子&,並指定其參考的變數,可以讓兩者共用同一塊記憶體位置。因此,修改參考型別變數的同時,同時會修改原本的變數值。例如:

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) 的技巧,讓自訂函式的參數與主程式輸入的引數共用記憶體位址,直接在函式內改變其值。例如:

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

如果實在不想理解上面在幹嘛,其實還有一個方法:將自訂函式改成有回傳值的函式,如此便可回傳參數更正後的結果:

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

指標與陣列

陣列變數的意義

當我們在對陣列進行運算時,其實隱含了陣列指標與指標的轉換。陣列變數本身,其資料型態其實是一個指向陣列元素的指標,其值為陣列第一個元素的地址。例如執行以下程式碼:

int a[] = {1, 2, 3}; cout << a << endl;

如同指標變數的值,直接輸出陣列變數,輸出值會是一個記憶體位址,這就是陣列的起始記憶體位址。當我們在主程式將陣列變數傳入函式,輸入參數其實就是這個位址。由於陣列使用連續的記憶體空間,有了這個位址,就可以在函式內直接存取整個陣列。

傳遞陣列到函式內

以一維陣列為例,常見的兩種定義方式如下:

#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)。
實務上來說,若宣告整數變數,區域變數大概

107 以上的量級就會遭遇Segmentation Fault,全域變數則可能要到
109
1010
或更大的量級才會遭遇Segmentation Fault

Q. 全域變數好不好?
A. 考量記憶體容量,若擔心資料太大而超過上限,就應試策略上,使用全域變數較為保險。然而就程式撰寫習慣來說,在任何地方都能存取到同一個變數,並不是一件好事,須盡量避免。

Q. 為何發生Segmentation Fault
A. 這個詞彙的意思是「記憶體區段錯誤」。在操作陣列、指標的時候,當存取到不存在的記憶體位址,就會發生此錯誤。