蔡銘軒 @ Sprout 2021 C/C++語法班
我們把記憶體想成是一個超大型陣列
Recall: 陣列用索引(index)取值
a[0]
a[1]
a[3]
a[2]
a[4]
a[5]
memory 這個大陣列用什麼取值?
16 進位 - hexadecimal 簡稱 hex
16 進位裡有16種符號
0
1
2
3
4
5
6
7
8
9
a
b
c
d
e
f
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
16 進位範例
二進位
1
0
1
1
十進位
1
2
7
十六進位
2
a
f
hex 跟十進位一樣是一種表達數字的方式
16 進位表示法
hex 通常會在最前面加上 "0x" 來表示是16進位
範例:
0xabc
0x127
記憶體用 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";
都可以通喔
小總結
已知:指標是個變數
當然可以拿來傳啊
傳遞指標的主要用途:
可以讓函式修改外來的變數
複習: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";
重點筆記
傳遞指標
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 #
}
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 #
}
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 時,回傳指標就會有很多用途
Recall:
reference 也是一種型別
宣告時一定要直接指定對象
int a = 0;
int& b = a // ok
/* not ok */
int& c;
c = a;
/* not ok */
怎麼理解 reference ?
int a = 3;
a
0xabc
int& b = a;
b
像是同一個變數有了兩個名稱
實際操作
#include <iostream>
int main() {
int a = 3;
int& b = a;
std::cout << &a << "\n";
std::cout << &b << "\n";
}
輸出
reference 是魔法?
剛剛說每個函式有自己的記憶體空間...
void swap(int& c, int& d){
...
}
int a = 3, b = 5;
swap(a, b);
又說 reference 像是兩個變數共用同一個空間...
a 跟 c 到底在記憶體裡面是什麼關係???
reference 是魔法?
其實...
很多時候 reference 背後的實作是 pointer
程式偷偷幫你操作 pointer 讓你用起來像同一個變數
P.S. 很多時候 = 不是一直都是
Part 2 (Advanced)
recall
arr 像是指向陣列第一個元素的指標
二(多)維陣列呢?
int a = 3;
int* b = &a;
b 是一個指向 int 的指標
可以有另一個指標指向 b 嗎?
?? c = &b;
指標也是一個變數
int** ptr;
沒錯就是兩顆星星
ptr
num
*
0x123
int num = 127;
int* ptr = #
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
這也是為什麼第二個維度不能留空
(Advanced)
為什麼會 Segmentation fault ?
電腦裡面有許多程式同時在運作,例如你同時在看資芽影片還開著一個遊戲視窗,可能還有 fb 跟 ig
每個程式都需要記憶體來運作,而電腦會分配給每個程式一部分的記憶體
當你存取了不是電腦分配給你的記憶體時,就會 Segmentation fault
recall
recall
arr[i] == *(arr + i)
你的程式可以用的記憶體
你的陣列用的記憶體
arr
arr[10];
arr + 10
電腦:
arr
arr[1000];
arr + 1000
電腦:
????
Seg Fault !!!
指標可以做加減法
Recall:
指標減法:可以用來算出位址之間的差距
(以型別大小為單位)
int arr[3];
std::cout << &arr[1] - &arr[0] << "\n";
輸出:1
指標減法:可以用來算出位址之間的差距
(以型別大小為單位)
int* a;
char* b;
int diff = a - b;
// error
// char* and int* not the same
注意:兩個指標必須同型別
#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?
關鍵:
先計算我跟 127 的差距
std::cout << &is_127 - me << "\n";
在我的電腦上輸出: 103
每台電腦可能有所不同
雖然每次程式執行時,is_127 跟 me 的位置都會改變,但相對位置不會變。
So.. *(me + 103) == me[103] == is_127
這樣就可以修改到 is_127 的內容了
指標讓我們有操作記憶體的手段,這是非常強大的功能
在操作指標時在腦中畫出箭頭,想像指標指向某個變數
記憶體位置
變數名稱
數值
0xff
god
127
*
&
指標其實就是取址跟取值的操作
熟悉指標的運作(以及記憶體內容的變化),會讓你對你的程式在做什麼有更深的理解
學習的路上免不了要吃很多 Segmentation fault,這是很正常的過程,不要放棄:)