陣列


現在要整理全班的程式設計小考成績了,現在希望寫個小程式,全班共有 40 名學生,必須有 40 個變數來儲存學生的成績,現在問題來了,根據之前學過的,難道要宣告 40 個名稱不同的變數來儲存學生成績嗎?

不會這麼麻煩的,C++ 提供陣列(array),可以宣告一個以索引(index)作為識別的資料結構,宣告陣列的方式如下:

資料型態 名稱[長度];

長度必須是個編譯時期常數,以下是幾個宣告的範例:

int number[10];    // 宣告 10 個元素的整數陣列
double score[10];  // 宣告 10 個元素的浮點數陣列
char ascii[10];    // 宣告 10 個元素的字元陣列

若要動態宣告陣列長度,可以使用一些資料結構與動態記憶體宣告來解決,這在之後才會說明。

宣告陣列之後,陣列的元素值是未初始的,若想在宣告時初始陣列全部的元素值,可以如下:

int number[10] = {0};
double score[10] = {0.0};
char ascii[10] = {'\0'};
bool flag[10] = {false};

上面的幾個宣告,整數陣列中的元素都會被初始為 0,浮點數陣列則會被初始為 0.0,字元陣列會被初始為空字元('\0'),而 bool 陣列會被初始為 false

也可以在宣告陣列時初始所有的陣列值,例如:

int number[5] = {0, 1, 2, 3, 4};
double score[5] = {87.0, 78.0, 99.5, 69.5, 82.5};
char ascii[5] = {'A', 'B', 'C', 'D', 'E'};
bool flag[5] = {false, true, false, true, false};

要存取陣列中的元素值時,可以使用下標(Subscript)運算子 [] 加上索引」,索引值由 0 開始,下面這個簡單的程式是個示範:

#include <iostream> 
using namespace std; 

int main() { 
    constexpr int LEN = 10;
    int number[LEN] = {0}; 

    for(int i = 0; i < LEN; i++) {
        cout << number[i] << " "; 
    }
    cout << endl; 

    for(int i = 0; i < LEN; i++) {
        number[i] = i; 
    }

    for(int i = 0; i < LEN; i++) {
        cout << number[i] << " "; 
    }
    cout << endl; 

    return 0; 
}

執行結果如下:

0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9

陣列在使用時,得知陣列長度是必要的,不可以存取超過陣列長度的記憶體,這會發生無法預期的結果,陣列本身並不知道自己的長度資訊,在上面的範例中,使用了 LEN 來記錄長度,不過,有沒有辦法計算出長度呢?可以使用底下的方式:

#include <iostream> 
using namespace std; 

int main() { 
    int number[5] = {0, 1, 2, 3, 4};
    int length = sizeof(number) / sizeof(number[0]);

    for(int i = 0; i < length; i++) {
        cout << number[i] << " "; 
    }
    cout << endl; 

    return 0; 
}

陣列索引值由 0 開始不是沒有原因的,陣列名稱儲存了陣列記憶體的首個位置的位址,而索引值表示陣列元素是相對於陣列首個記憶體位址的位移量(offset),位移的量與資料型態長度有關,如果是 int 整數,每次位移時是一個 int 整數的長度,例如在上例中 number[0] 索引值為 0 時,表示位移量為 0,自然就是指第一個元素,而 number[9] 就是指相對於首個元素的位移量為 9。

C++ 17 的 iterator 提供了 size 函式,可以用來計算陣列大小,不過目前撰寫文件時使用的 g++ 編譯器需要加上 -std=c++17 才可以使用。

在 C++ 11 提供了 beginend 函式,begin 會傳回陣列首個元素的位址,end 傳回最後一個元素下個位置的位址,當對位址值進行運算時,會以資料型態的長度偏移,因此能有以下循序走訪陣列的方式:

#include <iostream> 
using namespace std; 

int main() { 
    int number[5] = {0, 1, 2, 3, 4};

    for(auto offset = begin(number); offset != end(number); offset++) {
        auto n = *offset;
        cout << n << " "; 
    }
    cout << endl; 

    return 0; 
}

offset 是個指標(pointer),型態會是 int*,儲存的是位址,而 *offset 是取得儲存於該位址的值,因為之後才會談到指標,這邊就先用 auto 讓編譯器推斷型態,基於以上的原理,在 C++ 11 提供了 for range 語法,可用於循序走訪陣列的任務:

#include <iostream> 
using namespace std; 

int main() { 
    int number[5] = {0, 1, 2, 3, 4};

    for(auto n : number) {
        cout << n << " "; 
    }
    cout << endl; 

    return 0; 
}

若在宣告陣列時指定各個索引處的的值,可以不用宣告陣列元素大小,例如:

int number[] = {1, 2, 3};
double weight[] = {0.4, 3.2, 1.0, 4.2};
char ch[] = {'A', 'B'};

上面宣告中,number[] 的元素個數會是 3,weight[] 的個數會是 4,而 chs[] 的個數會是 2。

如果使用 constconstexpr 來修飾陣列,每個索引位置就成為唯讀。例如:

constexpr int number[] = {1, 2, 3};
number[1] = 10; // error: assignment of read-only location 'number[1]'

不可以將陣列直接指定給另一陣列,例如:

int arr1[5];
int arr2[5];
...
arr1 = arr2; // 錯誤!不能直接指定陣列給另一個陣列

若要將陣列指定給另一個陣列,只能循序逐個元素進行複製,例如:

constexpr int LENGTH = 5;
int arr1[LENGTH];
int arr2[LENGTH];
...
for(int i = 0; i < LENGTH; i++) {
    arr1[i] = arr2[i];
}

直接比較兩個陣列是否相同的話,並不是比較其內容,而是比較兩個陣列變數的位址值,若想比較兩個陣列元素內容是否相同,也要用逐個元素進行比對。

如果打算對陣列進行排序、尋找、反轉等操作,可以使用包含 algorithm 標頭檔:

#include <algorithm>

例如下面這個程式直接示範了排序、尋找、反轉等操作:

#include <algorithm>
#include <iostream> 

using namespace std; 

int main() { 
    int number[] = {30, 12, 55, 31, 98, 11};

    // 排序 
    sort(begin(number), end(number));
    for(auto n : number) {
        cout << n << " ";
    }
    cout << endl;

    cout << "輸入搜尋值:";
    int search = 0;
    cin >> search;

    int* addr = find(begin(number), end(number), search);
    cout << (addr != end(number) ? "找到" : "沒有")
         << "搜尋值" 
         << endl;

    // 反轉 
    reverse(begin(number), end(number));
    for(auto n : number) {
        cout << n << " ";
    }
    cout << endl;

    return 0; 
}

執行結果:

11 12 30 31 55 98
輸入搜尋值:30
找到搜尋值
98 55 31 30 12 11

sortfind 等函式,也可以作用在 arrayvector 等,實際上,這些函式搭配函式的傳遞會更有效用,這之後都會談到。