附錄五:從 C 到 C++
C++ 是從 C 衍生出來的物件化導向程式設計 (Object-Oriented Programming) 語言,C++ 幾乎包括了極大部份 C 語言的指令和語法,因此我們也可以在 C++ 的環境裡使用 C 語言。
C++ 裡面寫 C
我們來做一個簡單的測試,開啟新的 Code::Blocks 專案,在專案精靈操作過程中,選擇 C++ 而不要選擇 C,這樣開出來的專案,預設的程式碼如下:
這段程式碼一樣是印出「Hello, world!」,不過是使用 C++ 的指令而不是 C 的指令。另外,我們也可以看到主程式是 main.cpp,而不是 main.c。基本上,「.c」是 C 語言檔案預設的副檔名,而「.cpp」或「.cc」則是 C++ 語言檔案預設的副檔名。
現在把所有程式碼刪除,換成以下的程式碼:
#include <stdio.h>
int main()
{
printf("Hello world!\n");
return 0;
}
2
3
4
5
6
7
按編譯和執行,發現沒有任何問題,輸出也都正常。也就是說,我們在 C++ 的檔案裡面,是可以撰寫 C 語言的。
那麼在 C++ 的環境裡面寫 C 有什麼好處呢?有沒有什麼缺點呢?基本上這邊並不打算深入這個討論主題,而是想要介紹幾個簡單的 C++ 指令用法,這幾個指令用法搭配 C 語言使用,可能在線上解題的時候,可以省下一些時間和力氣。
在往下介紹之前,先介紹 C++ 語言裡面所謂命名空間 (Namespace) 的概念。基本上不管是變數或函數,都可以放在命名空間裡面。我們可以把命名空間想成是子目錄的概念,不同的子目錄可以放同名的檔案。同樣地,不同的命名空間,也可以有相同的變數或函數名稱,那我們在使用這些變數或函數的時候,要同時標示它的命名空間。例如指令 swap,是存放在 std 的命名空間中,在使用的時候,必須用 std::swap 的方式。如果覺得每次都要加上「std::」很麻煩,也可以在檔案前面加上以下指令:
using namespace std;
這表示要使用整個 std 命名空間裡的東西,那之後使用時,可以直接使用 swap,編譯器就會知道它用的是「std::swap」。以下所介紹的指令,都在 std 命名空間中。
swap
這個函數用來交換兩個變數的值,不管變數是什麼型態,只要兩個變數的型態相同就可以了。使用 swap 的時候,要引進 <iostream>
這個檔頭。參見以下範例:
#include <iostream>
#include <stdio.h>
int main()
{
int a=3, b=5;
float c=1.2, d=3.4;
std::swap(a, b);
std::swap(c, d);
printf("a=%d, b=%d\n", a, b);
printf("c=%f, d=%f\n", c, d);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
執行結果:
a=5, b=3
c=3.400000, d=1.200000
2
注意使用 swap 的時候,直接給變數名稱,前面不用加「&」。在 C 語言裡面,傳進函數的變數,如果前面不加「&」取地址,那麼呼叫函數之後,變數的值是不可能改變的。但在 C++ 裡面,有所謂變數別名的概念,可以達成這種效果。另外可以觀察,a 和 b 都是整數型態,而 c 和 d 都是浮點數型態,基本上 swap 可以針對任意的同型態的兩個變數進行交換。還有一點,有很多編譯器,例如GCC,在引入 <iostream>
檔頭的時候,會連帶引入 C 語言裡面的輸入輸出函數,這種情況下,第 2 行其實也可以省略。
sort
這個函數可以用來幫陣列排序,而且是很有效率的排序演算法,使用時,要引入 <algorithm>
。先看範例,再做說明:
#include <stdio.h>
#include <algorithm>
int main()
{
int a[]={3,4,5,2,1};
std::sort(a, a+5);
for (int i=0; i<5; i++) {
printf("%d ", a[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
第 6 行宣告 a 陣列有 5 個元素,依次為 3、4、5、2、1。第 7 行使用 sort 排序函數,第一個參數是陣列變數名稱,表示陣列的開始地址,第二個參數是變數名稱加長度 n,表示陣列開始地址再往後移 n 個元素的地址,也就是陣列最後元素的地址的下一個地址。排序完之後,陣列資料會從小到大排好,所以這個程式的輸出會變成 1 2 3 4 5
。
上面的範例是針對整數陣列做排序,實際上 sort 函數也和 swap 函數一樣,可以針對任意型態的陣列進行排序,不過要排序的型態必須能夠比較大小。如果是自訂的型態,例如結構,則必須自己定義比較大小的運算規則,這部份較複雜,有興趣的讀者可自行查閱其他資料。
reverse
這個函數可以用來把陣列元素顛倒過來,使用時,要引入 <algorithm>
。例如上一個範例,如果我們希望將陣列從大排到小,我們可以先用 sort 做排序,然後再把 reverse 把陣列反過來,程式碼如下:
#include <stdio.h>
#include <algorithm>
int main()
{
int a[]={3,4,5,2,1};
std::sort(a, a+5);
std::reverse(a, a+5);
for (int i=0; i<5; i++) {
printf("%d ", a[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
使用 reverse 的方式和 sort 的方式是相同的,本程式的輸出為 5 4 3 2 1
。和 sort 一樣,reverse 也可以針對不同型態的陣列進行操作。在APCS 的實作題中,有很多會用到排序的功能,如果讀者可以熟悉這兩個指令,可以省下許多寫排序函數的時間。
vector
向量和陣列類似,不過它比較有彈性,可以隨時擴充大小,也可以直接把其中任一個元素刪除,還可以隨時獲取向量目前的長度,不過使用上比之前的幾個指令還要複雜一些。以下就以「10510 實作題-定時K彈」那題為例,之前我們曾用陣列和遞迴方式都分別解過,但使用陣列的話,沒辦法通過所有測資。下面試著改用 vector 來處理看看:
#include <stdio.h>
#include <vector>
int main()
{
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
std::vector<int> v(n); // 宣告長度為n的整數vector
for (int i=0; i<n; i++) v[i] = i+1; // 儲存啟始數字
int idx = 0;
while (k--) { // 引爆K次
idx = (idx + m - 1) % v.size(); // 計算引爆點
v.erase(v.begin() + idx); // 刪除引爆位置
}
idx = idx % v.size(); // v個數已減1,要重新計算位置
printf("%d\n", v[idx]); // 輸出
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
實際上測試的結果,還是沒辦法通過所有測資,不過拿到了 95 分,也就是 20 組測資裡面,只有 1 組沒過,效率已經算相當不錯了!
這邊第 8 行宣告一個長度為 n 的整數向量。第 9 行給予向量元素初始值。第 12 行,使用 v.size() 取得目前向量的長度,用來計算下一次的引爆點。第13 行,直接將引爆點所在的數字從向量中刪除。對照原先的題目,應該不難了解這個演算法。我們也可以看到,這個題目使用向量來處理,比用陣列要容易和有效得多。
以上所介紹的幾個 C++ 指令,還有更多的功能和用法,例如我們也可以用 sort 函數來幫 vector 排序,不過用法稍有不同,但這些都已經超出本書所要討論的範圍。有興趣的讀者,可以在 C 語言的基礎之上,繼續學習其他書籍或資料,了解 C++ 語言和它的應用。