指標是一種變數或常數,它的值是另一個變數或常數的位址。 指標是 C 語言中既強大又可能潛藏危險的一項特色。 要理解指標並不容易。
int i = 5;
當我們把值指定給一個變數之後,就可以問兩個問題。
你是誰?
我是i。
你有什麼?
我有整數5。
還有第三個問題可以問。
你在哪裡?
我在0x6dfed4。
等等,什麼?
變數和常數會把值存放在記憶體的某個位置。 記憶體中的這個位置,就稱為該變數或常數的位址。
要取得變數的位址,我們使用取址運算子,也就是&。
int i = 5;
int *i_address = &i;
等等,i_address前面的*是什麼意思?那不就是乘法嗎?
型別後面接著*,再接上一個識別字,就是宣告一個指向該型別之變數的指標的方式。
這個*稱為間接運算子,因為它間接地指向指標所存放位址的那個值。
好,但為什麼要特地建立一個變數,只為了存放另一個變數的位址?
話說回來,我們一開始為什麼會在意某個變數的位址是什麼?
嗯,當變數位於陣列之中時,我們就會在意它們的位置。
當我們像這樣建立一個陣列
int my_ints[] = {1, 2};
my_ints的值其實就是它的位址,而這也正好和它第一個元素的位址相同。
它第二個元素的位址,會是第一個元素的位址加上系統上一個int的大小。
想更清楚地看出這一點,試著執行下面的程式
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
前一個範例說明了為什麼陣列索引從0開始,而不是從1開始。
陣列的索引會把索引值乘以型別的大小,再加到陣列的起始位址上。
由於第一個元素對齊陣列的開頭,所以我們把型別大小乘以0再加到起始位址,就得到第一個元素。
要取得第二個元素,我們把型別大小乘以1再加到起始位址。
如果該型別的長度是四個位元組,第二個元素的位址就會比第一個元素高出四個位元組。
這也是為什麼指標要宣告成某種特定的型別。
如果指標不知道自己指向什麼型別,它在被加上1時,就不知道該前進多少個位元組。
把1加到指標上,並不一定是把位址加上一個位元組。
它的意思是要加上1倍的型別長度(以位元組為單位)。
但還沒完呢!你不會以為*就這樣結束了吧?
既然我們已經了解位址如何與陣列索引搭配運作,但如果拿不到某個位址上存放的值,那又有什麼用?
我們該怎麼做?
要取得某個位址上存放的值,我們使用間接運算子,你可能還記得,就是*。
在這種情況下,間接運算子有時也稱為解參考運算子,因為它會對指標所存放位址的那個值進行解參考。
在解參考時稱它為解參考運算子,是為了把它和定義指標時的間接運算子區分開來,但就技術上來說,這兩種用法的*都叫做間接運算子。
拜託!
你在開玩笑嗎?
*到底有幾種用法?!
乘法、宣告指標,現在又加上解參考!
真讓人混亂!
而且解參考到底是什麼?
下面的範例示範如何用間接運算子來解參考
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
使用括號有助於在進行指標運算時,消除意圖上的模糊地帶。 下面的範例印出來的結果,可能不是程式設計師原本想要的
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
*和++的優先順序相同,但它們是由右往左求值。
在第一行,my_ptr先遞增到第二個元素,接著再對它解參考。
在第二行,my_ptr(已在第一行遞增到第二個元素)先被解參考,然後再對解參考後的值做遞增。
如果我們把這一行加到上面的範例
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
在那一行,我們把指標遞增到超出陣列的範圍。
我們已經越過了陣列,指向記憶體中接下來的四個位元組,指標會把它解讀成一個int,但事實上,那幾個位元組可能屬於另一個(或多個)變數。
那四個位元組可能是某個double的一部分,也可能是四個char。
問題在於,我們根本就不該用指標去查看它們。
我們本來也可以改用
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
由於陣列索引是用指標實作的,因此同樣有越過陣列的危險。
而且,由於字串是char的陣列,指標也可以用於字串。
字串常常用指標的語法來宣告,像這樣
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
不過有個問題。 為什麼我們要把星號放在識別字旁邊? 既然它是指向該型別的指標,為什麼不放在型別旁邊? 因為這樣寫
int* my_ptr1, my_ptr2_that_is_really_an_int;
並不會宣告兩個指向int的指標,而是宣告一個指向int的指標,和一個int。
要宣告兩個指向int的指標,就要寫成這樣
int *my_ptr1, *my_ptr2;
所以把*放在識別字附近是比較好的做法,可以盡量減少模糊地帶。
或者每一行只宣告一個識別字。
而如果你對*還意猶未盡,歡迎來到……
字串陣列常常會像這樣表示
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
這可能不太明顯,但這個字串陣列是以「指向char陣列之陣列的指標」的形式宣告並初始化的。
當這個char陣列的陣列被傳入print_strings()時,函式簽章把它描述成指向char的指標的指標。
那麼,我們怎麼知道char **真的是指向「指向char陣列之指標」的陣列的指標,而不是指向單一char之指標的指標?
我們無從得知。
這是讓指標使用起來令人困惑的眾多原因之一。 下面的程式可能可以編譯過關
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
但print_strings()並沒有按照原本的用意被使用。
換一個不同的簽章也沒用。
把簽章改成這樣
void print_strings(char *passed_in_strings[]) {
// code snipped
}
可能還是能編譯過關,而且仍然印出亂碼。
但傳入一個指向單一char之指標的指標,其實沒有任何意義。
通常,當你看到char **時,它代表的是指向「指向char陣列之指標」的陣列的指標。
我們已經看過把指標傳入函式了。 但為什麼要把指標傳入函式?
在 C 語言中,引數是以傳值的方式傳入函式的。 這表示值會從引數複製一份,而這份複本只存活於函式內部。 函式結束時,這份複本也隨之消失。
看看當我們試著在函式內改變值時會發生什麼事
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
這是因為,即使這些變數名稱相同,swap()裡的a和b並不是main()裡的同一個a和b。
它們是a和b的值的_複本_。
我們能不能修改swap,讓它照我們的本意運作?
當然可以!
我們可以把a和b的位址以指標的形式傳進去。
但這麼做有什麼用,反正函式還是會複製指標?
有用,因為複製後的位址仍然是同一個位址。
藉由對位址解參考,我們就能取用在main()裡原本定義的那個值。
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
現在swap裡出現了許多*,但它終於能照原本的用意運作了。
間接運算子可以讀成「在……的值」。
所以*a就是「在a的值」。
這套讀法用在字串上效果如何?
我們知道,陣列變數的值就是該陣列開始的位址。
我們也知道,可以用char陣列(char my_string[])來定義字串,也可以用指向char的指標(char *)來定義。
它們有什麼不同?
在這段程式碼中,我們把一個char *引數傳給describe_me()。
adjective引數被定義成char陣列,而函式則以指向char的指標的形式接受它。
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
我們能對adjective做出持續有效的修改,這說明了定義成char的陣列可以當作指向char的指標來接受。
現在讓我們試著直接把adjective定義成指向char的指標。
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
還記得指標既強大、又可能潛藏危險嗎?
但它為什麼行不通?
陣列就是指標,而指標剛才明明可以搭配char陣列運作啊!
原因在於,之前adjective被定義成char陣列,那是屬於main()的一個變數。
現在,adjective被定義成字串_字面值_,這表示那些chars被寫入唯讀的靜態記憶體,而那塊記憶體是屬於整個程式的。
兩者都是指標,但它們指向不同的東西。
試圖改變字串字面值,就像試圖改變5這個字面值一樣。
這是不被允許的。
所以,問題其實不在於它是不是指標。
問題在於它指向什麼。