# Lec.2 **指標** [TOC] <br> ## **用法** ### **指標變數 vs. 一般變數** * 一般變數:值代表資料 * 指標變數(pointer variable):值代表另一個變數的記憶體位址 <br> ### **宣告** * 宣告變數時在變數名稱前加"\*",就代表這是一個指標變數。 * "\*"前面的資料類別就是知個指標變數能指到的變數類別。 * ex. 指向整數的指標變數就不能指向浮點數 <br> > 範例一&emsp;宣告一個指向整數的指標變數 ```c= int *iptr; //ptr常用作於當指標變數的變數名稱,iptr常用於當指向整數的指標變數名稱(int-pointer) //此iptr不能指向除了整數以外的資料類別 ``` <br> > 範例二&emsp;指向浮點數的指標變數 ```c= float *fptr; //float-pointer double *dfptr; // double-float-pointer ``` :::info * 可以利用sizeof(ptr)來看看指標所佔的位元組數 > :::spoiler <font style="vertical-align: inherit;"><font style="vertical-align: inherit;">👉</font></font> 點開看程式碼 <font style="vertical-align: inherit;"><font style="vertical-align: inherit;">👈</font></font> > ```c= > #include <stdio.h> > > int main(void){ > int *iptr; //integer pointer > int *fptr; //float pointer > int *dfptr; //double pointer > printf("sizeof(iptr) = %d\n", sizeof(iptr)); > printf("sizeof(fptr) = %d\n", sizeof(fptr)); > printf("sizeof(dfptr) = %d\n", sizeof(dfptr)); > return 0; > } > ``` > ::: > :::spoiler OUTPUT > ``` > sizeof(iptr) = 8 > sizeof(fptr) = 8 > sizeof(dfptr) = 8 > ``` > ::: * 跟一般變數所佔的位元組數比較看看 > :::spoiler <font style="vertical-align: inherit;"><font style="vertical-align: inherit;">👉</font></font> 點開看程式碼 <font style="vertical-align: inherit;"><font style="vertical-align: inherit;">👈</font></font> > ```c= > #include <stdio.h> > > int main(void){ > int i; > float f; > double d; > printf("sizeof(int) = %lu\n", sizeof(i)); > printf("sizeof(float) = %lu\n" , sizeof(f)); > printf("sizeof(double) = %lu\n", sizeof(d)); > return 0; > } > //%lu: long unsign integer > ``` > :::spoiler OUTPUT > ``` > sizeof(int) = 4 > sizeof(float) = 4 > sizeof(double) = 8 > ``` > ::: ::: <br> ### **指定** (後續使用**整數指標變數**來代替**指向整數的指標變數**來說明內容) * **iptr指向i**的意思:**一個整數指標變數iptr的值**是另一個**整數變數i的記憶體位址**時。 <br> > 指定整數指標變數的值 ```c= int i; int *iptr1; int *iptr2; iptr1 = &i; //iptr1指向i iptr2 = iptr1; //把iptr1的值給iptr2當作值,這樣iptr2也指向i ``` <br> ### **取值** * 在指標變數前面加上星號,代表從這個記憶體位址**取值**(dereference) * 當一個指標iptr指向一個變數i時,\*iptr就代表變數i的值 > 使用指標變數所指到的變數 ```c= i = *iptr; //從iptr取值,並將值賦給i *iptr = i; //將來從iptr中取值時,會取到i (可以想成把從*iptr取到的值變成i的值) /* 在此假設指標變數iptr已經指向一個變數,這是才能使用*iptr的語法取值 因程式在使用記憶體時,能使用的記憶體位址有一定的範圍,超出範圍就會執行錯誤 如果一個指標變數沒有經過正確的初始化,他的值極有可能不在正確範圍內,就無法從記憶體正確取值。 */ ``` :::info * 當指標iptr變數指向一個變數i時,可以想像成: 1. \*iptr就代表變數i 2. \*iptr如同一個int一樣 ::: :::danger * ⚠️ 注意! iptr需要先經過正確初始化,才能取值 ::: <br> ### **NULL (空)** * 當需要指標變數**不指向**任何有效的記憶體位址時使用,讓程式判斷這個指標變數**絕對**不指向任何的有效記憶體位址。 * 跟指標變數沒有正確初始化不同。 :::danger * ⚠️ 注意! 任何使用NULL的程式都必須引入 <stdio.h> ::: <br> > ex. 此程式沒有經過正確的初始化,執行會導致segmentation-fault ```c= #include <stdio.h> int main(void) { int *iptr; iptr = NULL; printf("*iptr = %d\n", *iptr); } ``` <br> ### **小結** > ex. input: 5 ```c= #include <stdio.h> int main(void) { int i, k; int *iptr1, *iptr2; scanf("%d", &i); iptr1 = &i; iptr2 = iptr1; printf("i = %d\n", i); printf("&i = %p\n", &i); printf("iptr1 = %p\n", iptr1); printf("&iptr1 = %p\n", &iptr1); printf("iptr2 = %p\n", iptr2); printf("&iptr2 = %p\n", &iptr2); *iptr1 = 8; printf("i = %d\n", i); k = *iptr2 + 3; printf("&k = %p\n", &k); printf("k = %d\n", k); return 0; } ``` :::spoiler OUTPUT ``` i = 5 &i = 0x7fffb8327e78 iptr1 = 0x7fffb8327e78 &iptr1 = 0x7fffb8327e68 iptr2 = 0x7fffb8327e78 &iptr2 = 0x7fffb8327e60 i = 8 &k = 0x7fffb8327e74 k = 11 ``` ::: <br> :::spoiler 說明 Before line16: | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7fffb8327e60 | 0x7fffb8327e78 | iptr2 | | 0x7fffb8327e68 | 0x7fffb8327e78 | iptr1 | | 0x7fffb8327e78 | 0x000000000005 | i | After line16: | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7fffb8327e60 | 0x7fffb8327e78 | iptr2 | | 0x7fffb8327e68 | 0x7fffb8327e78 | iptr1 | | 0x7fffb8327e74 | 0x000000000011 | k | | 0x7fffb8327e78 | 0x000000000008 | i | ::: <br> ## **取址(位址)** > ex.比較\*取值與&取址,input: 5 ```c= #include<stdio.h> int main(void) { int i; int *iptr = &i; scanf("%d", &i); printf("iptr = %p\n", iptr); printf("&iptr = %p\n", &iptr); printf("*iptr = %d\n", *iptr); printf("*(&iptr) =%p\n", *(&iptr)); printf("&(*iptr) = %p\n", &(*iptr)); printf("*(*(&iptr)) = %d\n", *(*(&iptr))); printf("*(&(*iptr)) = %d\n", *(&(*iptr))); printf("&(*(&iptr)) = %p\n", &(*(&iptr))); printf("i = %d\n", i);/* errorcase */ printf("&i = %p\n", &i); /* printf("*i = %p\n", *i); do not do this */ printf("*(&i) = %d\n", *(&i)); /* printf("&(*i) = %p\n", &(*i)); do not do this either */ return 0; } ``` :::spoiler OUTPUT ``` iptr = 0x7ffe1d785fa8 &iptr = 0x7ffe1d785fa0 *iptr = 5 *(&iptr) =0x7ffe1d785fa8 &(*iptr) = 0x7ffe1d785fa8 *(*(&iptr)) = 5 *(&(*iptr)) = 5 &(*(&iptr)) = 0x7ffe1d785fa0 i = 5 &i = 0x7ffe1d785fa8 *(&i) = 5 ``` ::: <br> :::spoiler 說明 | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7ffe1d785fa0 | 0x7ffe1d785fa8 | iptr1 | | 0x7ffe1d785fa8 | 0x000000000005 | i | :::warning **\*(&iptr1)和&(\*iptr1)的結果相同,但意義不同** * \*(&iptr1) 是先對 iptr1 取址,會得到 iptr1 的記憶體位址,在對得到的記憶體位址取值,因此得到i的記憶體位址,因為 iptr1 裡面存的(值)是i的記憶體位址( iptr1 指向i)。 * &(\*iptr1) 是先對iptr1取值,得到i(因 iptr1 指向i,由前面可以知道 \*iptr1 和i是一樣的),所以對 \*iptr1 取址,就等同於對i取址,也救世會得到i的記憶體位址。 > 想想看為什麼 \*(\*(&iptr)) 和 \*(&(\*iptr)) 的結果都是5? :::danger **Why \*(&i) 的結果是5,但 \*i 卻不合法?** * 因為 i 並不是一個有效的記憶體位址,所以不能取值。 ::: <br> ## **參數傳遞** 透過將傳遞指標變數傳給被呼叫方,可以幫助我們透過記憶體位址改動呼叫方的變數。 ### 利用指標交換變數值 ### > ex. swap ```c= #include<stdio.h> void swap(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; } int main(void) { int i, j; scanf("%d", &i); scanf("%d", &j); swap(&i, &j); printf("i = %d, j = %d\n", i, j); return 0; } ``` <br> ### 藉由指標改變變數的值 ### > ex. 指標參數傳遞,input: 10 20 ```c= #include<stdio.h> void pointer(int *p1, int *p2) { printf("The address of p1 is %p\n", &p1); printf("The value of p1 is %p\n", p1); printf("The address of p2 is %p\n", &p2); printf("The value of p2 is %p\n", p2); *p1 += 1; p1 = p2; *p1 += 2; } int main(void) { int i, j, *iptr = &i; scanf("%d%d", &i, &j); printf("The address of i is %p\n", &i); printf("The address of j is %p\n", &j); printf("The address of iptr is %p\n", &iptr); printf("i = %d, j = %d\n", i, j); pointer(iptr, &j); printf("i = %d, j = %d\n", i, j); *iptr += 5; printf("i = %d, j = %d\n", i, j); return 0; } ``` :::spoiler OUPUT ``` 10 20 The address of i is 0x7fffd5056b58 The address of j is 0x7fffd5056b54 The address of iptr is 0x7fffd5056b48 i = 10, j = 20 The address of p1 is 0x7fffd5056b08 The value of p1 is 0x7fffd5056b58 The address of p2 is 0x7fffd5056b00 The value of p2 is 0x7fffd5056b54 i = 11, j = 22 i = 16, j = 22 ``` ::: :::spoiler 說明 p1和p2由實際參數得到初始值: | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7fffd5056b00 | 0x7fffd5056b54 | p2 | | 0x7fffd5056b08 | 0x7fffd5056b58 | p1 | | 0x7fffd5056b48 | 請找出 | iptr | | 0x7fffd5056b54 | 0x000000000014 | j | | 0x7fffd5056b58 | 0x00000000000a | i | <br> \*p1 += 1: | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7fffd5056b00 | 0x7fffd5056b54 | p2 | | 0x7fffd5056b08 | 0x7fffd5056b58 | p1 | | 0x7fffd5056b48 | 請找出 | iptr | | 0x7fffd5056b54 | 0x000000000014 | j | | 0x7fffd5056b58 | 0x00000000000b | i | <br> p1 = p2; \*p1 += 2: | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7fffd5056b00 | 0x7fffd5056b54 | p2 | | 0x7fffd5056b08 | 0x7fffd5056b54 | p1 | | 0x7fffd5056b48 | 0x7fffd5056b58 | iptr | | 0x7fffd5056b54 | 0x000000000016 | j | | 0x7fffd5056b58 | 0x00000000000b | i | <br> \*iptr += 5: | 位址 | 值 | 變數名稱 | | -------------- | -------------- | -------- | | 0x7fffd5056b00 | 0x7fffd5056b54 | p2 | | 0x7fffd5056b08 | 0x7fffd5056b54 | p1 | | 0x7fffd5056b48 | 0x7fffd5056b58 | iptr | | 0x7fffd5056b54 | 0x000000000016 | j | | 0x7fffd5056b58 | 0x000000000010 | i | <br> ::: <br> ## **指標與陣列的關係** :::spoiler 陣列名稱的值是什麼? > 陣列的起始位置 ::: :::spoiler 陣列名稱+常數n是什麼? > 第n個元素的位址 ::: --->指標變數可以拿來當陣列名稱使用:指標變數+1就是指向下一個的意思 ```c *iptr = a; *(iptr + i) 即為 a[i] ``` >ex. 利用指標修改陣列,input: 1 2 3 4 5 ```c= #include <stdio.h> #define ARRAYSIZE 5 int main(void) { int a[ARRAYSIZE]; for (int i = 0; i < ARRAYSIZE; i++) scanf("%d", &(a[i])); int *ptr = a; for (int i = 0; i < ARRAYSIZE; i++) printf("a[%d] = %d\n", i, a[i]); printf("\n"); ptr = &(a[2]); for (int i = 0; i < 2; i++) ptr[i] += 3; for (int i = 0; i < ARRAYSIZE; i++) printf("a[%d] = %d\n", i, a[i]); return 0; } ``` :::spoiler OUTPUT ``` a[0] = 1 a[1] = 2 a[2] = 3 a[3] = 4 a[4] = 5 a[0] = 1 a[1] = 2 a[2] = 6 a[3] = 7 a[4] = 5 ``` ::: :::spoiler 說明 :::warning **指標變數使用 \[] 和陣列使用 \[] 時有一些不同** * 陣列 a 使用 \[] 時,a\[1],永遠是同一個陣列元素 * 指標 ptr 使用 \[] 時,ptr\[1],是ptr目前所指道位置的下一個陣列元素 * ex. ptr 目前指到 a\[0],則 ptr\[1] 指到 a\[1];如果 ptr 目前只 到a\[2],則 ptr\[1] 就是指到 a\[3]。 | 陣列 | 指標 | | -------------- | -------------- | | a\[0] | ptr\[0] | | a\[1] | ptr\[1] | | a\[2] | | | a\[3] | | | a\[4] | | ---> | 陣列 | 指標 | | -------------- | -------------- | | a\[0] | | | a\[1] | | | a\[2] | ptr\[0] | | a\[3] | ptr\[1] | | a\[4] | | :::danger 一般宣告的陣列像是絕對座標,第幾個元素的記憶體位址是固定的。 指標變數的陣列香是相對座標,第幾個元素的記憶體位址是會隨著指標變數的時而改變的。 ::: <br> ### 指標算數(pointer arithmetic) ### 將一個指標加減一個常數 --->得到一個指標 > 常數以指標所指到的元素大小為單位 > ex. 如果是整數指標,常數的單位就是sizeof(int),也就是4個位元組 ```c= #include <stdio.h> #define ARRAYSIZE 10 int main(void) { int array[ARRAYSIZE]; int *iptr1 = &(array[3]); int *iptr2 = iptr1 + 4; printf("iptr1 = %p\n", iptr1); printf("iptr2 = %p\n", iptr2); printf("iptr2 - iptr1 = %ld\n", iptr2-iptr1); return 0; } ``` :::spoiler OUTPUT ``` ptr1 = 0x7ffedbcef81c iptr2 = 0x7ffedbcef82c iptr2 - iptr1 = 4 ``` ::: <br> ### 多維陣列指標 ### :::spoiler 如何讓指標指向一個多維陣列? > 在指標變數後面加上維度 ::: ```c int a[2][3][4]; int (*matrixPtr)[3][4] = a; ``` :::danger :::spoiler 如果 (\*matrixPtr) 沒有括號會怎麼樣? 會變成宣告多維指標陣列,而不是指向多維陣列的指標。 ::: >ex. intPtr 指向 int,arrayPtr 指向 int\[4],matrixPtr 指向 int\[3]\[4] : ```c= #include <stdio.h> #define X 2 #define Y 3 #define Z 4 int main() { int a[X][Y][Z]; int *intPtr = a[0][0]; int (*arrayPtr)[Z] = a[0]; int (*matrixPtr)[Y][Z] = a; printf("sizeof(intPtr) = %ld\n", sizeof(intPtr)); printf("sizeof(arrayPtr) = %ld\n", sizeof (arrayPtr)); printf("sizeof(matrixPtr) = %ld\n\n", sizeof(matrixPtr)); printf("intPtr + 1 = %p\n", intPtr + 1); printf("a[0][0] + 1 = %p\n", a[0][0] + 1); printf("&(a[0][0][0])+1 = %p\n", &(a[0][0][0]) + 1); printf("&(a[0][0][1]) %p\n\n", &(a[0][0][1])); printf("arrayPtr + 1 = %p\n", arrayPtr + 1); printf("a[0] + 1 = %p\n", a[0] + 1); printf("a[0][1] = %p\n", a[0][1]); printf("a[0][0] + 4 = %p\n", a[0][0] + 4); printf("&(a[0][0][0]) + 4 = %p\n", & (a[0][0][0]) + 4); printf("&(a[0][1][0]) = %p\n\n", & (a[0][1][0])); printf("matrixPtr + 1 = %p\n", matrixPtr + 1); printf("a + 1 = %p\n", a + 1); printf("a[1] = %p\n", a[1]); printf("a[0] + 3 = %p\n", a[0] + 3); printf("a[0][0] + 12= %p\n", a[0][0] + 12); printf("&(a[0][0][0])+ 12 = %p\n", &(a[0][0][0]) + 12); printf("&(a[1][0][0]) = %p\n", & (a[1][0][0])); return 0; } ``` :::spoiler OUTPUT ``` sizeof(intPtr) = 8 sizeof(arrayPtr) = 8 sizeof(matrixPtr) = 8 intPtr + 1 = 0x7ffd5e556f44 a[0][0] + 1 = 0x7ffd5e556f44 &(a[0][0][0])+1 = 0x7ffd5e556f44 &(a[0][0][1]) 0x7ffd5e556f44 arrayPtr + 1 = 0x7ffd5e556f50 a[0] + 1 = 0x7ffd5e556f50 a[0][1] = 0x7ffd5e556f50 a[0][0] + 4 = 0x7ffd5e556f50 &(a[0][0][0]) + 4 = 0x7ffd5e556f50 &(a[0][1][0]) = 0x7ffd5e556f50 matrixPtr + 1 = 0x7ffd5e556f70 a + 1 = 0x7ffd5e556f70 a[1] = 0x7ffd5e556f70 a[0] + 3 = 0x7ffd5e556f70 a[0][0] + 12= 0x7ffd5e556f70 &(a[0][0][0])+ 12 = 0x7ffd5e556f70 &(a[1][0][0]) = 0x7ffd5e556f70 ``` ::: <br> > ex. 如果 (\*arrayPtr)\[Z] 和 (\*matrixPtr)\[Y]\[Z] 沒有括號 : ```c= #include <stdio.h> #define X 2 #define Y 3 #define Z 4 int main() { int a[X][Y][Z]; int *intPtr; int *arrayPtr[Z]; int *matrixPtr[Y][Z]; printf("sizeof(intPtr) = %ld\n", sizeof(intPtr)); printf("sizeof(arrayPtr) = %ld\n", sizeof (arrayPtr)); printf("sizeof(matrixPtr) = %ld\n\n", sizeof(matrixPtr)); return 0; } ``` :::spoiler OUTPUT ``` sizeof(intPtr) = 8 sizeof(arrayPtr) = 32 sizeof(matrixPtr) = 96 ``` ::: <br> ## 回傳值 ## 指標也可以當作回傳值,方法為在函式名稱前面加一個星號 ```c int *func1(int *iptr); // 如同一個整數 ``` > ex. 傳回iptr後第一個正整數記憶體位址,input: 0 0 0 5 9 0 0 6 0 2 ```c= #include <stdio.h> int *firstPositive (int *ptr) { while (*ptr <= 0) ptr++; return ptr; } #define ARRAYSIZE 10 int main(void) { int array[ARRAYSIZE]; for (int i = 0; i < ARRAYSIZE; i++) scanf("%d", &( array[i])); int *iptr = firstPositive (array); printf("*iptr = %d\n", *iptr); printf("iptr = -array = %ld\n", iptr-array); iptr = firstPositive(&(array[5])); printf("*iptr = %d\n", *iptr); printf("iptr - array = %ld\n", iptr-array); return 0; } ``` :::spoiler OUTPUT ``` *iptr = 5 iptr = -array = 3 *iptr = 6 iptr - array = 7 ``` ::: :::spoiler 說明 iptr = firstPositive (array): | 陣列 | 值 | | | ----- | -- | ----- | | a\[0] | 0 | array | | a\[1] | 0 | | | a\[2] | 0 | | | a\[3] | 5 | ptr | | a\[4] | 9 | | | a\[5] | 0 | | | a\[6] | 0 | | | a\[7] | 6 | | | a\[8] | 2 | | | a\[9] | 2 | | <br> iptr = firstPositive(&(array\[5])); | 陣列 | 值 | | | ----- | -- | ----- | | a\[0] | 0 | | | a\[1] | 0 | | | a\[2] | 0 | | | a\[3] | 5 | array | | a\[4] | 9 | | | a\[5] | 0 | | | a\[6] | 0 | | | a\[7] | 6 | ptr | | a\[8] | 2 | | | a\[9] | 2 | | ::: <br> ## **使用限制** ### 用途 ### :::spoiler 由以上的討論我們發現,大部分的指標功能可以用陣列的語法取代,那麼為何我們仍需要指標呢? > 在程式中經常需要將一個記憶體位址保存下來,以備之後使用。這時我們就必須使用指標變數。 ::: <br> * ex1. 在動態配置記憶體時,我們可以直接向作業系統要求一塊記憶體使用。那麼作業系統要如何告訴我們記憶體在何處?顯然固定的陳列無法滿足這個需求,所以我們需要一個機制,讓程式有辦法能記住一個記憶體位址,這個機制就是指標變數。我們可以用指標變數指向這塊作業系統給我們的記憶體,之後指標變數就可透過簡便的陣列語法存取這塊記憶體。 * ex2. 在處理動態資料結構時,我們會將資料串連起來形成結構。此時結構的大小與形狀都是依動態要求而調整,無法存在固定大小的陣列中,此時我們就需要指標來描述資料之間的連結關係。 * ex3. 在處理字串時,程式常常需要以記憶體位址來溝通。此時溝通的雙方未必能夠使用陣列的 \[] 語法,因為其中一方未必能夠知道這個陣列的起始記憶體位址。此時使用指標直接指到記憶體是最有效的 方法。 ### 限制 ### :::warning **指標注意事項** * 除非是上述的動態配置記憶體,動態資料結構及字串處理,否則盡量避免使用指標。 * C程式語言中對指標的使用沒有安全機制,初學很容易弄錯,而且雖以除錯。 * 很多指標的使用是可以用陣列語法代替的。這樣不但容易閱讀,也容易除錯。 * 有人認為使用指標可增加效能,但是現代編譯器已經能產生非常好的執行檔。為了效能犧牲可讀性也許並不值得。 ::: :::danger **不到真正需要,例如動態配置記憶體、動態資料結構及字串處理,否則請儘量避免使用指標。** :::