陣列(array)

  • 有序數據的集合。

  • 陣列中每個元素的資料型態都相同

  • 元素可透過陣列名加上『下標』來取得。

  • 下標從 0 開始計數。

  • 整個陣列代表一組同類型的變數。

  • 注意:字元陣列

一維陣列

  • 一般型式:

    型態 陣列名[常數表達式]

    int a[10];                              // 定義陣列a,這個陣列具有10個int的元組。
    • a:陣列名就是變數名

    • 中括號([])括起來的『常數表達式』,一般多為數值字面值,如 10。或是常數表達式 2 + 8

    • C++/C 程式不允許對陣列的大小做動態定義,故陣列大小無法依賴於程式執行過程中的變數值。

    • 陣列定義的時候,大小必確定。

    • 一維陣列帶有一組『中括號([])』。

    • a[10] 具有 10 個元素。

    • 合法可以使用的的元素是 a[0] ~ a[9]

    • 但非法使用 a[10](如為a[10]賦值),系統並不會提示錯誤,但會給程式帶來不確定的未爆彈。

    • a[10] 所屬的記憶體空間並不屬於開發者所定義來使用,故有可能會將其他程式用到該位址的部分給覆蓋掉,則會引發許多非預期的錯誤。

    int a[10];             // a陣列的元素下標為 0 ~ 9。
    a[10] = 8;             // 這是危險的。雖然系統不會提示錯誤,但會給程式留下巨大的隱憂。
  • 一維陣列的引用:

    • C++/C 程式規定只能引用陣列的元素『無法引用整個陣列』

    • 一般型式:

      陣列名[下標]

      • 下標為整數型態。
      int i, a[10];
      for (i = 0; i <= 9; i++)          // i = 0 ~ 9
        a[i] = i;                       // for loop若不加{},for loop的有效範圍是到第一個分號結束。
      
      for (i = 0; i <= 9; i++)
        printf("a[%d] = %d\n", i, a[i]);
  • 一維陣列初始化:

    • 定義陣列時不初始化

      int a[10];      // 只給定義不給初始值,則陣列內部元素值為不確定的隨機值。
    • 定義陣列時初始化

      int a[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    • 只給一部分元素初始化

      int a[10] = {9, 8, 7, 6};       // 其他陣列元素值則自動設為0。
      char s[10] = {'A', 'B', 'C'};   // 其他陣列元素值則自動設為''。
    • 若對全部陣列設定初始化,可以不給陣列長度。

二維陣列

  • 一般型式:

    型態 陣列名[常數表達式][常數表達式]

    float a[3][4];                              // 定義二維陣列a,注意a[3, 4]是錯誤的用法。
    • 定義出3列(rows)4行(columns)的陣列。

    • 可將二維陣列理解為含有多個元素的的一維陣列。

    • 意指 a 是一個具有『3個元素的一維陣列』a[0]a[1]a[2]),每個元素則是一個包含 4 個元素的『一維陣列』。

    • a 共有12個元素。

    • 在C/C++語言,二維陣列存放的順序為:『按列順序(by-row order)』

    • 第一維下標變化最慢最右邊的維度變化最快

  • 二維陣列的引用:

    • 引用型式:

      陣列名[下標][下標]

      • 注意:引用『二維陣列』必須帶有『兩個中括號』。其他多維陣列以此類推。

      • 下標可以為整數表達式,如 a[5-2][4-1],但一般不會這樣寫。直接寫成整數。

      • 陣列元素可以出現在表達式中,就像變數一樣使用,亦也可被賦值操作。

      • 無論是一維陣列或是二維陣列,其陣列元素(a[1]或是a[1][2])都應該被看成一個普通變數。

      int a[3][4];            // 定義二維陣列
      int i, j;
      for (i = 0; i < 3; i++)
      {
          for (j = 0; j < 4; j++)
          {
            a[i][j] = i * j;
          }
      }
      
      for (i = 0; i < 3; i++)
      {
        for (j = 0; j < 4; j++)
        {
          printf("a[%d][%d] = %d\t", i, j, a[i][j]);
        }
        std::cout << std::endl;
      }
  • 二維陣列初始化:

    • 按列(row)給二維陣列初始化:

       int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
      • 按列(row)賦值。
    • 將所有數據放在大括號({})內:

      • 但初始化看起來不清晰,容易遺漏出錯。

        int a[3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
    • 對部分元素設定初始化:

      int a[3][4] = {{1}, {3, 4}};
      • 這裡大括號的內容代表一列,這裡省略了第三列,也就是沒給第三列元素賦值。

      • 其他未被賦值的元素都會被預設為 0

      int a[3][4] = {{1}, {}, {9}};
      • 第二列不賦值。

      • 其他未被賦值的元素都會被預設為 0。

    • 若對全部元素設定初始化,則定義陣列時第一維度的長度可以不指定,但第二維陣列的長度(其他維度長度)不能省略。

      int a[][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; 
      // 等同於
      // int a[3][4] = {1, 2, 3, 4, ,5, 6, 7, 8, 9, 10, 11, 12}; 
      
      int a[][4] = {{0, 0, 3}, {}, {0, 10}};
      // 等同於
      // int a[][4] = {{0, 0, 3, 0}, {0, 0, 0, 0}, {0, 10, 0, 0}};
      
      int a[][2][2] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};     // 三維陣列時
      int a[][3] = {{1, 2, 3}, {4, 5}, {7, 8}, {9}};
      
      for (size_t i = 0; i < sizeof(a)/sizeof(a[0]); i++)
      {
        for (size_t j = 0; j < sizeof(a[0])/sizeof(a[0][0]); j++)
        {
          std::cout << "a[" << i << "]" << "[" << j << "] = " << a[i][j] << std::endl;
        }
      
      }
      int a[][5] = {{1, 2, 3}, {4, 5, 6}};
      
      size_t n = sizeof(a)/sizeof(a[0]);
      std::cout << "row = " << n << std::endl;
      
      size_t m = sizeof(a[0])/sizeof(a[0][0]);
      std::cout << "column = " << m << std::endl;
      
      std::cout << "第二列為:";
      for (size_t i = 0; i < sizeof(a[0])/sizeof(a[0][0]); i++)
      {
        std::cout << a[1][i] << ", ";
      }
      std::cout << std::endl;
      float a[2][3][4];
      
      // a[0][0][0], a[0][0][1], a[0][0][2], a[0][0][3], 
      // a[0][1][0], a[0][1][1], a[0][1][2], a[0][1][3], 
      // a[0][2][0], a[0][2][1], a[0][2][2], a[0][2][3], 
      // a[1][0][0], a[1][0][1], a[1][0][2], a[1][0][3], 
      // a[1][1][0], a[1][1][1], a[1][1][2], a[1][1][3], 
      // a[1][2][0], a[1][2][1], a[1][2][2], a[1][2][3], 
  • 實務上一維與二維陣列較常用,三維陣列與多維陣列較少用。

不允許複製與賦值

int a[] = {100, 200, 300};
// int a2[] = a;                            // 錯誤:不允許使用一個陣列來初始化另一個陣列。

// int a2[3];
// a2 = a;                                  // 錯誤:不能把一個陣列直接賦值給另一個陣列。

複雜陣列宣告

  • 陣列可存放許多型態的物件。例如:

    • 指標的陣列:一堆指標。

    • 陣列的指標:指向陣列的指標

    • 陣列的參考:綁定陣列的參考

  • 範例:

    int arr[10];                          // 含有10個整數的陣列。
    int* ptr[10];                         // ptr是含有10個整數型態指針的陣列。指標的陣列。
    // int& ref[10];                      // 錯誤:因為參考並非物件,故不存在參考的陣列。
    int (*Parray)[10] = &arr;             // Parray指向一個含有10個整數的陣列。陣列的指標。注意陣列的大小。
    int (&arrRef)[10] = arr;              // arrRef是一個含有10個整數陣列的別名。陣列的參考。 
    • 預設情況下,型態修飾符『由右至左』依次設定。
    • 複雜情況下,先由內而外來解讀會更簡單。
    • 對於 ptr,從右至左:首先知道是一個長度為 10 的陣列,它的名字是 ptr,然後知道陣列中存放的是指向 int 的指標( int*)。
    • 對於 Parray由內而外:先看() 的部分,*Parray 意指 Parray 是一個指標,接著觀察右邊,可知 Parray 是一個指向大小為 10 之陣列的指標。最後觀察左邊,得知陣列中的元素是 int
    • 對於 &arrRef,由內而外,先看()的部分,可知 arrRef 是一個參考,它綁定的物件是一個大小為 10 的陣列,而陣列中的元素為 int
    • 要理解『陣列宣告』的含義,最好的方式是從陣列名稱開始按照『由內而外』的順序解讀。
    int arr[] = {1, 2, 3, 4, 5};
    int (*ptr2)[5] = &arr;
    
    // 使用指標 ptr2 存取陣列元素
    std::cout << "第一個元素:" << (*ptr2)[0] << std::endl;
    std::cout << "第二個元素:" << (*ptr2)[1] << std::endl;
    int a[] = {100, 200, 300};
    std::cout << "a = " << a << std::endl;
    
    int *b = a;
    int (*ptr)[3] = &a;
    
    std::cout << *b << std::endl;
    std::cout << (*ptr)[0] << std::endl;
    • b 可以通過解參考操作 (*b) 來取得其指向的整數值。

    • ptr 也可以通過解參考操作來取得整個陣列但需要使用陣列下標或指標算術操作來存取陣列中的元素。

    • 在這段程式碼中,當嘗試輸出 (*ptr) 時,會遇到一個問題,因為 (*ptr) 代表的是 ptr 所指向的整個陣列,它不是一個單一的數值,而是一個整個陣列的地址。

    • C++ 標準程式庫中 std::coutoperator<< 在處理 (*ptr) 時會遇到一個 int (*)[3] 型態的指標,因此會將其視為一個記憶體位置,而不是陣列中的值。因此,這樣的輸出不會產生預期的結果,而是顯示該陣列的起始記憶體位置。

指標與陣列

  • 指標與陣列關係密切。

  • 使用陣列時,編譯器會將其轉換為指標。

    • 使用『陣列』型態的物件時,其實就是使用指向『該陣列首位址的指標』

      int a[]  = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
      // int (*ptr)[] = &a;                         // 錯誤:指向的陣列,其大小必須確定。
      int (*ptr)[10] = &a;                          // 正確。
      int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
      auto ia2(ia);                        // ia2是一個『整數型態指標』,指向ia的第一個元素。
      int* ia3(ia);                        // ia3是一個『整數型態指標』,指向ia的第一個元素。
      auto ia4 = ia;                       // ia4是一個『整數型態指標』,指向ia的第一個元素。
      
      // ia2 = 42;                           // 錯誤:ia2是一個指標,不能用int賦值給指標。
      // 在大部分表達式中時,陣列名 a 被隱式轉換為指向其首元素的指標。
      // 這意味著如果 a 是一個一維陣列,例如 int a[10];,則在大多數上下文中 a 被視為指向 int 的指標(即 int*)。
      int a[][3] = {1, 2, 3, 4, 5, 6};
      std::cout << "a is the 2 * 3 array: " << std::endl;
      std::cout << "a = " << a << std::endl;
      std::cout << "a[0] = "<< a[0] << std::endl;
      std::cout << "a[0][0] = " << a[0][0] << std::endl;
      
      int (*ptr11)[3] = a;
      int *ptr12 = &a[0][0];
      std::cout << ptr11 << std::endl;
      std::cout << ptr12 << std::endl;
      
      // 使用 &a 可以獲得整個陣列的地址,這會返回一個『指向整個陣列的指標』,其類型與陣列維度有關。
      int (*ptr21)[2][3] = &a;
      int (*ptr22)[3] = &a[0];
      
      int (*ArrPtr)[][3] = &a;
      
      std::cout << "ptr21 = " << ptr21 << std::endl;
      std::cout << "ptr22 = " << ptr22 << std::endl;
      std::cout << "ArrPtr = " << ArrPtr << std::endl;
      int a[] = {100, 200, 300};
      int *p1 = nullptr;
      int *p2 = nullptr;
      p1 = a;
      p2 = &a[0];     // p1 和 p2 均初始化為 nullptr,接著指向陣列 a 的首位址。注意,陣列名 a 本身在表達式中可以被當成指向第一个元素的指標。
      
      std::cout << "a = " << a << std::endl;
      std::cout << "p1 = " << p1 << std::endl;
      std::cout << "p2 = " << p2 << std::endl;   // 因 p1 和 p2 都指向陣列的第一個元素,這三行打印出相同的位址。
      
      std::cout << a[0] << std::endl;
      std::cout << *p1 << std::endl;
      
      std::cout << a[1] << std::endl;
      std::cout << *(p1 + 1) << std::endl;  // 透過陣列下標和透過指標加上偏移量(下標)存取陣列元素的方式。
      
      
      int (*ArrPtr)[3] = &a;                  // ArrPtr 是一個指向『整個陣列』的指標。
      std::cout << ArrPtr << std::endl;       // 輸出陣列a的位址
      std::cout << *ArrPtr << std::endl;      // 輸出陣列a的位址,因『解參考』指向陣列的指標得到『陣列本身』。
      std::cout << **ArrPtr << std::endl;     // 輸出陣列第一個元素,即100。
      std::cout << (*ArrPtr)[0] << std::endl;
      std::cout << (*ArrPtr)[1] << std::endl;
      std::cout << (*ArrPtr)[2] << std::endl;
      int a[] = {100, 200, 300};
      int (*ArrPtr)[3] = &a;       // ArrPtr 是一個指向包含三個整数的陣列的指標,並初始化為指向 a。
      
      // 複製陣列指標:
      int (*ptr)[3] = ArrPtr;      // ptr 是另一個指向整個陣列的指標,與 ArrPtr 型態一致。此行程式碼將 ArrPtr 之值赋值给 ptr,故 ptr 也指向陣列 a。
      
      auto *p1 = &ArrPtr;          // p1 是一個『自動型態推論』的指標,指向 ArrPtr。由於 ArrPtr 的型態是 int (*)[3](指向包含三個整数的陣列的指標),故 p1 的型態為 int (*(*)[3]) — 指向 int (*)[3] 型態的指標。
      int (*(*p2))[3] = &ArrPtr;

字元陣列

  • 定義:『字元陣列』中每個元素都是字元(char)。

    char c[10];            // 能引用的元素c[0] ~ c[9]。
    c[0] = 'I';
    c[1] = ' ';
    c[2] = 'a';
    c[3] = 'm';
    c[4] = ' ';
    c[5] = 'h';
    c[6] = 'a';
    c[7] = 'p';
    c[8] = 'p';
    c[9] = 'y';
  • 字元陣列初始化

    • 逐個字元賦值給陣列中的元素

    • 若提供的初始值個數預定的陣列長度相同,定義時可以省略長度。

    • 若初始值大於陣列長度,則出現『語法錯誤』。

    • 若初始值個數小於陣列長度,則只將這些字元賦值給前面的元素,其餘元素可能給 '\0',也可能無法確定之。故強烈建議不要使用這些未確定之元素值。

  • 字串與字串的結束標記('\0'

    • 比較並思考下列二者的涵義與差異:

      char c[] = {'I', ' ', 'a', 'm', ' ', 'h', 'a', 'p', 'p', 'y'};      // 定義一個包含10個元素的字元陣列
      char c[] = {"I am happy"};    // 利用字串常數來初始化字元陣列,其中{}可以省略。
      // char c[] = "I am happy"; 
    • 第二例實際上是定義一個類似 char c[11] 的陣列,長度為 11。

    • 第二例可以引用的元素為 c[0] ~ c[10],且在 c[10] 裡面被系統自動填入一個 '\0' 字元。

    • '\0' 為字元的 ASCII碼,而'\0' 就是 0。其則為字串『結束標記(結束字元)』。代表字串結束。

    • 上例字串常數 "I am happy" 共有10 個『可見字元』(含空白字元 " ")。

    • 每個『可見字元』佔 1 個位元組,但實際上該字串常數在內存共佔 11位元組,其中最後 1 個位元組放的正是 '\0'

    • 當有了字串結束標記 '\0' 後,字串陣列的長度即可確定(因程式就是以 '\0' 來判斷字串是否結束)。

      char c[10] = "I am happy";                     // 錯誤:因為 c陣列無法儲存 "I am happy"。
      char c[11] = "I am happy";                     // 沒問題。剛好放得下。
      char c[100] = "I am happy";                    // 當然可以。
    • 比較下列兩行程式碼差異:

      char c[] = {'I', ' ', 'a', 'm', ' ', 'h', 'a', 'p', 'p', 'y'};
      char c[] = {"I am happy"};
      • 上列兩行程式碼不等價。因為後一種寫法系統自動會在字串末尾加入 '\0'
    • 再比較下列兩行程式碼差異:

      char c[] = {'I', ' ', 'a', 'm', ' ', 'h', 'a', 'p', 'p', 'y', '\0'};
      char c[] = {"I am happy"};
      • 上列兩行程式碼為等價。長度相同,內容也相同。
    • 字元陣列並沒有規定最後一個字元為 '\0'。是否加入 '\0' 取決於程式設計師。

    • 若使用字元陣列並對其進行初始化,建議與字串常數保持一致,請加入 '\0',以確定字串常數的長度。

      char c[100] = "I am happy";               // 最普遍與常見的寫法
      char c[] = {'I', ' ', 'a', 'm', ' ', 'h', 'a', 'p', 'p', 'y'};
      printf(" %s\n", c);
      char c[] = "I am happy";
      printf(" %s\n", c);
  • 字元陣列的輸入/輸出

    • printf 函數可利用 %s 格式符可輸出打印字串。

      char c[] = "NCCU";
      printf(" %s\n", c);
    • printf 函數向螢幕輸出結果時,遇到字元 '\0' 就會停止輸出打印。

      • printf 函數輸出打印字串中並不包含 '\0',且 '\0'也不是可見字元。

      • printf(" %s\n", c);,其輸出項為『字元陣列名』,不可以是 c[0]c[1] 等。

      • 即便陣列定義時的長度大於字符串實際長度時,也只輸出到 '\0' 結束。

        char c[100] = "NCCU";
        printf(" %s\n", c);
      • 若字元陣列裡包含多個 '\0',則printf 函數遇到第一個 '\0' 時就會停止輸出。

  • 字串處理函數(HW.)

    • strcat(字元陣列1, 字元陣列2)

    • strcpy(字元陣列1, 字元陣列2)

    • strcmp(字串1, 字串2)

    • strlen(字元陣列)

字串

  • 字串是特殊的字元陣列。

  • 與普通的字元陣列不同的是字串是在末尾處有一個『結束字元 '\0'』,用以表示字串的結束。

  • 所以可以這麼認為:並沒有一種專門的字串型態。字串其實是字元陣列

  • 字串也有字面值(用雙引號 "")的定義方式:"Hello Quant"

字元指標與字元陣列

  • 雖然字元指標(字元陣列首位址)與字元陣列都能實現字串的儲存,但兩者還是有區別。

  • 字元陣列由若干元素組成,每個元素存放1個字元。

  • 字元指標存放的僅是字串的『首位址』(千萬不要誤解成將字串存放至字元指標)。

  • 賦值方式:

    char str[100] = "I am happy!";                   // 定義時初始化。
    char str[100];
    //str = "I am happy!";                            // 錯誤:無法直接賦值。
    
    strcpy(str, "I am happy!");                     // 利用strcpy進行複製完成。
    const char *a;                                  // 可採字元指標
    a = "I am happy!";
  • 指標變數是可以修改的:指標指向的位置可以被改變。

    const char *a = "I am happy!";
    a = a + 7;
    printf("%s\n", a);
  • 陣列名稱雖然代表陣列首位址,但其值無法改變(Top-Level const)。

    char a[] = "I am happy!";
    // a = a + 7;                      // 錯誤:因為陣列名稱所代表的首位址是無法被更改的。
    printf(" %s\n", a);
    int a[]{100, 200, 300};            
    int *p = a;
    
    std::cout << a << std::endl;
    std::cout << p << std::endl;
    
    std::cout << (*a) << std::endl;
    std::cout << (*p) << std::endl;
    
    std::cout << "*(a + 1) = " << *(a + 1) << std::endl;      // *(a + 1) = 200
    // std::cout << *(++a) << std::endl;       // 錯誤:因為『前遞增運算子』會修改『不能修改的 a』。
    
    std::cout << "*(++p) = "<< *(++p) << std::endl;        // *(++p) = 200       
    std::cout << "*(p++) = " << *(p++) << std::endl;       // *(p++) = 200 
    const char *p = "Hello"; 
    std::cout << "p = " << p << std::endl;          // p = Hello
    std::cout << "*p = " << *p << std::endl;        // *p = H
    printf("%p\n", p);                              //  0x104887f3c
    char arr1[] = "I am Happy";
    char arr2[] = {'A', 'B', 'C'};
    int  arr3[] = {100, 200, 300};
    
    const char *ptr = "I am Happy";
    
    std::cout << arr1 << std::endl;
    std::cout << arr2 << std::endl;
    std::cout << arr3 << std::endl;
    std::cout << ptr << std::endl;
    • 在 C++ 中,當使用 std::cout 打印一個字元指標 (char*) 時,std::cout 被設計來特別處理這種情況。

    • 字元指標通常被認為是指向一個 C 字串(一個 null 字元 '\0' 結尾的字元數組)。因此,標準輸出流(std::cout)對『字元指標』進行特殊處理:

      • 不會打印出指標的記憶體地址,而是從指標所指向的地址開始,逐字元輸出,直到遇到結束字元(null 字元 '\0')為止。

      • 該特性是根據 operator<< 重載實現的,該重載專門處理 char* 參數。這樣做的目的是因為在 C 和 C++ 中,字串通常以這種方式表示和處理。例如,標準函數庫中的字串函數(如 strlenstrcpy 等)都預期以 '\0' 結尾的字元陣列作為輸入。

陣列的缺點

  • 陣列的長度在定義時必須確定,不能是變數。

  • 陣列定義後,其長度也不能改變。

  • 無法直接使用陣列為另一個陣列賦值。

  • 無法對陣列進行插入的操作。

  • 在使用陣列時無法直接取得陣列的長度。

因此,C++ 標準程式庫提供功能強大的『類別模板(class template)』- vector,來克服上述缺點。

標準程式庫型態:vector

vector 型態簡介

  • vector 型態是標準程式庫內的一種型態。

  • 是一種 容器(container)、集合與動態陣列的概念。

  • vector 內的元素型態必須相同(同質性):可以將一堆同型態的物件放在 vector 容器中。

  • 要使用 vector 型態,需要在原始檔(.cpp)包含 vector 標頭檔:#include <vector>

  • 用法:

    #include <vector>              // 需要在使用vector型態的原始檔加入vector的標頭檔
    std::vector <int> vec;          // 定義一個vector型態的物件,名稱為 vec。
    • 上列程式碼定義一個 vector 型態的物件。

    • 此物件(容器)內存放 int 型態的數據。

    • <int>寫法,為一種『類別模板(template)』實例化的概念。

    • vector 不是一個完整的類別型態。

    • 透過『實例化』後, vector<int>才是一個定義完整的類別型態。

      vector <int*> vec2;                          // 正確:向量內存放的是指向 int 的指標。
      // vector <int&> vec3;                       // 錯誤:參考只是一個別名,並不是物件。
      struct student
      {
        int num;
      };
      std::vector <student> studList;             // 正確:向量內存放的是struct型態的物件。
      std::vector <std::vector<student>> V;       // 正確:該向量內每個元素都一個vector物件。

定義與初始化 vector 物件

  • vector 物件

    std::vector <std::string> mystr;          // 創建一個string型態的空vector物件(容器)
    mystr.push_back("abcd");
    mystr.push_back("def");
  • vector 物件內元素型態相同下,進行 vector 物件的『元素複製(新的副本)』

    std::vector <std::string> mystr2(mystr);         // 將mystr元素複製給mystr2。
    std::vector <std::string> mystr3 = mystr;        // 將mystr元素複製給mystr3。
  • 使用 C++ 11 初始化列表

    std::vector <std::string> def = {"aaa", "bbb", "ccc"};
    std::vector <int> v = {};                               // v為空 vector 物件。
  • 創建指定個數的元素:具有元素個數概念的初始化,都是使用 ()

    std::vector <int> v2(3, -200);      // 創建一個包含3個int型態(值為-200)的向量,向量名稱為v2。
    std::vector <std::string> v3(5, "Hello");
    • 若不為元素設定初值,則元素的初值乃根據元素型態而定。例如:若元素型態為 int,則系統設定初值為 0。若元素型態為 std::string,則系統設定初值為 ""

    • 但也存在某些型態,必須設定初值,否則程式將出錯。

    std::vector <int> v4;
    std::vector <std::string> v5;
  • 其他初始化,使用 {}

    • {}一般表示元素內容的概念。

      std::vector <int> i1(10);            // 10為元素個數,每個元素預設為0。
      std::vector <int> i2{10};            // {}括住單個數字10,表示1個元素。
      
      std::vector <std::string> s1{"hello"};    // 1個元素,其值為 "hello"。
      std::vector <std::string> s2{10};         // 10個元素,每個元素都為""。因為10這個數字無法作為std::string的物件,故系統將它轉換為元素的個數。不建議這樣的寫法。
      std::vector <std::string> s3(10, "hello");    // 10個元素,每個元素內容都是 "hello"。
    • 建議當使用 {} 進行初始化時,則 {} 內的值型態應與 <> 內的元素型態一致。

二維 vector

  • 對於二維 vector,可視為一個一維 vector,其每個元素都是都是一維 vector。

  • 下列範例 std::vector<double>(cols) 用於建立一個包含 colsdouble 型態元素的一维 std::vector,然後 std::vector<std::vector<double>> array(rows, ...) 用於創建一個包含 rows 個一维 std::vector 的二维 std::vector

    #include <iostream>
    #include <vector>
    // #include <string>
    #include <random>
    
    int main(int argc, char const *argv[])
    {
        // 使用隨機設備,隨機產生亂數種子。
        // std::random_device rd;
        // std::default_random_engine generator(rd());  // 從 random_device 來初始化一個隨機數生成器。
    
        int seed = 42;
        std::default_random_engine generator(seed);            // 使用固定種子初始化隨機數生成器
    
        std::normal_distribution<double> distribution(0.0, 1.0); // 常態分配,均值為0,標準差為1。
    
        // 建立一个1000x365的陣列(矩陣)
        const int rows = 1000;
        const int cols = 365;
        std::vector<std::vector<double>> array(rows, std::vector<double>(cols)); // 嵌套式定義。
    
        // 對矩陣填值。
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                array[i][j] = distribution(generator);
            }
        }
    
        // 打印矩陣一部分數據。
        for (int i = 0; i < 5; ++i)
        {
            for (int j = 0; j < 5; ++j)
            {
                std::cout << array[i][j] << "\t";
            }
            std::cout << std::endl;
        }
    
        return 0;
    }

vector 物件的操作

  • 判斷是否為空:empty(),回傳 布林值。

    std::vector <int> ivec;              // 定義ivec為空向量。
    if (ivec.empty())
    {
      std::cout << "ivec為空向量" std::endl;
    }
  • push_back():用於向 vector 物件末尾增加元素。

    std::vector <int> ivec;
    ivec.push_back(1);
    ivec.push_back(2);
    
    for (int i = 3; i <= 100; i++)
    {
      ivec.push_back(i);
    }
  • size():回傳元素個數。

    std::cout << ivec.size() << std::endl;
  • clear():移除所有元素,將容器清空。

    ivec.clear();
    std::cout << ivec.size() << std::endl;
  • v[n]:回傳 v 中的第 n 個元素:

    • 回傳 v 中第 n 個元素(n為整數型態)。其位置從 0 開始計算。n也必須小值 size()值。

    • 若下標值 n 超過 size() 的範圍,或用下標存取空向量,則會發生不可預的後果(編譯器可能不會檢查出這類錯誤)。

  • 賦值運算子(

    vector <int> ivec;                     // 先宣告一個空vector物件
    ivec.push_back(1);
    ivec.push_back(2);
    for (int i = 3; i <= 100; i++ )
    {
      ivec.push_back(i);
    }
    std::vector <int> ivec2;
    ivec2.push_back(111);
    ivec2 = ivec;                           // 用ivec中的內容取代ivec2中原有內容。
    ivec2 = {12, 13, 14, 15};               // 用{}中的值取代ivec2原有內容。
    std::cout << ivec2.size() << std::endl; // 4。 
  • 判斷相等與不相等(==!=

    • 兩個 vector 物件相等:

      • 元素數量相等。

      • 對應位置的元素值相等。

      std::vector<int> ivec;
      ivec.push_back(1);
      ivec.push_back(2);
      for (int i = 3; i <= 100; i++ )
      {
        ivec.push_back(i);
      }
      
      std::vector <int> ivec2;
      ivec2 = ivec;
      if (ivec2 == ivec)
        std::cout << "ivec2 == ivec" << std::endl;
      if (ivec2 != ivec)
        std::cout << "ivec2 != ivec" << std::endl;
  • range for 的運用。

    std::vector <int> vec{1, 2, 3, 4, 5};
    for (auto& vecitem: vec)
      vecitem *= 2;
    for (auto& vecitem: vec)
      std::cout << vecitem << std::endl;
    • 使用 range for 可遍歷 vector 容器內所有的元素。

    • vecitem 被定義為一個變數,而 vec 則為容器(序列)。

    • 使用 auto 來確保序列中的每個元素都能順利轉換為 vecitem 對應的型態(由編譯器來進行型態推論)。

    • 不要在 range for 中加入改變 vector 內容的程式碼,結果以及輸出則會出現錯誤或混亂:

      std::vector <int> vec{1, 2, 3, 4, 5};
      for (auto& vecitem: vec)
      {
        vec.push_back(888);
        std::cout << vecitem << std::endl;
      }
    • std::vector 中大量插入新資料時,最好的做法是先預先分配足夠的空間以避免多次內存重新分配,然後再插入資料。這樣可以提高效率並減少內存重新分配的開銷。

    • 這些方法可以幫助你在 std::vector 中高效地插入大量新資料,確保程序性能和穩定性。

      • 預先分配空間

        • 使用 vectorreserve 方法來預先分配足夠的空間,這樣可以避免在插入資料過程中多次內存重新分配。

          #include <vector>
          #include <iostream>
          
          int main() {
              std::vector<int> vec;
          
              // 預先分配空間,假設我們知道要插入 1000 個元素
              vec.reserve(1000);
          
              // 插入大量新資料
              for (int i = 0; i < 1000; ++i) {
                  vec.push_back(i);
              }
          
              // 輸出結果
              std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
          
              return 0;
          }
      • 批量插入

        • 如果你已經有一個容器(如另一個 vector 或一個陣列)包含了所有要插入的新資料,可以使用 insert 方法進行批量插入。

          #include <vector>
          #include <iostream>
          
          int main() {
              std::vector<int> vec = {1, 2, 3, 4, 5};
              std::vector<int> new_data = {6, 7, 8, 9, 10};
          
              // 預先分配足夠的空間
              vec.reserve(vec.size() + new_data.size());
          
              // 批量插入新資料
              vec.insert(vec.end(), new_data.begin(), new_data.end());
          
              // 輸出結果
              for (const auto& elem : vec) {
                  std::cout << elem << " ";
              }
          
              std::cout << "\nSize: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
          
              return 0;
          }
      • 使用迭代器範圍插入

        • 如果要從另一個容器中插入元素,可以使用迭代器範圍來插入。

          #include <vector>
          #include <list>
          #include <iostream>
          
          int main() {
              std::vector<int> vec = {1, 2, 3, 4, 5};
              std::list<int> new_data = {6, 7, 8, 9, 10};
          
              // 預先分配足夠的空間
              vec.reserve(vec.size() + new_data.size());
          
              // 使用迭代器範圍插入新資料
              vec.insert(vec.end(), new_data.begin(), new_data.end());
          
              // 輸出結果
              for (const auto& elem : vec) {
                  std::cout << elem << " ";
              }
          
              std::cout << "\nSize: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
          
              return 0;
          }

標準程式庫型態:string

string 型態簡介

  • 在 C++ 中,因為有標準程式庫的存在,亦定義很多標準程式庫定義的型態。

  • 最常見的 C++ 標準程式庫型態:std::vector 型態與 std::string 型態。

  • 其中,std::string 型態是用來處理可變長度(variable-length)字串(字元序列)』使用。

  • 在C++中,仍然可以使用字元陣列來表示字串,也可以使用 std::string 來表示字串。

  • 『字元陣列』與 『std::string』 可以進行型態互相轉換。

    • std::string 轉換到 C 風格字元陣列:std::string 提供成員函數 c_str(),此函數回傳一個指向以 null 結尾的字元陣列的常數指針(const char*),該陣列包含與 std::string 相同的數據。
    #include <iostream>
    #include <string>
    
    int main() {
        std::string str = "Hello, world!";
        const char* cstr = str.c_str();
    
        std::cout << "std::string: " << str << "\n";
        std::cout << "C string: " << cstr << std::endl;
    
        return 0;
    }
    • 從 C 風格字元陣列轉換到 std::string可直接使用 C 風格的字元陣列來初始化 std::string,或者將其賦值給一個已經存在的 std::string 物件。這是因為 std::string 的構造函數和賦值運算符重載支持從 const char* 進行初始化和賦值。

      #include <iostream>
      #include <string>
      
      int main() {
          const char* cstr = "Hello, world!";
          std::string str = cstr;
      
          std::cout << "C string: " << cstr << "\n";
          std::cout << "std::string: " << str << std::endl;
      
          return 0;
      }
    • 當使用 c_str() 獲得 const char* 時,回傳的指標不應被修改,且其壽命與原 std::string 物件綁定。如果 std::string 被修改或銷毀,則該指標可能指向無效的內存。

    • 確保從 const char* 轉換到 std::string 時,該 const char* 必須是有效的以 null 結尾的字元陣列。如果它不是以 null 結尾,則轉換可能導致運行時錯誤(如讀取越界)。

  • 要使用 std::string 型態,需要在原始檔(.cpp)包含 string 標頭檔:#include <string>

定義與初始化 string 物件

  • 如何初始化一個物件是由其類別所決定。

  • 一個類別可以有很多種初始化物件的方法。可能因下列因素有所不同:

    • 初始值數數量的不同。

    • 初始值的型態不同。

初始化方法 意義
std::string s1 預設初始化,s1為一個空字串。
std::string s2(s1) s2為s1的複製。
std::string s2 = s1 s2為s1的複製。
std::string s3("value") s3是字面值 "value"的複製,但不包括字面值最後的空字元。
std::string s3 = "value" s3是字面值 "value"的複製,但不包括字面值最後的空字元。
std::string s4(n, 'c') 將s4初始化為連續n個字元 'c' 組成的字串。
  • string 物件的『預設初始化』是設定為『空字串』。
std::string s1;                         // 預設初始化:s1 = ""; 代表為空字串。
std::string s2 = "I am happy!";         // 將 "I am happy!" 這個字串複製至s2代表的一塊記憶體空間中。 
std::string s3("I am happy!");          // 效果與s2一樣。
std::string s4 = s2;                    // 將s2的內容複製至s4代表的一塊記憶體空間中。
int num = 6;
std::string s5(num, 'a');               // 將s5初始化為連續num個字元組成的字串。但此方法系統內部會創建臨時物件,故不推薦此寫法。

直接初始化與複製初始化

  • 使用等號( = )初始化:複製初始化(copy initialization)。

  • 否則為直接初始化(direct initialization)。

string 物件的操作(HomeWork)

讀寫 string 物件

讀寫未知數量 string 物件

使用 getline() 讀取一整列

empy()size() 操作

string::size_type 型態

string 物件的比較

string 物件的賦值操作

string 物件的相加

字面值與 string 物件的相加

處理 string 物件中的字元

補充:迭代器(iterator

  • 迭代器功能:可遍歷容器內元素的數據型態。

  • 迭代器類似指標,可用來指向容器內的某個元素(如第一個元素或最後一個元素後面的元素)。

  • stringvector 可透過[下標]來取得字元與元素。但實際上,C++很少透過下標來進行存取,一般來說都是採用『迭代器』進行遍歷操作。

  • 透過迭代器可讀取元素值、修改容器中(如 listmap 等容器)某個迭代器所指向的元素值。

  • 也可進行 ++–- 的操作:從容器中某個元素移到另一個元素。

迭代器型態

  • C++ 標準程式庫為每一個容器定義了對應的迭代器型態。

    std::vector<int> iv = {100, 200, 300};     // 定義容器。
    std::vector<int>::iterator iter;           // 定義 vector<int>::iterator 型態的迭代器iter。  

迭代器 begin/end 與反向迭代器 rbegin/rend 操作

每一個容器(如 vector),都會定義一個名為 beginend 的成員函數:

  • begin 回傳一個迭代器類型。

    iter = iv.begin();          // begin 回傳的迭代器指向容器中第一個元素。
  • end 回傳一個迭代器類型。

    iter = iv.end();          // end 回傳的迭代器指向最尾端元素後面的位置。故指向一個不存在的元素。
  • 若容器為空,則 begin 回傳的迭代器與 end 回傳的迭代器『相同』

    std::vector<int> iv2;
    std::vector<int>::iterator iterbegin = iv2.begin();
    std::vector<int>::iterator iterend = iv2.end();
    if (iterbegin == iterend)
    {
      std::cout << "The container iv2 is empty." << std::endl;
    }
  • end 回傳的迭代並不指向容器中的任何元素,它比較像是『標示』的概念。

  • 若迭代器從 begin 位置開始遍歷容器中的元素,當 iter 走到 end 的位置,則表示已經遍歷完整個容器的內容。

    std::vector<int> iv = {100, 200, 300};
    for (std::vector<int>::iterator iter = iv.begin(); iter != iv.end(); ++iter)
    {
      std::cout << *iter << std::endl;
    }
    std::list<int> li = {100, 200, 300};
    std::list<int>::iterator iterbegin = li.begin();
    std::list<int>::iterator iterend = li.end();
    
    for (std::list<int>::iterator i = iterbegin; i != iterend; i++)
    {
      std::cout << *i << std::endl;
    }
    
    // std::cout << *(iterbegin + 1) << std::endl; // 錯誤:迭代器不支援直接加1。
    
    
    std::list<int>::iterator nextIter = iterbegin;
    std::advance(nextIter, 1);  // 移動迭代器位置
    std::cout << *nextIter << std::endl; // 打印移動後的值
    
    std::list<int>::iterator prevIter = iterend;
    std::advance(prevIter, -1); // 移動迭代器位置
    std::cout << *prevIter << std::endl; // 打印移動後的值
  • rbegin 回傳一個反向迭代器類型,指向容器最後一個元素。

  • rend 回傳一個反向迭代器類型,指向容器第一個元素前面的位置。

    std::vector<int> iv = {100, 200, 300};
    for (std::vector<int>::reverse_iterator riter = iv.rbegin(); riter != iv.rend(); ++riter)
    {
      std::cout << *riter << std::endl;
    }

迭代器運算子

  • *iter:回傳迭代器 iter 所指向元素的引用。但需確保迭代器指向的是有效的容器元素,故不能指向 end

  • ++iter:與 iter++ 是一樣的功能。讓迭代器指向下一個元素。但指向 end 的迭代器不能在進行 ++ 操作。

  • --iter:與 iter-- 是一樣的功能。

    std::vector<int> iv = {100, 200, 300};
    std::vector<int>::iterator iter;
    for (iter = iv.begin(); iter != iv.end(); ++iter)
    {
      std::cout << *iter << std::endl;
    }
    // ++iter;    
    --iter;                              // 等於iter--。
    std::cout << *iter << std::endl;     // 300
  • iter1 == iter2iter1 != iter2:判斷兩個迭代器是否相等。若兩個迭代器指向的是同一個元素,則相等,否則不相等。

  • struct 成員的引用:

    struct student
    {
      int num;
    };
    std::vector<student> sv;
    student mystu;
    mystu.num = 100;
    sv.push_back(mystu);    // 將物件mystu『複製』至sv容器中。
    mystu.num = 200;        // 修改mystu中的內容,並不會對容器中元素值造成影響,因容器中的內容是複製進去的。
    std::vector<student>::iterator iter;
    iter = sv.begin();      // 指向第一個元素。
    std::cout << (*iter).num << std::endl;     // 引用的方法
    std::cout << iter-> num << std::endl;      // 引用的方法

const_iterator 迭代器

  • const_iterator:該迭代器所指向的元素不能改變,但不代表該迭代器本身不能改變。

  • 故該迭代器只能讀取容器中的元素,不能透過該迭代器修改容器中的元素。

  • 若容器為 const,則必須使用 const_iterator,否則會出現錯誤。

    const std::vector<int> iv = {100, 200, 300};
    std::vector<int>::const_iterator iter;
    for (iter = iv.begin(); iter != iv.end(); ++iter)   // 這裡的begin與end回傳const_iterator。回傳是iterator還是const_iterator取決於該容器是否為const。
    {
      std::cout << *iter << std::endl;
    }
  • C++ 11 引入兩個新函數:cbegincend ,兩成員函數皆回傳 const_iterator

    std::vector<int> iv = {100, 200, 300};
    for (auto iter = iv.cbegin(); iter != iv.cend(); ++iter)
    {
      *iter = 99;  //  錯誤:無法賦值。
    }

迭代器失效

  • 任何一種改變容器物件大小的操作,如 push_back,都會使當下的容器物件的迭代器失效(無法代表或指向容器的元素)。

  • 不同容器實作的原理不同(例如有些容器內部數據是連續儲存,當插入元素時一旦原有的記憶體空間不夠用,則可能會導致容器中原有數據全部遷移至新的記憶體空間位置。例如:vector),不同的插入操作,或是不同的插入位置,會導致迭代器、指標、參考等產生失效。

  • 當進行刪除操作時:若從容器刪除元素,則當下指向這個被刪除的迭代器、指標與參考則會失效。

迭代器的運用

  • 迭代器強大的地方在於它們與 C++ 標準程式庫中的算法(位於 <algorithm> 標頭檔中)的結合使用。

  • 迭代器允許以幾乎相同的方式對所有容器進行排序、搜索、變換等操作。例如,使用 std::sortstd::vector 進行排序:

    #include <algorithm>
    #include <vector>
    #include <iostream>
    
    int main(int argc, char const *argv[])
    {
        std::vector<int> vec = {5, 3, 1, 4, 2};
    
        // 使用 std::sort 進行排序
        std::sort(vec.begin(), vec.end());
    
        for (int num : vec)
        {
            std::cout << num << " ";
        }
        std::cout << std::endl;
        return 0;
    }
    #include <iostream>
    #include <map>
    #include <string>
    
    int main() {
        std::map<std::string, int> myMap = {{"apple", 2}, {"banana", 3}, {"cherry", 5}};
    
        // 使用迭代器遍歷 map
        for (std::map<std::string, int>::iterator it = myMap.begin(); it != myMap.end(); ++it) {
            std::cout << it->first << ": " << it->second << std::endl;
        }
    
        return 0;
    }