1. 引言
當你第一次看到 std::vector 時,會覺得一切很簡單:v[0]、v[1],就這樣。但在真實程式中,索引幾乎從來不是「自動正確」的。它可能來自輸入、由公式計算、取決於條件,或因為 off-by-one 錯誤而不正確。因此,存取元素不是單純語法,而是一種紀律:如何避免程式崩潰或損壞資料。
可以把 vector 想像成倉庫裡的一排儲位。v.size() 告訴你有多少儲位實際放了箱子。如果你試圖從不存在的儲位取箱子,倉庫機器人不會禮貌地告訴你錯了,它會直接撞牆。在程式裡這會更戲劇化。
2. 索引與邊界:在哪裡可以、在哪裡不行
在討論哪個更好—[] 或 at()—之前,先把邊界說得非常清楚。
如果 size() == n,那麼合法的索引是:
- 從 0
- 到 n - 1
- 且只有當 n > 0 時
示意上可以這樣記:
v.size() == 5
索引: 0 1 2 3 4
元素: [ ] [ ] [ ] [ ] [ ]
v[5] — 不行(這是「最後一個之後」)
v[-1] — 不行(在 C++ 中這還會是另個陷阱)
我們來做個小範例,印出大小並提醒最後一個元素的索引(如果存在):
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
std::cout << "size = " << v.size() << '\n'; // size = 3
std::cout << "last index = " << (v.size() - 1) << '\n'; // last index = 2
}
這段程式只有在 v 非空時才正確。如果 v 是空的,表達式 v.size() - 1 就會成為壞主意。記住簡單規則:在確定大小 > 0 之前,不要從 size() 裡減去 1。
謹慎地印出「最後一個索引」
很常見的想法是:「想印出最後一個索引——那就是 size() - 1」。沒錯,但前提是 size() > 0。這時 empty() 是最可讀又不會出錯的方式。
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
if (!v.empty()) {
std::cout << "last index = " << (v.size() - 1) << '\n';
} else {
std::cout << "no last index: vector is empty\n"; // no last index: vector is empty
}
}
為什麼這很重要?因為空容器不是「稀有情況」,而是程式的常態。使用者還沒加入任何東西、檔案是空的、輸入沒來——這些都會讓 size() == 0。程式應能處理這種情況,而不是只在「一切都填滿且完美」的世界裡運作。
3. 以索引存取:operator[] 與 at()
operator[]:快速但沒有保護
接下來要說一個容易被低估的重要觀念。
v[i](也就是 operator[])不會檢查邊界。它最快且最「信任你」:你說「給我第 i 個元素」——它就嘗試給你。如果 i 不正確,程式行為會變成未定義:可能崩潰、可能印出垃圾、可能「暗中」破壞資料。最糟的是,有時錯誤不會馬上顯現,而是在 20 行程式碼之後才冒出來,屆時你可能已忘了錯在哪裡。
使用 [] 的簡單例子:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{5, 6, 7};
std::cout << v[0] << '\n'; // 5
std::cout << v[2] << '\n'; // 7
}
下面這個例子雖然能編譯,但邏輯上有風險(不要把它當作正常做法):
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{5, 6, 7};
std::cout << v[3] << '\n'; // 索引不正確:沒有索引 3 的元素
}
那為何還有 []?因為:
- 有時索引是保證正確的(例如你在正確範圍內遍歷 i),
- 有時效能很重要(在某些高效能情況下會需要),
- 有時你願意為了更簡潔的程式碼自行負責檢查。
一個實用的模型:
[] — 「我確定索引是正確的」。
at(i):有邊界檢查並明確報錯
方法 v.at(i) 與 v[i] 做的事情差不多,但有一個關鍵不同:它會檢查邊界。如果索引不正確,標準函式庫會明確地以執行期錯誤來示警。具體機制(內部如何實作)此處不深入;重點是:錯誤不會「暗中發生」。
當 at() 正常工作的例子:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{100, 200, 300};
std::cout << v.at(1) << '\n'; // 200
}
若索引錯誤,你會得到明確的執行期錯誤:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{100, 200, 300};
std::cout << v.at(3) << '\n'; // 索引錯誤:程式將異常終止
}
這並不代表 at() 可以取代思考。它的意義是:若發生錯誤,問題會立即浮現,較容易找出原因。
實用模型:at() — 「我幾乎確定,但希望一旦出錯能明顯顯示出來」。
主要的安全模式:先檢查,然後存取
最實際的場景是:索引來自外部。例如使用者輸入想查看的元素編號。在這種情況下,無論是 [] 還是 at(),在你檢查邊界之前都不該直接呼叫。
基本的檢查條件是:
如果 i < v.size(),則索引合法
否則 — 不合法
我們做一個小片段,模擬教學用的主控台應用。假設購物車裡有一組價格(暫時不使用任何類或函式),使用者輸入索引想查看價格:
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices{120, 80, 250};
std::size_t i = 0;
std::cin >> i;
if (i < prices.size()) {
std::cout << prices.at(i) << '\n'; // 例如,輸入 1 -> 80
} else {
std::cout << "沒有這個索引\n";
}
}
注意風格:我們先確認 i < prices.size(),然後才使用 prices.at(i)。看似多餘,但在實務上這是個好習慣:檢查是程式邏輯,at() 則是額外的保護帶。如果哪裡檢查失誤或索引在之後被改變,你會得到明顯的錯誤訊號。
如果你確信檢查無誤(例如在迴圈中),可以改用 []。但在學習期建議保守一些,以減少難以追蹤的崩潰。
簡易選擇表:使用 [] 還是 at()
| 存取方式 | 是否檢查邊界 | 索引錯誤時會怎樣 | 何時使用 |
|---|---|---|---|
|
否 | 未定義行為(可能發生任何事) | 當索引絕對正確時(例如正確的迴圈) |
|
是 | 明確的執行期錯誤 | 索引來自外部或在除錯時使用 |
4. empty()、front() 與 back():存取第一與最後元素
empty():如何優雅地問「vector 是空的嗎?」
判斷「vector 是否為空」是很常見的操作,尤其在呼叫 front() / back() 之前。
技術上 v.empty() 幾乎等同於 v.size() == 0。但 empty() 更接近自然語言:問「是空的嗎?」這讓條件式更易讀。
範例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
if (v.empty()) {
std::cout << "Vector is empty\n"; // Vector is empty
}
}
更實用的情況:假設應用中的「購物車」可能為空,我們要妥善處理:
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices; // 空的購物車
if (!prices.empty()) {
std::cout << "First price = " << prices[0] << '\n';
} else {
std::cout << "購物車為空\n"; // 購物車為空
}
}
關鍵是:在確認 vector 非空之前,不能使用 prices[0]。
front() 與 back():第一與最後元素
有時你不需要索引,只需要「第一個」或「最後一個」元素。例如購物車的第一件商品或最後加入的商品。
可以使用:
- v.front() — 第一個元素
- v.back() — 最後一個元素
這很方便,但有個嚴格契約:vector 必須非空。若為空,不能使用 front() 或 back()。
非空 vector 的範例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices{120, 80, 250};
std::cout << prices.front() << '\n'; // 120
std::cout << prices.back() << '\n'; // 250
}
現在看一個不會在空容器上崩潰的寫法:
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices;
if (!prices.empty()) {
std::cout << prices.front() << ' ' << prices.back() << '\n';
} else {
std::cout << "沒有價格:清單為空\n"; // 沒有價格:清單為空
}
}
這是很常見的範式:if (!v.empty()) { … front()/back() … }。
front/back 與索引:哪個更易讀
初學者有時會到處寫 v[0] 與 v[v.size() - 1] 來取得第一與最後元素。這在 vector 非空時可行,但可讀性較差,且更容易出錯。
比較看看:
// 選項 A
int first = v.front();
int last = v.back();
與
// 選項 B
int first = v[0];
int last = v[v.size() - 1];
選項 A 比較簡潔明瞭。選項 B 也可以,但有更多「尖角」:要記得 size() - 1、要記得不能在 size() == 0 時使用,且肉眼檢查是否安全也更困難。
因此今天的實務規則:如果你需要第一或最後元素,搭配 empty() 使用 front()/back()。索引用於需要特定編號的情況。
5. 實作範例:可瀏覽元素的「迷你購物車」
我們組一個小而完整的片段:有一個價格向量,我們希望能:
- 印出第一與最後價格(若存在),
- 印出使用者輸入索引的價格,
- 在空向量或索引錯誤時不崩潰。
暫時不做選單或拆成函式(課程後面會講)。只寫直接的 main,以便看清邏輯:
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices{120, 80, 250};
if (!prices.empty()) {
std::cout << "first=" << prices.front() << '\n'; // first=120
std::cout << "last=" << prices.back() << '\n'; // last=250
}
std::size_t i = 0;
std::cin >> i;
if (i < prices.size()) {
std::cout << "prices[" << i << "]=" << prices.at(i) << '\n';
// 輸入 1 -> prices[1]=80
} else {
std::cout << "Index out of range\n";
}
}
此程式中可見三個工具如何協同工作:empty()、front()/back() 與 at()。
6. 存取 std::vector 元素時常見的錯誤
錯誤 #1:在 vector 為空時存取 v[0]。
這是經典錯誤。程式能編譯,看起來合乎邏輯,但事實上你存取的是不存在的元素。通常發生在忘了 reserve() 不會建立元素,或剛建立 std::vector<int> v; 就立刻去取 v[0]。解法是簡單的原則:在取第一/最後元素或固定索引之前,問自己「size() 真的是 > 0 嗎?」並且不要害羞地寫 if (!v.empty())。
錯誤 #2:混淆「有記憶體容量」與「有元素」。
如果你呼叫 v.reserve(100),capacity() 可能增加,但 size() 保持不變。換句話說:元素仍然是零個,透過索引存取仍然非法。新手有時會認為「我 reserve 了,所以 vector 有 100 個元素」。不,是有 0 個元素,只是預留了較大的空間。要真正新增元素需使用 push_back() 或 resize()。
錯誤 #3:以為 at() 會「回傳某個空值」或自動修正錯誤索引。
at() 不會回傳「空值」來代替錯誤索引,也不會「修正」你的索引。它的用途是讓錯誤明顯可見。因此正確用法是:先檢查 i < v.size(),再呼叫 at(i)(或在確定無誤時使用 [])。若你期待 at() 自行處理錯誤,程式會在你預期之外的時刻異常終止。
錯誤 #4:在沒檢查 size() > 0 前,直接寫 v.size() - 1。
這個錯誤特別狡猾,因為程式看起來數學上正確。但在空的 vector 上並不存在「最後一個索引」。避免此陷阱的最簡單方法是在計算 v.size() - 1 之前檢查 v.empty(),以及在任何預期存在元素的操作前先確認存在。
錯誤 #5:邊界檢查寫成包含最後一個的形式。
有時會看到 if (i <= v.size()),以為這樣包括了最後一個元素。不是的:size() 是元素數量,而最後一個索引是 size() - 1(若有元素)。因此正確的檢查應為嚴格的 i < v.size()。若你想寫 <=,請停下來重檢你的索引模型:end 位置存在,但那裡沒有元素。