指標 I - 指標與參考
指標 II - 應用
Pointer & Reference
臣亮言 @ NTU CSIE
May 16 & 29, 2026
Sprout 資訊之芽北區 C++ 班
在開始之前......
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
陣列與指標
Pointer〉
5
陣列與指標
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
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
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
陣列遍歷
之前迴圈放在補充教材,現在可以來好好講講了
動態配置記憶體
動態配置記憶體
動態配置記憶體〉
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 也支援許多特殊操作
動態配置記憶體〉
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
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
Dynamic Array
Dynamic Array〉
1
動態陣列
還記得 STL 教的 vector 嗎?
有很多人學會 vector 以後就不會再用 array 了
那 vector 究竟比 array 好在哪?
Dynamic Array〉
1
動態陣列
我們用倍增法來實作動態陣列
只要有新的元素就拓寬一次陣列?太慢了
我們在有新元素加入時,如果陣列大小不夠,就直接開兩倍大
時間複雜度上,一次拓寬需要搬動整個陣列的元素
每次插入都只拓寬一格的總複雜度是 \(O(n^2)\)
倍增法則是 \(O(n)\)
Dynamic Array〉
1
動態陣列
我們用倍增法來實作動態陣列
只要有新的元素就拓寬一次陣列?太慢了
我們在有新元素加入時,如果陣列大小不夠,就直接開兩倍大
時間複雜度上,一次拓寬需要搬動整個陣列的元素
每次插入都只拓寬一格的總複雜度是 \(O(n^2)\)
倍增法則是 \(O(n)\)
均攤 \(O(n)\)
均攤\(O(1) \Rightarrow\) 常數時間
Dynamic Array〉
1
動態陣列
Dynamic Array〉
1
動態陣列
還記得 STL 教的 vector 嗎?
有很多人學會 vector 以後就不會再用 array 了
那 vector 究竟比 array 好在哪?
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