Dynamic Arrays
黃祥陞 @ Sprout 2023 C/C++語法班
Modified from @ Sprout 2022
什麼是動態陣列?
Static Arrays
問:請幫我建立一個陣列,裡面放 0 到 9
答:...
int arr[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
問:再幫我加 10 到 19 進去好了
答:
/* same code above */
for (int i = 10; i < 20; i++) {
arr[i] = i; // ??
}
Static Arrays
沒辦法在寫程式時知道陣列要多大,怎麼辦?
int n;
std::cin >> n;
int arr[n];
這樣有什麼問題?
試試看輸入 100000000
[3] 70787 segmentation fault
Why?
簡單了解 stack 與 heap
小結論:一般程式執行時用 stack 為主,但
stack 的空間比較小
Conclusion
int n;
std::cin >> n;
int arr[n];
動態記憶體(Heap)
Heap Memory
用 new 來跟 heap 要一塊記憶體
用 delete 來把記憶體還給 heap
int* ptr = new int; // get memory from heap
// we can use the memory now
*ptr = 3;
std::cout << *ptr << '\n';
delete ptr; // return memory to heap
ptr = nullptr; // why do this?
Heap Memory Explained
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
圖示解說(記憶體位置純數虛構)
0x10
0x18
0x20
0x28
0x30
0x38
Heap Memory Explained
int* ptr = new int;
0xa0 |
---|
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
0x10(ptr)
0x18
0x20
0x28
0x30
0x38
Heap Memory Explained
delete ptr;
0xa0 |
---|
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
0x10(ptr)
0x18
0x20
0x28
0x30
0x38
Heap Memory Explained
ptr = nullptr;
nullptr |
---|
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
0x10(ptr)
0x18
0x20
0x28
0x30
0x38
Dangling Pointer
dangling pointer: 指向已經還給 heap 的記憶體的 pointer
int* ptr = new int;
*ptr = 7;
delete ptr; // now ptr is a dangling pointer
std::cout << *ptr; // undefined behavior
delete ptr; // undefined behavior
謎之聲:還不簡單?delete 之後就不要再用就好啦!
使用注意事項之一
Dangling Pointer
dangling pointer 之沒這麼簡單
int* ptr = new int;
*ptr = 7;
int* ptr2 = ptr;
delete ptr; // now ptr is a dangling pointer
ptr = nullptr; // ptr is not dangling anymore
std::cout << *ptr2; // but ptr2 is dangling
delete ptr2; // double deletion here
使用動態記憶體時要特別小心!
Memory Leak
memory leak: 沒有還給 heap 的記憶體
int n = 5;
int* ptr = new int;
ptr = &n;
使用注意事項之二
有什麼問題??
Memory Leak Explained
5 |
---|
0x10(n)
0x14
0x1c
0x24
0x2c
0x34
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
int n = 5;
Memory Leak Explained
5 | 0xa0 |
---|
0x10(n)
0x14(ptr)
0x1c
0x24
0x2c
0x34
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
int* ptr = new int;
Heap Memory Explained
5 | 0x10 |
---|
0x10(n)
0x14(ptr)
0x1c
0x24
0x2c
0x34
0xa0
0xa4
0xa8
0xac
0xb0
0xb4
stack
heap
可用的 heap 記憶體
被佔用的 heap 記憶體
ptr = &n;
Memory Leak
memory leak: 沒有還給 heap 的記憶體
int n = 5;
int* ptr = new int;
ptr = &n;
使用注意事項之二
從 heap 要來的記憶體(上頁的 0xa0)沒有還回去,
也還不回去了(想想看為什麼)
註:當程式結束執行後記憶體還是會被系統回收,但是執行期間這支程式會一直佔據這塊還不回去的記憶體
Memory Leak
memory leak 之另一個例子
void leak_memory() {
int* ptr = new int;
}
int main() {
leak_memory();
}
使用注意事項之二
有看出哪裡 leak 了嗎?:)
小結論:有借有還,再借不難
Heap & Arrays
new 出一個新陣列
int* arr = new int[10]; // get an array from heap
// use it like a normal array
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
delete[] arr; // release the memory
注意:陣列的 delete 後面要加上 []
題外話之想想看:delete 沒有寫大小,程式怎麼知道要還多少回去?
小複習:arr[i] 相當於 *(arr + i) 哦
Heap & Arrays
new 出一個新陣列
int n;
std::cin >> n;
int* arr = new int[n]; // get an array from heap
delete[] arr; // release the memory
這時候用變數就沒什麼問題囉!
再試試看 10000000 應該不會有問題了
小實驗:heap 通常比 stack 大上不少,試試看 n 可以到多大才會出現空間不夠的錯誤?
Heap & 2D-Arrays
進階:動態二維陣列
int n, m;
std::cin >> n >> m;
int** arr = new int*[n];
for (int i = 0; i < n; i++) {
arr[i] = new int[m];
}
// now we can use arr as an array of size (n * m)
arr[0][0] = 0; // like this
for (int i = 0; i < n; i++) {
delete[] arr[i]; // notice we delete arr[i] first
}
delete[] arr; // and then delete arr
Stack v.s. Heap
Heap 的記憶體在 delete 之前都一直可以用
int* stack_memory() {
int n = 5;
return &n;
}
int* heap_memory() {
int* ptr = new int;
*ptr = 5;
return ptr;
}
int main() {
int* s_ptr = stack_memory(); // NOT OK!
int* h_ptr = heap_memory(); // OK!
delete h_ptr; // remember to return memory
return 0;
}
- Stack: 在 function 結束後記憶體自動回收
- Heap: 呼叫 delete 之前整個程式都可以用
Conclusion
- 當你需要大量記憶體,或是寫程式時不知道會需要多少,就使用動態記憶體
- 用 new 來取得記憶體,用 delete 來把記憶體還回去。有 new 就要有 delete!
- 使用動態記憶體時,特別注意 dangling pointer 以及 memory leak 的問題
謎之聲:好像教完動態陣列了?
才沒有,這只是小試身手!
Class 基本介紹
小回顧
先來回想一下 Struct
Struct 讓我們可以把許多資料有意義地組合起來
Class 語法
Class 怎麼做?
struct Shop {
int pos_x;
int pos_y;
bool type;
int net_income;
};
class Shop {
int pos_x;
int pos_y;
bool type;
int net_income;
};
第一個 Class 誕生了
Class 的語法其實跟 Struct 一模一樣,兩個可以互相轉換
Class 語法
member functions
除了資料之外,Class 也可以有 member function
#include <cmath>
class Shop {
int pos_x;
int pos_y;
bool type;
int net_income;
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
Class 語法
玩玩看 Class
#include <iostream>
/* Definition of Shop Class here */
int main() {
Shop shop;
shop.pos_x = 10;
shop.pos_y = 20;
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
Class 語法
main.cpp:17:10: error: 'pos_x' is a private member of 'Shop'
shop.pos_x = 10;
^
main.cpp:5:7: note: implicitly declared private here
int pos_x;
^
main.cpp:18:10: error: 'pos_y' is a private member of 'Shop'
shop.pos_y = 20;
^
main.cpp:6:7: note: implicitly declared private here
int pos_y;
^
main.cpp:19:23: error: 'DistanceToOrigin' is a private member of 'Shop'
std::cout << shop.DistanceToOrigin() << '\n';
^
main.cpp:10:10: note: implicitly declared private here
double DistanceToOrigin() {
^
3 errors generated.
編譯失敗... 為什麼??
Private v.s. Public
Class 的 member variable 跟 function 可以是 public 或 private
- private: 只有這個 Class 自己的 member function 可以存取
- public: 所有人、所有地方都可以存取
Class 預設所有東西都是 private
Private v.s. Public
private vs public
回頭看一次剛剛的 main
#include <iostream>
/* Definition of Shop Class here */
int main() {
Shop shop;
shop.pos_x = 10;
shop.pos_y = 20;
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
main 並不是 Shop 的 member function,所以不能存取 Shop 的 private member
Private v.s. Public
設定 private/public
#include <cmath>
class Shop {
private:
bool type;
int net_income;
int pos_x;
int pos_y;
public:
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
Conclusion
- Class 與 Struct 語法一樣,唯一的差別在於 Class 預設 member 都是 private;Struct 預設為 public
- 建議不要把 Class 跟 Struct 混著用,Struct 的功能主要是資料導向,避免使用 member function
- Class 是 Object-Oriented Programming (OOP) 的基石,水很深
一般 C++ Program: 以 function 為基礎
char s[10] = "hello";
std::cout << strlen(s) << '\n';
OOP C++ Program: 以物件為基礎
std::string s = "hello";
std::cout << s.length() << '\n';
Constructor
Constructor 是一個特別的 function
- 沒有 return type
- 名字必須與 Class 名稱一模一樣
- 在物件被建立時自動呼叫
- 必須是 public
class Shop {
private:
bool type;
int net_income;
int pos_x;
int pos_y;
public:
Shop() {
std::cout << "A shop is created\n";
}
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
Constructor
試試看
int main() {
Shop shop; // create a shop object
return 0;
}
Output:
A shop is created
Constructor
初始化物件
Constructor 最常見的用途是用來初始化這個物件,例如 member variable
class Shop {
private:
bool type;
int net_income;
int pos_x;
int pos_y;
public:
Shop(int x, int y) {
pos_x = x;
pos_y = y;
net_income = 10000;
type = false;
}
double DistanceToOrigin() {
return sqrt(pos_x*pos_x + pos_y*pos_y);
}
};
Constructor
試試看
int main() {
Shop shop(3, 4); // create a shop object
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
Output:
5
Constructor
原本的 constructor 呢?
int main() {
Shop shop; // create a shop object
std::cout << shop.DistanceToOrigin() << '\n';
return 0;
}
Output:
main.cpp: In function 'int main()':
main.cpp:23:10: error: no matching function for call to 'Shop::Shop()'
23 | Shop shop;
| ^~~~
Constructor
發生什麼事?
當你沒有定義 constructor 時,C++ 會自動提供 default constructor,不吃任何參數
當你定義了自己的 constructor 之後,C++ 就不再提供 default constructor 了
你可以定義很多 constructor,但他們的參數型態必須不同
Constructor
Dive Deeper
如果 class 裡有 const 該怎麼初始化呢?
class Shop {
private:
const int pos_x;
const int pos_y;
public:
Shop(int x, int y) {
pos_x = x;
pos_y = y;
}
};
試著執行看看!
Constructor
為什麼編譯錯誤?
上頁的作法其實類似於以下的 code
int x = 0;
int y = 0;
const int pos_x;
const int pos_y;
pos_x = x;
pos_y = y;
但是 const 變數一旦宣告之後就不能更改了!
那麼應該怎麼做呢?
Constructor
我們想要達成以下的效果
int x = 0;
int y = 0;
const int pos_x = x;
const int pos_y = y;
class Shop {
private:
const int pos_x;
const int pos_y;
public:
Shop(int x, int y) : pos_x(x), pos_y(y) {}
};
Constructor 可以這樣寫
通常建議用第二種做法
Destructor
Destructor 是一個特別的 function
- 沒有 return type,不接受參數
- 名字必須與 Class 名稱一模一樣,前面加上 ~
- 在物件要消失(例如 out of scope)時自動呼叫,通常用來清理資源
- 必須是 public
class Shop {
private:
int pos_x;
int pos_y;
public:
~Shop() {
std::cout << "We are closed.\n";
}
};
Destructor
何時被呼叫?
void out_of_scope() {
Shop shop;
} // will call destructor
int main() {
out_of_scope();
return 0;
}
Out of Scope
delete
void free_from_heap() {
auto new_shop = new Shop;
delete new_shop; // will call destructor
}
int main() {
free_from_heap();
return 0;
}
Lab: Dynamic Arrays
目標
1. 支援不同型態
DynamicArray<int> int_arr;
DynamicArray<std::string> string_arr;
2. 把新的元素加到最後面
DynamicArray<int> arr; // empty
arr.push_back(0); // 0
arr.push_back(1); // 0 1
3. 像一般陣列一樣存取元素
DynamicArray<int> arr;
arr.push_back(0);
std::cout << arr[0]; // print 0
arr[0] = 1;
std::cout << arr[0]; // print 1
4. 取得陣列裡有幾個元素
DynamicArray<int> arr;
std::cout << arr.size(); // print 0
先備知識
- Class:用來作為 Dynamic Array 的基礎,提供必要的 member function 來提供操作介面
- 動態記憶體:動態陣列的功能必須有動態記體的支援才能達成
- Template:使 Dynamic Array 可以支援各種不同型別
Class 架構
class DynamicArray {
size_t size_;
size_t capacity_;
int* arr_;
public:
DynamicArray();
size_t size();
void push_back(int value);
~DynamicArray();
};
先以 int 為例
Class 架構
設計說明
- 常見的習慣是把 member variable 設為 private 不讓外界直接存取;但提供 public member function 讓外界操作
- private member variable 通常會在名字裡加上底線 _,有人習慣加在前面,有人習慣加在後面
- member variable:
- capacity_: 用來記錄現在的陣列容量有多少,用滿之後容量必須「長大」才能繼續用
- size_: 用來記錄現在實際裝了多少元素
- arr_: 內部實際上用來裝元素的陣列。記憶體從 heap 而來
Constructor
物件被建立時,需要做些什麼?
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
說明:
容量從 1 開始
Constructor
物件被建立時,需要做些什麼?
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
說明:
一開始沒有任何元素
Constructor
物件被建立時,需要做些什麼?
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
說明:
實際取得 capacity_ 大小的空間
Push Back
void push_back(int value) {
arr_[size_++] = value;
}
一般情況下沒有問題,但如果 arr_ 的空間不夠了呢?
從 heap 拿更多的空間,但要拿多少呢?
常見的做法是拿一塊相當於現在容量 1.5 或是 2 倍的空間。這裡我們使用 2 倍
Grow
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
取得一塊更大的空間
Grow
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
把舊的元素搬到新的空間裡
Grow
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
把舊的空間還給 heap
Grow
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
把 arr_ 指向新的空間
Grow
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
說明:
更新現在的容量
Push Back + Grow
void push_back(int value) {
if (size_ == capacity_) {
grow();
}
arr_[size_++] = value;
}
動態陣列最重要的功能:在空間不夠時自動增長,使用者只需要像操作一般陣列就好
喘一口氣
class DynamicArray {
size_t size_;
size_t capacity_;
int* arr_;
const int growth_rate_ = 2;
void grow() {
int* temp = new int[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
public:
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new int[capacity_];
}
size_t size();
void push_back(int value) {
if (size_ == capacity_) {
grow();
}
arr_[size_++] = value;
}
~DynamicArray();
};
Size
最簡單的啦!
size_t size() {
return size_;
}
看起來好冗,為什麼不直接把 size_ 變成 public?
當 size_ 變成 public 時,外界就可以自由操作,例如
DynamicArray arr; // empty
arr.size_ = 2; // messing up
維持 member variable 是 private 確保只有自己可以去更改
[] Operator
要如何用 [] 來操作動態陣列?
[] 其實是一個 operator,跟 + 或是 = 一樣
我們需要提供 [] 的操作,稱為 operator overloading
直接上語法
int& operator [](int index) {
return arr_[index];
}
現在跟一般陣列一樣可以用 [] 來存取元素囉
Destructor
在消失前,還得先善後
~DynamicArray() {
delete[] arr_;
}
永遠記得要把借來的記憶體還回去
支援多型別
現在加入 template 來支援不同
template <typename T>
class DynamicArray {
size_t capacity_;
size_t size_;
const int growth_rate_ = 2;
T* arr_;
// ...
支援多型別
現在加入 template 來支援不同
void grow() {
T* temp = new T[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
T& operator [](int index) { return arr_[index]; }
void push_back(T value) {
if (size_ == capacity_) {
// ...
整合
template <typename T>
class DynamicArray {
size_t capacity_;
size_t size_;
const int growth_rate_ = 2;
T* arr_;
void grow() {
T* temp = new T[growth_rate_ * capacity_];
for (int i = 0; i < size_; i++) {
temp[i] = arr_[i];
}
delete[] arr_;
arr_ = temp;
capacity_ *= growth_rate_;
}
public:
DynamicArray() {
capacity_ = 1;
size_ = 0;
arr_ = new T[capacity_];
}
T& operator [](int index) { return arr_[index]; }
size_t size() { return size_; }
size_t capacity() { return capacity_; }
void push_back(T value) {
if (size_ == capacity_) {
grow();
}
arr_[size_++] = value;
}
~DynamicArray() { delete[] arr_; }
};
成品
整合
int main() {
DynamicArray<int> arr;
for (int i = 0; i < 10; i++) {
arr.push_back(i);
}
for (int i = 0; i < arr.size(); i++) {
arr[i]++;
std::cout << arr[i] << ' ';
}
return 0;
}
登登~動態陣列
Output
1 2 3 4 5 6 7 8 9 10
Sprout 2023 Dynamic Arrays
By gtcoding
Sprout 2023 Dynamic Arrays
- 163