# Lec.2 **指標**
[TOC]
<br>
## **用法**
### **指標變數 vs. 一般變數**
* 一般變數:值代表資料
* 指標變數(pointer variable):值代表另一個變數的記憶體位址
<br>
### **宣告**
* 宣告變數時在變數名稱前加"\*",就代表這是一個指標變數。
* "\*"前面的資料類別就是知個指標變數能指到的變數類別。
* ex. 指向整數的指標變數就不能指向浮點數
<br>
> 範例一 宣告一個指向整數的指標變數
```c=
int *iptr;
//ptr常用作於當指標變數的變數名稱,iptr常用於當指向整數的指標變數名稱(int-pointer)
//此iptr不能指向除了整數以外的資料類別
```
<br>
> 範例二 指向浮點數的指標變數
```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
**不到真正需要,例如動態配置記憶體、動態資料結構及字串處理,否則請儘量避免使用指標。**
:::