C語言-指標

指標與運算

變數除了它儲存的數值以外,還有記憶體位址,下面是示範程式

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(void) {
int var = 10;
printf("var 的值:%d\n", var);
printf("var 的記憶體位址:%X\n", &var);
return 0;
}

執行結果

var 的值:10
var 的記憶體位址:22FE4C

一般變數提供對記憶體空間的直接存取,指標變數則提供間接存取

指標可以指向特定的記憶體位址而不直接操作到變數

指標變數的宣告語法如下

1
變數型態 *變數名稱;

指標變數可以透過取址運算子&來指定,下面是示範程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main(void) {
int var = 10;
int *ptr = &var; // 取得var的記憶體位址,讓ptr指向這位址
printf("var 的位址:%X\n", &var);
printf("var 的值:%d\n", var);
printf("ptr 指向的位址:%X\n", ptr);
printf("ptr 指向的位址的值:%d\n", *ptr);
// '*'運算子除了乘法以外,也當作取值運算子,負責取出記憶體位址裡的值
return 0;
}

執行結果

var 的位址:22FE44
var 的值:10
ptr 指向的位址:22FE44
ptr 指向的位址的值:10

當指標指向每個記憶體位址之後,如果你指定某個值給*ptr,被指向的值也會被改變

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main(void) {
int var = 100;
int *ptr = &var ;
printf("var = %d\n", var);
printf("*ptr = %d\n", *ptr);
*ptr = 200;
printf("var = %d\n", var);
printf("*ptr = %d\n", *ptr);
return 0;
}

執行結果

var = 100
*ptr = 100
var = 200
*ptr = 200

指標如果指向不明的記憶體位址是危險的,有可能造成程式爆炸,所以指定初始值是安全的作法

1
int *ptr = NULL; // 指向空指標

NULL是透過#define定義為0的常數

#define會在之後的章節提到

另外新手可能會以為以下語法是宣告兩個指標變數

1
int* ptr1,ptr2;

而實際上要宣告兩個指標變數要這樣寫

1
int *ptr1,*ptr2;

如果只打算拿記憶體位址做運算,不會牽涉到型態問題,可以使用void指標

void *ptr;

因為void指標只儲存位址資訊,所以不能直接提取資料,必須先轉換型態,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(void) {
int num = 10;
void *ptr = &num ;
// 下面這句不可行,void型態指標不可取值
// printf("%d\n", *ptr);
// 轉型為int型態指標並指定給iptr
int *iptr = (int*) ptr;
printf("%d\n", *iptr);
return 0;
}

執行結果

10

除了指定運算子、取址運算子與取值運算子,指標還可以使用+、-、++、–、+=與-=等運算子

指標的加減法與一般數值的加減法不同,指標加1代表往後一個資料型態的長度

例如int指標加1表示記憶體位址加4位元組,double指標則加8位元組

減法則是往前N個資料型態的長度,下面是double指標的範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(void) {
double num = 10;
double *ptr = &num;
printf("ptr 位置:%d\n", ptr);
printf("ptr + 1:%d\n", ptr + 1);
printf("ptr + 2:%d\n", ptr + 2);
return 0;
}

執行結果

ptr 位置:2293312
ptr + 1:2293320
ptr + 2:2293328

傳指標給函數

C語言的函數呼叫都是傳值呼叫(Call by value),也就是每次呼叫時都複製一份給函數

但是因為區域變數的關係,變數離開當前區域就失去效力了

如果想要傳參數給函數並且真正修改到變數的值的話,就必須靠傳址呼叫(Call by address)了

下面將透過交換兩個數字的函數說明傳值呼叫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int swap(int,int);
int main(void) {
int num1 = 10,num2 = 20;
printf("交換前:%d %d\n",num1,num2);
swap(num1,num2);
printf("交換後:%d %d\n",num1,num2);
return 0;
}
int swap(int a,int b){
int c = a;
a = b;
b = c;
}

執行結果

交換前:10 20
交換後:10 20

下面將透過交換兩個數字的函數說明傳址呼叫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int swap(int*,int*);
int main(void) {
int num1 = 10,num2 = 20;
printf("交換前:%d %d\n",num1,num2);
swap(&num1,&num2);
printf("交換後:%d %d\n",num1,num2);
return 0;
}
int swap(int *a,int *b){
int c = *a;
*a = *b;
*b = c;
}

執行結果

交換前:10 20
交換後:20 10

指標與陣列

其實陣列就是指標的應用,陣列名稱就是指向陣列第一個元素的指標,以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(void) {
int arr[10] = {0};
printf("arr :\t\t%X\n", arr);
printf("&arr[0] :\t%X\n", &arr[0]);
return 0;
}

執行結果

arr :           22FE20
&arr[0] :       22FE20

陣列的索引值其實是相對於開頭的位移量,所以第一個元素是0,表示位移量是0

以下是索引值的範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main(void) {
int arr[10] = {0};
int *ptr = arr;
int i;
for(i = 0; i < 10; i++) {
printf("&arr[%d]: %X", i ,&arr[i]);
printf("\t\tptr + %d: %X\n", i, ptr + i);
}
return 0;
}

執行結果

&arr[0]: 22FE10         ptr + 0: 22FE10
&arr[1]: 22FE14         ptr + 1: 22FE14
&arr[2]: 22FE18         ptr + 2: 22FE18
&arr[3]: 22FE1C         ptr + 3: 22FE1C
&arr[4]: 22FE20         ptr + 4: 22FE20
&arr[5]: 22FE24         ptr + 5: 22FE24
&arr[6]: 22FE28         ptr + 6: 22FE28
&arr[7]: 22FE2C         ptr + 7: 22FE2C
&arr[8]: 22FE30         ptr + 8: 22FE30
&arr[9]: 22FE34         ptr + 9: 22FE34

以下範例程式用來說明陣列跟指標的關係

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
int i;
// 以指標方式存取資料
for(i = 0; i < 5; i++) {
printf("*(ptr + %d): %d\n", i , *(ptr + i));
}
puts("");
// 以陣列方式存取資料
for(i = 0; i < 5; i++) {
printf("ptr[%d]: %d\n", i, ptr[i]);
}
puts("");
// 以指標方式存取資料
for(i = 0; i < 5; i++) {
printf("*(arr + %d): %d\n", i , *(arr + i));
}
puts("");
// 以陣列方式存取資料
for(i = 0; i < 5; i++) {
printf("arr[%d]: %d\n", i, arr[i]);
}
puts("");
return 0;
}

執行結果

*(ptr + 0): 10
*(ptr + 1): 20
*(ptr + 2): 30
*(ptr + 3): 40
*(ptr + 4): 50

ptr[0]: 10
ptr[1]: 20
ptr[2]: 30
ptr[3]: 40
ptr[4]: 50

*(arr + 0): 10
*(arr + 1): 20
*(arr + 2): 30
*(arr + 3): 40
*(arr + 4): 50

arr[0]: 10
arr[1]: 20
arr[2]: 30
arr[3]: 40
arr[4]: 50

指標與字串

字元指標可以指向一個字串常數,該字串常數會佔有一個記憶體空間,以下是示範程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(void) {
char *str = "hell";
char *add = 0;
add = str;
printf("%s\t%X\n", str, add);
str = "world";
add = str;
printf("%s\t%X\n", str, add);
return 0;
}

執行結果

hell    404000
world    40400C

可以看到兩個字串不同記憶體位址

但是要注意的是,如果使用陣列的方式宣告字串,是無法使用=指定運算子的

宣告陣列使用的空間是固定的,但是指標指定的字串長度可以不固定

動態記憶體配置

到目前為止,我們使用的都是先宣告好要使用什麼變數,程式執行時就配置好記憶體

但是有時候要程式執行到一半才知道需要多少記憶體,這時候就要使用malloc函數

下面這個語法會以動態的方式配置一個int型態大小的記憶體給ptr變數

1
int *ptr = malloc(sizeof(int));

但是這語法只配置空間,不會初始化變數的值

而動態配置的記憶體在程式結束前並不會自動歸還空間,所以必須需要使用free函數歸還記憶體

以下是陣列動態配置的範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h> // malloc跟free都定義在這
int main(void) {
int size = 0;
printf("請輸入陣列長度:");
scanf("%d", &size);
int *arr = malloc(size * sizeof(int)); // 配置記憶體
int i;
printf("顯示元素值:\n");
for(i = 0; i < size; i++) {
printf("arr[%d] = %d\n", i, *(arr+i));
}
printf("指定元素值:\n");
for(i = 0; i < size; i++) {
printf("arr[%d] = ", i);
scanf("%d" , arr + i);
}
printf("顯示元素值:\n");
for(i = 0; i < size; i++) {
printf("arr[%d] = %d\n", i, *(arr+i));
}
free(arr); // 歸還空間
return 0;
}

執行結果

請輸入陣列長度:5
顯示元素值:
arr[0] = 7952432
arr[1] = 0
arr[2] = 7930200
arr[3] = 0
arr[4] = 0
指定元素值:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
顯示元素值:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5

雙重指標

雙重指標簡單講就是指向指標的指標,指標指向一般變數,雙重指標則指向指標變數

以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int main(void) {
int p = 10;
int *ptr1 = &p;
int **ptr2 = &ptr1;
printf("p\t%d\n", p);
printf("&p\t%X\n\n", &p);
printf("*ptr1\t%d\n", *ptr1);
printf("ptr1\t%X\n", ptr1);
printf("&ptr1\t%X\n\n", &ptr1);
printf("**ptr2\t%d\n", **ptr2);
printf("*ptr2\t%X\n", *ptr2);
printf("ptr2\t%X\n", ptr2);
printf("&ptr2\t%X\n\n", &ptr2);
return 0;
}

執行結果

p       10
&p      22FE4C

*ptr1   10
ptr1    22FE4C
&ptr1   22FE40

**ptr2  10
*ptr2   22FE4C
ptr2    22FE40
&ptr2   22FE38

可以看得出來,ptr1指向了p的位址,而ptr2指向了ptr1的位址

雙重指標的使用時機通常用於傳回指標

例如想將函數裡面配置的記憶體空間取出來,以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
int new_int_array(int,int**);
int main(void) {
int* arr_ptr = NULL;
if(new_int_array(5,&arr_ptr)){ // 如果有配置成功的話
int i;
for(i = 0; i < 5; i++)
arr_ptr[i] = i+1;
for(i = 0; i < 5; i++)
printf("%d ",arr_ptr[i]);
puts("");
}
return 0;
}
int new_int_array(int length, int** arr_ptr){
int* pData = (int*)malloc(sizeof(int)*length);
if (pData == NULL){ // malloc配置失敗會回傳NULL
*arr_ptr = NULL;
return 0; //配置失敗
}
*arr_ptr = pData;
return 1; // 配置成功
}

執行結果

1 2 3 4 5

函數指標

程式在執行時,函數本身也在記憶體中佔有一段空間,而函數名稱則指向該記憶體空間

當呼叫函數的時候,程式就會去執行所指向的記憶體空間中的指令

指標也可以指向函數,語法如下

回傳值型態 (*指標名稱)(參數列);

函數型態由回傳值型態與參數列決定,不包括函式名稱

一個函數指標可指向具有相同型態的函式,也就是具有相同回傳值型態和參數列的函數

下面示範程式裡面,ptr指向func函數,可以看到ptr跟func有相同功效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
int func(int);
int main(void) {
int (*ptr)() = 0;
ptr = func;
func(5);
ptr(5);
printf("address of foo:%X\n", (int) func);
printf("address of foo:%X\n", (int) ptr);
return 0;
}
int func(int num) {
printf("Print:%d\n",num);
return 0;
}

執行結果

Print:5
Print:5
address of foo:401597
address of foo:401597

函數指標的優點在於彈性

例如以下程式碼使用了函數指標,省去了switch的麻煩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
void func1();
void func2();
void func3();
void func4();
void func5();
int main(void) {
void (*key[5])() = {0};
key[0] = func1;
key[1] = func2;
key[2] = func3;
key[3] = func4;
key[4] = func5;
while(1){
char c = getch();
if(c>='1'&&c<='5') key[c-'1']();
}
return 0;
}
void func1(){
puts("你按了1");
}
void func2(){
puts("你按了2");
}
void func3(){
puts("你按了3");
}
void func4(){
puts("你按了4");
}
void func5(){
puts("你按了5");
}

參考

  1. C語言
  2. 指標與記憶體位址
  3. 指標的運算
  4. 指標與陣列
  5. 指標與字串
  6. malloc()、free()、calloc() 與 realloc()
  7. 雙重指標
  8. 陣 列 與 指 標