指標、引用和取值
什麼是指標?什麼是記憶體地址?什麼叫做指標的取值?指標是一個儲存計算機記憶體地址的變數。在這份教程裡“引用”表示計算機記憶體地址。從指標指向的記憶體讀取資料稱作指標的取值。指標可以指向某些具體型別的變數地址,例如int、long和double。指標也可以是void型別、NULL指標和未初始化指標。本文會對上述所有指標型別進行探討。
根據出現的位置不同,操作符 * 既可以用來宣告一個指標變數,也可以用作指標的取值。當用在宣告一個變數時,*表示這裡宣告瞭一個指標。其它情況用到*表示指標的取值。
&是地址操作符,用來引用一個記憶體地址。通過在變數名字前使用&操作符,我們可以得到該變數的記憶體地址。
1 2 3 4 5 6 7 8 9 |
// 宣告一個int指標 int *ptr; // 宣告一個int值 int val = 1; // 為指標分配一個int值的引用 ptr = &val; // 對指標進行取值,列印儲存在指標地址中的內容 int deref = *ptr; printf("%d\n", deref); |
第2行,我們通過*操作符宣告瞭一個int指標。接著我們宣告瞭一個int變數並賦值為1。然後我們用int變數的地址初始化我們的int指標。接下來對int指標取值,用變數的記憶體地址初始化int指標。最終,我們列印輸出變數值,內容為1。
第6行的&val是一個引用。在val變數宣告並初始化記憶體之後,通過在變數名之前使用地址操作符&我們可以直接引用變數的記憶體地址。
第8行,我們再一次使用*操作符來對該指標取值,可直接獲得指標指向的記憶體地址中的資料。由於指標宣告的型別是int,所以取到的值是指標指向的記憶體地址儲存的int值。
這裡可以把指標、引用和值的關係類比為信封、郵箱地址和房子。一個指標就好像是一個信封,我們可以在上面填寫郵寄地址。一個引用(地址)就像是一個郵件地址,它是實際的地址。取值就像是地址對應的房子。我們可以把信封上的地址擦掉,寫上另外一個我們想要的地址,但這個行為對房子沒有任何影響。
void指標、NULL指標和未初始化指標
一個指標可以被宣告為void型別,比如void *x。一個指標可以被賦值為NULL。一個指標變數宣告之後但沒有被賦值,叫做未初始化指標。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int *uninit; // int指標未初始化 int *nullptr = NULL; // 初始化為NULL void *vptr; // void指標未初始化 int val = 1; int *iptr; int *castptr; // void型別可以儲存任意型別的指標或引用 iptr = &val; vptr = iptr; printf("iptr=%p, vptr=%p\n", iptr, vptr); // 通過顯示轉換,我們可以把一個void指標轉成 // int指標並進行取值 castptr = (int *)vptr; printf("*castptr=%d\n", *castptr); // 列印null和未初始化指標 printf("uninit=%p, nullptr=%p\n", uninit, nullptr); // 不知道你會得到怎樣的返回值,會是隨機的垃圾地址嗎? // printf("*nullptr=%d\n", nullptr); // 這裡會產生一個段錯誤 // printf("*nullptr=%d\n", nullptr); |
執行上面的程式碼,你會得到類似下面對應不同記憶體地址的輸出。
1 2 3 |
iptr=0x7fff94b89c6c, vptr=0x7fff94b89c6c *castptr=1 uninit=0x7fff94b89d50, nullptr=(nil) |
第1行我們宣告瞭一個未初始化int指標。所有的指標在賦值為NULL、一個引用(地址)或者另一個指標之前都是未被初始化的。第2行我們宣告瞭一個NULL指標。第3行宣告瞭一個void指標。第4行到第6行宣告瞭一個int值和幾個int指標。
第9行到11行,我們為int指標賦值為一個引用並把int指標賦值為void指標。void指標可以儲存各種其它指標型別。大多數時候它們被用來儲存資料結構。可以注意到,第11行我們列印了int和void指標的地址。它們現在指向了同樣的記憶體地址。所有的指標都儲存了記憶體地址。它們的型別只在取值時起作用。
第15到16行,我們把void指標轉換為int指標castptr。請注意這裡需要顯示轉換。雖然C語言並不要求顯示地轉換,但這樣會增加程式碼的可讀性。接著我們對castptr指標取值,值為1。
第19行非常有意思,在這裡列印未初始化指標和NULL指標。值得注意的是,未初始化指標是有記憶體地址的,而且是一個垃圾地址。不知道這個記憶體地址指向的值是什麼。這就是為什麼不要對未初始化指標取值的原因。最好的情況是你取到的是垃圾地址接下來你需要對程式進行除錯,最壞的情況則會導致程式崩潰。
NULL指標被初始化為o。NULL是一個特殊的地址,用NULL賦值的指標指向的地址為0而不是隨機的地址。只有當你準備使用這個地址時有效。不要對NULL地址取值,否則會產生段錯誤。
指標和陣列
C語言的陣列表示一段連續的記憶體空間,用來儲存多個特定型別的物件。與之相反,指標用來儲存單個記憶體地址。陣列和指標不是同一種結構因此不可以互相轉換。而陣列變數指向了陣列的第一個元素的記憶體地址。
一個陣列變數是一個常量。即使指標變數指向同樣的地址或者一個不同的陣列,也不能把指標賦值給陣列變數。也不可以將一個陣列變數賦值給另一個陣列。然而,可以把一個陣列變數賦值給指標,這一點似乎讓人感到費解。把陣列變數賦值給指標時,實際上是把指向陣列第一個元素的地址賦給指標。
1 2 3 4 5 6 7 8 |
int myarray[4] = {1,2,3,0}; int *ptr = myarray; printf("*ptr=%d\n", *ptr); // 陣列變數是常量,不能做下面的賦值 // myarray = ptr // myarray = myarray2 // myarray = &myarray2[0] |
第1行初始化了一個int陣列,第2行用陣列變數初始化了一個int指標。由於陣列變數實際上是第一個元素的地址,因此我們可以把這個地址賦值給指標。這個賦值與int *ptr = &myarray[0]效果相同,顯示地把陣列的第一個元素地址賦值到了ptr引用。這裡需要注意的是,這裡指標需要和陣列的元素型別保持一致,除非指標型別為void。
指標與結構體
就像陣列一樣,指向結構體的指標儲存了結構體第一個元素的記憶體地址。與陣列指標一樣,結構體的指標必須宣告和結構體型別保持一致,或者宣告為void型別。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct person { int age; char *name; }; struct person first; struct person *ptr; first.age = 21; char *fullname = "full name"; first.name = fullname; ptr = &first; printf("age=%d, name=%s\n", first.age, ptr->name); |
第1至6行宣告瞭一個person結構體,一個變數指向了一個person結構體和指向person結構體的指標。第8行為age成員賦了一個int值。第9至10行我們宣告瞭一個char指標並賦值給一個char陣列並賦值給結構體name成員。第11行我們把一個person結構體引用賦值給結構體變數。
第13行我們列印了結構體例項的age和name。這裡需要注意兩個不同的符號,’.’ 和 ‘->’ 。結構體例項可以通過使用 ‘.’ 符號訪問age變數。對於結構體例項的指標,我們可以通過 ‘->’ 符號訪問name變數。也可以同樣通過(*ptr).name來訪問name變數。
總結
希望這份簡短的概述能夠有助於瞭解不同的指標型別。在後續的博文中我們會探討其它型別的指標和高階用法,比如函式指標。
歡迎提出提問並給出評論。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式