指標
楊宗儒@Sprout 2023
改寫自Sprout 2022講義
一些複習
變數
- 想像成是可以儲存資料的一個箱子
- 資料儲存在記憶體中
陣列
- 一次宣告大量變數
- 透過 index 存取跟修改
- 宣告:datatype arr[size];
- 存取:arr[index]
打開黑盒子
進位制
- 二進位:0和1
10010 ->
10010
2⁰
2¹
2²
2³
2⁴
- 十進位:
48763 ->
48763
10⁰
10¹
10²
10³
10⁴
4 * 10⁴ + 8 * 10³ + 7 * 10² + 6 * 10¹ + 3 * 10⁰
16 + 0 + 0 + 2 + 0 = 18
- 十六進位:A-F代表10-15
0x00AF8 ->
00AF8
16⁰
16¹
16²
16³
16⁴
0 * 16⁴ + 0 * 16³ + A(10) * 16² + F(15) * 16¹ + 8 * 16⁰ = 2808
程式如何儲存資料?
- 程式執行時都要把資料/程式本身載入記憶體
- 硬碟雖然也可以儲存資料,但實際執行時還是得存入記憶體才能執行,兩者是不同的東西
程式如何儲存資料?
- 記憶體可以想像成是一個大陣列
- 每個位置儲存一個 byte(8 個 0 或 1)
- 每個位置都有一個 address(index)
- 連續的address 會放在連續的位置
程式如何儲存資料?
- 變數:可以儲存資料的箱子,並且每個箱子都有它的地址
- 陣列:宣告大量類似的變數,並儲存在連續的地址
程式如何儲存資料?
- 每個資料型別的大小都不太一樣
- int: 4 bytes, long long: 8 bytes
- 以前題目出現過的 2³¹-1就是因為 int 的大小最大只能存到這麼多
- 取得一個變數/型別大小: sizeof (以 byte 為單位)
cout << sizeof(int) << endl; // 4
char c = 'C';
cout << sizeof(c) << endl; // 1
指標
變數取址
- 在變數前面加上&可以取得變數的位址
- 每次執行程式時可能不一樣
cout << &a << endl;
char a
'A'
0xFFF00A40
變數名
變數值
變數地址
指標變數
- 將 a 的地址存到 ptr 中 -> ptr 指到 a
int *ptr = &a;
int a
30
0xFFF00A40
int *ptr
0xFFF00A40
0xFFF00A44
指標自己也會有地址!
指標型別
- 地址中儲存的是什麼型別,指標就要是什麼型別
- a 是 int ,所以要用 (int *) 而不是 (char *)
int a = 0;
char *ptr = &a; // ERROR
指標操作--取值
- 在一個指標變數前面加上*代表操作指標指到的數值
- 這裡的 (*ptr)++ 其實就是指 (a)++
int a = 0;
int *ptr = &a;
(*ptr)++;
cout << *ptr << endl;
cout << a << endl;
宣告時的*是代表是指標變數
取值時的*代表操作指到的數值
別搞混!
指標操作--取值
- 把在地址 0xFFF00A40 的數值改成 10
- 也就是把 a 改成 10
*ptr = 10
int *ptr
0xFFF00A40
0xFFF00A44
int a
30 -> 10
0xFFF00A40
指標操作--移情別戀
- 指標可以在宣告後再指到其他位址
- 這裡是操作指標本體,所以沒有*
int *ptr = &a;
ptr = &b;
指標操作--移情別戀
int *ptr = &a;
ptr = &b;
int a
30
0xFFF00A40
int *ptr
0xFFF00A40
-> 0xFFF00A36
0xFFF00A44
int b
30
0xFFF00A36
小練習
int a = 2023
int b = 3
int *ptr1 = &a;
int *ptr2 = &b;
*ptr2 = *ptr1;
ptr1 = ptr2;
a, b 最後是多少?
ptr1, ptr2 指向誰?
小練習
int a = 2023
int b = 3
int *ptr1 = &a;
int *ptr2 = &b;
*ptr2 = *ptr1;
ptr1 = ptr2;
a, b 最後是多少?
ptr1, ptr2 指向誰?
a = 2023, b = 2023
ptr1 = &b, ptr2 = &b
小結
int *ptr
*ptr
ptr
- 宣告指標
- 指標所指之處
- 指標本體
空指標
空指標
- 代表一個不指向任何東西的指標
int *ptr = NULL;
空指標
- C++11 後才有
- 和 NULL 相同
- 建議寫 C++ 時用這個
int *ptr = nullptr;
空指標
- 使用指標取值前先檢查是否有指到東西
if (ptr != nullptr)
指標X陣列
陣列
int arr[5];
int arr[0]
1
0xFFF00A40
int arr[1]
2
0xFFF00A44
int arr[2]
3
0xFFF00A48
int arr[3]
4
0xFFF00A4C
int arr[4]
5
0xFFF00A50
指標X陣列
int *ptr = &arr[0]
int arr[0]
1
0xFFF00A40
int arr[1]
2
0xFFF00A44
int arr[2]
3
0xFFF00A48
int arr[3]
4
0xFFF00A4C
int arr[4]
5
0xFFF00A50
int *ptr
0xFFF00A40
0xFFF00A38
指標遞增?
(A) 指標指到的位址+1
(B) 指標指到的數值+1
(C) 指標指向下一個數值
(D) Syntax Error
ptr++
指標X陣列
int *ptr = &arr[0];
int arr[0]
1
0xFFF00A40
int arr[1]
2
0xFFF00A44
int arr[2]
3
0xFFF00A48
int arr[3]
4
0xFFF00A4C
int arr[4]
5
0xFFF00A50
int *ptr
0xFFF00A40
0xFFF00A38
指標X陣列
ptr++
int arr[0]
1
0xFFF00A40
int arr[1]
2
0xFFF00A44
int arr[2]
3
0xFFF00A48
int arr[3]
4
0xFFF00A4C
int arr[4]
5
0xFFF00A50
int *ptr
0xFFF00A44
0xFFF00A38
指標遞增?
(C) 指標指向下一個數值
指標所指的地址 += sizeof(指標指到的型別)
ptr++
- 小心不要指到不可控的地方
優先序
(A) 指標指到的數值+1
(B) 取值後指標指向下一個數值
*ptr++;
*ptr++;
(*ptr)++;
*(ptr++);
詳細的優先序 (不用記)
陣列生指標
- 陣列的名字不加[]代表指向其第一個元素的指標
int arr[5];
int *ptr = arr;
陣列生指標
- char* / char[4]
- char*
- char
char arr[4];
char *ptr;
char c;
arr
0xFFF00A40
0xFFF00A36
char arr[0]
'A'
0xFFF00A40
'B'
0xFFF00A41
'C'
0xFFF00A42
'D'
0xFFF00A43
char arr[1]
char arr[2]
char arr[3]
指標生陣列
- 把 ptr 當作陣列的頭,存取這個陣列第 idx 個元素
ptr[idx]
指標生陣列
ptr = &arr[2]
char *ptr
0xFFF00A42
0xFFF00A36
char arr[0]
'A'
0xFFF00A40
'B'
0xFFF00A41
'C'
0xFFF00A42
'D'
0xFFF00A43
char arr[1]
char arr[2]
char arr[3]
指標生陣列
ptr[1] = 'E'
char *ptr
0xFFF00A42
0xFFF00A36
char arr[0]
'A'
0xFFF00A40
'B'
0xFFF00A41
'C'
0xFFF00A42
'D' -> 'E'
0xFFF00A43
char arr[1]
char arr[2]
char arr[3]
ptr[1]
指標 vs 陣列
- *ptr: 指標所指的數值
- ptr[i]: 存取所指位址後面第 i 個數值
- ptr + i: 所指處後面第 i 個數值的位址
- ptr++: 指向下一個數值
- ptr = &a: 指向新的地方
- sizeof(ptr): 指標本身的大小(4/8 byte)
int *ptr;
int arr[5];
- *arr: arr[0]
- arr[i]: 存取陣列頭後第 i 個數值
- arr + i: 陣列頭後面第 i 個數值的位址
- arr++: 不支援
- arr = &a: 不支援
- sizeof(arr): 整個陣列的 bytes 數
小(ㄉㄚˋ)練習
int arr[10];
int *ptr = arr;
後面的code都延續這個宣告
小練習
arr = ptr;
Error
陣列不能指向其他地方
小練習
ptr[10] = 2;
Runtime Error
ptr 一開始指向 arr 的頭
但 arr 只有 10 個元素
arr[10] 是越界存取 arr 的第 11 個元素
會出現不可預期的行為
小練習
*ptr += 1;
AC
ptr 指到的數值 + 1
小練習
ptr = arr[1];
Error
ptr 是 (int *)
arr[1] 是 int
不同型別不能直接賦值
小練習
*(arr + 1) = 3;
AC
先把 arr 當作指到陣列頭的指標
+1 代表所指之處後一個數值的地址
再對這個地址進行取值
等同於 arr[1] = 3
小練習--同場加映
9[arr] = 3;
AC !!!!
先把 arr 當作指標
前面說過 a[b] 可以當成 *(a + b)
等同於 arr[9] = 3
小練習
ptr = arr + 10;
AC
先把 arr 當作指標
+ 10 代表把所指的地址後 10 個數值的位址
再把這個位址存入 ptr
等同 ptr = &arr[10]
但要注意 ptr 指向一個未知的記憶體位址
指向未知記憶體不會有錯誤,但對未知記憶體取值就會有錯誤
小練習
ptr = *arr + 10
Error
先把 arr 當作指標指向 arr[0]
對這個指標取值後 + 10 (型別:int, 值:a[0] + 10)
不同型別不能直接賦值 (int *)
小練習
*ptr = *arr
AC
先把 arr 當作指標指向 arr[0]
對這個指標取值後,賦值給 ptr 指到的數值
等號兩邊都是 int
隨堂練習題
#OJ 8857
題解
分解動作:
- 多開一個指標 ptr
- 指標往右一格
- 比較數值
- 把 max_ptr 指到最大值
- 什麼時候結束?
int *ptr
ptr++ (操作指標->沒有*)
*ptr > *max_ptr(比較數值->*)
max_ptr = ptr (操作指標->沒有*)
ptr == end (比較指標->沒有*)
注意 max_ptr 一開始是 nullptr
一些好習慣
宣告指標
- 先空格再*
int *ptr; // 星號跟著變數名
- a 是 int* ,但 b 只是 int
int* a, b;
指標初始化
int *ptr = &a;
int *nothing = nullptr;
- 宣告時記得初始化
- 如果暫時沒有辦法賦值,初始化為 nullptr/NULL
檢查是否為空
if (ptr != nullptr) {
*ptr = 123;
}
- 對指標取值前記得檢查指標是否為nullptr
注意邊界
ptr[idx]
- 確保 idx 在可存取的範圍內
你已經死了
int *ptr = nullptr;
for (int i = 0; i < 2; i++) {
ptr = &i;
}
cout << *ptr << endl; // RE
- 指標不要活得比指到的變數還久
- 不得已的話,記得離開所指變數的生命週期時清空指標
小結
- 宣告指標先 空格 再 *
- 宣告指標務必初始化
- 使用指標前檢查是不是nullptr
- 注意邊界
- 注意生命週期
第一次翻車就上手
我的程式在我的電腦上好好的,丟到 OJ 卻 RE 了!
記憶體分段
- 為了方便管理,電腦會將記憶體分好幾段
- 不同段需要不同權限
- 不符合權限的存取會觸發Segmentation Fault
0x00000000
0xFFFFFFFF
不同系統遇到記憶體存取錯誤
Windows
Linux
總結
- 宣告指標
- 取址、取值
- 陣列與指標的關係
- 指標使用的習慣
- Segmentation fault
參照(Reference)
參照
- 參照:幫變數取別名
- 操作參照就等於在操作變數本身
int a = 0;
int &ref = a;
ref++;
cout << ref << endl; // 1
cout << a << endl; // 1
有點像指標?
參照:
幫變數取別名
操作參照就等於在操作變數本身
不能中途參照其他變數
語法與直接使用變數相同
記憶體裡(通常)不會佔空間
int a = 0;
int &ref = a;
ref++;
cout << ref << endl; // 1
cout << a << endl; // 1
指標:
操作指標指到的數值就等於在操作變數本身
可以指向其他變數
語法與直接使用變數不同(*, &)
記憶體裡會佔空間
int a = 0;
int *ref = &a;
(*ref)++;
cout << ref << endl; // 1
cout << a << endl; // 1
附錄
pointer to pointer
- 其實還是 pointer (?
- 只是指到的型別是 (type *)
int a = 3;
int b = 1;
int *ptr1 = &a;
int *ptr2 = &b;
int **ptr2ptr = &b;
cout << **ptr2ptr << endl;
const
const 指標有分以下幾種:
- const int*: 指向 const int 的指標,不能修改數值(指標指到的東西是const)
- int * const: 指向 int 的 const 指標,不能指到其他位址(指標本體是 const)
- const int * const: 指向 const int 的 const 指標,不能修改數值,也不能指到其他位址
如果覺得記不起來的話...
const
const int a = 3; // a 是 const int,所以不能修改a的值
int b = 4; // b 是正常的 int ,可以隨意修改 b 的值
int d = 6;
const int *ptr1 = &a; // ptr1 是指到 const int 的指標,可以指向別人,但不能修改指到的值
int* const ptr2 = &b; // ptr2 是指到 int 的 const 指標,不能指向別人,但可以修改別人
const int* const ptr3 = &a; // ptr3 是指向 const int 的 const 指標,不能指向別人,也不能修改指到的值
ptr1 = &d; // ok
*ptr1 = 5; // Error
ptr2 = &d; //Error
*ptr2 = 6; // ok
ptr3 = &d; //Error
*ptr3 = 6; // Error
Endianness
- 一個記憶體位址只會有一個 byte
- 但有些型別的大小不只一個 bytes(e.g. int, long long etc.)
- 把一整個資料放到一段記憶體中
- C++存取時會用最小的位址來代表一個元素
Endianness
big-endian: 最大的 byte 的記憶體位址最大
little-endian: 最大的 byte 記憶體位址最小
void pointer
- 不帶型別的指標
- 可以去指不同型別的變數
- 使用時要轉型成你要的型別指標
void pointer
int a = 3;
char c = 'C';
void *vptr = &a;
cout << *(int*) vptr << endl;
vptr = &c;
cout << *(char *)vptr << endl;
cout << *ptr << endl; //ERROR
資芽2023-指標
By s0n9yu
資芽2023-指標
- 225