本章節簡單介紹一下指標,這部分算是語法的延續,不過通常是搭配後續介紹的STL使用所以就放在這裡了,雖然指標在實作部分很複雜的演算法以及開發都會用到,但由於本教學以易懂為導向,因此僅簡單介紹而已。
在使用C++宣告變數的時候只要簡單寫下如int n;就可以了,但實際上電腦會需要處理一個資訊「該變數儲存在電腦裡記憶體的哪個位置」,稱做位址,使用指標紀錄。
記憶體是電腦暫存程式資料的零件,也就是說C++執行過程中的變數都需要放在記憶體內,但記憶體並不是隨變戳一個點就能拿到需要的資料的,這時候就需要透過指標儲存位址來記錄變數儲存在哪。
簡單來說,可以把位址當作一個房子的地址,房子內可以住我們宣告的變數,而指標就是拿來儲存地址的字條,電腦可以利用指標找到地址後取用變數。
接下來要介紹兩種新的符號,並且在宣告時與非宣告時有不同意義,有四種新操作,有點複雜,可以先簡單看過,注意有註記常用的部分就好,最後會再進行重點整理。
「*」在宣告時(不常用):宣告指標。此處的「*」跟乘法的符號一樣,但用法為「*變數名稱」,與乘法的「變數1名稱*變數2名稱」不同。
「&」在非宣告時(不常用):取得變數位址。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n=123;
int *pointer = &n;//此處*pointer代表宣告pointer為指向int的指標,&n代表取得變數n之位址
cout<<pointer;//指標是可以輸出的,筆者測試的結果為輸出「0xb49fdffc04」,這就是儲存變數的位址,看不懂正常,是給電腦看的
int *x,y;//注意宣告指標的*效果只有其後的變數名稱,如左所示x是int指標,y為一般int
}
為何會說以上兩種操作不常用,因為平常不會這樣宣告指標,要到非常高級的演算法才會必須這樣宣告指標。
這裡埋個伏筆,後續會有更常見的「指標宣告」。
「*」在非宣告時(常用):取值,也就是將指標變回其所指的變數。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n=123;
int *pointer = &n;
cout<< *pointer;//*pointer等價於pointer指向的變數n
n=456;
cout<< *pointer<<'\n';//輸出「456」,因*pointer指向的n值被修改了
*pointer=789;//修改pointer指向的變數為789,等同賦值n為789
cout<<n;//輸出「789」
}
「&」在宣告時(常用):我習慣稱其為取別名,實際是「引用」的概念,將宣告變數的位址指定為現有變數的位址。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n=123;
int &m = n;//宣告&m時後面必須有=另一個變數,此時可以理解為m是變數n的別名,兩者完全綁定
cout<<n<<' '<<m<<'\n';//輸出「123 123」
n=456;
cout<<n<<' '<<m<<'\n';//輸出「456 456」
m=789;
cout<<n<<' '<<m<<'\n';//輸出「789 789」
cout<<&n<<' '<<&m<<'\n';//由於是同一個變數,其位址也會相同
}
看到這裡可能會想問,為何「取別名」這樣的功能會屬於常用?明明一樣的變數有一個名字就好。其實取別名在許多時候是有不可取代的功能的,如以下例子。
#include<bits/stdc++.h>
using namespace std;
void set_to_100_a(int x){
x=100;
cout<<x<<'\n';
}
void set_to_100_b(int &x){
x=100;
cout<<x<<'\n';
}
int main(){
//我們有兩個看似一樣的函式set_to_100_a和set_to_100_b,唯一差別是參數中是int x跟int &x
//set_to_100_a的參數是int x,代表x是新宣告的變數,呼叫該函式時會將輸入的引數賦值給這個x
//set_to_100_b的參數是int &x,以上述取別名的概念,呼叫該函式時輸入的引數在函式中會被取別名x
//然後兩個函式同樣會將x設為100,在輸出x後就結束
int n=5;
set_to_100_a(n);//執行到此會輸出「100」,因在set_to_100_a中有個新的變數x,雖然一開始是5但隨後被改成100並輸出
cout<<n<<'\n';//此處會輸出「5」,因n值只是在呼叫set_to_100_a時將5丟給x,後續的操作都跟n無關了
set_to_100_b(n);//執行到此會輸出「100」,因在set_to_100_b中的x是main函式中n的別名,然後在set_to_100_b中修改x的值後輸出其值
cout<<n<<'\n';//此處會輸出「100」,因n值在set_to_100_b函式中有別名x,然後x遭到修改,因此n值也會改變
}
如此一來在這種需要另外開函式但又要能處理main函式中變數的情況,取別名就有不可取代的功用了。
但需要注意若函式的參數有以「取別名」方式宣告的,呼叫函式時該值必須要填入單一個變數。
請記住用&宣告的所有變數都不會產生新的變數,都是跟現有的變數進行綁定。
如此一來應該很好理解「呼叫函式時該值必須要填入單一個變數」這件事,而後續也還有其他使用&宣告變數的情況,謹記以上的重點能幫助後續的理解。
以下是本部分的重點整理,希望能幫助讀者記憶。
運算符號 | 宣告時 | 非宣告時 |
& | 取別名(常用) | 取得變數位址 |
* | 宣告指標 | 取得指標所指變數(常用) |
還記得上面埋的伏筆嗎,下面要來介紹指標的進階應用。
如果今天宣告了一個整數陣列int a[105];,然後嘗試執行cout<<a;,也許有人會想陣列的使用方式不是要a[索引值]嗎,但馬上就能發現不僅沒有報錯,還能正常輸出內容,仔細一看可以發現輸出的內容與前面指標的輸出內容很像。
這是因為C++陣列的實現方式其實是連續記憶體,還記得上面將記憶體比喻成房子嗎,其實這些房子的地址是按照順序一間一間編的,而陣列是用連續記憶體就表示陣列的變數實際上是住在連續的房子內,也就是我們只要知道第一個變數的地址,就可以算出每個變數的地址。
然後C++中我們如果直接使用陣列名稱a不加上[],實際上給的就是a[0]的位址,有趣的是我們如果要算出後面的元素住的位址,可以直接寫a+1、a+2、...、a+n來取得(若要進行取值須加上括號,因取值會比加法優先導致先取a[0]再加),剛好也會對應a[1]、a[2]、...、a[n]。
以下的程式可以證明這點。
#include<bits/stdc++.h>
using namespace std;
int a[5]={0,1,2,3,4};
int main(){
for(int i=0;i<5;i++){
cout<<a[i]<<' ';//直接輸出a[i]
cout<<*(a+i)<<'\n';//利用a開頭的位址加上索引值計算出特定項的位址後再取值輸出
}
}
可以發現以上程式碼不管陣列a的值怎麼改,每行輸出的內容都會相等。
在使用陣列的指標時,請確保指標都在陣列宣告的範圍內,若對指向陣列外的指標進行操作可能會發生錯誤,甚至導致程式意外中斷。
有趣的是,既然我們可以透過知道陣列開頭的位址,再加上要找到幾號元素,來得到其位址。那我們是不是也可以從陣列中某個未知位址,看跟陣列開頭差多遠,來計算該變數是幾號元素,答案是沒錯。
假設我們有指標pointer儲存陣列a中的未知位址,透過「pointer-a」就可以得到其索引值了,如果有點難想像的話,不妨思考「開頭位址+索引值=元素位址」,所以「元素位址-開頭位址=索引值」,或者思考「(a+x)-a=x」(其中a是陣列,x是索引值),雖然這樣的解釋方式看似隨意,但卻滿好理解的~
既然陣列的指標可以利用加法來得到下一個,那麼不就跟int很類似嗎,而假設要用簡短的寫法來修改int n的值,是有n+=5、n-=5、n++、n--的寫法,也因此指標也有+=、-=、++、--的操作,其中++、--會較為常用,因為可以快速將指標移動到原本位置的下一位、上一位。
以上就是C++將陣列以指標操作的方法,這個做法在後面會經常使用,請熟記其意義。
如果在以上程式的迴圈中再加入「cout<<&a[i]<<' ';」或「cout<<a+i<<' ';」,可以發現一件有趣的事。
筆者測試的輸出為「0x9fc1fff8f0 0x9fc1fff8f4 0x9fc1fff8f8 0x9fc1fff8fc 0x9fc1fff900」,因為每次程式執行時變數儲存的位址會變,因此結果可能不盡相同,但輸出的結果都會是一串只有最後一兩個字元不同的位址。
這是因為前面講了陣列是以連續記憶體儲存,但為何不是差1的等差數列呢,其實是一個int就佔了4 bytes的關係(可以暫時理解為四棟房子),而位址中出現c,f等等字母是因為採用十六進位的表示法,a代表10、b代表11、...、f代表15。
若有回覆需求可以選擇提供email。