指標與位址


在〈變數〉談過,變數提供一個有名稱的記憶體儲存空間,變數可包含的資訊包含變數資料型態、變數記憶體位址與變數儲存值。

如果想知道變數的位址為何,可以使用 & 取址運算子(Address-of operator),例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 

    cout << "n 的值:" << n << endl
         << "n 位址:" << &n << endl; 

    return 0; 
}

執行結果:

n 的值:10
n 位址:0x61febc

這個程式中,宣告了一個 int 整數變數 nn 的位址是 0x61febc,使用 16 進位表示法,若 int 長度為 4 個位元組,從 0x61febc 後 4 個位元組都是配置給 n 的空間,現在這個空間中儲存值為 10。

直接存取變數會對分配到的空間作存取,指標(Pointer)是一種變數,指標可儲存特定的記憶體位址,要宣告指標,使用以下的語法:

type *ptr;

ptr 可儲存位址,而 type 為該位址儲存值的型態,實際宣告的方式如下:

int *n;
float *s;
char *c;

雖然宣告指標時,C++ 習慣將 * 前置在變數名稱前,不過 n 的型態是 int*s 的型態是 float*,而 c 的型態是 char*,指標的型態決定了位址上的資料如何解釋,以及如何進行指標運算(Pointer arithmetic)。

可以使用 & 運算子取得變數位址並指定給指標,例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    int *p = &n ; 

    cout << "n 變數的位址:" << &n << endl
         << "p 儲存的位址:" << p << endl; 

    return 0; 
}

執行結果:

n 變數的位址:0x61feb8
p 儲存的位址:0x61feb8

以上的程式使用 & 來取得變數 n 儲存的位址,然後指定給指標 p,因此 p 儲存的值就與 &n 取出的值相同。

可以使用提取 (Dereference)運算子 * 來提取指標儲存位址處的物件。例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    int *p = &n;

    cout << "指標 p 儲存的位址:" << p << endl
         << "提取 p 儲存位址處的物件:" << *p << endl;

    return 0; 
}

*p 提取了 p 儲存的位址處之物件,這個物件就是 n 變數,因此執行結果如下:

指標 p 儲存的位址:0x61feb8
提取 p 儲存位址處的物件:10

*p 提取了變數 n,將值指定給 *p 時,就是指定給變數 n,例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    int *p = &n ; 

    cout << "n = " << n << endl
         << "*p = " << *p << endl; 

    *p = 20; 

    cout << "n = " << n << endl
         << "*p = " << *p << endl;

    return 0; 
}

執行結果:

n = 10
*p = 10
n = 20
*p = 20

如果宣告指標但不指定初值,指標儲存的位址是未知的,存取未知位址的記憶體內容是危險的,例如:

int *p;
*p = 10;

這會造成不可預知的結果,最好為指標設定初值,如果指標一開始不儲存任何位址,可設定初值為 0,或者是使用 nullptr,例如:

int *p = nullptr;

在指標宣告時,可以靠在變數旁邊,也可以靠在型態關鍵字旁邊,或者是置中,例如:

int *p1;
int* p2;
int * p3;

這三個宣告方式都是可允許的,C++ 開發者傾向用第一個,因為可以避免以下的錯誤:

int* p1, p2;

這樣的宣告方式,初學者可能以為 p2 也是指標,但事實上並不是,以下的宣告 p1p2 才都是指標:

int *p1, *p2;

有時只希望儲存位址而不關心型態,可以使用 void* 來宣告指標,例如:

void* p;

由於 void* 型態的指標沒有任何型態資訊,只用來持有位址,不可以使用 * 運算子對 void* 型態指標提取值,編譯器也不會允許將 void* 指標直接指定給具有型態資訊的指標,必須使用 reinterpret_cast 明確告知編譯器,這個動作是你允許的,例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    void *p = &n ; 

    int *iptr = reinterpret_cast<int*>(p);
    cout << *iptr << endl; // 顯示 10

    return 0; 
}

reinterpret_cast 用於指標,它告訴編譯器,你就是要以指定型態重新解釋 p 位址處的資料。

const 宣告的變數指定值後,就不能再改變變數值,也無法對該變數取址:

const int n = 10;
int *p = &n; // error,  invalid conversion from `const int*' to `int*'

const 宣告的變數,必須使用對應的 const 型態指標才可以:

const int n = 10;
const int *p = &n;

同樣地,也就不能如下試圖改變位址處的資料:

*p = 20; // error: assignment of read-only location '* p'

const 宣告的變數指定值後,就不能再改變變數值,也無法對該變數取址,編譯會不通過,不過必要時,可以用 const_cast 叫編譯器住嘴:

const int n = 10;
int *p = const_cast<int*>(&n); 

在〈算術運算、型態轉換〉最後也提到過 const_cast,這只是叫編譯器住嘴罷了,後續程式碼也是別對 pi 位址處的資料做變動,以避免執行時期不可預期的結果。

要留意的是,const int *p 宣告的 p 並不是常數,可以儲存不同的位址。例如:

#include <iostream> 
using namespace std; 

int main() { 
    const int n = 10;
    const int m = 20;

    const int *p = &n;
    cout << p << endl;

    p = &m;
    cout << p << endl;

    return 0; 
}

執行結果:

0x61feb8
0x61feb4

如果想令指標儲存的值無法變動,必須建立指標常數,先來看看來源變數沒有 const 的情況:

int n = 10;
int m = 20;

int* const p = &n;
p = &m;  //  error: assignment of read-only variable 'p'

如果 nmconst 修飾,那麼就必須如下建立指標常數:

const int n = 10;
const int m = 20;

const int* const p = &n;
p = &m; // error: assignment of read-only variable 'p'