陣列是存放同型別元素的連續記憶體區塊;字串則是用來處理文字的基本工具。C++ 同時支援 C 風格陣列 / 字串以及更安全的
std::array/std::string。理解「陣列在記憶體裡是怎麼擺的」,是理解後續指標、字串、資料結構的關鍵。本章會大量使用記憶體示意圖。
學習目標
- 宣告、初始化與存取 C 風格陣列,並理解其記憶體佈局與「退化為指標」
- 使用多維陣列處理矩陣,理解其列優先(row-major)記憶體佈局
- 理解 C 風格字串(
char[])與'\0'結尾的危險 - 熟練
std::string的所有常用操作(find/substr/replace/append/insert/erase/compare/stoi/to_string…) - 認識
std::array的優勢,知道何時用它取代 C 陣列 - 認識
std::string_view(C++17)做為高效唯讀字串 - 區分
getline與>>,掌握<cctype>字元函式 - 學會將陣列安全地作為函式參數傳遞
一、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 字串函式(strlen、strcpy、printf("%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 + s2、s.append(t)、s += t |
接在後面 |
| 比較 | s1 == s2、s1 < 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再用getline,getline會讀到>>留在緩衝區的換行符而得到空字串。解法:在getline前加std::cin.ignore()清掉殘留換行。
最佳實踐(Best Practices)
- SL.str.1:用
std::string持有並擁有字元序列,別用char*。 - SL.str.2 / F.16:函式只讀字串時,用
std::string_view(C++17)或const std::string&當參數,避免複製。 - ES.27 / SL.con.1:用
std::array(固定大小)或std::vector(動態大小)取代 C 陣列。 - ES.50:避免越界——優先用範圍 for 或
.at(),少用裸索引。 - 比較字串用
==(std::string)而非strcmp;只有在處理char*時才用strcmp。 - 需要安全複製時,
std::string自動管理長度,免去strcpy溢位風險。 - 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 讀整行,>> 讀單詞 |
常見錯誤與陷阱
- 陣列越界:存取
arr[size]或更大索引是 UB,編譯器不擋。 - C 字串忘記
'\0':手動建立char陣列時漏掉空字元結尾 → 讀到垃圾。 strcpy/strcat緩衝區溢位:目標太小,多出的字元覆蓋相鄰記憶體。- 用
==比較 C 字串:比的是指標位址而非內容,要用strcmp。 - C 陣列不能整個賦值或比較:
arr1 = arr2、arr1 == arr2不會做你想的事;用std::array。 find回傳值誤判:找不到是npos(巨大值),別跟 -1 比較。- 在函式內用
sizeof(陣列參數):得到的是指標大小(8),不是陣列大小。 getline接在>>後讀到空行:殘留換行作祟,需先cin.ignore()。strncpy不補'\0':複製滿 n 個字元時不會加結尾,需手動補。- 多維陣列傳參省略行數:
void f(int m[][])不合法,除第一維外都要指定。
練習題(至少 5 題)
基礎
練習 1:反轉陣列(基礎)
寫函式 void reverse(int arr[], int size),將整數陣列原地反轉(不另開新陣列)。
提示:用
left=0、right=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
延伸閱讀
- cppreference — Arrays
- cppreference — std::array
- cppreference — std::string
- cppreference — std::string_view
- cppreference —
- C++ Core Guidelines — SL.str: String
- 下一章預告:Ch06 將深入指標與參考,揭開「陣列退化為指標」與字串底層的真相。