指標

指標 在 C

{other: "%{count} 個練習"}

關於 指標

指標是一種變數或常數,它的值是另一個變數或常數的位址。 指標是 C 語言中既強大又可能潛藏危險的一項特色。 要理解指標並不容易。

int i = 5;

當我們把值指定給一個變數之後,就可以問兩個問題。

你是誰?

我是i。

你有什麼?

我有整數5。

還有第三個問題可以問。

你在哪裡?

我在0x6dfed4。

等等,什麼?

變數和常數會把值存放在記憶體的某個位置。 記憶體中的這個位置,就稱為該變數或常數的位址。

取址運算子與間接運算子(第 1 部分)

要取得變數的位址,我們使用取址運算子,也就是&。

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 開始

前一個範例說明了為什麼陣列索引從0開始,而不是從1開始。 陣列的索引會把索引值乘以型別的大小,再加到陣列的起始位址上。 由於第一個元素對齊陣列的開頭,所以我們把型別大小乘以0再加到起始位址,就得到第一個元素。 要取得第二個元素,我們把型別大小乘以1再加到起始位址。 如果該型別的長度是四個位元組,第二個元素的位址就會比第一個元素高出四個位元組。

這也是為什麼指標要宣告成某種特定的型別。 如果指標不知道自己指向什麼型別,它在被加上1時,就不知道該前進多少個位元組。 把1加到指標上,並不一定是把位址加上一個位元組。 它的意思是要加上1倍的型別長度(以位元組為單位)。

但還沒完呢!你不會以為*就這樣結束了吧?

間接運算子(第 2 部分)

既然我們已經了解位址如何與陣列索引搭配運作,但如果拿不到某個位址上存放的值,那又有什麼用? 我們該怎麼做? 要取得某個位址上存放的值,我們使用間接運算子,你可能還記得,就是*。 在這種情況下,間接運算子有時也稱為解參考運算子,因為它會對指標所存放位址的那個值進行解參考。 在解參考時稱它為解參考運算子,是為了把它和定義指標時的間接運算子區分開來,但就技術上來說,這兩種用法的*都叫做間接運算子。

拜託! 你在開玩笑嗎? *到底有幾種用法?! 乘法、宣告指標,現在又加上解參考! 真讓人混亂! 而且解參考到底是什麼?

下面的範例示範如何用間接運算子來解參考

#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陣列(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這個字面值一樣。 這是不被允許的。 所以,問題其實不在於它是不是指標。 問題在於它指向什麼。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟