了解了前面文章的內容後,已經可以使用程式解決一些基本的問題了。但目前為止能夠解決的問題有兩個很大的侷限:
1. 無法一次宣告多個變數
舉例來說,使用者依序輸入了 n 個數字,並且希望用相反的順序將所有的數字印出來的話,這時候就會需要「一個一個」宣告 n 個變數,因為需要用變數記住所有輸入的數字,才有辦法用相反的順序印出來。以下為 n = 3 的範例程式:
int x, y, z;
cin >> x >> y >> z;
cout << z << ' ' << y << ' ' << x <<;
這有一個顯而易見的問題,假如使用者想輸入一百個數字,難道要宣告一百個變數嗎? 顯然是不可行的。
2. 需要事先知道資料的數量。
接續上述的例子,我們在寫程式的時候,需要知道 n 的值為多少,才有辦法宣告對應數量的變數。這樣的問題也很明顯,大部分的程式在寫的時候根本不知道資料的數量會有多少。
為了解決上述的兩個問題,我們要來學習 C++ 標準函式庫中提供的一個強大的容器 (container): vector。
補充: C++ 的標準函式庫定義了許多專門用來儲存資料的容器 (container),比起一般的變數如: int 或 char,容器擁有更多強大的功能,使用起來非常方便。前面所介紹的 string 也是容器之一,它的功用應該不難猜: string 是專門用來儲存一堆 char 的容器。
注意: 在程式中使用 vector 前,要先導入相關的函式庫 <vector>,如下所示:
#include <vector>
如果把變數比喻成一個盒子,可以把資料放進盒子,那 vector 就是一堆黏在一起的盒子。下列程式宣告了一個型態為整數的變數 x 以及型態為整數的 vector vec:
int x = 0;
vector<int> vec(6, 0);
宣告與初始化 vector 的方式幾乎就跟 x 一樣:
可以把 vector<int> vec(6, 0) 想像成把六個資料型態為 int 的變數黏在一起,並且幫它們取一個新名字: vec。下面是 vec 的示意圖:
vector 裡面包含的資料數量稱為 vector 的「長度」。換句話說, vec 是一個資料型態為 vector<int> 且長度為 6 的 vector。
我們也可以在 vector 裡面裝其他的資料型態,下面是一些範例:
// vector of char, initialized with 2 char: 'a'
vector<char> vec_c(2, 'a');
// vector of float, initialized with 10 float: '3.14'
vector<char> vec_f(10, 3.14);
// vector of bool, initialized with 6 bool: true
vector<bool> vec_b(6, true);
事實上,目前為止 (還有以後會學到的) 資料型態都可以裝到 vec 裡面,包含 string! 因為 string 也是一種資料型態,我們可以宣告一個「用來儲存 string 的 vector」,方法就跟上述的其他例子一模一樣:
// vector of string, initialized with 2 string: "Hi!"
vector<string> vec_str(2, "Hi!");
補充: string 是由一堆 char 組成的,所以 string 跟 vector<char> 概念上很接近。它們的差別在於,因為許多程式都需要進行大量的文字、符號的處理,所以特別設計了獨立的 string 資料型態,裡面定義了許多好用的功能。string 基本上就是 vector<char> 加上很多專門用來操作字元和字串的功能。
還有另一種初始化 vector 的方法,叫做 list initialize,它的用途是直接指定一串常值存到 vector 裡面。list initialize 的好處是每個 element 的初始值不一定要相同:
// list initialize a vector
vector<char> vec{-1, 5, 10, 2};
上述的 vec 有四個 element,分別為 -1, 5, 10, 2。
前面有提到 vector<int> vec(6, 0) 就是把六個 int 變數黏在一起,這六個變數有各自的編號,如下圖所示:
編號從 0 開始到 5,每個編號都代表一個型態為整數的變數。在程式語言中,有專門的術語來稱呼編號和其對應到的變數: index (索引) 和 element (元素)。
舉例來說,vec 中的第一個變數是「index 為 0 的 element」,最後一個變數是「index 為 5 的 element」,依此類推。
注意: 在電腦和程式的世界中,所有東西都是從 0 開始算,不是 1。所以 vec 的第一個 element 的 index 是 0,不是 1。
我們可以透過 index 來使用/修改 vector 中 element 的資料,就跟一般的變數一樣。實際的寫法是在 vector 的變數名後面加上中括號,中括號裡面填 element 的 index: 變數名稱[index]。
下面的程式將 vec 中 index 為 2 的 element (第三個變數) 加上 10 以後印出來:
vector<int> vec(6, 0);
vec[2] += 10;
// print 10
cout << vec[2];
用法就跟一般的變數一樣。
下面的例子使用 list initialize 初始化 vector:
vector<int> vec{0, 11, -3};
vec[2] += 10;
// print 7
cout << vec[2];
會將 index 為 2 的 element (第三個) 加上 10,並印出來。
vector 可以幫助我們解決「待解決的問題」中的第一個問題: 無法一次宣告多個變數。
也可以不對 vector 進行初始化,如下所示:
vector<int> vec;
上面宣告了一個資料型態為 vector<int> 且沒有任何 element 的 vector。這種 vector 目前對我們來說還沒有用處,但它在後面會派上用場的。
走訪 (iterate) vector 的意思是對 vector 的 element 進行存取 (拿出裡面的資料、修改資料、或是任何使用的行為) 。有時候會使用另一個術語: 迭代,它跟走訪是相同的意思。
我們可以利用 for 的 induction variable 來計算 vector element 的 index。下面的程式會走訪 vector 中的所有 element,並計算它們的總和:
// vector of int, initialized with 10 integer: 2
vector<int> vec(10, 2);
int sum = 0;
for (int i = 0; i < 10; ++i)
sum += vec[i];
cout << sum;
首先宣告了一個資料型態為 vector<int> 的 vector: vec,且初始狀態為 10 個整數 2。接著我們又宣告了一個變數 sum 初始化為 0,用來計算 vec 所有 element 的總和。
for 迴圈的部分,因為第一個 element 的 index 為 0,所以將 i 初始化為 0,然後每走訪完一個 element 並將它的值加到 sum 以後,就將 i 的值加 1。注意 i 必須要小於 10 (最後一個 element 的 index 為 9)。
vector 有一個好用的函式,叫做 size(),它會回傳目前這個 vector 的長度 (有幾個 element),使用方式為: 變數名稱.size(),範例如下:
// vector of string, initialized with 10 string: "?"
vector<string> vec(10, "?");
cout << vec.size();
注意: 需要在變數名稱和函式中間加上 . 的原因是,我們這邊使用的 size() 是 vector<int> 這個資料型態所定義的函式。沒錯! 資料型態不只是定義資料的格式而已,還可以為這個資料型態定義專屬的函式。string 也有定義 size() 這個函式,它會回傳目前這個 string 中包含了幾個字元,範例如下:
string str = "Hello C++!";
cout << str.size();
可以發現兩個範例的用法一模一樣,想達成的目的也一樣,但它們一個定義在 vector<int> 中,一個定義在 string 中,所以是不一樣的函式。
了解了 size() 怎麼使用以後,我們可以將一開始計算 element 總和的程式改得更通用,本來 i 的上限寫死成 10,現在可以把它替換成 vec.size(),讓電腦幫我們算出目前的 vec 有多少 element 並設成 induction variable 的上限:
// vector of int, initialized with 10 integer: 2
vector<int> vec(10, 2);
int sum = 0;
for (int i = 0; i < vec.size(); ++i)
sum += vec[i];
cout << sum;
下列是使用 list initialize 的範例:
// list initialize
vector<int> vec{-1, 2, 10, 3, 5, -4};
int sum = 0;
for (int i = 0; i < vec.size(); ++i)
sum += vec[i];
cout << sum;
注意: 我們可以使用 vec.size() 算出利用 list initialize 進行初始化以後,vec 擁有多少個 element。
範例1: 計算 vector 中所有偶數的總和
// list initialize
vector<int> vec{-1, 2, 10, 3, 5, -4};
int sum = 0;
for (int i = 0; i < vec.size(); ++i) {
if (vec[i] % 2 == 0)
sum += vec[i];
}
cout << sum;
範例2: 將兩個長度相同的 vector 中相同 index 所對應到的 element 相加,存入第三個 vector 對應的 element 中,並將第三個 vector 的 element 印出來。
// list initialize
vector<int> vec1{-1, 2, 10, 3, 5, -4};
vector<int> vec2{12, 0, 11, -3, 2, -10};
// initialize with six 0
vector<int> vec3(6, 0);
for (int i = 0; i < vec1.size(); ++i)
vec3[i] = vec1[i] + vec2[i];
for (int i = 0; i < vec1.size(); ++i)
cout << vec3[i] << ' ';
範例3: 比較兩個長度相同的 vector 中,每個 index 所對應到的 element 誰比較大,將比較大的數值存入第三個 vector 對應的 element 中,並將第三個 vector 的 element 印出來。
// list initialize
vector<int> vec1{-1, 2, 10, 3, 5, -4};
vector<int> vec2{12, 0, 11, -3, 2, -10};
// initialize with six 0
vector<int> vec3(6, 0);
for (int i = 0; i < vec1.size(); ++i) {
if (vec1[i] > vec2[i])
vec3[i] = vec1[i];
else
vec3[i] = vec2[i];
}
for (int i = 0; i < vec1.size(); ++i)
cout << vec3[i] << ' ';
vector 像前面介紹過的 C++ 內建資料型態一樣,也可以使用 =
運算子,如:
vector<int> vec1{1, 2, 3};
vector<int> vec2{-1};
// assign vec2 to vec1
vec1 = vec2;
// print 1 2 3
for (int i = 0; i < vec2.size(); ++i)
cout << vec2[i] << ' ';
vec2 = vec1 的意思是將 vec2 儲存的資料清空,並將 vec1 所有 element 的值複製給 vec2。也就是說,vec1 的內容會跟 vec2 變得一模一樣。
也可以利用 =
來進行 vector 的初始化:
vector<int> vec1{1, 2, 3};
vector<int> vec2 = vec1;
// print 1 2 3
for (int i = 0; i < vec2.size(); ++i)
cout << vec2[i] << ' ';
就像一般變數一樣,我們可以利用 =
運算來交換兩個 vector 的資料:
vector<int> vec1{1, 2, 3};
vector<int> vec2{-1};
// temparary vector for swap
vector<int> tmp;
tmp = vec1;
vec1 = vec2;
vec2 = tmp;
不過 vector 其實有提供專門的函式來交換資料,就叫做 swap():
vector<int> vec1{1, 2, 3};
vector<int> vec2{-1};
vec1.swap(vec2);
上述兩段程式會得到一樣的結果,但第二個寫法明顯比較簡潔,讓未來讀程式碼的人一眼就看出這段程式在做什麼。
注意: 往後會學到更多 C++ 標準函式庫提供給我們的函式,善用這些函式來撰寫程式不只可以加快開發流程,還可以讓程式碼更簡潔、更好讀,執行起來通常也會更有效率。
對一個長度為 n 的 vector 來說,第一個 element 的 index 為 0,最後一個 element 的 index 為 n -1。
這件事情很重要的原因是,使用 index 來存取 element 的時候,程式設計師有責任確保這個 index 是在正確的範圍內的,也就是 index 必須要 >= 0 且 <= n-1。
假如有個 vector<int> vec 的長度為 5,vec[5] 這個操作是錯誤的! 因為最後一個 element 的 index 是 5 - 1 = 4,index 5 已經超出 vec 目前涵蓋的範圍了。
最可怕的事情是,這種錯誤有時候很難被發現,因為程式有可能會像是沒事一樣的繼續執行 (原因之後會解釋)。而且不只是新手可能犯這個錯,就算是有經驗的程式設計師,也可能在撰寫比較複雜的程式的時候,不小心用超出範圍的 index 去存取 vector。
這種類型的錯誤可能會成為資安漏洞。事實上,許多軟體的漏洞都是這類錯誤造成的。
這裡要介紹 vector 另一個很常用的函式: push_back()。使用這個函式的時候需要傳入一個常值或變數,或可以產生數值的運算式 (如兩個變數相加),如下所示:
vec.push_back(1);
int x = 10;
vec.push_back(x);
vec.push_back(x + 4);
這個函式的功用是在目前這個 vector 的尾端加入一個新的 element,並將它的值初始化為參數傳入的值。舉個例子來說,假設我現在有一個 vector<int> vec,如下所示,它目前有三個 element,分別為 3, -1, 0:
假設我們執行 vec.push_back(7),vec 就會在尾端加上一個整數型態的 element 並初始化為 7:
範例: 將兩個長度相同的 vector 中相同 index 所對應到的 element 相加,存入第三個 vector 對應的 element 中,並將第三個 vector 的 element 印出來。
// list initialize
vector<int> vec1{-1, 2, 10, 3, 5, -4};
vector<int> vec2{12, 0, 11, -3, 2, -10};
// size of vec3 is 0
vector<int> vec3;
for (int i = 0; i < vec1.size(); ++i)
vec3.push_back(vec1[i] + vec2[i]);
for (int i = 0; i < vec1.size(); ++i)
cout << vec3[i] << ' ';
注意: 這題其實在前面有出現過,差別在於這次我們不對 vec3 進行初始化 (長度為 0)。這樣寫的好處是,我們可以讓使用者決定 vec1 和 vec2 要裝什麼資料,無論使用者想輸入多少資料,我們都能順利算出兩個 element 的和並利用 push_back() 加到 vec3 中。
學會使用 push_back() 以後,我們就克服了「待解決的問題」中的第二個問題: 需要事先知道資料的數量。
現在我們有能力解決「待解決的問題」所提出的程式問題: 讓使用者依序輸入 n 個數字,並且用相反的順序將所有的數字印出來:
int n;
cin >> n;
// the size of vec is 0
vector<int> vec;
// read n numbers
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
vec.push_back(x);
}
// print n numbers in reverse order
for (int i = n-1; i >= 0; --i)
cout << vec[i] << ' ';
注意: 第二個 for 的功用是把 element 以相反的順序印出來,對於長度為 n 的 vector 來說,第一個 element 的 index 是 0,最後一個 element 的 index 是 n - 1,所以 i 的範圍才會設成 0 ~ n-1,並且在每次迴圈後就會減 1。
clear() 的作用是清空目前的 vec,如下所示:
vector<int> vec{1, 2, 3};
vec.clear();
// print 0
cout << vec.size();
在呼叫 clear() 以後,因為所有 element 都被清除了,所以 size() 會回傳 0。