Pointers

蔡銘軒 @ Sprout 2021 C/C++語法班

先備知識/設定

Memory

我們把記憶體想成是一個超大型陣列

Memory

Recall: 陣列用索引(index)取值

a[0]

a[1]

a[3]

a[2]

a[4]

a[5]

memory 這個大陣列用什麼取值?

Memory

16 進位 - hexadecimal 簡稱 hex

16 進位裡有16種符號

0

1

2

3

4

5

6

7

8

9

a

b

c

d

e

f

=

=

=

=

=

=

=

=

=

=

=

=

=

=

=

=

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Memory

16 進位範例

二進位

1

0

1

1

2^0
2^1
2^2
2^3
=
1\cdot 2^3 + 0\cdot 2^2 + 1\cdot 2^1 + 1\cdot 2^0=11

十進位

1

2

7

10^0
10^1
10^2
=
1\cdot 10^2 + 2\cdot 10^1 + 7\cdot 10^0 = 127

十六進位

2

a

f

16^0
16^1
16^2
=
2\cdot 16^2 + 10\cdot 16^1 + 15\cdot 16^0 = 687

hex 跟十進位一樣是一種表達數字的方式

Memory

16 進位表示法

hex 通常會在最前面加上 "0x" 來表示是16進位

範例:

0xabc

0x127

=
=
2748
295

Memory

記憶體用 hex 當作 index 來取值

0x00

0x01

0x02

0x03

0x04

0x05

0x06

0x07

0x08

0x09

0x0a

0x0b

記憶體的 index 被稱為 記憶體的地址(位址)

變數與記憶體

變數宣告

int god = 127;

1. 程式找到記憶體裡面一個可以用的格子

2. 標記變數的名稱

god

2. 把數值存在記憶體

127

0xff

變數存取

std::cout << god;

1. 找到變數名稱對應的記憶體地址

2. 根據地址去存取記憶體存放的數值

god

127

0xff

小總結

1. 變數存放在記憶體

2. 操作變數時,就是在操作記憶體的內容

3. 變數名稱是方便我們操作,程式是靠記憶體位址在做事

記憶體位置

變數名稱

數值

0xff

god

127

取址

取址

有了變數之後,怎麼知道他存在記憶體的哪個位置?

取址符號:

&
int god = 127;
std::cout << &god << "\n";

以上圖的例子,會得到 0xff

記憶體位置

變數名稱

數值

0xff

god

127

&

取址

動手玩玩看

#include <iostream>
int main() {
  int god = 127;
  std::cout << &god << "\n";
  return 0;
}

輸出

0x7ffe4a53a864

再跑一次

0x7fff4a666de4

取址

輸出

0x7ffe4a53a864

再跑一次

0x7fff4a666de4

變數會放在記憶體的哪個位址是電腦決定的

通常每次執行時會不同,這是一種安全保護機制

取值

取值

有了變數地址之後,怎麼存取他的數值?

std::cout << *(&god) << "\n";

以上圖的例子,會得到 127

記憶體位置

變數名稱

數值

0xff

god

127

取址符號:

*

*

取值

作為取值運算子時,只能對記憶體位址操作

*

拿一塊記憶體來試試看

std::cout << *(0x7ffe4a53a864) << "\n";

結果

a.cpp: In function ‘int main()’:
a.cpp:3:18: error: invalid type argument of unary ‘*’ (have ‘long int’)
    3 |     std::cout << *(0x7ffe4a53a864) << "\n";
      |                  ^~~~~~~~~~~~~~~~~

0x7ffe4a53a864 會被視為 long int,不能用 * 操作

地址雖然用 hex 表示,但他是不同於整數的型態

取值

作為取值運算子時,只能對記憶體位址操作

*

"地址"型態的數值不能讓你用手打的方式製造出來

請善用取址符號

如果隨便讓你生出一個地址去取值,電腦的秘密都給你看光光了><

取址+取值

取址+取值

記憶體位置

變數名稱

數值

0xff

god

127

*

&

用一張圖結束這堂課

指標

指標

指標

二階段

指標

地址雖然用 hex 表示,但他是不同於整數的型態

Recall:

記憶體位置是一種變數型態

int god = 127;
??? ptr = &god;

int 地址的型別:

int*

指標

int god = 127;
int* ptr = &god;

ptr 是個型別為 int* 的變數,內容存著 god 的記憶體位址

像 ptr 這種存別人地址的變數,就稱為指標

注意

這裡的 * 是型別的一部份,跟取值符號用途不同

指標

注意事項 1

int* ptr1, ptr2;

並不會得到兩個指標!!

他的效果跟以下是一樣的

int* ptr1;
int ptr2;
// ptr2 is int, not pointer

想要兩個指標的話

int *ptr1, *ptr2;

指標

宗教戰爭又來啦!!

指標

宗教戰爭

宣告指標時的 * 要跟著誰呢?

跟著型別

跟著變數

int* ptr;
int *ptr;

教義:

int* 本身是一種型別,

所以 * 要跟著型別的部份

教義:

跟著變數才能讓同一行宣告許多指標時格式統一

指標

宗教戰爭

小孩子才做選擇教 邪教

int*ptr;

Coding Style 有教:寫 code 要加空白

指標

注意事項 2

int 的指標只能拿來存 int 變數的地址

int a;
int* int_ptr = &a; // ok

float b;
float* float_ptr = &b; // ok

int_ptr = &b; // error

指標

圖解指標

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x20

0x24

0x28

0x2c

0x30

int god = 127;

god

127

int* ptr = &god;

ptr

0x10

int ntu = 112;

112

ntu

指標

圖解指標 - 對指標操作

god

127

std::cout << ptr << "\n";

ptr

0x10

112

ntu

: 操作對象

輸出: 0x10

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x20

0x24

0x28

0x2c

0x30

指標

圖解指標 - 對指標操作

god

127

ptr = &ntu

ptr

0x14

112

ntu

: 操作對象

讓 ptr 指向別人

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x20

0x24

0x28

0x2c

0x30

指標

圖解指標 - 對指標的對象操作

god

127

*ptr = 113

ptr

0x14

113

ntu

: 操作對象

std::cout << ntu << "\n";

輸出: 113

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x20

0x24

0x28

0x2c

0x30

指標

圖解指標

指標

小總結

2. 把指標想成是箭頭

ptr

變數

3. 對指標取值 = 想成走到箭頭的另一端,操作指到的對象

*

1. 宣告時,需要加上 * 才會變成指標

小練習

指標與陣列

Part 1

指標與陣列

陣列是記憶體裡面一塊連續的區間

Recall:

int arr[3] = {0};

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

題外話

指標與陣列

題外話:為什麼地址4個一跳?

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

0x04 -> 0x08,那0x05去哪了?

指標與陣列

實際操作

#include <iostream>

int main() {
  int arr[3];
  std::cout << &arr[0] << "\n";
  std::cout << &arr[1] << "\n";
  return 0;
}

輸出

指標與陣列

sizeof

sizeof 可以知道一個型態佔了多少 byte

std::cout << sizeof(char) << "\n";
std::cout << sizeof(int) << "\n";
std::cout << sizeof(long long) << "\n";
std::cout << sizeof(float) << "\n";
std::cout << sizeof(double) << "\n";
std::cout << sizeof(int*) << "\n";
std::cout << sizeof(char*) << "\n";

// 1 4 8 4 8 8 8

想想看:指標的大小是固定的(通常是8 byte),即使是 char* 也跟 int* 一樣大小,為什麼?

指標與陣列

sizeof

一個 int 佔了 4 byte

所以 0x04 ~ 0x07 都是被 arr[0] 佔據

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

指標與陣列

記憶體放大鏡

一個 int 佔了 4 byte

所以 0x04 ~ 0x07 都是被 arr[0] 佔據

0x04

0x05

0x06

0x07

0x08

0x09

0x0a

0x0b

0x0c

0x0d

arr[0]

arr[1]

回歸正題

指標與陣列

解密陣列

指標與陣列

int arr[3] = {0};
std::cout << arr << "\n";

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

輸出: 0x04

指標與陣列

實際操作

#include <iostream>

int main() {
  int arr[3];
  std::cout << &arr[0] << "\n";
  std::cout << arr << "\n";
  return 0;
}

輸出

指標與陣列

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

arr

arr 的行為就是一個指標指向陣列的第一個元素

指標與陣列

指標: 哈哈是我啦

指標與陣列

指標與陣列

其實指標跟陣列還是不完全一樣的

指標與陣列

 陣列 != 指標

int arr[3];
int* ptr = arr;

std::cout << sizeof(arr) << "\n";
// output 12
std::cout << sizeof(ptr) << "\n";
// output 8

指標與陣列

 陣列 != 指標

int arr[3];
int* ptr = arr;

std::cout << arr << "\n";
std::cout << &arr << "\n";
std::cout << &ptr << "\n";

輸出

指標與陣列

 陣列 != 指標

嚴格來說 arr 的型別是 int[3]

跟 ptr 的 int* 不太一樣

但是 arr 的行為跟指標幾乎一樣,我們先把他當成指標來看吧

int arr[3];
int* ptr = arr;

指標與陣列

指標可以做加法(+)與減法(-)的運算

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

arr

arr + 1

arr + 2

加法:將指標移動到下一個元素。注意因為 int 是 4 byte,所以 arr + 1 會指向 0x08(下一個元素位置),而不是 0x05

指標與陣列

arr[0]

arr[1]

arr[2]

0x00

0x04

0x08

0x0c

0x10

0x14

0x18

0x1c

0x20

0x24

0x28

0x2c

0

0

0

arr

arr + 1

arr + 2

小知識

arr[1] 其實跟 *(arr + 1) 是一樣的

*

*

*

指標與陣列

小知識

arr[1] 其實跟 *(arr + 1) 是一樣的

int arr[3] = {1, 2, 3};
int* ptr = arr;

std::cout << arr[1] << "\n";
std::cout << ptr[1] << "\n";

都可以通喔

指標與陣列

小總結

  1. 陣列說穿了就是指標指向一塊連續的記憶體
  2. 指標可以做加法與減法的運算。
    • 加法:指標的下一個元素
    • 減法:指標的上一個元素
  3. 上面就是一個原因為什麼指標要分型別(例如 int*, char*),做加減法位址移動的量就會不同

指標與函式

已知:指標是個變數

指標與函式

當然可以拿來傳啊

傳遞指標的主要用途:

可以讓函式修改外來的變數

複習:Pass By Value

指標與函式

void swap(int a, int b) {
  int c = a;
  a = b;
  b = c;
}
int a = 3, b = 5;
swap(a, b);
std::cout << a << ' ' << b << "\n";

並不會交換到 a 跟 b

記憶體裡面發生了什麼事?

指標與函式

int a = 3, b = 5;

a

3

b

5

main 可以用的記憶體

記憶體裡面發生了什麼事?

指標與函式

int a = 3, b = 5;

a

3

b

5

swap(a, b);

a

b

3

5

main 可以用的記憶體

swap 可以用的記憶體

記憶體裡面發生了什麼事?

指標與函式

int a = 3, b = 5;

a

3

b

5

swap(a, b);

main 可以用的記憶體

還給程式的記憶體

std::cout << a << ' ' << b << "\n";

重點筆記

指標與函式

  1. 每個函式有自己的記憶體空間
  2. 參數會被複製到被 call 的函式空間裡 (所以 swap 完 main 裡面的 a,b 沒有改變)
  3. 函式 return 之後他的空間會被釋放出來

指標與函式

傳遞指標

void swap(int* pa, int* pb) {
  int c = *pa;
  *pa = *pb;
  *pb = c;
}
int a = 3, b = 5;
swap(&a, &b);
std::cout << a << ' ' << b << "\n";

main 裡面的 a,b 會被交換

指標與函式

記憶體裡面發生了什麼事?

int a = 3, b = 5;

a

3

b

5

main 可以用的記憶體

指標與函式

記憶體裡面發生了什麼事?

int a = 3, b = 5;

a

3

b

5

swap(&a, &b);

pa

&a

main 可以用的記憶體

swap 可以用的記憶體

pb

&b

指標與函式

記憶體裡面發生了什麼事?

int a = 3, b = 5;

a

3

b

5

swap(&a, &b);

main 可以用的記憶體

還給程式的記憶體

std::cout << a << ' ' << b << "\n";

指標與函式

recall

傳遞陣列

指標與函式

int my_strlen(char s[]) {
  ...
}

char str[6] = "hello";
my_strlen(str);

記憶體裡面發生了什麼事?

str[0]

h

e

str[1]

str[2]

str[3]

str[4]

l

l

o

\0

s

main 可以用的記憶體

swap 可以用的記憶體

指標與函式

int my_strlen(char s[]) {
  ...
}

傳遞陣列

int my_strlen(char* s) {
  ...
}

所以其實這兩種寫法是一模一樣的意思

指標與函式

int* return_ptr() {
  int num = 3;
  return &num;
}

int* ptr = return_ptr();
std::cout << *ptr << "\n";

回傳指標?

輸出: ???

指標與函式

出了什麼問題?

num

3

main 可以用的記憶體

swap 可以用的記憶體

int num = 3; // in return_ptr

指標與函式

出了什麼問題?

main 可以用的記憶體

int num = 3; // in return_ptr
int* ptr = return_ptr();

ptr

還給程式的記憶體

???

指標與函式

出了什麼問題?

當函式 return 時,他的空間會被釋放出來

所以 num 已經消失了,他的地址沒有意義了

不要 return 函式 local variable 的地址!!

指標與函式

舉一反三小測驗

int* return_ptr(int num) {
  return &num;
}

int num = 3;
int* ptr = return_ptr(num);
int* return_ptr(int* num) {
  return num;
}

int num = 3;
int* ptr = return_ptr(&num);

其中一個是 ok 的,你知道是哪個 && 為什麼嗎?

指標與函式

什麼時候需要回傳指標?

現在可能還沒有很多適合的時機

之後課程進入 linked-list 時,回傳指標就會有很多用途

Reference

Reference

Recall:

Reference

reference 也是一種型別

宣告時一定要直接指定對象

int a = 0;

int& b = a // ok

/* not ok */
int& c;
c = a;
/* not ok */

Reference

怎麼理解 reference ?

int a = 3;

a

0xabc

int& b = a;

b

像是同一個變數有了兩個名稱

Reference

實際操作

#include <iostream>

int main() {
  int a = 3;
  int& b = a;
  std::cout << &a << "\n";
  std::cout << &b << "\n";
}

輸出

Reference

reference 是魔法?

剛剛說每個函式有自己的記憶體空間...

void swap(int& c, int& d){
  ...
}

int a = 3, b = 5;
swap(a, b);

又說 reference 像是兩個變數共用同一個空間...

a 跟 c 到底在記憶體裡面是什麼關係???

Reference

reference 是魔法?

Reference

其實...

很多時候 reference 背後的實作是 pointer

程式偷偷幫你操作 pointer 讓你用起來像同一個變數

P.S. 很多時候 = 不是一直都是

小結論

  1. 傳遞指標讓我們可以在函式內操作外面的變數
  2. 大部分的時候用 Pass By Value 就好,如果需要讓函式有修改外界變數的需求,優先考慮使用 reference
  3. 傳遞陣列背後其實就是在傳遞指標
  4. 不要回傳函式內 local variable 的地址,當你回傳時他已經消失了

指標與陣列

Part 2 (Advanced)

指標與陣列

recall

arr 像是指向陣列第一個元素的指標

二(多)維陣列呢?

題外話

指標^2

int a = 3;
int* b = &a;

b 是一個指向 int 的指標

可以有另一個指標指向 b 嗎?

?? c = &b;

指標也是一個變數

指標^2

int** ptr;

沒錯就是兩顆星星

指標^2

ptr

num

*

0x123

int num = 127;
int* ptr = &num;
int** dptr = &ptr;

127

0x456

0x123

*

dptr

0x789

0x456

小練習

回歸正題

指標與陣列

拆解二維陣列

arr[0][0]

arr[0][1]

arr[0][2]

arr[1][0]

arr[1][1]

arr[1][2]

arr[2][0]

arr[2][1]

arr[2][2]

是許多一維陣列

arr[0]

arr[1]

arr[2]

指標與陣列

拆解二維陣列

arr[0][0]

arr[0][1]

arr[0][2]

arr[0] 為例

arr[0]

arr[0] 的表現是一個 int* 的指標

arr[0] + 1

arr[0] + 2

*

*

*

記得陣列指標的行為雖然幾乎一樣,但其實是不同型別/概念

指標與陣列

拆解二維陣列

arr[0][0]

arr[0][1]

arr[0][2]

arr[1][0]

arr[1][1]

arr[1][2]

arr[2][0]

arr[2][1]

arr[2][2]

arr[0]

arr[1]

arr[2]

arr

arr + 1

arr + 2

arr 指向 arr[0]

那麼 arr 是 int** 嗎?

指標與陣列

拆解二維陣列

arr 是 int** 嗎?

指標與陣列

拆解二維陣列

arr 是 int** 嗎?

int arr[3][3];
int** ptr = arr;
a.cpp:8:17: error: cannot convert 'int (*)[3]' to 'int**' in initialization
    8 |     int** ptr = arr;
      |                 ^~~
      |                 |
      |                 int (*)[3]

arr 無法 cast(decay) 成 int** 型態

指標與陣列

出了什麼問題?

arr[0][0]

arr[0][1]

arr[0][2]

arr[1][0]

arr[1][1]

arr[1][2]

arr[2][0]

arr[2][1]

arr[2][2]

arr[0]

arr[1]

arr[2]

arr

arr + 1

arr + 2

記得指標的型態要能夠反應出操作指標 (例如 ptr + 1) 時記憶體位置的改變

arr arr + 1 跳過了整個一維陣列,如果用 int** 無法反映出這樣的記憶體位置變化

指標與陣列

解答

int arr[3][3];
int (*ptr)[3] = arr;

這樣 ptr 就有足夠的資訊可以在記憶體裡面移動了

指標與陣列

把點連成線

recall

這也是為什麼第二個維度不能留空

指標與陣列

More on Memory

(Advanced)

Segmentation fault

Segmentation fault

為什麼會 Segmentation fault ?

電腦裡面有許多程式同時在運作,例如你同時在看資芽影片還開著一個遊戲視窗,可能還有 fb 跟 ig

每個程式都需要記憶體來運作,而電腦會分配給每個程式一部分的記憶體

當你存取了不是電腦分配給你的記憶體時,就會 Segmentation fault

recall

Segmentation fault

recall

arr[i] == *(arr + i)

Segmentation fault

你的程式可以用的記憶體

你的陣列用的記憶體

Segmentation fault

arr

arr[10];

arr + 10

電腦:

Segmentation fault

arr

arr[1000];

arr + 1000

電腦:

????

Seg Fault !!!

Segmentation fault

Hacking Memory

指標可以做加減法

Recall:

指標減法:可以用來算出位址之間的差距

(以型別大小為單位)

int arr[3];
std::cout << &arr[1] - &arr[0] << "\n";

輸出:1

Hacking Memory

指標減法:可以用來算出位址之間的差距

(以型別大小為單位)

int* a;
char* b;

int diff = a - b;
// error
// char* and int* not the same

注意:兩個指標必須同型別

Hacking Memory

#include <iostream>

int main() {
  int is_127 = 0;
  int me[100];
  
  int index, value;
  std::cin >> index >> value;
  me[index] = value;
  
  if (is_127) {
    std::cout << "127 is god <(_ _)>\n";
  }
  
  return 0;
}

如何讓我們膜拜 127?

Hacking Memory

關鍵: 

先計算我跟 127 的差距

std::cout << &is_127 - me << "\n";

在我的電腦上輸出: 103

每台電腦可能有所不同

雖然每次程式執行時,is_127 跟 me 的位置都會改變,但相對位置不會變。

So.. *(me + 103) == me[103] == is_127

這樣就可以修改到 is_127 的內容了

Hacking Memory

Hacking Memory

結語

結語

指標讓我們有操作記憶體的手段,這是非常強大的功能

在操作指標時在腦中畫出箭頭,想像指標指向某個變數

記憶體位置

變數名稱

數值

0xff

god

127

*

&

指標其實就是取址跟取值的操作

結語

熟悉指標的運作(以及記憶體內容的變化),會讓你對你的程式在做什麼有更深的理解

學習的路上免不了要吃很多 Segmentation fault,這是很正常的過程,不要放棄:)

Made with Slides.com