Learning Objectives
- 宣告與初始化一維陣列,用從 0 開始的索引存取元素,並用 sizeof 算出元素個數
- 說明越界存取是未定義行為,C++ 不會替你檢查索引
- 把陣列傳給函式,說明它會退化成指標、所以要另外傳長度
- 宣告與走訪二維陣列,說明它在記憶體裡一列接一列排列
- 分辨 C 字串(以 '\0' 結尾的字元陣列)與 std::string,使用 length、+、比較、substr、find 與逐字元走訪
一次存很多個值
全班 40 個人的成績,不可能宣告 40 個變數 score1、score2……。陣列(array)是一排相同型別、連續存放的元素,
用一個名稱加上索引來存取:
int scores[5] = {78, 92, 65, 88, 70};
cout << scores[0]; // 第一個元素:78
scores[4] = 75; // 最後一個元素的索引是 4
- 陣列的大小在宣告時決定,之後不能改變;大小必須是編譯時就知道的常數
- 索引從 0 開始,n 個元素的合法索引是 0 到 n - 1
- 初始化時給的值比大小少,剩下的元素補 0;
int a[5] = {};全部是 0;可以省略大小讓編譯器自己數:int a[] = {1, 2, 3}; - 宣告時沒有初始化的區域陣列,元素是不確定的垃圾值
文字也是一排字元,所以本課後半會介紹兩種字串:C 語言留下來的字元陣列,以及 C++ 標準函式庫的 std::string。
陣列的索引、sizeof 與越界
右邊的 scores 會畫成一排格子,每格下面有索引與位址,相鄰兩格的位址差 4(一個 int 的大小)。最後一行讀取 scores[5],超出範圍,動畫會停下來說明。
C++ 不會檢查索引
很多語言(Python、Java)在索引超出範圍時會立刻報錯。C++ 為了速度,不檢查陣列索引:scores[5] 只是「從 scores 開頭往後算第 5 格」,
那裡放的可能是別的變數,也可能是不屬於程式的記憶體。這是未定義行為(undefined behavior):可能印出奇怪的數字、悄悄改壞別的變數,也可能當掉。
最常見的寫法錯誤是迴圈條件寫成 i <= n:
int a[5];
for (int i = 0; i <= 5; i++) { // 錯:i 會等於 5
a[i] = 0;
}
本站的直譯器會在越界的那一步停下來說明,真正編譯出來的程式不會。
把陣列傳給函式
陣列當作函式參數時,不會複製整個陣列,傳進去的是第一個元素的位址:
double average(const int arr[], int n); // arr 其實是 const int*
這叫做陣列退化(decay)成指標,帶來三個結果:
- 函式裡
sizeof(arr)是指標的大小(64 位元系統上是 8),不是陣列的大小,所以元素個數要另外用參數傳進去。g++ 預設就會對這種寫法提出警告 - 函式裡修改
arr[i],改到的是呼叫端的陣列(和第 6 課的傳值呼叫不同) - 只要讀、不改的話,參數寫成
const int arr[],編譯器會擋下意外的修改
指標與陣列的關係(arr[i] 就是 *(arr + i))在第 8 課〈指標〉會詳細說明。
陣列參數其實是指標
進入 average 時,看右邊框架裡的 arr:它的值是一個位址,箭頭指回 main 的 scores。addBonus 透過這個位址修改元素,函式結束後 main 的陣列已經變了。注意兩個 sizeof 印出來的數字不同。
二維陣列
表格、座位表、棋盤這類有「列」和「行」的資料,可以用二維陣列:
int seats[2][3] = {{1, 2, 3},
{4, 5, 6}}; // 2 列、每列 3 個
seats[1][2] // 第 1 列、第 2 行(都從 0 算):6
二維陣列其實是「陣列的陣列」。在記憶體裡它仍然是一整排連續的空間,先放完第 0 列的 3 個,再接著放第 1 列,這叫列優先(row-major)。
所以 seats[0][2] 的下一格就是 seats[1][0],整個陣列佔 2 × 3 × 4 = 24 個位元組。
走訪二維陣列通常用巢狀迴圈:外層跑列、內層跑行。
二維陣列與列優先排列
巢狀迴圈裡看 r 和 c 的變化:c 跑完 0、1、2,r 才加 1。最後兩行印出的位址只差 4,表示 seats[0][2] 和 seats[1][0] 在記憶體裡是相鄰的。
想一想:陣列
下面的程式會印出什麼?
int a[5] = {1, 2};
cout << a[1] + a[4];
下面的程式會印出什麼?
int a[] = {3, 1, 4, 1, 5, 9};
cout << sizeof(a) / sizeof(a[0]);
下面的程式會印出什麼?
int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
cout << m[1][0] + m[0][2];
在 64 位元的 Linux 上用 g++ 編譯,下面的程式會印出什麼?
void f(int arr[]) {
cout << sizeof(arr);
}
int main() {
int a[10] = {};
f(a);
}
C 字串:以 '\0' 結尾的字元陣列
C 語言沒有字串型別,文字用字元陣列存放,並在最後放一個值為 0 的字元 '\0'(null 字元)表示「字串到這裡結束」:
char name[] = "Amy"; // 實際內容:'A' 'm' 'y' '\0',共 4 個字元
- 用雙引號寫的字串字面常數,編譯器會自動在最後加
'\0',所以sizeof(name)是 4 <cstring>的strlen從頭數到'\0'為止,不含'\0',所以strlen(name)是 3- 陣列不能用
=整個指定,也不能用==比較內容;要用strcpy、strcat、strcmp等函式 - 這些函式不檢查陣列夠不夠大,
strcat接上去的結果超出陣列就是越界
C 字串容易出錯,寫 C++ 時請優先使用 std::string;不過很多舊程式與函式庫還在用,main 的命令列參數也是 C 字串,所以要看得懂。
數到 '\0' 為止
右邊的 name 有 4 格,最後一格是 '\0'。while 迴圈每次檢查 name[len] 是不是 '\0',數到它就停,這就是 strlen 的做法。
std::string
<string> 標頭檔的 std::string 會自己管理記憶體,長度可以變化,用起來和其他語言的字串差不多:
| 操作 | 寫法 | 結果(s = "Hello") |
|---|---|---|
| 長度 | s.length() 或 s.size() |
5 |
| 存取字元 | s[0](不檢查範圍)、s.at(0)(越界會丟出例外) |
'H' |
| 串接 | s + " World"、s += "!" |
"Hello World" |
| 比較 | ==、!=、<、> |
依字元編碼逐字比較(字典順序) |
| 取子字串 | s.substr(起點, 長度) |
s.substr(1, 3) 是 "ell" |
| 搜尋 | s.find("ll") |
回傳第一次出現的索引 2;找不到回傳 string::npos |
| 走訪 | for (char c : s) |
一次拿一個字元 |
注意:
- 比較是依字元編碼:大寫字母的編碼比小寫小,所以
"Apple" < "apple"成立 find找不到時回傳的string::npos是一個很大的數,判斷要寫if (s.find(x) == string::npos),不要寫< 0length()算的是位元組數。中文字在 UTF-8 編碼下一個字通常佔 3 個位元組,所以string("你好").length()是 6,不是 2
std::string 的常用操作
full 是 "Ada Lovelace",索引 0 是 'A'、索引 3 是空白、索引 4 是 'L'。走訪字元的迴圈裡看 c 的變化;大寫的 'A' 不等於 'a',所以沒有算進母音。
想一想:字串
下面的程式會印出什麼?
char s[] = "hello";
cout << sizeof(s) << strlen(s);
下面的程式會印出什麼?
string s = "programming";
cout << s.substr(3, 4) << s.find('g');
下面的程式會印出什麼?
string a = "apple", b = "Apple";
cout << (a == b) << (a < b);
動手寫:判斷回文
回文是正著讀和倒著讀都一樣的字,例如 level。完成 isPalindrome:從兩端往中間比較字元,有一組不同就不是回文。main 已經寫好。
重點回顧
- 陣列是相同型別、連續存放的元素;大小固定,索引從 0 到 n - 1
sizeof(陣列) / sizeof(陣列[0])算元素個數,但只在陣列宣告的同一個作用域裡有效- 越界存取是未定義行為,C++ 不會檢查;迴圈條件要寫
i < n - 陣列傳給函式時退化成指向第一個元素的指標:長度要另外傳,函式可以修改原陣列,只讀時加
const - 二維陣列
a[列][行]一列接一列連續存放(列優先) - C 字串是以
'\0'結尾的字元陣列,用<cstring>的函式操作;新程式優先用std::string std::string:length()、+、==/<、substr(起點, 長度)、find找不到回傳string::npos