指標觀念整理

沒用過指標,別說你會 C / C++

看到最近朋友在找延替時被指標考題炸的淒淒慘慘,為了避免我之後面試時也被雷到,先把蒐集到常見的指標問題整理在下方。

Introduce

何謂指標:

指標是一種資料類型,用來表示物件或者變數的記憶體位置

聽起來很難懂,直接用程式來輔助思考就比較清楚了:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a, b;
int *pa, *pb;
printf("address of a: %p\n", &a);
pa = &a;
printf("address of pa: %p\n", pa);
pb = pa;
printf("address of pb: %p\n", pb);

############ output ############

address of a: 0x7ffd130569ec
address of pa: 0x7ffd130569ec
address of pb: 0x7ffd130569ec

當a、b宣告出來後,CPU會馬上在記憶體中規畫空間給這兩個變數
& 符號可以取得該變數的記憶體位置,pa = &a 意思即是取得a 的記憶體位置並附值給pa,此時pa 便會指向 a 的記憶體位置。
同理,把pa 所儲存的記憶體位置附值給pb後,pa跟pb 兩者便都指向a 的位址了。

這時我們加點變化:

1
2
3
4
5
6
7
8
9
a = 1;
printf("value of a: %d\n", a);
*pa = 10;
printf("value of a: %d\n", a);

############ output ############

value of a: 1
value of a: 10

*pa = 10 表示把pa 所存記憶體位置的值更改成10,而pa 所指向的記憶體便是a 的記憶體位置,也就是直接把a 所存的值改成10。

Call by value VS Call by address

接下來我們來看函數的應用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// swap: call by value
void swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
int a = 3, b = 4;
swap(a, b);
printf("a = %d, b = %d\n", a, b);
############ output ############
a = 3, b = 4

// swap: call by address
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int a = 3, b = 4;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
############ output ############
a = 4, b = 3

當呼叫function 時,C 會檢查傳入的參數並複製成一模一樣的變數,雖然兩者一模一樣,但其記憶體的位置卻是不一樣,也因此第一個swap function 在交換的時候,雖然在function 內有置換成功,但是並不會影響到外部變數的結果。

而第二個swap 在接收參數的時候是傳入記憶體的位置,所以function 內更改值時是直接至記憶體位置去做改變,因此傳遞進去的兩個參數才會做交換。

Call by address 即是 Call by value

這樣講似乎有點饒舌,不過我們直接看程式碼來幫助我們思考

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int globali = 0;
void changeAddress(int *pi) {
pi = &globali;
}
int main() {
int locali = 1;
int *pi = &locali;
changeAddress(pi);
printf("final pi = %d\n", *pi);
return 0;
}

上方程式中的 pi 輸出為 1,雖然是傳指標進去,但是main function 內的pi 記憶體位置與 changeAddress function 內的pi 記憶體位置是不同的,如同上方所說的,當呼叫function 時,C 會檢查傳入的參數,並複製一個一模一樣的變數來使用,我們可以把記憶體位置印出來檢查看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int globali = 0;
void changeAddress(int *pi) {
printf("changeAddress function, pi address = %p\n", &pi);
pi = &globali;
}
int main() {
int locali = 1;
int *pi = &locali;
printf("main function, pi address = %p\n", &pi);
changeAddress(pi);
printf("final pi = %d\n", *pi);
return 0;
}

############ output ############
main function, pi address = 0x7ffedc834500
changeAddress function, pi address = 0x7ffedc8344e8
final pi = 1

可以看到當你在 changeAddress 內更改 pi 的值時,並不會影響到 main function 的pi,除非你改的是 pi 的記憶體位置。

但如果想要把 pi 改成0 該怎麼辦,這邊提供一個方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
int globali = 0;
void changeAddress(int **pi) {
printf("changeAddress *pi address: %p\n", *pi);
printf("changeAddress **pi address: %p\n", **pi);
printf("changeAddress pi address: %p\n", pi);
*pi = &globali;
}
int main() {
int locali = 1;
int *pi = &locali;
printf("main pi address: %p\n", &pi);
printf("main pi value: %p\n", pi);
changeAddress(&pi);
printf("final pi = %d\n", *pi);
return 0;
}
############ output ############
main pi address: 0x7fffdc60eef0
main pi value: 0x7fffdc60eefc
changeAddress *pi address: 0x7fffdc60eefc
changeAddress **pi address: 0x1
changeAddress pi address: 0x7fffdc60eef0
final pi = 0

在main 內把pi 的記憶體位置傳進 changeAddress,changeAddress 收到後依據傳來的值複製相同的變數,第一層 * 表示的是指向原有的pi 所存的值,而第二層的 * 則是指把pi 所存的值當參考並指向其位置,所以為了更改main function 中pi 的值,必須下*pi = &globali

多層pointer 的範例可能比較難懂,沒關係我們再看一個例子

1
2
3
4
5
6
7
8
int i = 0;
int *p1 = &i;
int **p2 = &pi;
int ***p3 = &p2;
***p3 = 1
printf("i = %d", i)
############ output ############
i = 1

*p3存的是p2的記憶體位置,*p2 存的是p1 的記憶體位置,而*p1 存的是i 的記憶體位置
***p3 的意思即是取得*p2 的的值,再藉由 *p2 的值取得 *p1 的值,最後便可取得 i。

什麼是void 指標

void 指標本身不具任何意義,當你宣告完後它還是不具任何意義,void 是一種特殊的指標,可以指向任意的資料型態,而void 指標所存的大小取決於你把它轉型成哪種資料型態,站在物件導向的立場來看,void 指標與多型 類似。
int *i -> i 是一個指標,指向int 的空間
void *v -> v 是一個指標,但不指向void 的空間

當你撰寫的function 不曉得會傳遞何種資料類型的參數時就可以透過void * 來實作,以qsort 為例:
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
int values[] = { 40, 10, 100, 90, 20, 25 };
int compare (const void * a, const void * b) {
return ( *(int*)a - *(int*)b );
}
int main () {
int n;
qsort (values, 6, sizeof(int), compare);
for (n=0; n<6; n++)
printf ("%d ",values[n]);
return 0;
}

當排序時並不曉得會排序何種資料型態,可能是int、double,也有可能是class,所以我們需要客製化我們比較的compare 方法,因為只有我們才知道傳遞了怎樣的資料型態進去

1
2
3
int compareCustomType (const void * a, const void * b) {
return ( *(CustomType*)a - *(CustomType*)b );
}

把我們所傳遞進去的void 指標轉型(cast)成變數原有的指標型態(CustomType 可以是任意資料型態),再透過最外層的 * 取得該變數原有的值去做比較,下方是struct 的寫法:

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
#include <stdio.h>
#include <stdlib.h>
struct mytype {
char name[10];
int value;
} mytype;

struct mytype typelists[] = {
"first", 40,
"second", 10,
"third", 100,
"forth", 90,
"fifth", 20,
"sixth", 25,
};
int compare (const void * a, const void * b) {
struct mytype *typea = (struct mytype *)a;
struct mytype *typeb = (struct mytype *)b;
return ( typea->value - typeb->value );
}

int main ()
{
int n;
qsort (typelists, 6, sizeof(struct mytype), compare);
for (n=0; n<6; n++)
printf ("%d ",typelists[n].value);
return 0;
}

void *很好用,但著實也是一把雙面刃。

指標的部分先介紹到這邊,待之後有想到其他內容再更新上來

參考資料:

  1. indiabix.com
  2. learncpp.com
  3. wikipedia