Q. 函式中參數被修改,是否影響原值?
A. 有兩種可能性。
int
、char
、bool
、float
、double
。a
,這個a
是區域變數,和主程式的a
是不一樣的。無論對函式內的a
如何修改,都不會影響到主程式內的a
值。
string
:字串,為C++ 標準函式庫string
所定義的資料型態。vector
、map
。a
傳入函式reset
後,reset
直接透過記憶體存取同一個陣列,然後將陣列a
的所有元素都設為數字 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
的記憶體空間:
一開始我們知道,在呼叫函式時傳遞參數的值(pass by value
),如果不是陣列,就沒有辦法在函式內存取主程式的變數。為了解決這個問題,我們需要一個新的工具來儲存記憶體位址,透過傳遞記憶體位址(pass by address
),來改變該位址內存放變數的值,這個工具就是指標
。
指標(pointer)
:用來儲存資料在記憶體的位址(address
)。
在此*
號指的是「資料型態」,代表宣告的變數是一個拿來存放位址的指標。
是故,int *p
代表p
是一個儲存整數變數記憶體位置的指標,double *q
則代表q
是一個儲存浮點數變數記憶體的指標。
假設我們在主程式宣告以下四個變數:
指摽變數p
、q
被宣告的時候,同時被指定了初始值。p
的初始值是整數變數i
的記憶體位址,而q
的初始值則是倍精度浮點數d
的記憶體位址。值得注意的是,如果指向的變數變數佔用了不只一塊記憶體空間,指標變數會指向其起始位址。
&
使用取址符號&
,可以用來獲得變數的記憶體位址。我們可以將其直接輸出,或是指派給指標變數。我們再來看一段程式碼:
執行完以上程式,在記憶體堆疊將會依序出現age
、average
、p
、q
四個變數,裡面存放的值如圖所示:
其中0022FF4C
、0022FF48
代表的是變數age
和average
的起始記憶體位址,也分別是指標變數p
和q
的值。其值只是舉例,在每次的執行結果,記憶體位址可能都不相同,我們甚至無法保證在執行後,變數會被儲存在相鄰的位址。
由於實際的記憶體位址無從知曉,我們可以使用箭頭,來表示變數與指標之間的關係:
*
提領運算子同樣為*
符號,但他的意思並不是乘法,也不是用來定義變數為指標型態,而是用來提領變數的記憶體位址,目的是存取指標所指向的變數。
即使第 4 行程式並沒有動到變數age
,我們也會從輸出發現,age
的值被改變了。由於變數p
存放的是變數age
的記憶體位址,*p = 45
這行程式的意思是:
*
,存取指標p
指向的變數,也就是變數age
。age
的值改成45
。&
參考運算子同樣為&
符號,但他的意思並不是用來獲得記憶體的位址,而是讓變數變成參考型別(reference type)。
每個變數都有其所屬的記憶體空間,相互獨立。即使我們指定一個變數的值等於另一個變數的值,改變其中一個,另一個變數的值並不會跟著改變,例如:
在這個例子中,將age2
的值修改為 45,age1
的值仍然維持在30。
在變數前面添加參考運算子&
,並指定其參考的變數,可以讓兩者共用同一塊記憶體位置。因此,修改參考型別變數的同時,同時會修改原本的變數值。例如:
在這個例子中,將age2
的值修改為 45,age1
的值也跟著變成了 45。
了解其功用後,我們就可以在自訂函式使用 傳參考(pass by reference
) 的技巧,讓自訂函式的參數與主程式輸入的引數共用記憶體位址,直接在函式內改變其值。例如:
跟最一開始的例子相比,只不過把reset(int a)
改成了reset(int &a)
,主程式的a
值就成功在自訂函式內被改成 2 了。
如果實在不想理解上面在幹嘛,其實還有一個方法:將自訂函式改成有回傳值的函式,如此便可回傳參數更正後的結果:
當我們在對陣列進行運算時,其實隱含了陣列指標與指標的轉換。陣列變數本身,其資料型態其實是一個指向陣列元素的指標,其值為陣列第一個元素的地址。例如執行以下程式碼:
如同指標變數的值,直接輸出陣列變數,輸出值會是一個記憶體位址,這就是陣列的起始記憶體位址。當我們在主程式將陣列變數傳入函式,輸入參數其實就是這個位址。由於陣列使用連續的記憶體空間,有了這個位址,就可以在函式內直接存取整個陣列。
以一維陣列為例,常見的兩種定義方式如下:
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)。
實務上來說,若宣告整數變數,區域變數大概 以上的量級就會遭遇Segmentation Fault
,全域變數則可能要到 、 或更大的量級才會遭遇Segmentation Fault
。
Q. 全域變數好不好?
A. 考量記憶體容量,若擔心資料太大而超過上限,就應試策略上,使用全域變數較為保險。然而就程式撰寫習慣來說,在任何地方都能存取到同一個變數,並不是一件好事,須盡量避免。
Q. 為何發生Segmentation Fault
?
A. 這個詞彙的意思是「記憶體區段錯誤」。在操作陣列、指標的時候,當存取到不存在的記憶體位址,就會發生此錯誤。