20230520

盧冠綸 @ Sprout 2023

實作動態陣列

大綱

上半堂:                              下半堂:

0.  動態陣列的概念              4.  實作動態陣列

1.  (複習) template

2.  (複習) class

3.  new / delete

 

不定期出現:

5.  一些爛爛的冷笑話

 

提醒

這堂課下半堂會讓大家從無到有,從開始到放棄,動手寫出一個動態陣列。

上半堂的部分,請一定要弄清楚概念!

 

 

註:冷笑話會不定期出現,但不會放在投影片裡。

動態陣列概念介紹

今天的課程是要幹嘛?

不知道大家是否有這種經驗...

 

題目:測資大小在 1000 以下。

我:開個大小為 1000 的陣列。

 

題目:測資大小在 1000000 以下。

我:開個大小為 1000000 的陣列。

idea

 

題目:測資大小在 1000000000 以下。

我:開個大小為 1000000000 的陣列。

程式:segmentation fault !!!

 

題目:測資大小未知。

我:

idea

此時,普通的陣列可能不再能滿足你的需求。

我們需要「動態陣列」!

動態陣列

動態陣列在做的事情:

你需要多少空間,就開多少空間給你,

你不需要的空間,就把它還回去。

動態陣列的優點:

優缺點

  • 有多少元素才用多少空間
  • 可以超大 (1,0000,0000 以上也不會爆) 

動態陣列的缺點:

  • 跑得比較慢(等等告訴大家為什麼)

今天要讓大家動手,運用至今為止所學,

從零「設計」出一個動態陣列。

課程目標

  • 支援不同變數型態
  • 在陣列最後追加一個元素 / 移除一個元素
  • 存取陣列的第 i 個元素
  • 求出陣列現在有幾個元素

這個動態陣列會有以下功能:

有用過 STL 的 vector 嗎?(沒的話,下週會教)

今天就是要告訴你,vector 是怎麼實作出來的哦!!

小補充

(複習) template

讓你把一個函式套用多種型態的好東西

當講師叫你寫一個函式,來把兩個 int 相加:

idea

int add(int a, int b){
    return a+b;
}

簡單!

結果講師又跟你說,請新增 double 加法的功能。

idea

double add(double a, double b){
    return a+b;
}

把 int 的程式複製貼上改成 double,依舊簡單!

idea

這時台上這位超機車的講師又跟你說,

再新增 float、long long int、string ... 的加法功能。

於是你就......複製貼上,複製貼上,複製貼上,...

總算完成了講師的要求。

idea

機車的講師:

        欸抱歉我搞錯了,我希望你們做的是「相減」,

        不是「相加」啦!麻煩幫我全部重新修改一下!

你:

template

只要學會 template,就算遇到再機車的講師也不怕!

template <typename T>
T add(T a, T b){
    return a+b;
}

用上面這個函式,就能夠應付以下所有情況:

std::cout << add<int>(9,9) << std::endl;
std::cout << add<double>(4.5,9.1) << std::endl;
std::cout << add<char>('A',' ') << std::endl;
std::string a = "Hello", b = "World";
std::cout << add<std::string>(a,b) << std::endl;

template

當然,typename 可以不只一個。

template <typename T1, typename T2>
T1 add(T2 a, T2 b){
    return a+b;
}

你就可以餵很多奇怪的東西給這個函式 (X

std::cout << add<int, int>(-1,-1) << std::endl;
std::cout << add<unsigned int, int>(-1,-1) << std::endl;
std::cout << add<int, double>(2.1,2.1) << std::endl;
std::cout << add<char, int>(65,1) << std::endl;

template

甚至,struct 或 class 也可以使用 template。

#include <iostream>

template<typename T1, typename T2>
struct pair {
    T1 data1;
    T2 data2;
};

int main(){
    pair<int, double> a;
    a.data1 = 3;
    a.data2 = 5.6;
    pair<char, bool> b;
    b.data1 = 'n';
    b.data2 = false;
}

(複習)class 1

語法、public / private

何謂類別 (class)

類別 (class) 是將資料以及函數組織在一起的方法。

 

 

class 和 struct 的差別,在於 struct 比較偏「資料導向」,而 class 比較偏「物件導向」。

struct 語法

今天,如果我想用 C++ 存資芽二階每個同學的資料。

只是想記資料的話,我可能會想用 struct 來存。

struct Student{
    int homework; // 作業成績
    int project;  // 階段考成績
    int attend;   // 出席成績
};

class 語法

但是,class 除了資料 (data member) 之外,

還可以放一些函數 (member function),讓我們可以對這個 class 做一些操作。

class Student{
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
    double FinalScore(){ // 總成績
        return 0.7 * homework + 0.2 * project + 0.1 * attend;
    }
    void TellJoke(){ // 課堂上說笑話的話出席分加 50 分
        attend += 50;
    }
};

public / private

在 class 的所有 member 中,分為 public 和 private。

 

public:所有人、所有地方都可以存取。

private:只有這個 class 的 member function 可以存取。

 

 

重要:class 的成員都預設為 private。

public / private

所以,如果程式這樣寫的話會出錯。

#include <iostream>

class Student{
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
};

int main(){
    Student a;
    a.homework = 100;
}

public / private

應該要這樣寫。

#include <iostream>

class Student{
public:
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
};

int main(){
    Student a;
    a.homework = 100;
}

public / private

或者是這樣寫。

#include <iostream>

class Student{
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
public:
    void set_homework(int score){
        homework = score;
    }
};

int main(){
    Student a;
    a.set_homework(100);
}

小結論

  • class 與 struct 語法一樣,不過 class 會預設成員為 private。
  • private 和 public 要設清楚!
  • struct 主要是資料導向, class 主要是物件導向。

(複習)class 2

constructor / destructor

constructor

  • 名字必須與 class 名稱一模一樣。
  • 必定是 public。
  • 沒有 return type。

物件被宣告時會自動執行的 member function。通常用來做初始化。

#include <iostream>

class Student{
public:
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
    Student(){ // constructor
        homework = 100;
        project  =  80;
        attend   =  90;
    }
};

int main(){
    Student a;
    std::cout << a.homework << std::endl;
    std::cout << a.project  << std::endl;
    std::cout << a.attend   << std::endl;
}
100
80
90

例一:

#include <iostream>

class Student{
public:
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
    Student(int a, int b, int c){ // constructor
        homework = a;
        project  = b;
        attend   = c;
    }
};

int main(){
    Student a(90, 85, 80);
    std::cout << a.homework << std::endl;
    std::cout << a.project  << std::endl;
    std::cout << a.attend   << std::endl;
}
90
85
80

例二:

#include <iostream>

class Student{
public:
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
    Student(int a, int b, int c){ // constructor
        homework = a;
        project  = b;
        attend   = c;
    }
    Student(int a){ // constructor
        homework = a;
        project  = 0;
        attend   = 0;
    }
};

int main(){
    Student a(90, 90, 90), b(90); // 某兩位學生,使用不同的 constructor
    std::cout << a.attend << std::endl; // 輸出學生 a 出席成績
    std::cout << b.attend << std::endl; // 輸出學生 b 出席成績
}

constructor overloading

你可以在一個 class 裡面定義很多 constructor,

但他們的參數型態必須不同。

90
0
#include <iostream>

class Student{
public:
    const double homework; // 作業成績
    const double project;  // 階段考成績
    const double attend;   // 出席成績
    Student(int a, int b, int c) : homework(a), project(b), attend(c) {}
};

int main(){
    Student a(90, 85, 80);
    std::cout << a.homework << std::endl;
    std::cout << a.project << std::endl;
    std::cout << a.attend << std::endl;
}

補充:const

如果要初始化的 member data 是 const,先前的 constructor 會造成編譯錯誤。(因為改到 const)

 

這時候,我們的 constructor 就應該用這種寫法。

90
85
80

destructor

  • 名字必須與 class 名稱一模一樣,前面加上~
  • 必定是 public。
  • 沒有參數。
  • 沒有 return type。

當物件被釋放時 (如 out of range 的時候) ,

自動執行的 member function。通常用來清理資源。

destructor

範例:

#include <iostream>

class Student{
public:
    std::string name;
    Student(std::string str){ // constructor
        name = str;
        std::cout << "Hello, " << name << std::endl;
    }
    ~Student(){ // destructor
        std::cout << "Goodbye, " << name << std::endl;
    }
};

int main(){
    Student a("Benson");
}
Hello, Benson
Goodbye, Benson

(複習)class 3

迷你練習題

#include <iostream>

class Student{
private:
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
public:
    Student(int a, int b, int c){ // constructor
        homework = a;
        project  = b;
        attend   = c;
    }
    void TellJoke(){ // 課堂上說笑話的話出席分加 50 分
        attend += 50;
    }
    double FinalScore(){ // 計算總成績
        return 0.7 * homework + 0.2 * project + 0.1 * attend;
    }
};

int main(){
    Student a(90, 90, 40); // 某個學生,初始化時成績長這樣
    a.TellJoke(); // 但這個學生課堂上說了一個笑話
    std::cout << a.FinalScore() << std::endl; // 輸出該學生總成績
}

小練習1

請猜猜看這個程式會輸出什麼?

#include <iostream>

class Student{
public:
    std::string name;
    Student(std::string str){ // constructor
        name = str;
        std::cout << "Hello, " << name << std::endl;
    }
    ~Student(){ // destructor
        std::cout << "Goodbye, " << name << std::endl;
    }
};

void summon(){
    Student b("Ruby");
    return;
}

int main(){
    Student a("Benson");
    summon();
    return 0;
}

小練習2

請猜猜看這個程式會輸出什麼?

#include <iostream>

class Student{
private:
    double homework; // 作業成績
    double project;  // 階段考成績
    double attend;   // 出席成績
public:
    Student(int a, int b, int c){ // constructor
        homework = a;
        project  = b;
        attend   = c;
    }
    double FinalScore(){ // 計算總成績
        return 0.7 * homework + 0.2 * project + 0.1 * attend;
    }
    bool pass(){ // 如果學生的總成績在60以上,就通過(return true),反之沒通過。
        // 請把你的 code 插在這裡
    }
};

小練習3

請試著寫寫看 pass 這個 member function,

來判斷學生是否通過資芽第二階段。

總成績 60 以上則通過,低於 60 則未通過。

#include <iostream>

class Student{
    const double homework; // 作業成績
    const double project;  // 階段考成績
    const double attend;   // 出席成績
    Student(int a, int b, int c){ // constructor
        homework = a;
        project  = b;
        attend   = c;
    }
    double FinalScore(){ // 計算總成績
        return 0.7 * homework + 0.2 * project + 0.1 * attend;
    }
};

int main(){
    Student a(100,100,100);
    std::cout << a.FinalScore() << std::endl;
}

小練習4

小明寫出了如下的 code,但是卻無法編譯。

小明不知道要怎麼修改才能順利編譯,你可以幫他找找這個程式問題出在哪嗎?

以上這些練習題是為了確保你們夠熟悉 class 了。

 

等等的實作環節會大量用到 class 哦!

解答

解答如下:(防劇透所以用黑色字。請用選取文字的方式來看答案。)

小練習1:

小練習2:

                           90

                           Hello, Benson

                           Hello, Ruby

                           Goodbye, Ruby

                           Goodbye, Benson

解答

小練習3:

小練習4:

                       return (FinalScore() >= 60);

                       (1) 因為 FinalScore() 在這裡沒有特別

                             說要 public,故預設是 private,

                             因此不能在 main 被存取。

                       (2) 因為這裡的三種分數都是 const,

                             所以 constructor 不能這樣寫。

new / delete

動態配置記憶體

記憶體配置

先跟大家介紹程式執行時,電腦的記憶體配置方式:

stack:平常 call 函數時,會用 stack 的空間。

              空間是由編譯器來自動分配與收回。

stack 和 heap

heap:在所需記憶體空間難以預測時會用到,

             由寫程式的人隨時手動分配與收回。

idea

但是,stack 並不是那麼的大,

所以沒辦法直接開出太大的陣列。

平常我們所使用的,都是用 stack 的空間。

因此,當我們有可能需要大量空間的時候,

最好是去跟 heap 要。

(也就是等等要講的 new 和 delete)

當你需要空間時,用 new 可以從 heap 中,

配置變數需要的空間。

當你不再需要當初 new 得到的空間時,就使用 delete 來歸還。

new / delete

語法(例一)

#include <iostream>

int main(){
    int *x = new int;
    *x = 20230520;
    std::cout << *x << std::endl;
    delete x;
}

             配置記憶體:     指標變數 = new 資料型別;

             釋放記憶體:     delete 變數名稱;

20230520

語法(例二)

#include <iostream>

int main(){
    int *x = new int(20230520);
    std::cout << *x << std::endl;
    delete x;
}

             配置記憶體並初始化:     

20230520

上面例子在 new 之後的記憶體分配,大概如下圖:

int *x = new int(20230520);

那 delete 又是做啥用的呢?

在 delete 過後,就會把當初 new 的那塊空間還回去。

delete x;

delete 可以避免空間的浪費。

語法(例三)

#include <iostream>

int main(){
    int *array = new int[10];
    delete []array;
}

             配置陣列記憶體 與 釋放陣列記憶體:

語法(例四)

#include <iostream>

int main(){
    int *array = new int[3]{2,5,8};
    std::cout << array[0] << std::endl;
    std::cout << array[1] << std::endl;
    std::cout << array[2] << std::endl;
    delete []array;
}

             配置陣列記憶體並初始化:

2
5
8

在例三、例四中,陣列可以開到很大很大。

語法(例五)

             配置二維陣列 與 釋放二維陣列:

#include <iostream>

int main(){
    int **array = new int*[3];
    array[0] = new int[3]{0,1,2};
    array[1] = new int[3]{3,4,5};
    array[2] = new int[3]{6,7,8};
    for (int i=0 ; i<3 ; i++){
        for (int j=0 ; j<3 ; j++){
            std::cout << array[i][j] << " ";
        }
        std::cout << std::endl;
    }
    delete []array[0];
    delete []array[1];
    delete []array[2];
    delete []array;
}
0 1 2 
3 4 5 
6 7 8 
#include <iostream>

int main(){
    int **array = new int*[3];
    array[0] = new int[3]{0,1,2};
    array[1] = new int[3]{3,4,5};
    array[2] = new int[3]{6,7,8};
    std::cout << &array << " " << array << std::endl;
    std::cout << &array[0] << " " << array[0] << std::endl;
    std::cout << &array[1] << " " << array[1] << std::endl;
    std::cout << &array[2] << " " << array[2] << std::endl;
    std::cout << &array[0][0] << " " << array[0][0] << std::endl;
    delete []array[0];
    delete []array[1];
    delete []array[2];
    delete []array;
}
0x7ffee1a14940 0x7fc17e4058c0
0x7fc17e4058c0 0x7fc17e4058e0
0x7fc17e4058c8 0x7fc17e4058f0
0x7fc17e4058d0 0x7fc17e405900
0x7fc17e4058e0 0

範例程式(例五)

             關於這個二維陣列的記憶體...

解釋(例五)

(array)       0x7ffee1a14940 0x7fc17e4058c0
(array[0])    0x7fc17e4058c0 0x7fc17e4058e0
(array[1])    0x7fc17e4058c8 0x7fc17e4058f0
(array[2])    0x7fc17e4058d0 0x7fc17e405900
(array[0][0]) 0x7fc17e4058e0 0

解釋(例五)

為什麼上面的例子,是先 delete array[0]、[1]、[2],

而不是先 delete array?

因為如果先 delete array,

就會變成如右的狀況:

 

這樣再 delete array[0] 時,

就會出問題。

中場休息

實作動態陣列 1

動態陣列的架構

等等會讓大家開始實作。

請確定你已經具備以下先備知識:

先備知識

  • class
  • template
  • new / delete
  • 剛剛講過的所有笑話

我們希望做出的動態陣列,具有以下功能:

目標

  • 支援不同變數型態
  • 在陣列最後追加一個元素 / 移除一個元素
  • 存取陣列的第 i 個元素
  • 求出陣列現在有幾個元素
std::cout << arr.size(); // 印出 2
  • 存取陣列的第 i 個元素
  • 求出陣列現在有幾個元素
  • 支援不同變數型態
  • 在陣列最後追加一個元素 / 移除一個元素
DynamicArray<int>     arr;  // {}
DynamicArray<double>  arr2; // {}
arr.push_back(0);      // {0}
arr.push_back(1);      // {0, 1}
arr.push_back(2);      // {0, 1, 2}
arr.pop_back();        // {0, 1}
std::cout << arr[0];   // 印出 0
std::cout << arr[1];   // 印出 1
arr[0] = 3;            // {3,1}
std::cout << arr[0];   // 印出 3

我們或許會用 class 來完成這個動態陣列的架構

 

以 int 為例,我們的 class 架構可能長這樣...

架構

class DynamicArray {
  int size_;       // 現在有幾個元素
  int capacity_;   // 有多少空間能用
  int *arr_;       // 陣列內容
public:
  DynamicArray();  // constructor
  ~DynamicArray(); // destructor
  void push_back(int value); // 追加元素
  void pop_back();           // 移除元素
  int size();                // 回傳有幾個元素
  // 存取陣列第 i 個元素 (等等會教大家怎麼寫)
};

支援不同型別

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();
  ~DynamicArray(); 
  void push_back(T value);
  void pop_back(); 
  int size();
};

把前面 int 的版本改成使用 template 的版本:

class DynamicArray {
  int size_;
  int capacity_;
  int *arr_; 
public:
  DynamicArray();
  ~DynamicArray(); 
  void push_back(int value);
  void pop_back(); 
  int size();
};

修改前

修改後

動動腦

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();
  ~DynamicArray(); 
  void push_back(T value);
  void pop_back(); 
  int size();
};
  • 如果我們不用 class,而是用 struct 呢?

    這樣有什麼好處跟壞處?

  • *arr_ 如果做成 public 的話有什麼影響?

常見習慣

  • member variable 通常設為 private, 並提供 public member function 讓外界操作。

 

  • private member variable,通常會在名字裡加上底線 _,(如這裡的 *arr_)。

    有些人會加在前面,有些人會加在後面。

目前進度

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();           // TODO: constructor
  ~DynamicArray();          // TODO: destructor
  void push_back(T value);  // TODO: 新增元素
  void pop_back();          // TODO: 移除元素
  int size();               // TODO: 回傳有幾個元素
  // TODO: 存取陣列第 i 個元素
};

實作動態陣列 2

各函式的大綱

想法

在開始動手寫之前,先讓大家釐清一下,

這五個 public member function 在做什麼。

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();
  ~DynamicArray(); 
  void push_back(T value);
  void pop_back(); 
  int size();
};
template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();  // 初始化各個 private member
  ~DynamicArray(); // 把當初 new 下來的資源 delete 回去
  void push_back(T value);
  void pop_back(); 
  int size();
};
  • constructor:初始化三個 private member。
  • destructor  :歸還當初 new 過的資源。  

      (哪個 private member 是需要被歸還的?)

  • push_back():把 value 加到陣列最後。

           (這需要動到哪些 private member?)

           (如果 new 的空間滿了怎麼辦?)

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();
  ~DynamicArray(); 
  void push_back(T value){
    // TODO: if (陣列空間滿了) 再 new 更多空間給 arr_
    // TODO: 把 value 新增到 arr_ 陣列最後
    // TODO: 記得要修正 size_
  }
  void pop_back(); 
  int size();
};
  • pop_back():把最後一個元素移除。

           (這需要動到哪些 private member?)

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray();
  ~DynamicArray(); 
  void push_back(T value);
  void pop_back(); // 記得要修正 size_
  int size(); // 回傳 size_
};
  • size():回傳有幾個元素。

           (這跟哪個 private member 有關?)

目前進度

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
public:
  DynamicArray(){
    // TODO: 初始化三個 private member
  }
  ~DynamicArray(){
    // TODO: 把當初 new 下來的資源 delete 回去
  }
  void push_back(T value){
    // TODO: if (陣列空間滿了) 再 new 更多空間給 arr_
    // TODO: 把 value 新增到 arr_ 陣列最後
    // TODO: 記得要修正 size_
  } 
  void pop_back(){
    // TODO: 記得要修正 size_
  }
  int size(){
    // TODO: return size_
  }
  // TODO: 存取陣列第 i 個元素
};

目前進度與代辦事項(可下拉)

實作動態陣列 3

constructor & push_back()

constructor

constructor 用來初始化 size_、capacity_、arr_。

 

(先別看下一頁)要初始化成多少?

DynamicArray(){
  size_ = // ?
  capacity_ = // ?
  arr_* = // ?
}

constructor

DynamicArray(){
  size_ = 0;
  capacity_ = 1;
  arr_ = new T[capacity_];
}

我的作法:

動動腦:

  • size_         「一定」    要是 0。為什麼?
  • capacity_ 「不一定」要是 1。為什麼?

push_back

這是剛剛討論出,push_back 要做的事情。

怎麼用 size_、capacity_、arr_,來達成:

void push_back(T value){
    // TODO: if (陣列空間滿了) 再 new 更多空間給 arr_
    // TODO: 把 value 新增到 arr_ 陣列最後
    // TODO: 記得要修正 size_
}
  • 判斷空間是否已滿?
  • 把 value 這個元素新增到 arr_ 最後?
  • size_ 應該改成多少?

push_back

這是上面幾個小問題的解法:

void push_back(T value){
  if (size_ == capacity_){ // if (陣列空間滿了)
    grow();                // 再 new 更多空間給 arr_
  }
  arr_[size_] = value;     // 把 value 新增到 arr_ 陣列最後
  size_ ++;                // 修正 size_
}

方便起見,我們再定義一個 grow() 函式,

那 grow() 詳細要怎麼實現呢?

grow

grow() 在做的事,是 new 更多空間給 arr_ 用。

我們可以這樣子寫:

  • new 一個更大的陣列
  • 把 arr_ 陣列的內容,複製到新的大陣列去
  • 修改 capacity_。

grow

  • new 一個更大的陣列
  • 把 arr_ 陣列的內容,複製到新的大陣列去
  • 修改 capacity_。

以下是程式碼:

void grow(){
  int *temp = new T[capacity_+1]; // new 一個更大的陣列
  
  for (int i=0 ; i<size_ ; i++){  // 把 arr_ 陣列的內容,
    temp[i] = arr_[i];            //   複製到新的大陣列去
  }
  
  delete[] arr_;                  // 記得 delete 原本的陣列
  arr_ = temp;
  
  capacity_ += 1;                 // 修改 capacity_
}

grow

常見疑問:

void grow(){
  int *temp = new T[capacity_+1]; // new 一個更大的陣列
  for (int i=0 ; i<size_ ; i++){  // 把 arr_ 陣列的內容,
    temp[i] = arr_[i];            //   複製到新的大陣列去
  }
  delete[] arr_;                  // 記得 delete 原本的陣列
  arr_ = temp;
  capacity_ += 1;                 // 修改 capacity_
}
  • 新的大陣列的大小,一定要是 capacity_+1 嗎?
  • 為什麽是先 delete[] arr_,才做 arr_ = temp?

常見疑問 回覆

其實,比起 capacity_+1,我們更常用 capacity_*2 !

void grow(){
  int *temp = new T[capacity_*2];
  for (int i=0 ; i<size_ ; i++){
    temp[i] = arr_[i];
  }
  delete[] arr_;
  arr_ = temp;
  capacity_ *= 2;
}

為什麼?(可從效率方面去想)

常見疑問 回覆

void grow(){
  int *temp = new T[capacity_*2];
  for (int i=0 ; i<size_ ; i++){
    temp[i] = arr_[i];
  }
  delete[] arr_;
  arr_ = temp;
  capacity_ *= 2;
}

如果你先做 arr_ = temp,才做 delete[] arr_,

那你就會把 temp 的新陣列給 delete 掉...

目前進度

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
  void grow(){
    int *temp = new T[capacity_*2];
    for (int i=0 ; i<size_ ; i++){
      temp[i] = arr_[i];
    }
    delete[] arr_;
    arr_ = temp;
    capacity_ *= 2;
  }
  
public:
  DynamicArray(){
    size_ = 0;
    capacity_ = 1;
    arr_ = new T[capacity_];
  }
  ~DynamicArray(){
    // TODO: 把當初 new 下來的資源 delete 回去
  }
  void push_back(T value){
    if (size_ == capacity_){
      grow();
    }
    arr_[size_] = value;
    size_ ++;
  }
  void pop_back(){
    // TODO: 記得要修正 size_
  }
  int size(){
    // TODO: return size_
  }
  // TODO: 存取陣列第 i 個元素
};

目前進度與代辦事項(可下拉)

加油!剩下的部分就簡單很多了!

實作動態陣列 4

其餘的功能

destructor

~DynamicArray(){
  delete[] arr_;
}

把當初 new 下來的資源通通 delete 掉。

pop_back()

void pop_back(){
  if (size_ > 0) size_ --;
}

最重要的是把 size_ 減一。

動動腦:

  • 若移除元素後,發現 size_ 遠小於 capacity_,把多餘的空間 delete 掉是值得的嗎?

size()

int size(){
  return size_;
}

回傳陣列有幾個元素。即 size_。

動動腦:

  • 為什麼不直接把 size_ 設成 public,而是要用一個 size() 函式來用它?

目前進度

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
  void grow(){
    int *temp = new T[capacity_*2];
    for (int i=0 ; i<size_ ; i++){
      temp[i] = arr_[i];
    }
    delete[] arr_;
    arr_ = temp;
    capacity_ *= 2;
  }
  
public:
  DynamicArray(){
    size_ = 0;
    capacity_ = 1;
    arr_ = new T[capacity_];
  }
  ~DynamicArray(){
    delete[] arr_;
  }
  void push_back(T value){
    if (size_ == capacity_){
      grow();
    }
    arr_[size_] = value;
    size_ ++;
  }
  void pop_back(){
    if (size_ > 0) size_ --;
  }
  int size();{
    return size_;
  }
  // TODO: 存取陣列第 i 個元素
};

目前進度與代辦事項(可下拉)

剩最後一步了!

存取元素

目標:用 arr_[index],來求出動態陣列 arr_ 的

            第 index 個元素。

方法:運算子多載 (operator overloading)。

operator overloading

舉例來說,兩個 int 可以用 + 來做相加。

                   兩個 double 可以用 + 來做相加。

                   兩個 std::string 可以用 + 來做相加。

但,兩個 struct 或 class,不能用 + 來做相加,

然而我們可以用 operator overloading 來做這件事。

operator overloading

operator overloading,可以定義 operator 在該 struct 或 class 中的定義,讓我們直接拿來用。

#include <iostream>
struct shop{
    int apple;  // 蘋果數量
    int orange; // 橘子數量
    shop operator + (shop a){
        shop b;
        b.apple = apple + a.apple;
        b.orange = orange + a.orange;
        return b;
    }
};

int main() {
    shop a = {10,20}, b = {30, 10};
    shop c = a + b; // 可以直接用 + 來相加
    std::cout << c.apple << std::endl;
    std::cout << c.orange << std::endl;
}
40
30

存取元素

我們要用的 [],其實也是一種 operator。

用 operator overloading 就可以定義它。

T& operator [](int index) {
  return arr_[index];
}

完成了!

template <typename T>
class DynamicArray {
  int size_;
  int capacity_;
  T *arr_; 
  void grow(){
    int *temp = new T[capacity_*2];
    for (int i=0 ; i<size_ ; i++){
      temp[i] = arr_[i];
    }
    delete[] arr_;
    arr_ = temp;
    capacity_ *= 2;
  }
  
public:
  DynamicArray(){
    size_ = 0;
    capacity_ = 1;
    arr_ = new T[capacity_];
  }
  ~DynamicArray(){
    delete[] arr_;
  }
  void push_back(T value){
    if (size_ == capacity_){
      grow();
    }
    arr_[size_] = value;
    size_ ++;
  }
  void pop_back(){
    if (size_ > 0) size_ --;
  }
  int size(){
    return size_;
  }
  T& operator [](int index) {
    return arr_[index];
  }
};

如此一來,我們就能使用這個動態陣列!

#include <iostream>
int main(){
  DynamicArray<int> arr;
  arr.push_back(0); 
  arr.push_back(1); 
  arr.push_back(2); 
  std::cout << arr[0] << std::endl;
  std::cout << arr[1] << std::endl;
  arr[2] = 3;
  std::cout << arr[2] << std::endl;
  std::cout << arr.size() << std::endl;
  arr.pop_back();   
  std::cout << arr.size() << std::endl;
}
0
1
3
3
2

而且這個動態陣列可以開到很大很大的空間!

不會 runtime error !!

#include <iostream>
int main(){
  DynamicArray<int> arr;
  for (int i=0 ; i<1000000000 ; i++){
    arr.push_back(i);
  }
  std::cout << arr.size() << std::endl;
}
1000000000

課堂練習

謝謝大家

20230520 實作動態陣列

By allen522019

20230520 實作動態陣列

  • 415