陣列是存放同型別元素的連續記憶體區塊;字串則是用來處理文字的基本工具。C++ 同時支援 C 風格陣列 / 字串以及更安全的 std::array / std::string

理解「陣列在記憶體裡是怎麼擺的」,是理解後續指標、字串、資料結構的關鍵。本章會大量使用記憶體示意圖。


學習目標

  1. 宣告、初始化與存取 C 風格陣列,並理解其記憶體佈局與「退化為指標」
  2. 使用多維陣列處理矩陣,理解其列優先(row-major)記憶體佈局
  3. 理解 C 風格字串(char[])與 '\0' 結尾的危險
  4. 熟練 std::string 的所有常用操作(find/substr/replace/append/insert/erase/compare/stoi/to_string…)
  5. 認識 std::array 的優勢,知道何時用它取代 C 陣列
  6. 認識 std::string_view(C++17)做為高效唯讀字串
  7. 區分 getline>>,掌握 <cctype> 字元函式
  8. 學會將陣列安全地作為函式參數傳遞

一、C 風格陣列

1.1 宣告與初始化

int nums[5];                     // 未初始化(區域陣列內容是垃圾值!)
int a[5] = {1, 2, 3, 4, 5};     // 完全初始化
int b[5] = {1, 2};              // 部分初始化,其餘自動補 0 → {1,2,0,0,0}
int c[]  = {10, 20, 30};        // 編譯器自動推算大小為 3
int d[5] = {};                  // 全部初始化為 0

1.2 記憶體佈局(關鍵概念)

陣列元素在記憶體中是連續且緊鄰排列的。以 int a[5] = {10,20,30,40,50}(假設 int 為 4 bytes,起始位址 0x1000)為例:

索引:        a[0]    a[1]    a[2]    a[3]    a[4]
          ┌───────┬───────┬───────┬───────┬───────┐
值:       │  10   │  20   │  30   │  40   │  50   │
          └───────┴───────┴───────┴───────┴───────┘
位址:      0x1000  0x1004  0x1008  0x100C  0x1010
            └─ 4 bytes ─┘

正因為連續,編譯器只要知道起始位址 + 索引 × 元素大小就能算出任一元素的位置:

a[i] 的位址 = 起始位址 + i × sizeof(元素)
a[3] 的位址 = 0x1000 + 3 × 4 = 0x100C   ✓

這也解釋了為什麼索引從 0 開始:索引其實是「距離起點的偏移量」,第一個元素偏移 0。

1.3 存取元素

a[0] = 100;          // 索引從 0 開始
int x = a[4];        // 最後一個元素(大小 5 → 合法索引是 0~4)
// a[5] → 越界!未定義行為(UB),C++ 不會幫你檢查

⚠ C 陣列沒有邊界檢查a[5]a[-1]a[1000] 都能編譯通過,但會讀寫到不屬於該陣列的記憶體,可能讀到垃圾、損毀資料,甚至崩潰。這是 C/C++ 最常見的 bug 來源。

1.4 走訪

int a[5] = {10, 20, 30, 40, 50};

// 傳統 for 迴圈
for (int i = 0; i < 5; i++) std::cout << a[i] << " ";   // 10 20 30 40 50

// 範圍 for 迴圈(C++11,推薦)——不必管索引,不會越界
for (int val : a) std::cout << val << " ";              // 10 20 30 40 50

// 取得區域陣列大小的慣用法(僅對「真正的陣列」有效,對指標無效!)
int n = sizeof(a) / sizeof(a[0]);   // 20 / 4 = 5

1.5 陣列「退化為指標」(Array-to-pointer Decay)

當你把陣列傳給函式,或對它賦值給指標時,陣列名會退化(decay)成「指向第一個元素的指標」,大小資訊就消失了

int a[5] = {1,2,3,4,5};
int* p = a;          // a 退化為 &a[0],p 指向第一個元素

std::cout << sizeof(a);   // 20(整個陣列,在原作用域)
std::cout << sizeof(p);   // 8(指標大小,大小資訊已丟失!)

這就是為什麼陣列傳入函式一定要另外傳大小(見第七節)。


二、多維陣列

2.1 宣告與記憶體佈局(列優先)

int matrix[3][4] = {
    {1,  2,  3,  4},
    {5,  6,  7,  8},
    {9, 10, 11, 12}
};

雖然我們在腦中把它想成 3 列 4 行的表格,但記憶體裡其實是一條連續的直線,以列優先(row-major)順序排列——第 0 列全部、接著第 1 列全部……:

邏輯上的 3×4 表格:           實際記憶體(連續,列優先):
┌────┬────┬────┬────┐       ┌─第0列──────────┬─第1列──────────┬─第2列─────────────┐
│ 1  │ 2  │ 3  │ 4  │       │ 1  2  3  4 │ 5  6  7  8 │ 9  10  11  12 │
├────┼────┼────┼────┤       └────────────┴────────────┴───────────────┘
│ 5  │ 6  │ 7  │ 8  │        位址遞增 →
├────┼────┼────┼────┤
│ 9  │ 10 │ 11 │ 12 │       matrix[i][j] 的偏移 = (i × 4 + j) 個 int
└────┴────┴────┴────┘       例:matrix[1][2] 偏移 = 1×4 + 2 = 6 → 值 7

效能啟示:因為列優先,逐列走訪(先固定 i 跑 j)比逐行走訪更符合記憶體順序,快取命中率高、速度快:

for (int i = 0; i < 3; i++)        // ✓ 好:順著記憶體走
    for (int j = 0; j < 4; j++)
        use(matrix[i][j]);

2.2 走訪

for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++)
        std::cout << matrix[i][j] << '\t';
    std::cout << '\n';
}

多維陣列傳入函式時,除了第一維,其餘維度大小必須指定(編譯器需要它來計算偏移): cpp void printMatrix(const int m[][4], int rows); // 第二維 4 不可省略


三、std::array(C++11,推薦取代 C 陣列)

#include <array>

std::array<int, 5> arr = {1, 2, 3, 4, 5};

arr.size();      // 5(永遠知道自己的大小,即使傳入函式也不丟失)
arr.at(2);       // 3(有邊界檢查,越界丟 std::out_of_range 例外)
arr[2];          // 3(無邊界檢查,較快)
arr.front();     // 1(第一個元素)
arr.back();      // 5(最後一個元素)
arr.fill(0);     // 全部填 0

C 陣列 vs std::array 比較

特性 C 陣列 int a[5] std::array<int,5>
知道自己大小 傳入函式後遺失 永遠保留 size()
邊界檢查 at()
可整個賦值 a = b
可用 == 比較 ✗(比的是位址) ✓(比內容)
可當回傳值
STL 演算法相容 有限 完全(有 begin()/end()
執行期開銷 (與 C 陣列一樣快)

C++ Core Guidelines SL.con.1 / ES.27:優先使用 std::array(固定大小)或 std::vector(動態大小),而非 C 風格陣列。std::array 是零開銷的安全升級,沒有理由不用。


四、C 風格字串(char[]

C 字串本質是'\0'(空字元,數值 0)結尾的 char 陣列

char greeting[] = "Hello";   // 大小其實是 6!編譯器自動加結尾 '\0'
char name[20];               // 可儲存最多 19 個字元 + 1 個 '\0'

4.1 '\0' 為什麼這麼重要

"Hello" 在記憶體中:

索引:   [0]  [1]  [2]  [3]  [4]  [5]
       ┌────┬────┬────┬────┬────┬──────┐
       │'H' │'e' │'l' │'l' │'o' │ '\0' │  ← 結尾標記
       └────┴────┴────┴────┴────┴──────┘
       sizeof = 6,strlen = 5(不含 '\0')

所有 C 字串函式(strlenstrcpyprintf("%s")…)都靠 '\0' 判斷字串到哪裡結束。'\0' 不見了,函式會一直讀下去,讀到不屬於字串的記憶體(讀到垃圾或崩潰):

char bad[] = {'H', 'i'};   // ✗ 沒有 '\0'!
std::cout << bad;          // UB:會印出 "Hi" 後繼續讀垃圾直到偶然碰到 0
char good[] = {'H', 'i', '\0'};  // ✓ 手動補上結尾

4.2 常用函式(<cstring>

函式 說明 危險性
strlen(s) 字串長度(不含 \0 若無 \0 會越界
strcpy(dst, src) 複製字串 不檢查 dst 大小 → 緩衝區溢位
strncpy(dst, src, n) 最多複製 n 字元 不保證補 \0,需手動補
strcat(dst, src) 串接到 dst 後面 同樣可能溢位
strcmp(s1, s2) 比較(0=相同、<0、>0)
char dst[20];
strcpy(dst, "Hello");        // dst = "Hello"
strcat(dst, ", World!");     // dst = "Hello, World!"

if (strcmp(dst, "Hello") == 0) { /* 相同 */ }   // 注意:== 0 才是相等!

絕對不要用 == 比較 C 字串s1 == s2 比的是指標位址,不是內容!必須用 strcmp。這是初學者的經典陷阱(見第八節)。


五、std::string(現代 C++ 首選)

std::string 自動管理記憶體、自動處理 '\0'、可動態增長、功能豐富。新程式碼幾乎都該用它

#include <string>

std::string s1;                  // 空字串
std::string s2 = "Hello";        // 從字串字面量
std::string s3("World");         // 直接初始化
std::string s4(5, '*');          // "*****"(重複字元)
std::string s5(s2, 1, 3);        // "ell"(從 s2 位置 1 取 3 字元)

5.1 串接(Concatenation)

std::string a = "C++", b = "好玩";
std::string c = a + " " + b;     // "C++ 好玩"
a += "11";                       // a 變 "C++11"
a.append("17");                  // a 變 "C++1117"

5.2 完整常用操作速查

操作 範例 結果 / 說明
長度 s.length() / s.size() 字元數
是否空 s.empty() true/false
串接 s1 + s2s.append(t)s += t 接在後面
比較 s1 == s2s1 < s2 直接用運算子(可!)
compare s1.compare(s2) 0=相同、<0、>0
搜尋 s.find("lo") 回傳位置,找不到回傳 std::string::npos
反向搜尋 s.rfind("lo") 從尾端找
子字串 s.substr(2, 3) 從位置 2 取 3 個字元
取代 s.replace(0, 5, "Hi") 把位置 0 起 5 個字元換成 "Hi"
插入 s.insert(5, "!") 在位置 5 插入
刪除 s.erase(0, 3) 從位置 0 刪 3 個字元
清空 s.clear() 變空字串
存取 s[i] / s.at(i) at 有邊界檢查
頭尾 s.front() / s.back() 第一/最後字元
數值→字串 std::to_string(42) "42"
字串→int std::stoi("42") 42
字串→double std::stod("3.14") 3.14
字串→long std::stol("99999") 99999

5.3 搜尋與 npos 的正確用法

std::string text = "Hello World, Hello C++!";

size_t pos = text.find("Hello");        // 0
size_t pos2 = text.find("Hello", 1);    // 13(從位置 1 之後再找)
size_t r = text.rfind("Hello");         // 13(最後一個)

if (text.find("Python") == std::string::npos) {
    std::cout << "找不到 Python\n";     // npos 代表「找不到」
}

find 找不到時回傳 std::string::npos(一個非常大的 size_t不是 -1)。千萬別寫 if (pos == -1)——雖然有時湊巧成立,但語意錯誤且不可移植。一律用 == std::string::npos

5.4 用 find + substr 分割字串(實用範例)

std::string csv = "Alice,Bob,Charlie";
size_t pos;
while ((pos = csv.find(',')) != std::string::npos) {
    std::cout << csv.substr(0, pos) << '\n';   // 取出逗號前的部分
    csv.erase(0, pos + 1);                     // 刪掉已處理的部分 + 逗號
}
std::cout << csv << '\n';                       // 最後一段
// 輸出:Alice / Bob / Charlie

5.5 走訪字串

std::string s = "C++17";

for (size_t i = 0; i < s.size(); i++) std::cout << s[i];   // 索引
for (char ch : s) std::cout << ch;                         // 範圍 for(推薦)
for (auto it = s.begin(); it != s.end(); ++it) std::cout << *it;  // 迭代器

六、std::string_view(C++17,高效唯讀)

std::string_view 是對既有字串的唯讀「視窗」——它不擁有、不複製字元,只記住「起點 + 長度」。當函式只需要讀取字串時,用 string_view 當參數可避免不必要的複製:

#include <string_view>

// 不論呼叫者給的是 std::string、C 字串還是字面量,都不會複製
void printLength(std::string_view sv) {
    std::cout << sv << " 長度 " << sv.size() << '\n';
}

printLength("hello");                 // 字面量,零複製
std::string name = "world";
printLength(name);                    // std::string,零複製

string_view 不擁有資料:若它指向的原字串被銷毀,string_view 就變成懸垂(dangling)。別回傳指向區域字串的 string_view。初學階段先知道概念即可,深入用法見後續章節。


七、把陣列傳入函式

C 陣列傳入函式會退化為指標,喪失大小,因此必須額外傳 size

// 以下三種等價,arr 都是 int*
void printArray(const int arr[], int size);
void printArray(const int* arr, int size);

void printArray(const int arr[], int size) {
    for (int i = 0; i < size; i++) std::cout << arr[i] << ' ';
    std::cout << '\n';
    // 此處 sizeof(arr) 是 8(指標),不是陣列大小!
}

int data[] = {1, 2, 3, 4, 5};
printArray(data, 5);                 // 必須手動傳大小
printArray(data, sizeof(data)/sizeof(data[0]));  // 在呼叫端算大小

更安全的現代做法是傳 std::array(用 const& 保留大小)或 std::vector

void printArr(const std::array<int, 5>& a) {
    for (int v : a) std::cout << v << ' ';   // a.size() 仍可用,不會丟失
}

八、字元函式(<cctype>

處理單一字元時,標準庫提供一組好用的判斷/轉換函式:

函式 說明 範例
isalpha(c) 是否為字母 isalpha('A') → 真
isdigit(c) 是否為數字 isdigit('7') → 真
isalnum(c) 字母或數字
isspace(c) 空白(空格/tab/換行)
isupper(c) / islower(c) 大/小寫
toupper(c) 轉大寫 toupper('a')'A'
tolower(c) 轉小寫 tolower('A')'a'
#include <cctype>
std::string s = "Hello, C++ 2026!";
int letters = 0, digits = 0;
for (char c : s) {
    if (std::isalpha(static_cast<unsigned char>(c))) letters++;
    else if (std::isdigit(static_cast<unsigned char>(c))) digits++;
}
// letters = 8, digits = 4

小技巧:用 ASCII 性質手動轉大寫 c = c - 'a' + 'A',但 std::toupper 更清楚、可移植。傳給 <cctype> 函式前轉成 unsigned char 可避免負值 UB。


九、getline vs >>(輸入字串)

方式 讀取範圍 遇到空白
std::cin >> s 一個「單詞」 停止(空白當分隔)
std::getline(std::cin, s) 一整行 照常讀入,遇換行才停
std::string word, line;
std::cin >> word;              // 輸入 "Hello World",word 只拿到 "Hello"
std::getline(std::cin, line);  // 讀整行(含空格)

經典陷阱:先用 cin >> x 再用 getlinegetline 會讀到 >> 留在緩衝區的換行符而得到空字串。解法:在 getline 前加 std::cin.ignore() 清掉殘留換行。


最佳實踐(Best Practices)

對應 C++ Core Guidelines

  1. SL.str.1:用 std::string 持有並擁有字元序列,別用 char*
  2. SL.str.2 / F.16:函式只讀字串時,用 std::string_view(C++17)或 const std::string& 當參數,避免複製。
  3. ES.27 / SL.con.1:用 std::array(固定大小)或 std::vector(動態大小)取代 C 陣列。
  4. ES.50:避免越界——優先用範圍 for 或 .at(),少用裸索引。
  5. 比較字串用 ==std::string)而非 strcmp;只有在處理 char* 時才用 strcmp
  6. 需要安全複製時,std::string 自動管理長度,免去 strcpy 溢位風險。
  7. C 字串只在與 C 函式庫 / 作業系統 API 互動時才用。

重點整理

概念 要點
C 陣列 連續記憶體、索引從 0、無邊界檢查、會退化為指標
陣列大小 sizeof(a)/sizeof(a[0]),但對指標無效
多維陣列 列優先連續排列,m[i][j] 偏移 = i×行數 + j
std::array 固定大小、零開銷、保留 size、有 at() 檢查
C 字串 char[] + '\0' 結尾;忘記 '\0' 是大坑
== 比較 C 字串 ✗ 比位址;要用 strcmp
std::string 自動管理、功能豐富、可用 == 比較,首選
find 找不到 回傳 std::string::npos,不是 -1
string_view 唯讀視窗、零複製,但不擁有資料
<cctype> isdigit/toupper 等字元函式
getline vs >> getline 讀整行,>> 讀單詞

常見錯誤與陷阱

  1. 陣列越界:存取 arr[size] 或更大索引是 UB,編譯器不擋。
  2. C 字串忘記 '\0':手動建立 char 陣列時漏掉空字元結尾 → 讀到垃圾。
  3. strcpy / strcat 緩衝區溢位:目標太小,多出的字元覆蓋相鄰記憶體。
  4. == 比較 C 字串:比的是指標位址而非內容,要用 strcmp
  5. C 陣列不能整個賦值或比較arr1 = arr2arr1 == arr2 不會做你想的事;用 std::array
  6. find 回傳值誤判:找不到是 npos(巨大值),別跟 -1 比較。
  7. 在函式內用 sizeof(陣列參數):得到的是指標大小(8),不是陣列大小。
  8. getline 接在 >> 後讀到空行:殘留換行作祟,需先 cin.ignore()
  9. strncpy 不補 '\0':複製滿 n 個字元時不會加結尾,需手動補。
  10. 多維陣列傳參省略行數void f(int m[][]) 不合法,除第一維外都要指定。

練習題(至少 5 題)

基礎

練習 1:反轉陣列(基礎) 寫函式 void reverse(int arr[], int size),將整數陣列原地反轉(不另開新陣列)。

提示:用 left=0right=size-1 兩個索引向中間靠攏,交換 arr[left]arr[right]

練習 2:陣列統計(基礎) 寫函式分別回傳整數陣列的最大值、最小值與平均值,在 main 印出。

提示:先把最大/最小設為 arr[0],再掃描比較;平均記得用 static_cast<double>(sum)/size 避免整數除法。

練習 3:字串大小寫轉換(基礎) 讀入一個 std::string(用 getline),輸出它的全大寫與全小寫版本。

提示:對每個字元用 std::toupper / std::tolower#include <cctype>),傳入前轉 unsigned char

中級

練習 4:字母頻率統計(中級) 統計一個 std::string 中每個英文字母(a–z,忽略大小寫)出現的次數,並印出非零者。

提示:用大小為 26 的 int count[26] = {};對字母 c,索引為 std::tolower(c) - 'a'

練習 5:矩陣轉置(中級) 把一個 3×3 的二維陣列轉置(行列互換)後印出。

提示:轉置即 result[j][i] = matrix[i][j];也可原地交換上三角與下三角(i<j 時 swap)。

練習 6:回文判斷(中級) 判斷一個 std::string 是否為回文,忽略大小寫與非字母字元(如 "A man, a plan, a canal: Panama" 算回文)。

提示:用兩個索引從兩端往內;用 isalpha 跳過非字母,用 tolower 統一大小寫再比較。

挑戰

練習 7:凱撒密碼(挑戰) 實作 std::string caesarEncrypt(const std::string& s, int shift) 與對應的解密函式。只移位英文字母,保留大小寫與其他字元。

提示:對字母 c,基底取 'A''a',新字元 = base + (c - base + shift) % 26;解密用 26 - shift(或負位移取模時補正)。

練習 8:字串分割成 vector(挑戰)std::vector<std::string> split(const std::string& s, char delim),把字串依分隔字元切成多段回傳。處理連續分隔與結尾分隔的邊界情況。

提示:用 find 找分隔位置、substr 取段、再從分隔之後繼續;或逐字元累積,遇到 delim 就推入結果並清空暫存。

練習 9:簡易單字計數器(挑戰) 讀入多行文字(直到 EOF),用 std::string 操作統計總字數與最長單字。

提示:可用 std::cin >> word 逐詞讀取(自動以空白分隔),維護計數器與目前最長者。


對應範例程式

檔案 說明
arrays.cpp C 陣列、多維陣列、std::array
c_strings.cpp C 風格字串操作與安全性
std_string.cpp std::string 完整操作

編譯與執行

g++ -std=c++17 -Wall -o arrays arrays.cpp && ./arrays
g++ -std=c++17 -Wall -o c_strings c_strings.cpp && ./c_strings
g++ -std=c++17 -Wall -o std_string std_string.cpp && ./std_string

延伸閱讀