National Chengchi University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

政治大學 AQI 58 25°C PM2.5 12
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 07

陣列與字串

一維與二維陣列、C 字串與 std::string
Interaction實作 25 minute(s)

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)成指標,帶來三個結果:

  1. 函式裡 sizeof(arr) 是指標的大小(64 位元系統上是 8),不是陣列的大小,所以元素個數要另外用參數傳進去。g++ 預設就會對這種寫法提出警告
  2. 函式裡修改 arr[i],改到的是呼叫端的陣列(和第 6 課的傳值呼叫不同)
  3. 只要讀、不改的話,參數寫成 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] 在記憶體裡是相鄰的。

想一想:陣列

1.

下面的程式會印出什麼?

int a[5] = {1, 2};
cout << a[1] + a[4];
2.

下面的程式會印出什麼?

int a[] = {3, 1, 4, 1, 5, 9};
cout << sizeof(a) / sizeof(a[0]);
3.

下面的程式會印出什麼?

int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
cout << m[1][0] + m[0][2];
4.

在 64 位元的 Linux 上用 g++ 編譯,下面的程式會印出什麼?

void f(int arr[]) {
    cout << sizeof(arr);
}
int main() {
    int a[10] = {};
    f(a);
}
第 1 次作答

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),不要寫 < 0
  • length() 算的是位元組數。中文字在 UTF-8 編碼下一個字通常佔 3 個位元組,所以 string("你好").length() 是 6,不是 2

std::string 的常用操作

full 是 "Ada Lovelace",索引 0 是 'A'、索引 3 是空白、索引 4 是 'L'。走訪字元的迴圈裡看 c 的變化;大寫的 'A' 不等於 'a',所以沒有算進母音。

想一想:字串

1.

下面的程式會印出什麼?

char s[] = "hello";
cout << sizeof(s) << strlen(s);
2.

下面的程式會印出什麼?

string s = "programming";
cout << s.substr(3, 4) << s.find('g');
3.

下面的程式會印出什麼?

string a = "apple", b = "Apple";
cout << (a == b) << (a < b);
第 1 次作答

動手寫:判斷回文

回文是正著讀和倒著讀都一樣的字,例如 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
這一課完成了!
Next lesson:指標
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。