附錄五:從 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;
}
1
2
3
4
5
6
7

按編譯和執行,發現沒有任何問題,輸出也都正常。也就是說,我們在 C++ 的檔案裡面,是可以撰寫 C 語言的。

那麼在 C++ 的環境裡面寫 C 有什麼好處呢?有沒有什麼缺點呢?基本上這邊並不打算深入這個討論主題,而是想要介紹幾個簡單的 C++ 指令用法,這幾個指令用法搭配 C 語言使用,可能在線上解題的時候,可以省下一些時間和力氣。

在往下介紹之前,先介紹 C++ 語言裡面所謂命名空間 (Namespace) 的概念。基本上不管是變數或函數,都可以放在命名空間裡面。我們可以把命名空間想成是子目錄的概念,不同的子目錄可以放同名的檔案。同樣地,不同的命名空間,也可以有相同的變數或函數名稱,那我們在使用這些變數或函數的時候,要同時標示它的命名空間。例如指令 swap,是存放在 std 的命名空間中,在使用的時候,必須用 std::swap 的方式。如果覺得每次都要加上「std::」很麻煩,也可以在檔案前面加上以下指令:

using namespace std;
1

這表示要使用整個 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;
}
1
2
3
4
5
6
7
8
9
10
11
12

執行結果:

a=5, b=3
c=3.400000, d=1.200000
1
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;
}
1
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;
}
1
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;
}
1
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++ 語言和它的應用。