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()

存取方式 是否檢查邊界 索引錯誤時會怎樣 何時使用
v[i]
否 未定義行為(可能發生任何事) 當索引絕對正確時(例如正確的迴圈)
v.at(i)
是 明確的執行期錯誤 索引來自外部或在除錯時使用

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 位置存在,但那裡沒有元素。