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