From Gossip@caterpillar

C Gossip: 指標與記憶體位址

變 數 中曾經說過, 變數(Variable)提供一個有名稱的記憶體儲存空間,一個變數關係至一個資料型態、一個變數本身的值與一個變數的位址值。

變數資料型態決定了變數所分配到的記憶體大小;變數本身的值是指儲存於記憶體中的某個數值,而您可以透過變數名稱取得這個數值,這個數值又稱為 rvalue或 read value;而變數的位址值則是指變數所分配到的記憶體之位置,變數本身又稱為lvalue或 location value。

如果您想知道變數的記憶體位址為何,您可以使用&運算子,&是「取址運算子」(Address-of operator),它可以取出變數的記憶體位址,例如:
#include <stdio.h>

int main(void) {
int var = 10;

printf("變數 var 的值:%d\n", var);
printf("變數 var 的記憶體位址:%X\n", &var);

return 0;

}

執行結果:
變數 var 的值:10
變數 var 的記憶體位址:22FF54

這個程式中,您宣告了一個int整數變數var,var指向的記憶體位址是22FF54,這是記憶體位址的16進位表示法,從 22FF54後的4個位元組都是var所配置到的記憶體空間,現在這個空間中儲存值為10。

直接存取變數即直接對所分配到的記憶體空間作存取,指標(Pointer)則提供了間接性,指標可指向特定的記憶體位址,而不直接操作變數或物件,要宣告 一個指標,使用以下的語法:
type *ptr;

其中type是指標的型態,每一個指標都有一個相對應的型態,用以指出所指向的資料或物件之型態有所不同,編譯器根據指標型態來確定特定記憶體位址上的資 料如何解釋,以及如何進行指標運算(Pointer arithmetic),以下是幾個指標宣告的範例:
int *iptr;
float *fptr;
char *cptr;

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

#include <stdio.h>

int main(void) {
int var = 10;
int *ptr = &var ;

printf("變數 var 的位址:%X\n", &var);
printf("指標 ptr 指向的位址:%X\n", ptr);

return 0;

}
執行結果:
變數 var 的位址:22FF54
指標 ptr 指向的位址:22FF54

如以上的程式結果所示,您使用&來取出變數var所指向的記憶體位址,然後將這個位址指定給指標ptr,因此ptr所儲存的值就與 &var所取出的值相同。

指標擁有兩種操作特性,一是操作指標所儲存的位址,一是操作指標所指向位址之資料,您可以使用提取 (Dereference)運算子*來提取指標所指向位址的資料,例如:

#include <stdio.h>

int main(void) {
int var = 10;
int *ptr = &var;

printf("指標 ptr 儲存的值:%X\n", ptr);
printf("取出 ptr 指向的記憶體位置之值:%d\n", *ptr);

return 0;

}

執行結果:
指標 ptr 儲存的值:22FF54
取出 ptr 指向的記憶體位置之值:10

如果已經取得了記憶體位置,當將某個值指定給*prt時,該記憶體位置的值也會跟著改變,相當於告訴程式,將值放到ptr所指向的記憶體位址,例如:
#include <stdio.h>

int main(void) {
int var = 10;
int *ptr = &var ;

printf("var = %d\n", var);
printf("*ptr = %d\n", *ptr);

*ptr = 20;

printf("var = %d\n", var);
printf("*ptr = %d\n", *ptr);

return 0;
}

執行結果:
var = 10
*ptr = 10
var = 20
*ptr = 20

如以上所表示的,當指標ptr所儲存的值與變數var所指向的記憶體位置相同時,當您對*ptr進行指定的動作時,就會將值直接存入該記憶體位置,因此再 透過變數var所取出的值也就改變了。

如果宣告指標但不指定初值,則指標指向的位址是未知的,存取未知位址的記憶體內容是危險的,例如:
int *ptr;
*ptr = 10;

這個程式片段並未初始指標就指定值給*ptr,所以會造成不可預知的結果(通常是記憶體區段錯誤),最好為指標設定初值,如果指標一開始不指向任何的位址,則可設定初值為0,表示不指向任何位址,例如:
int *iptr = 0;

在這邊必須注意一個指標宣告常犯的錯誤,在指標宣告時,可以靠在名稱旁邊,也可以靠在關鍵字旁邊,例如:
int *ptr1;
int* ptr2;

這兩個宣告方式都是可允許的,一般比較傾向用第一個,因為可以避免以下的錯誤:
int* prt1, ptr2;

這樣的宣告方式,初學者可能以為ptr2也是指標,但事實上並不是,以下的宣告ptr1與ptr2才都是指標:
int *ptr1, *ptr2;

有時候,您只希望儲存記憶體的位址,然後將之與另一個記憶體位址作比較,這時並不需要關心型態的問題,您可以使用void*來宣告指標,例如:
void* ptr;

由於void型態的指標沒有任何的型態資訊,所以只用來持有位址資訊,您不可以使用*運算子對void型態指標提取值,而必須作轉型動作至對應的型態,例 如

#include <stdio.h>

int main(void) {
int var = 10;
void *vptr = &var ;

// 下面這句不可行,void型態指標不可取值
// printf("%d\n", *vptr);

// 轉型為int型態指標並指定給iptr
int *iptr = (int*) vptr;
printf("%d\n", *iptr);

return 0;

}

執行結果:
10

順便來看一下const宣告的變數,被const宣告的變數一但被指定值,就不能再改變變數的值,
但是以下這樣是可以改變變數值的:
const int var = 10;
int *vptr = &var;
*vptr = 20;
printf("%d\n", var);

以上的程式會顯示20,如果您不想要該記憶體位置的值被改變,則可以用const宣告指標,例如:
const int var = 10;
const int *vptr = &var;
*vptr = 20; // error, assignment of read-only location

另外還有指標常數,也就是您一旦指定給指標值,就不能指定新的記憶體位址值給它,例如:
int x = 10;
int y = 20;
int* const vptr = &x;
vptr = &y;  // error,  assignment of read-only variable `vptr'