回目錄

第八章 函數

原 Google Sites 連結:/cppzero/di-ba-zhang-han-shu

目錄

8.1 使用者自定義函數

c184. 盈虧互補

n 的真因數:除了 n 本身以外的所有因數,包含 1。友好數:如果 n 真因數和為 m,m 的真因數和為 n,則 n、m 互為「友好數」。題目:給定 n,求 n 的友好數。若 n 的友好數為 n 本身,請輸出「=n」,若 n 沒有友好數,請輸出「0」。 這題可以用以下的程式碼來 AC。

//2007jfB. 友好數 by Snail
#include <iostream>
using namespace std;

int main () {
    int n, n1, m, f;
    while (cin >> n, n) {
        m = 1;                                  // 加入因數 1
        for (f=2; f*f<n; f++)                   // f(actor) -- 因數
            if (n % f == 0)                     // 若 f 為 n 的因數
                m += f + n/f;                   // 加入 f 及 f/n
        if (f*f == n)                           // 若 n 為完全平方數
            m += f;                             // 加入平方根
        if (m == n)                             // 若 n 的友好數為自身
            cout << '=' << m << endl;           // 加一個「=」
        else {
            n1 = 1;                             // 求 m 的真因數和 n1
            for (f=2; f*f<m; f++)
                if (m % f == 0)
                    n1 += f + m/f;
            if (f*f == m)
                n1 += f;
            if (n1 == n)                        // 若 m 的真因數和為 n
                cout << m << endl;              // m 即是 n 的友好數
            else                                // 否則
                cout << "0\n";                  // n 沒有友好數
        }
    }
}

從上面的程式中,我們先求 n 的真因數和 m,再求 m 的真因數和 n1,求真因數和的動作做了兩次,相同的程式碼也寫了兩次。這樣的寫法除了浪費記憶體外,在軟體工程上也有它的問題,因為它會造成維護上的困擾。將來如果求真因數和的程式需要修改,那麼我們得修改兩處,萬一其中一處遺漏了,就會造成程式的錯誤。

因此,像這樣重覆使用的程式碼,應讓有一個機制讓它只寫一次就好,這個機制叫作「函數」。我們把上面的程式碼用「函數」改寫如下:

//2007jfB. 友好數 by Snail
#include <iostream>
using namespace std;

void sof () {                                   // s(um) o(f) f(actors)
    m = 1;                                      // 加入因數 1
    for (f=2; f*f<n; f++)                       // f(actor) -- 因數
        if (n % f == 0)                         // 若 f 為 n 的因數
            m += f + n/f;                       // 加入 f 及 f/n
    if (f*f == n)                               // 若 n 為完全平方數
        m += f;                                 // 加入平方根
}

int main () {
    int n, n1, m, f;
    while (cin >> n, n) {
        sof();                                  // 呼叫 sof 函數
        if (m == n)                             // 若 n 的友好數為自身
            cout << '=' << m << endl;           // 加一個「=」
        else {
            n1 = 1;                             // 求 m 的真因數和 n1
            for (f=2; f*f<m; f++)
                if (m % f == 0)
                    n1 += f + m/f;
            if (f*f == m)
                n1 += f;
            if (n1 == n)                        // 若 m 的真因數和為 n
                cout << m << endl;              // m 即是 n 的友好數
            else                                // 否則
                cout << "0\n";                  // n 沒有友好數
        }
    }
}

在這個程式裡,一共有兩個「函數」(funtcion):int main () 及 void sof ()。void 所代表的意義我們待會再討論。int main () 是程式的「入口」,程式執行時,會先執行這個「函數」。至於其他的函數,只有在被「呼叫」(call) 時才會執行。如果你寫了一個函數卻不去呼叫它,那麼它就不會被執行,等於是白寫。因此,在 int main () 中有一個呼叫 sof () 函數的陳述式:

sof();                                  // 呼叫 sof 函數

執行到這行時,程式就會先跳到 void sof () 函數去,等 void sof () 裡的程式執行完後再跳回 int main ()。

不過,上面這個程式在編譯時得到以下的錯誤訊息:

1>d:\cpps\cpps\2007jfb. 友好數 a.cpp(6) : error C2065: 'm' : 未宣告的識別項
1>d:\cpps\cpps\2007jfb. 友好數 a.cpp(7) : error C2065: 'f' : 未宣告的識別項
1>d:\cpps\cpps\2007jfb. 友好數 a.cpp(7) : error C2065: 'n' : 未宣告的識別項

在 int main () 的裡面我們定義了 4 個變數。

int n, n1, m, f;

這種定義在函數內部的變數我們稱之為「區域變數」,它的有效範圍僅限於該函數的內部。所以這 4 個變數是 int main () 自己的變數,void sof () 是存取不到的。因此當 void sof () 中使用到 n, m, f 等變數時,便會產生「未宣告的識別項」的錯誤。

你當然也可以在 void sof () 中自己定義這些變數:

void sof () {                                   // s(um) o(f) f(actors)
    int n, m, f;
    m = 1;                                      // 加入因數 1
    for (f=2; f*f<n; f++)                       // f(actor) -- 因數
        if (n % f == 0)                         // 若 f 為 n 的因數
            m += f + n/f;                       // 加入 f 及 f/n
    if (f*f == n)                               // 若 n 為完全平方數
        m += f;                                 // 加入平方根
}

這樣程式雖然通過編譯並執行,但是卻產生的下列的「警告」:

1>d:\cpps\cpps\2007jfb. 友好數 a.cpp(8) : warning C4700: 使用了未初始化的區域變數 'n'
1>d:\cpps\cpps\2007jfb. 友好數 a.cpp(19) : warning C4700: 使用了未初始化的區域變數 'm'

根據程式的邏輯,在 void sof () 所使用的 n 應該是在 int main () 所輸入的 n。我們另行在 void sof () 所宣告的 n 卻是另一個 n,儘管兩個變數名稱都是 n,但是它們卻是不同的個體。你在 int main () 輸入了 n,但是 void sof () 的 n 卻沒有改變。 要讓 int main () 和 void sof () 共用一個 n,你需要把它定義為「全域變數」,方法是把這個 n 宣告在兩個函數的「外面」:

//2007jfB. 友好數 by Snail
#include <iostream>
using namespace std;

int n, m;

void sof () {                                   // s(um) o(f) f(actors)
    int f;
    m = 1;                                      // 加入因數 1
    for (f=2; f*f<n; f++)                       // f(actor) -- 因數
        if (n % f == 0)                         // 若 f 為 n 的因數
            m += f + n/f;                       // 加入 f 及 f/n
    if (f*f == n)                               // 若 n 為完全平方數
        m += f;                                 // 加入平方根
}

int main () {
    int n1, f;
    while (cin >> n, n) {
        sof();                                  // 呼叫 sof 函數
        if (m == n)                             // 若 n 的友好數為自身
            cout << '=' << m << endl;           // 加一個「=」
        else {
            n1 = 1;                             // 求 m 的真因數和 n1
            for (f=2; f*f<m; f++)
                if (m % f == 0)
                    n1 += f + m/f;
            if (f*f == m)
                n1 += f;
            if (n1 == n)                        // 若 m 的真因數和為 n
                cout << m << endl;              // m 即是 n 的友好數
            else                                // 否則
                cout << "0\n";                  // n 沒有友好數
        }
    }
}

記得要同時把函數內的同名區域變數拿掉,否則程式會優先取用區域變數,而不是全域變數。 由於 int main () 和 void sof () 都會用到 n, m,所以這兩個全域變數的宣告要出現在這兩個函數之前,否則還是會出現「未宣告的識別項」的錯誤。同理,由於 int main () 中會去呼叫 sof (),所以 void sof() 函數也要出現在 int main () 之前。

你可能會好奇,既然 n, m 要宣告為全域變數,為什麼不把 n1 和 f 也一起宣告為全域,這樣程式也比較簡短。基於易於維護的因素,除非真正必要的時候,我們通常會儘量避免全域變數的使用。由於每個函數都可以去修改全域變數的值,有時候甲函數修改了某變數的值,乙函數卻不知道,這就會造成一些錯誤了。在上面的程式中,int main () 輸入了 n 的值,而 void sof () 則需要求這個 n 的真因數和,void sof () 所求得的真因數和 m 也需要在 int main () 中做進一步的判斷與處理,所以這兩個變數一定要宣告成全域變數,其他的變數我們就宣告為區域變數。在這個程式中,即使你把 n, m, n1, f 都宣告為全域變數,程式仍能跑出正確的結果,但是基於培養良好的程式設計習慣,我們還是把 n1 和 f 宣告為區域變數比較好。

經過這番調整之後,這個程式終於可以正確執行了。但是我們當初寫 void sof () 這個函數的目的就是希望可以把相同的兩段程式碼簡化成一段,當我們試圖進一步用 void sof () 來求 m 的真因數和時,卻發現由於變數命名的關係, void sof () 只能用來求 n 的真因數和,沒有辦法用來求 m 的真因數和。為了解決這個問題,我們把 void sof () 所使用的變數名稱修改如下:

int p, q;

void sof () {                                   // s(um) o(f) f(actors)
    int f;
    q = 1;                                      // 加入因數 1
    for (f=2; f*f<p; f++)                       // f(actor) -- 因數
        if (p % f == 0)                         // 若 f 為 n 的因數
            q += f + p/f;                       // 加入 f 及 f/n
    if (f*f == p)                               // 若 n 為完全平方數
        q += f;                                 // 加入平方根
}

也就是說,我們用 p 來取代所有的 n、用 q 來取代所有的 m。然後我們在呼叫 void sof() 之前先把 n 代入 p,求出 p 的真因數和 q 之後,再把 q 代入 m:

p = n;                                  // 先把 n 代入 p
sof();                                  // 求 p 的真因數和 q
m = q;                                  // 再把 q 代入 m

雖然這樣麻煩一點,但是如此一來,我們就可以用同一個 void sof () 來求 m 的真因數和了:

p = m;                              // 先把 m 代入 p
sof();                              // 求 p 的真因數和 q
n1 = q;                             // 再把 q 代入 n1

完整程式碼如下:

//2007jfB. 友好數 by Snail
#include <iostream>
using namespace std;

int p, q;

void sof () {                                   // s(um) o(f) f(actors)
    int f;
    q = 1;                                      // 加入因數 1
    for (f=2; f*f<p; f++)                       // f(actor) -- 因數
        if (p % f == 0)                         // 若 f 為 n 的因數
            q += f + p/f;                       // 加入 f 及 f/n
    if (f*f == p)                               // 若 n 為完全平方數
        q += f;                                 // 加入平方根
}

int main () {
    int n, n1, m, f;
    while (cin >> n, n) {
        p = n;                                  // 先把 n 代入 p
        sof();                                  // 求 p 的真因數和 q
        m = q;                                  // 再把 q 代入 m
        if (m == n)                             // 若 n 的友好數為自身
            cout << '=' << m << endl;           // 加一個「=」
        else {
            p = m;                              // 先把 m 代入 p
            sof();                              // 求 p 的真因數和 q
            n1 = q;                             // 再把 q 代入 n1
            if (n1 == n)                        // 若 m 的真因數和為 n
                cout << m << endl;              // m 即是 n 的友好數
            else                                // 否則
                cout << "0\n";                  // n 沒有友好數
        }
    }
}

d255. 11417 - GCD

這一題,它把題目的要求都寫成程式給你了,你只要複製題目中的程式,再加上變數的宣告、0 尾版的迴圈、及結果的輸出,答案就出來了。

//11417 - GCD (by Snail)
#include <iostream>
using namespace std;

int main () {
    int G, N, i, j;                             // 變數的宣告
    while (cin >> N, N) {                       // 0 尾版的迴圈
        G=0;                                    // 複製題目中的程式
        for (i=1;i<N;i++)
            for (j=i+1;j<=N;j++) {
                G+=GCD(i, j);
            }
        cout << G << endl;                      // 結果的輸出
    }
}

但是再仔細看一下程式,其中用了一個 GCD 函數是 C++ 中沒有的。這時候你就得自己為它定義一個 GCD 函數來求最大公因數了。

8.2 特殊條件排序

上一章中我們用 <algorithm> 中的 sort 函數來排序。但是它只能依數值由小到大排序。可是如果遇到特殊條件的排序,Sort 就不知道要如何去排了。

d750. 11321 - Sort! Sort!! and Sort!!!

這題的排序條件很詭異。

//11321 - Sort! Sort!! and Sort!!! (by Snail)
#include <iostream>
#include <algorithm>                            // for sort
using namespace std;

int m;                                          // cmp 中要用到 m
                                                // 故宣告為全域
bool cmp (int a, int b) {
    if (a%m != b%m)                             // 若餘數不等
        return a%m < b%m;                       // 按餘數排序
    if ((a+b)%2)                                // 若一奇一偶
        return (a&1) > (b&1);                   // 奇數在前
    if (a%2)                                    // 同為奇數
        return a > b;                           // 大在前
    return a < b;                               // 否則小在前
}

int main () {
    int i, n, a[10000];
    while (cin >> n >> m, n) {
        for (i=0; i<n; i++)
            cin >> a[i];
        sort (a, a+n, cmp);
        cout << n << ' ' << m << endl;
        for (i=0; i<n; i++)
            cout << a[i] << endl;
    }
    cout << "0 0\n";
}

d731. 11039 - Building designing

遞迴

b250. F. 風鈴

DFS

c129. Oil Deposits

本題重點在於,要用整張地圖遞迴下去
但若每次遞迴都建一張地圖絕對會吃 RE 的

這題的大概做法是:
先從第一個開始搜,一搜到 @,就往下遞回直到沒有,答案加一,然後記得每找到ㄧ個 @,都把它換成其他字元(建議不用 #,因為 DEBUG 時比較好找到錯誤)