指標

楊宗儒@Sprout 2023

改寫自Sprout 2022講義

一些複習

變數

  • 想像成是可以儲存資料的一個箱子
  • 資料儲存在記憶體中

陣列

  • 一次宣告大量變數
  • 透過 index 存取跟修改
  • 宣告:datatype arr[size];
  • 存取:arr[index]

打開黑盒子

進位制

  • 二進位:0和1

10010 -> 

10010

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