指標 I - 指標與參考

指標 II - 應用

Pointer & Reference

臣亮言 @ NTU CSIE

May 16 & 29, 2026

Sprout 資訊之芽北區 C++ 班

在開始之前......

  1. 講師口齒不清,如果他忘記用力說話請大力提醒他
  2. 有任何問題都歡迎直接舉手或在 Slido 提問,不要害怕問問題
  3. 講師喜歡會動來動去的簡報,所以跟之前的簡報會長得不太一樣,如果覺得哪裡不太習慣或有哪些需要改善的歡迎填意見表單或直接跟我說!
  4. 這個章節很難!很難!很難!跟不上或聽不懂絕對要說!!!
  5. 講師快死了

Slido #sprout

References not reference

Pointer & Reference

指標 Pointer

參考 Reference

動態配置記憶體 Dynamic memory allocation

OOP with pointer

Application: Dynamic Array

Application: Linked list

Pointer

Pointer

1

  What is pointer?

還記得上上週教的記憶體嗎?

不記得就算了啊我真的沒關係

變數的值存在記憶體之中,可以想像成放在某個格子裡

並且每種變數所佔的記憶體大小(多少 byte)都不一樣

long long

char

int

寫程式便是透過操作這些記憶體來達到目的

但我們該怎麼知道操作他們呢?

Pointer

1

  What is pointer?

long long

char

int

0x3FA00 0x3FA01 0x3FA02 0x3FA03 0x3FA04 0x3FA05 0x3FA06 0x3FA07
0x3FA08 0x3FA09 0x3FA0A 0x3FA0B 0x3FA0C 0x3FA0D 0x3FA0E 0x3FA0F

記憶體裡面有無數個連續的 bytes

變數存在這些 bytes 中,也就要有個位址(Address)

就像住址一樣:羅斯福路一段 1 號、羅斯福路一段 2 號

指標(Pointer)便是紀錄記憶體位址的變數

透過指標,我們就可以對記憶體中的變數做操作(上門查水表)

想操作他們,首先我們得知道他們在哪裡

Pointer

1

  What is pointer?

2

  指標的宣告

int *iptr; //int ptr
char *cptr; //char ptr
int **pptr; //int ptr ptr

指標雖然存的資料是某一個記憶體位址

但指標並非特指一種資料型別

指標可以指向任意型別

甚至指向某種指標也可以

上述的兩個宣告方法等價

但要注意 * 只會作用於其後的第一個變數

int* a, b;
int *c, *d;

看似把 a, b 都設為指標

實際上只有 a 會是指標

b 仍然是一個整數

c, d 則都是指標

一個指標變數的型別是 指向<type>的指標

宣告是 <type> *var1/<type>* var2

或著說在變數名稱前加上一個 '*'

代表宣告為指標,不屬於變數名稱

Pointer

2

  指標的宣告

3

  取值/取址運算子 ( * / & )

剛剛說指標實際上存的是記憶體位址

那我們該怎麼得到記憶體位址呢?

int a

0x2F04

1024

 值(a : 1024)

位址(&a : 0x2F04)

取址運算子 &

加在變數前,對變數取址

可以得到該變數在記憶體中的位址

int a = 1024;
int *ptr = &a;

把位址賦值給指標 ptr

ptr 儲存了變數 a 的記憶體位址

或著說「ptr 指向變數 a

Pointer

3

  取值/取址運算子 ( * / & )

int a

0x2F04

1024

int a

0x2F04

1024

int a

0x2F04

1024

int* ptr

0x4DA8

0x2F04

那要怎麼透過指標對記憶體作存取與操作呢?

(得到地址後要怎麼查水表?)

ptr = &a

取值運算子 *

加在指標前,對指標(地址)取值

可以操作該地址的記憶體

藉此直接存取、更改該地址所存的值

*ptr

int a = 1024;
int *ptr = &a;
*ptr = 512;

512

Pointer

3

  取值/取址運算子 ( * / & )

int a

0x2F04

1024

int a

0x2F04

1024

a

&a

ptr = &a

&ptr

pptr = &ptr

&pptr

*ptr

*pptr

**pptr

既然指標紀錄的是記憶體位址

為什麼指標需要指向某個型別呢?

int* ptr

0x4DA8

0x2F04

int** pptr

0x501B

0x4DA8

Pointer

3

  取值/取址運算子 ( * / & )

int a

0x2F04

1024

int a

0x2F04

1024

int** pptr

0x501B

0x4DA8

a

&a

ptr = &a

&ptr

pptr = &ptr

&pptr

*ptr

*pptr

**pptr

long long

char

int

0x3FA00 0x3FA01 0x3FA02 0x3FA03 0x3FA04 0x3FA05 0x3FA06 0x3FA07
0x3FA08 0x3FA09 0x3FA0A 0x3FA0B 0x3FA0C 0x3FA0D 0x3FA0E 0x3FA0F

int* ptr

0x4DA8

0x2F04

記憶體紀錄的是單純的二進位制資料

而且每個型別所佔的記憶體不同

我們需要指向的型別才知道要讀取多長的記憶體(尤其等等提到陣列)

以及要怎麼解讀記憶體中的資料

由於指標指的都是記憶體位址

因此所有型別的指標都佔 8 bytes

Pointer

3

  取值/取址運算子 ( * / & )

4

  空指標(nullptr)

這裡再提到一個觀念:空指標(nullptr)

記得之前有說過變數宣告後不賦值

我們無法預測這些變數中到底存著什麼

而指標也是同個道理

尤其指標涉及對記憶體直接操作

如果沒適當處理會很可怕(亂戳記憶體)

因此在 C++ 中,我們利用空指標來初始化指標的值

有點像是 default 值,避免還沒賦值就亂用到指標

在對 nullptr 取值時,系統就會報錯

Pointer

4

  空指標(nullptr)

int *ptr = nullptr; //null pointer

其實 nullptr 更常用來維護資料結構

像是 Linked-list

不過我們不是算法班

暫時很難看到 nullptr 的用途

不過在不能第一時間賦值前

宣告為 nullptr 仍是重要的習慣

int *ptr = NULL;

另外 nullptr 其實是 C++ 才有的東西

C 語言中我們使用 NULL

但這東西在 C/C++ 中的定義不一樣

NULL 在 C++ 裡會有問題

因此我們通常寫 nullptr

詳細可以參考這篇文章

Pointer

4

  空指標(nullptr)

5

  陣列與指標

你是否曾經對 C++ 抱有妄想

期盼這樣能直接輸出一個陣列呢?

int a[10] = {};
cout << a;

0x7ffcc315aba0

Why?

Pointer

4

  空指標(nullptr)

5

  陣列與指標

陣列在記憶體中實際上是一段連續記憶體

例如: int a[10];

0x6C20 0x6C24 0x6C28 0x6C2C 0x6C30 0x6C34 0x6C38 0x6C3C 0x6C40 0x6C44

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

陣列可以當成一種指標指向連續記憶體的起點,再透過 [] 運算子取值

int a[10]

0x6C20

0x6C20

每一個 int 佔 4 bytes,因此每向右平移四格

就可以得到陣列下一個值的位址

那要怎麼做到位址的平移呢?其實就是加法

Pointer

5

  陣列與指標

0x6C20 0x6C24 0x6C28 0x6C2C 0x6C30 0x6C34 0x6C38 0x6C3C 0x6C40 0x6C44

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

int a[10]

0x6C20

0x6C20

a

a+1

a+3

a+2

a+4

a+5

a+6

a+7

a+8

a+9

Ok,現在我們會用位址的平移(加法)來得到每個值的位址了(%%%%%

距離得到陣列的值只差一步

Pointer

5

  陣列與指標

0x6C20 0x6C24 0x6C28 0x6C2C 0x6C30 0x6C34 0x6C38 0x6C3C 0x6C40 0x6C44

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

int a[10]

0x6C20

0x6C20

a

a+1

a+3

a+2

a+4

a+5

a+6

a+7

a+8

a+9

*(a+0)

*(a+1)

*(a+2)

*(a+3)

*(a+4)

*(a+5)

*(a+6)

*(a+7)

*(a+8)

*(a+9)

再加上取值運算子,就可以得到陣列的值

因此 a[i] 與 *(a+i) 實際上是等價的
其實也和 i[a] 是等價的

for(int i = 0; i < n; i++)
	cout << a[i] << ' '; //*(a + i)
cout << '\n';

for(int i = 0; i < n; i++)
	cout << i[a] << ' '; //*(i + a)
cout << '\n';

Pointer

5

  陣列與指標

0x6C20 0x6C24 0x6C28 0x6C2C 0x6C30 0x6C34 0x6C38 0x6C3C 0x6C40 0x6C44

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

int a[10]

0x6C20

0x6C20

a

a+1

a+3

a+2

a+4

a+5

a+6

a+7

a+8

a+9

再觀察一下,這些值之間位址其實是差 4

這是因為每個 int 佔 4 bytes

那為什麼對位址 + i 可以得到 a[i] 的位址?

這要歸功於指標的型別 int*

他可以讓每個平移都會自動跳 4 bytes

而不用我們再幫他乘 sizeof(int)

所以指標的型別才那麼重要

Pointer

5

  陣列與指標

0x6C20 0x6C24 0x6C28 0x6C2C 0x6C30 0x6C34 0x6C38 0x6C3C 0x6C40 0x6C44

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

a

a+1

a+3

a+2

a+4

a+5

a+6

a+7

a+8

a+9

這要歸功於指標的型別 int*

他可以讓每個平移都會自動跳 4 bytes

而不用我們再幫他乘 sizeof(int)

所以指標的型別才那麼重要

int a[10];
for(int i = 0; i < 10; i++) cout << a + i << ' ';
cout << '\n';

long long *b = (long long*) a;
for(int i = 0; i < 5; i++) cout << b + i << ' ';
cout << '\n';

這裡會輸出什麼?

如果取值輸出呢

Pointer

5

  陣列與指標

Exercise

Pointer

5

  陣列與指標

Exercise

Pointer

4

  空指標(nullptr)

5

  陣列與指標

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

那多維陣列呢?這裡拿二維陣列舉例(int a[3][4])

我們通常把二維陣列視覺化為矩陣的樣貌

就像座位是第幾排、第幾列的樣子

不過實際上他們是一整列串接起來的記憶體

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

Pointer

5

  陣列與指標

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

a[0], a[1], a[2] 都是長度為 4 的一維陣列

可以想像成他們是 int* (int[4])

那 a 又是什麼?

a[0]

a[1]

a[2]

既然 a[i] 是長度為 4 的一維陣列

那 a 就能當成指向 int[4] 的指標

int *ptr1[4];
//an array of 4 int pointer
int (*ptr2)[4];
//a pointer points to an array of 4 int

會這樣跟 C 的設計有關係

但我們不太會用到,這裡就不贅述了

可以去問 GPT 我覺得他說得很好

Pointer

5

  陣列與指標

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

a[0]

a[1]

a[2]

經過一點計算,可以發現

a[i][j] == *(&(a[0][0]) + i * 4 + j)

更廣義來說,對於二維陣列  arr[n][m]

arr[i][j] == *(&(arr[0][0]) + i * m + j)

對於三維陣列 arr[x][y][z]

arr[i][j][k] == *(&(arr[0][0][0]) + (i * y + j) * z + k)

但這些其實也沒很重要

我們就乖乖用陣列就好

(%%%%%)

Pointer

5

  陣列與指標

6

  Pass by pointer

嘗試實做一個函式:swap

作用是把兩個傳入變數的值交換

先不寫成函式,一般會怎麼寫?

int a = 1, b = 2;
a = b;
b = a;
// a:2, b:2?

a 在進第三行的時候已經被改成 b 了

我們需要第三者幫我們暫存 a 的值

int a = 1, b = 2;
int tmp = a;
a = b;
b = tmp;
//a:2, b:1!

這樣就完成了!

int a = 1, b = 2;
a ^= b ^= a ^= b;

或著你可能記得我有教過......

不管怎樣,現在來把他包成函式吧

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

int x

1

int y

2

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

int x

1

int y

2

int a

1

int b

2

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

int x

1

int y

2

int a

1

int b

2

int tmp

1

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

int x

1

int y

2

int a

2

int b

2

int tmp

1

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

int x

1

int y

2

int a

2

int b

1

int tmp

1

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

但實際用過以後

會發現執行完函式並不會交換到 x, y 的值

void swap()

int main()

int x

1

int y

2

int a

2

int b

1

int tmp

1

print

1 2

Pointer

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
int main(){
	int x = 1, y = 2;
	swap(x, y);
    cout << x << ' ' << y;
}

void swap()

int main()

int x

1

int y

2

int a

2

int b

1

int tmp

1

這是因為函式的呼叫實際上是在一開始給參數賦予傳入值

因此參數只是紀錄了傳入變數的值,就像建立分身

並不會動到傳入的變數

(且由於可視域的關係,在函式內也動不到傳入的變數)

這時我們就可以利用指標

把地址給函式,再讓他去查水表!

Pointer

5

  陣列與指標

6

  Pass by pointer

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
swap(x, y);
void swap(int* a, int* b){
  	int tmp = *a;
  	*a = *b;
	*b = tmp;
}
//...
swap(&x, &y); //記得要傳位址

Pass by value

Pass by pointer

藉由指標紀錄傳入變數的地址

進而直接修改可視域外變數的記憶體

參數的變動不會反映在傳入變數上

適合只需要拿傳入值來計算的函式

適合需要對變數本體操作的函式

Pointer

5

  陣列與指標

6

  Pass by pointer

除了一般變數以外,陣列該怎麼傳入函式?

0x6C20 0x6C24 0x6C28 0x6C2C 0x6C30 0x6C34 0x6C38 0x6C3C 0x6C40 0x6C44

a[0]

a[1]

a[2]

a[3]

a[4]

a[5]

a[6]

a[7]

a[8]

a[9]

int a[10]

0x6C20

0x6C20

a

既然我們可以把陣列視為一個指標

那麼直接讓參數也是指標就好了!

先考慮單純的一維陣列

void foo(int *arr){...}
void fooo(int arr[]){...}

int arr*

0x77A0

0x6C20

甚至直接寫 [] 也可以編譯

也更符合直覺

Pointer

5

  陣列與指標

6

  Pass by pointer

除了一般變數以外,陣列該怎麼傳入函式?

void foo(int *arr);
void foo2(int n, int arr[n]);

void foo3(int arr[100][]);//X
void foo4(int arr[][100]);//O
void foo5(int n, int m, int arr[n][m]);

但一旦遇到多維陣列就會出問題:編譯器猜不到長寬

一維陣列索引值多少就平移多少

但在多維陣列中每一維的索引值要平移多少會是問題

常見的解法:

  • 以常數宣告長度
  • 輸入長寬後以變數宣告

即可維持原先多維陣列操作的便利性

注意

Reference

CHECK POINT

Reference

Reference

1

  What is reference?

參考 (reference) 是 C++ 特有的變數型別

和指標類似,他們都依附一個原始型態<type>

宣告的方式也很像:<type>& name / <type> &name

透過剛剛的實作可以發現

指標其中一大用途便是透過傳遞位址

讓函式得以透過位址找到原始變數

進而實現原始變數的讀取與改動

不過剛剛那樣要傳地址進去

要使用變數還得先取值

有時候函式內只會對值操作

每次又要 & 又要 * 其實不太方便

int a;
int &ref = a;

我們直接把 a 賦值給 reference

不需要加任何取值或取址運算子

Reference

1

  What is reference?

int a;
int &ref = a;

參考就像給變數取個外號

而不像一般的變數只是互相賦值

參考 (ref) 和賦值給他的原始變數 (a) 綁定

參考 (ref) 帶著原始變數 (a) 的值

而對參考 (ref) 改動同時也會在原始變數 (a) 生效

這是楊晉宇

&養金魚 = 楊晉宇

&楊進與 = 楊晉宇

&老闆 = 楊晉宇

之後對老闆、養金魚、楊進與、行政 PM

操作與取值,都會連動影響到楊晉宇本人

Reference

1

  What is reference?

int a = 1;
int &ref = a; //ref = a = 1
a = 2; //ref = a = 2
ref = 3; //ref = a = 3

你也可以想成參考是把指標的取值取址行為包起來

紀錄地址並對值操作的一種變數型別

「自從學會參考後,我考試都考 100 分」

Reference

1

  What is reference?

2

  Pass by reference

既然參考是把指標包裝起來的操作

他當然也可以幫忙把變數送進函式,在可視域外對記憶體操作

void swap(int* a, int* b){
  	int tmp = *a;
  	*a = *b;
	*b = tmp;
}
//...
swap(&x, &y); //記得要傳位址

Pass by pointer

void swap(int &a, int &b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
swap(x, y); //傳本人就好

Pass by reference

void swap(int a, int b){
  	int tmp = a;
  	a = b;
	b = tmp;
}
//...
swap(x, y);

Pass by value

Pass by reference 可以直接對變數操作

函式本體幾乎跟 Pass by value 一樣

寫起來更直覺也更精煉!

Reference

2

  Pass by reference

3

  陣列遍歷

之前迴圈放在補充教材,現在可以來好好講講了

動態配置記憶體

CHECK POINT

動態配置記憶體

動態配置記憶體

1

  Why Pointer?

有了前兩堂課的舖墊,不難看出指標是為了配合系統底層記憶體運作而設計的操作系統

但之前縱使沒有指標的陪伴,我們依舊完好的度過了半個學期

唯一有差的 Function Scope 有 Reference 來幫我們解決,甚至比指標來得更直覺

在都用 Vibe Coding 的 2026,有什麼非學指標不可的理由?

實務上,部份高階語言更向開發者隱藏了指標

想解決這個疑惑

我們不妨看看為什麼需要指標

動態配置記憶體

1

  Why Pointer?

用一般變數又有什麼問題嗎?

試想一個情境

你正在開發一個社群軟體

一個帳號有基本資訊與不同數量的貼文

class account{
  string id, handle;
  string post[???];
};

一個帳號的貼文陣列要開多大?

每個帳號的貼文數量是隨時間變動的

傳統的 Array 宣告便固定了陣列大小

宣告太小有人不夠用

宣告太大也沒那麼多空間(記憶體好貴

甚至......你帳號數量也得在一開始決定好

動態配置記憶體

1

  Why Pointer?

用一般變數又有什麼問題嗎?

我們剛剛都從一般變數的角度思考

區域變數在記憶體分配上放在 stack 的區域

如同 STL stack 的結構是一層一層往上疊的

(因為一般變數生命週期取決於大括號

 且大括號需要括號匹配的性質

因此一般變數宣告時決定的大小就會佔據記憶體

直到他的生命週期結束

我們需要更彈性的方法維護資料

也就是動態配置記憶體

3

  (C++) new / delete

動態配置記憶體

1

  Why Pointer?

2

  動態配置記憶體?

所謂動態配置記憶體

是程式主動向系統索取記憶體使用

並且適時歸還

不同於一般變數

我們可以自己決定索取與歸還的時間

透過好好管理索取與釋放的時機

就能最大化利用空間

1

  Why Pointer?

3

  (C++) new / delete

動態配置記憶體

3

  new / delete

2

  動態配置記憶體?

<type> *ptr = new <type>;
<type> &ref1 = new <type>; //CE
<type> &ref2 = *(new <type>);

new 運算子

new 加在變數型別前

會向系統索取一塊記憶體

並回傳該記憶體位址

透過 new 得到的記憶體位址

我們便可以用指標操作他們!

甚至也可以用參考把「對位址取值」取別名

進一步變得像使用一般變數一樣

(注意:參考不能直接 = 記憶體位址)

動態配置記憶體

3

  new / delete

2

  動態配置記憶體?

int *ptr2 = new int(342);

int *arr1 = new int[3];
int *arr2 = new int[5]{1, 2, 3, 4, 5};
int *arr3 = new int[10]{};

new 運算子

new 加在變數型別前

會向系統索取一塊記憶體

並回傳該記憶體位址

new 也支援許多特殊操作

  • (value):初始化記憶體所存的值
  • [n]:索取存放 n 個對應變數的連續記憶體,以將指標作為陣列使用
  • [n]{...}:像初始化陣列一樣初始化記憶體內的值

動態配置記憶體

3

  new / delete

2

  動態配置記憶體?

delete 加在指標前

釋放該記憶體位置歸還給系統

讓該記憶體位址失效

delete 運算子

int *ptr = new int;
delete ptr;
int arr* = new int[];
delete [] arr;

delete 會把記憶體歸還給系統

該記憶體位址就會失效

要注意的是使用 new [] 索取的連續記憶體

必須使用 delete [] 歸還

否則系統只會回收第一項元素

若是 new 索取的記憶體位址遺失

便沒有辦法歸還這份記憶體

記憶體洩漏(Memory Leak)

只能整個程序結束再統一回收

動態配置記憶體

3

  new / delete

2

  動態配置記憶體?

#include <iostream>
using namespace std;
int main(){
  int *ptr = new int(10);
  delete ptr;
  cout << *ptr ;
}

在歸還記憶體後,該位址就交回系統掌控

也就無法預測裡面放著什麼

這時去操作他也是非常危險的行為

4

  動態配置陣列

動態配置記憶體

4

  動態配置陣列

剛剛我們學到可以利用 new <type>[size] 來動態配置一維陣列

那更高維度的陣列怎麼辦?

再拿二維陣列舉例

如果需要達成這樣的效果,我們希望 a[0], a[1], a[2] 都是陣列

剛剛我們提到在一般宣告中,a 會是指向 int[4] 的指標

也就是 int*[4]

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

3

  new / delete

動態配置記憶體

3

  (C++) new / delete

4

  動態配置陣列

如果需要達成這樣的效果,我們希望 a[0], a[1], a[2] 都是陣列

剛剛我們提到在一般宣告中,a 會是指向 int[4] 的指標

也就是 int*[4]

但之所以會是這樣,是因為宣告的時候系統是取連續的記憶體

也為了維護 pointer 地址平移的性質

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

仔細想想,陣列可以當作指標,而原型中是指向陣列的指標

其實我們需要的是指向指標的指標

動態配置記憶體

3

  (C++) new / delete

4

  動態配置陣列

a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]

int* a[0]

int* a[1]

int* a[2]

int** a

int** a = new int*[3]; // a is an array of 3 int pointer
for(int i = 0; i < 3; i++)
	a[i] = new int[4]; //a[i] is an array of 4 int

如此一來雖然得到的不是連續記憶體

我們仍然利用指標的存取規則構造出了多維陣列

動態配置記憶體

3

  (C++) new / delete

4

  動態配置陣列

a[0][0] a[0][1] a[0][2]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3] a[2][4]

int* a[0]

int* a[1]

int* a[2]

int** a

int** a = new int*[3]; // a is an array of 3 int pointer
a[0] = new int[3]; //a[0] is an array of 3 int
a[1] = new int[4]; //a[1] is an array of 4 int
a[2] = new int[5]; //a[2] is an array of 5 int

此外我們也能根據需求

調整每個指標元素指向的陣列大小

動態配置記憶體

3

  (C++) new / delete

4

  動態配置陣列

int** a = new int*[3]; // a is an array of 3 int pointer
a[0] = new int[3]; //a[0] is an array of 3 int
a[1] = new int[4]; //a[1] is an array of 4 int
a[2] = new int[5]; //a[2] is an array of 5 int

不過在歸還記憶體時較記得從最小的開始

由內往外慢慢刪掉

for(int i = 0; i < 3; i++) delete [] a[i];
delete [] a;

OOP with pointer

CHECK POINT

OOP with pointer

OOP with pointer

1

  struct / class with pointer / reference

剛剛在談到指標(與參考)的時候一個很重要的觀念就是他們的型別

指標必須宣告為指向 <type> 的指標(同理參考是宣告為 <type> 的參考)

而這裡的 <type> 並不限於內建的型別,也包括我們自己定義的 struct 與 class

struct S{/*...*/};
class C{/*...*/};

S s1;
C c1;
S *sptr = &(s1);
C *cptr = &(c1);
S &sref = s1;
C &cref = c1;

由於參考是取外號

操作跟使用 struct / class 本體一樣

那指標呢?

OOP with pointer

1

  struct / class with pointer / reference

struct Student{
	int id;
	string name;
};
//...
Student* S1 = new Student;
(*S1).id = 1024;
(*S1).name = "I want to SLEEP";

直覺上,對指標取值可以得到結構本身

這時再透過 .member 就能取得裡面的變數與函式

但這樣寫顯然很冗

struct Student{
	int id;
	string name;
};
//...
Student* S1 = new Student;
S1 -> id = 1024;
S1 -> name = "I want to SLEEP";

C/C++ 中有一種特別的訪問運算子:-> member

協助指標訪問成員

我們可以直接在指標後加上 -> member

來達到存取該成員的效果

與 (*ptr).member 等價

OOP with pointer

1

  struct / class pointer / reference

2

  class with dynamic memory allocation

其實有了參考以後,指標好像就沒必要了......嗎?

剛剛的動態配置記憶體中,我們透過 new / delete 產生的都是指標

並且透過 -> 來存取省去很多惱人的取值 / 取址

如果我們從頭到尾都動態配置記憶體

換句話說:你操作的一直都是指標

那麼實際用起來跟宣告一般的 class 變數沒什麼不同

OOP with pointer

1

  struct / class pointer / reference

2

  class with dynamic memory allocation

在 class 之中我們可以定義虛擬函式 (virtual)

來讓有繼承關係的物件們通用函式

也就是上節課講的多型 (Polymorphism)

上禮拜因為還沒教到指標暫時使用參考

但在動態配置後透過指標可以更靈活的操作物件

OOP with pointer

2

  class with dynamic memory allocation

void what_does_it_say(Animal* a){
	a -> say();
}

int main(){
	Animal* zoo[2];
	zoo[0] = new Dog();
    zoo[1] = new Fox();
    what_does_it_say(zoo[0]);
    //> Bark!
    what_does_it_say(zoo[1]);
    //> Gering-ding-ding-ding-dingeringeding!
}

宣告一個父類別的指標陣列

其元素 (Animal*) 便可利用 new 來動態配置

並且可以 new 子類別

實現在同一個陣列裡放入不同的物件

用指標實作 virtual 函式的外部呼叫

也能利用父類別參數達成的多型

而不需要為每個子類別撰寫函式

OOP with pointer

2

  class with dynamic memory allocation

class Animal{
public:
	Animal(){cout << "Animal created\n";}
    virtual ~Animal(){cout << "Animal deleted\n";}
};
class Fox: public Animal{
public:
    Fox(){cout << "Fox say hello\n";}
    ~Fox(){cout << "Fox sar goodbye\n";}
};

int main(){
	Animal* a = new Fox();
    //> Animal created
    //> Fox say hello
    delete a;
    //> Fox say goodbye
    //> Animal deleted
}

new / delete 可以幫忙呼叫建構子 / 解構子

要特別注意的是建構是從父到子(先長根再長葉子)

而解構是從子到父(先剪枝葉再除根)

但是函式執行都是先以父親為主

如果有 virtual 再以子類為主

因此建構子的設計不能使用多型

由父到子依序呼叫建構子,先長根再長葉子

解構子則需要利用 virtual 函式形成多型

由外到內一層一層解構

OOP with pointer

2

  class with dynamic memory allocation

3

  class: this

在 class 之中,除了我們宣告的成員以外

還有一個隱形成員:this

this 是一個指向自己的 class pointer

用處就是在 class 裡面定義函式的參數時

可能會用到與成員相同的變數名稱

(或著說另外取一個變數名稱也不直覺)

這時透過 this 指標訪問自己的成員

就能避開變數重複定義的問題

就算名稱不重複也能更清楚在對什麼變數操作

class Animal{
public:
	string name;
    int weight;
	Animal(string name, int weight){
    	this -> name = name;
        this -> weight = weight;
    }
};

Dynamic Array

CHECK POINT

Dynamic Array

Dynamic Array

1

  動態陣列

還記得 STL 教的 vector 嗎?

有很多人學會 vector 以後就不會再用 array 了

那 vector 究竟比 array 好在哪?

  1. 物件化,可以輕鬆賦值
  2. 操作模組化,不用自己維護性質
  3. 支援隨機存取,跟使用 array 的感覺差不多
  4. 動態長度陣列,不用一開始就把長度寫死

Dynamic Array

1

  動態陣列

我們用倍增法來實作動態陣列

只要有新的元素就拓寬一次陣列?太慢了

我們在有新元素加入時,如果陣列大小不夠,就直接開兩倍大

時間複雜度上,一次拓寬需要搬動整個陣列的元素

每次插入都只拓寬一格的總複雜度是 \(O(n^2)\)

倍增法則是 \(O(n)\)

Dynamic Array

1

  動態陣列

我們用倍增法來實作動態陣列

只要有新的元素就拓寬一次陣列?太慢了

我們在有新元素加入時,如果陣列大小不夠,就直接開兩倍大

時間複雜度上,一次拓寬需要搬動整個陣列的元素

每次插入都只拓寬一格的總複雜度是 \(O(n^2)\)

倍增法則是 \(O(n)\)

均攤 \(O(n)\)

均攤\(O(1) \Rightarrow\) 常數時間

Dynamic Array

1

  動態陣列

Exercise

Dynamic Array

1

  動態陣列

還記得 STL 教的 vector 嗎?

有很多人學會 vector 以後就不會再用 array 了

那 vector 究竟比 array 好在哪?

  1. 物件化,可以輕鬆賦值
  2. 操作模組化,不用自己維護性質
  3. 支援隨機存取,跟使用 array 的感覺差不多
  4. 動態長度陣列,不用一開始就把長度寫死

Exercise

class vector {
public:
    vector() {
        buffer = new int[1];
        capacity = 1;
        size = 0;
    }

    // push one item into vector's back
    void push_back(int item);

    // pop one item from vector's back
    void pop_back();

    // return whether the size of vector is zero
    bool empty();

    // return a pointer points to vector's last item
    int last();

private:
    int* buffer;
    int  capacity;
    int  size;
};

Dynamic Array

1

  動態陣列

2

  使用 [] 取值(運算子多載)

在 vector 中我們能直接對物件用 [] 取值,這是為什麼?

多型是 OOP 中重要的特徵

支援不同型別或類別使用同一種操作

我們之前看到的都是函式

而這裡再介紹另外一個:運算子多載

白話文:運算子多載就是在一般的運算子上做多型

在 C++ 中我們可以對 class 重新定義運算子的操作邏輯

進而簡化我們的實作邏輯與語法

Dynamic Array

1

  動態陣列

2

  使用 [] 取值(運算子多載)

在 C++ 中我們可以對 class 重新定義運算子的操作邏輯

進而簡化我們的實作邏輯與語法

class vector{
private:
	int* arr_;
    //...
public:
	//...
	int& operator[](size_t id){
    	return arr_[id];
    }
};

透過回傳該位值的參考

我們就能在外部對該索引值的變數進行存取與修改

Dynamic Array

2

  使用 [] 取值(運算子多載)

3

  Template

STL 是 Standard Template Library,提供了許多所有型別都能使用的容器

對於容器而言,型別不會改變其實作方式

但照之前學過的,我們可能得分開實作 int_vector, char_vector, ll_vector ......

template <typename T>
T getMax(T a, T b){
  return (a > b) ? a : b;
}

template 可以定義一個彈性變數型別

加在要使用的函式前

讓類似的函式實作不再需要分類別

template <typename T>
class vector<T>{
private:
  T *buffer;
  int capacity, size;
public:
  vector(T size);
};

同理也能套用在 struct, class 上

並用 struct/class name<type> 來使用之

e.g. vector<int> vec;

Linked list

CHECK POINT

Made with Slides.com